diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..23a7072c587b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +; top-most EditorConfig file +root = true + +; Unix-style newlines +[*] +end_of_line = lf + +[*.php] +indent_style = tab +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes index 51fea41744d4..29621a714026 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ phpdoc.dist.xml export-ignore readme.rst # They don't want all of our tests... +tests/bin/ export-ignore tests/codeigniter/ export-ignore tests/travis/ export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000000..e5278250eec1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +--- +name: Bug report +about: Help us improve the framework by reporting bugs! + +--- + +**Direction** +We use github issues to track bugs, not for support. +If you have a support question, or a feature request, raise these as threads on our +[forum](https://forum.codeigniter.com/index.php). + +**Describe the bug** +A clear and concise description of what the bug is. + +**CodeIgniter 4 version** +Which version (and branch, if applicable) the bug is in. + +**Affected module(s)** +Which package or class is the bug in, if known. + +**Expected behavior, and steps to reproduce if appropriate** +A clear and concise description of what you expected to happen, +and how you got there. +Feel free to include a text/log extract, but use a pastebin facility for any +screenshots you deem necessary. + +**Context** + - OS: [e.g. Windows 99] + - Web server [e.g. Apache 1.2.3] + - PHP version [e.g. 6.5.4] diff --git a/.gitignore b/.gitignore index c5870eb8a417..8c2410bfcace 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,9 @@ $RECYCLE.BIN/ # These should never be under version control, # as it poses a security risk. .env +.vagrant application/.env +Vagrantfile #------------------------- # Temporary Files @@ -55,10 +57,20 @@ writable/logs/* !writable/logs/index.html !writable/logs/.htaccess +writable/session/* +!writable/session/index.html +!writable/session/.htaccess + writable/uploads/* !writable/uploads/index.html !writable/uploads/.htaccess +writable/debugbar/* + +application/Database/Migrations/2* + +php_errors.log + #------------------------- # User Guide Temp Files #------------------------- @@ -88,6 +100,9 @@ composer.lock # Modules Testing _modules/* +# phpenv local config +.php-version + # Jetbrains editors (PHPStorm, etc) .idea/ *.iml @@ -102,10 +117,18 @@ nbactions.xml nb-configuration.xml .nb-gradle/ -## Sublime Text +# Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache *.sublime-workspace *.sublime-project -/api/ \ No newline at end of file +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml + diff --git a/application/Database/Migrations/.gitignore b/.nojekyll similarity index 100% rename from application/Database/Migrations/.gitignore rename to .nojekyll diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 7124269cd9d6..000000000000 --- a/.php_cs +++ /dev/null @@ -1,65 +0,0 @@ -in('system'); - -return Symfony\CS\Config\Config::create() - ->level(Symfony\CS\FixerInterface::NONE_LEVEL) - ->fixers(array( - 'psr0', - 'encoding', - 'short_tag', - 'braces', - 'eof_ending', - 'line_after_namespace', - 'linefeed', - 'lowercase_constants', - 'lowercase_keywords', - 'method_argument_space', - 'parenthesis', - 'php_closing_tag', - 'single_line_after_imports', - 'trailing_spaces', - 'visibility', - 'array_element_no_space_before_comma', - 'array_element_white_space_after_comma', - 'concat_without_spaces', - 'double_arrow_multiline_whitespaces', - 'duplicate_semicolon', - 'function_typehint_space', - 'include', - 'multiline_array_trailing_comma', - 'no_empty_lines_after_phpdocs', - 'object_operator', - 'operators_spaces', - 'phpdoc_indent', - 'phpdoc_no_empty_return', - 'phpdoc_params', - 'phpdoc_scalar', - 'phpdoc_separation', - 'phpdoc_to_comment', - 'phpdoc_trim', - 'phpdoc_type_to_var', - 'phpdoc_types', - 'print_to_echo', - 'remove_leading_slash_use', - 'remove_lines_between_uses', - 'return', - 'single_array_no_trailing_comma', - 'spaces_before_semicolon', - 'standardize_not_equal', - 'ternary_spaces', - 'trim_array_spaces', - 'unary_operators_spaces', - 'unused_use', - 'whitespacy_lines', - 'align_double_arrow', - 'align_equals', - 'logical_not_operators_with_successor_space', - 'multiline_spaces_before_semicolon', - 'no_blank_lines_before_namespace', - 'ordered_use', - 'phpdoc_order', - 'short_array_syntax', - )) - ->finder($finder); diff --git a/.travis.yml b/.travis.yml index 8cb5a1308217..d07890f543ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,46 @@ language: php php: - - 7 + - 7.1 + - 7.2 + - nightly -env: - - DB_GROUP=mysqli - - DB_GROUP=postgres +matrix: + fast_finish: true + allow_failures: + - php: nightly -script: - - php vendor/bin/phpunit -v +global: + - CI=true + - CI_ENVIRONMENT=testing + +# Recommended by Travis support +sudo: required +dist: precise + +env: + - DB=mysqli + - DB=postgres + - DB=sqlite services: + - memcached - mysql - postgresql + - redis-server + +script: + - php vendor/bin/phpunit -v before_install: - - if [ $DB_GROUP = 'mysqli' ]; then mysql -e "create database IF NOT EXISTS test;" -uroot; fi - - if [ $DB_GROUP = 'postgres' ]; then psql -c 'create database test;' -U postgres; fi + - mysql -e "CREATE DATABASE IF NOT EXISTS test;" -uroot; + - mysql -e "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = 'test';" -uroot + - psql -c 'CREATE DATABASE test;' -U postgres before_script: - - composer install \ No newline at end of file + - echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - echo 'extension = redis.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - composer install --prefer-source + +after_success: + - travis_retry php tests/bin/php-coveralls.phar -v diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000000..047a477a2945 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@codeigniter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..517473dd2d2d --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +Each pull request should address a single issue, and have a meaningful title. + +**Description** +Explain what you have changed, and why. + +**Checklist:** +- [ ] Securely signed commits +- [ ] Component(s) with PHPdocs +- [ ] Unit testing, with >80% coverage +- [ ] User guide updated +- [ ] Conforms to style guide + +---------Remove from here down in your description---------- + +**Notes** +- Pull requests must be in English +- If the PR solves an issue, reference it with a suitable verb and the issue number +(e.g. fixes 12345 +- Unsolicited PRs will be considered, but there is no guarantee of acceptance +- Pull requests should be from a feature branch in the contributor's fork of the repository + to the develop branch of the project repository + diff --git a/README.md b/README.md index d8499227158d..0b18eafbc1f7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CodeIgniter 4 Development -[![Build Status](https://travis-ci.org/lonnieezell/CodeIgniter4.svg?branch=develop)](https://travis-ci.org/lonnieezell/CodeIgniter4) +[![Build Status](https://travis-ci.org/bcit-ci/CodeIgniter4.svg?branch=develop)](https://travis-ci.org/bcit-ci/CodeIgniter4) +[![Coverage Status](https://coveralls.io/repos/github/bcit-ci/CodeIgniter4/badge.svg?branch=develop)](https://coveralls.io/github/bcit-ci/CodeIgniter4?branch=develop)
-[![StyleCI](https://styleci.io/repos/41463886/shield)](https://styleci.io/repos/41463886) ## What is CodeIgniter? CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure. @@ -16,6 +16,22 @@ while still keeping as many of the things intact that has made people love the f More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. +### Documentation + +The current documentation can be found [here](https://bcit-ci.github.io/CodeIgniter4/). As with the rest of the framework, it is currently a work in progress, and will see changes over time to structure, explanations, etc. + +## Important Change with index.php + +index.php is no longer in the root of the project! It has been moved inside the *public* folder, +for better security and separation of components. + +This means that you should configure your web server to "point" to your project's *public* folder, and +not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the +framework are exposed. + +**Please** read the user guide for a better explanation of how CI4 works! +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 @@ -39,14 +55,21 @@ Remember that some components that were part of CodeIgniter 3 are being moved to optional packages, with their own repository. ## Contributing -We are not accepting contributions from the public until a stable enough base has been formed, -and our plans fleshed out and things settle down a little bit. -At that point, we will welcome your comments and help creating the best framework for our community. +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 +specifically interested in at any point in time. Join the discussion for those issues, and let us know +if you want to take the lead for one of them. + +We are not looking for out-of-scope contributions, only those that would be considered part of our controlled evolution! -Please read the *Contributing to CodeIgniter* section in the user guide +Please read the [*Contributing to CodeIgniter*](https://github.com/bcit-ci/CodeIgniter4/blob/develop/contributing.md) section in the user guide ## Server Requirements -PHP version 7 or higher is required. +PHP version 7.1 or higher is required, with the following extensions installed: + +- intl + ## Running CodeIgniter Tests -Information on running CodeIgniter test suite can be found in the README.md file in the tests directory. +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 new file mode 100644 index 000000000000..37d36fb7d6c4 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,6 @@ +# CodeIgniter 4 Admin + +This folder contains tools or docs useful for project maintainers. + +- [docbot](./docbot.md) - build & deploy user guide +- [release](./release.md) - build & deploy framework release diff --git a/admin/docbot b/admin/docbot new file mode 100755 index 000000000000..d1734f441e71 --- /dev/null +++ b/admin/docbot @@ -0,0 +1,36 @@ +#!/bin/bash + +# Rebuild and deploy CodeIgniter4 user guide + +UPSTREAM=https://github.com/bcit-ci/CodeIgniter4.git + +# Prepare the nested repo clone folder +cd user_guide_src +rm -rf build/* +mkdir build/html + +# Get ready for git +cd build/html +git init +git remote add origin $UPSTREAM +git fetch +git checkout gh-pages +git reset --hard origin/gh-pages +rm -r * + +# Make the new user guide +cd ../.. +make html + +# All done? +if [ $# -lt 1 ]; then + exit 0 +fi + +# Optionally update the remote repo +if [ $1 = "deploy" ]; then + cd build/html + git add . + git commit -S -m "Docbot synching" + git push -f origin gh-pages +fi diff --git a/admin/docbot.md b/admin/docbot.md new file mode 100644 index 000000000000..af8074732abd --- /dev/null +++ b/admin/docbot.md @@ -0,0 +1,33 @@ +# docbot + +Builds & deploys user guide. + +The CI4 user guide, warts & all, is rebuilt in a nested +repository clone (`user_guide_src/build/html`), with the result +optionally pushed to the `gh-pages` branch of the repo. +That would then be publically visible as the in-progress +version of the [User Guide](https://bcit-ci.github.io/CodeIgniter4/). + +## Audience + +This script is intended for use by framework maintainers, +i.e. someone with commit rights on the CI4 repository. + +This script wraps the conventional user guide building, +i.e. `user_guide_src/make html`, with additional +steps. + +You will be prompted for your github credentials and +GPG-signing key as appropriate. + +## Usage + +Inside a shell prompt, in the project root: + + `admin/docbot [deploy]` + +If "deploy" is not added, the script execution is considered +a trial run, and nothing is pushed to the repo. + +Whether or not deployed, the results are left inside +user_guide_src/build (which is git ignored). diff --git a/admin/post_release b/admin/post_release new file mode 100755 index 000000000000..7f5e96d1bd42 --- /dev/null +++ b/admin/post_release @@ -0,0 +1,91 @@ +#~/bin/bash + +## Cleanup after a framework release + +UPSTREAM=https://github.com/bcit-ci/CodeIgniter4.git +version=4 +qualifier= + +branch=post-release- +devonly='.github/* admin/* build/* contributing/* user_guide_src/* CODE_OF_CONDUCT.md \ +DCO.txt PULL_REQUEST_TEMPLATE.md' +which=release + +BOLD='\033[1m' +NORMAL='\033[0m' +COLOR='\033[1;31m' +ERROR='\033[0;31m' + +echo -e "${BOLD}${COLOR}CodeIgniter4 release cleanup${NORMAL}" +echo '----------------------------' + +#--------------------------------------------------- +# Check arguments +echo -e "${BOLD}Checking arguments...${NORMAL}" + +if [ $# -lt 1 ]; then + echo "You really need to read the directions first!" + exit 1 +fi + +version=$1 +if [ $# -gt 1 ]; then + qualifier="-${2}" + which='pre-release' +fi +release_branch="release-$version$qualifier" +branch="post-${release_branch}" + +#--------------------------------------------------- +# Create the post-release branch +echo -e "${BOLD}Creating $branch${NORMAL}" + +git checkout $release_branch +git branch -d $branch # remove the branch if there +git checkout -b $branch + +#--------------------------------------------------- +# Put our house back in order +echo -e "${BOLD}Put our house back in order${NORMAL}" + +mv -r admin/previous-gitignore .gitignore +rm -Rf docs + +#--------------------------------------------------- +# Add next version block in changelog.rst +echo -e "${BOLD}Setup next release${NORMAL}" +sed -i '5 i\ +Version |release| +==================================================== + +Release Date: Not Released +' user_guide_src/source/changelog.rst + +git add . +git commit -S -m "Post ${branch} cleanup" + +#--------------------------------------------------- +# Cleanup master +echo -e "${BOLD}Cleanup the master branch${NORMAL}" +git checkout master +git merge ${branch} +git push origin master +#git push UPSTREAM master + +#--------------------------------------------------- +# Cleanup develop +echo -e "${BOLD}Cleanup the develop branch${NORMAL}" +git checkout develop +git merge ${branch} +git push origin develop +#git push UPSTREAM develop + +# keep or delete the release branch? up to you +# at this point, you should have uptodate develop and master, +# as well as the release and post-release branches + +#--------------------------------------------------- +# Phew! + +echo -e "${BOLD}Congratulations - we have liftoff${NORMAL}" +echo "Don't forget to announce this release on the forum and on twitter!" diff --git a/admin/previous-gitignore b/admin/previous-gitignore new file mode 100644 index 000000000000..8c2410bfcace --- /dev/null +++ b/admin/previous-gitignore @@ -0,0 +1,134 @@ +#------------------------- +# Operating Specific Junk Files +#------------------------- + +# OS X +.DS_Store +.AppleDouble +.LSOverride + +# OS X Thumbnails +._* + +# Windows image file caches +Thumbs.db +ehthumbs.db +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +#------------------------- +# Environment Files +#------------------------- +# These should never be under version control, +# as it poses a security risk. +.env +.vagrant +application/.env +Vagrantfile + +#------------------------- +# Temporary Files +#------------------------- +writable/cache/* +!writable/cache/index.html +!writable/cache/.htaccess + +writable/logs/* +!writable/logs/index.html +!writable/logs/.htaccess + +writable/session/* +!writable/session/index.html +!writable/session/.htaccess + +writable/uploads/* +!writable/uploads/index.html +!writable/uploads/.htaccess + +writable/debugbar/* + +application/Database/Migrations/2* + +php_errors.log + +#------------------------- +# User Guide Temp Files +#------------------------- +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +#------------------------- +# Test Files +#------------------------- +tests/coverage* + +# Don't save phpunit under version control. +phpunit + +#------------------------- +# Composer +#------------------------- +vendor/ +composer.lock + +#------------------------- +# IDE / Development Files +#------------------------- + +# Modules Testing +_modules/* + +# phpenv local config +.php-version + +# Jetbrains editors (PHPStorm, etc) +.idea/ +*.iml + +# Netbeans +nbproject/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml +.nb-gradle/ + +# Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml + diff --git a/admin/release b/admin/release new file mode 100755 index 000000000000..bb34e85fdc46 --- /dev/null +++ b/admin/release @@ -0,0 +1,134 @@ +#~/bin/bash + +## Build a framework release branch + +#--------------------------------------------------- +# Setup variables + +UPSTREAM=https://github.com/bcit-ci/CodeIgniter4.git +action=test +version=4 +qualifier= + +branch=release- +releasable='application docs public system writable README.md composer.json contributing.md env license.txt spark' +release_empty='tests tests/_support' +which=release + +BOLD='\033[1m' +NORMAL='\033[0m' +COLOR='\033[1;31m' +ERROR='\033[0;31m' + +echo -e "${BOLD}${COLOR}CodeIgniter4 release builder${NORMAL}" +echo '----------------------------' + +#--------------------------------------------------- +# Check arguments +echo -e "${BOLD}Checking arguments...${NORMAL}" + +if [ $# -lt 1 ]; then + echo -e "${BOLD}Usage: admin/release version# pre-release-qualifier${NORMAL}" + exit 1 +fi + + +version=$1 +if [ $# -gt 1 ]; then + qualifier="-${2}" + which='pre-release' +fi +branch="release-$version$qualifier" + +#--------------------------------------------------- +# Create the release branch +echo -e "${BOLD}Creating $which $branch to $action ${NORMAL}" + +git checkout develop +git branch -d $branch &>/dev/null # remove the branch if there +git checkout -b $branch +composer update + +#--------------------------------------------------- +# Update version dependencies +echo -e "${BOLD}Updating version dependencies${NORMAL}" + +function check_unique { + count=`grep -c '$1' < $2 | wc -l` + if [ $count -ne 1 ]; then + echo -e "${BOLD}${COLOR}$2 has ${count} occurences of '$1'${NORMAL}" + exit 1 + fi +} + +# Make sure there is only one line to affect in each file +check_unique "const CI_VERSION" 'system/CodeIgniter.php' +check_unique "release =" 'user_guide_src/source/conf.py' +check_unique "|release|" 'user_guide_src/source/changelog.rst' +check_unique "Release Date.*Not Released" 'user_guide_src/source/changelog.rst' + +# CI_VERSION definition in system/CodeIgniter.php +sed -i "/const CI_VERSION/s/'.*'/'${version}${qualifier}'/" system/CodeIgniter.php + +# release substitution variable in user_guide_src/source/conf.py +sed -i "/release =/s/'.*'/'${version}${qualifier}'/" user_guide_src/source/conf.py + +# version & date in user_guide_src/source/changelog.rst +sed -i "/|release|/s/|.*|/${version}${qualifier}/" user_guide_src/source/changelog.rst +sed -i "/Release Date/s/Not Released/$(date +'%B %d, %Y')/" user_guide_src/source/changelog.rst + +#--------------------------------------------------- +# Setup the distribution folders + +if [ -d dist ]; then + rm -rf dist/ +fi +mkdir dist + +if [ -d build ]; then + rm -rf build/ +fi +mkdir build + +#--------------------------------------------------- +# Generate the user guide +echo -e "${BOLD}Generate the user guide${NORMAL}" + +cd user_guide_src + +# make the UG +rm -rf build/* +make html +make epub + +cd .. + +mv user_guide_src/build/html build/docs + +#--------------------------------------------------- +# And finally, get ready for merging +git add . +git commit -m "Release ${version}${qualifier}" + +#--------------------------------------------------- +# Hide stuff from the release bundle +echo -e "${BOLD}Build the distributables${NORMAL}" + +for f in $releasable; do + cp -r $f build/ +done +for f in $release_empty; do + mkdir build/$f +done + +# add the docs +mv user_guide_src/build/epub/CodeIgniter4.epub dist/CodeIgniter-${version}${qualifier}.epub + +cd build +zip -r ../dist/v${version}${qualifier}.zip * +tar -zcf ../dist/v${version}${qualifier}.tar.gz * + +#--------------------------------------------------- +# Done for now +echo -e "${BOLD}Your $branch branch is ready to inspect.${NORMAL}" +echo -e "${BOLD}Follow the directions in release.md to continue.${NORMAL}" diff --git a/admin/release.md b/admin/release.md new file mode 100644 index 000000000000..28474ff9f693 --- /dev/null +++ b/admin/release.md @@ -0,0 +1,94 @@ +# release + +Builds the branches needed for a framework release. + +This tool is meant to help automate the process of +launching a new release, by creating the release +distribution files, (tagging everything properly), +and getting/keeping the repo branches in order. + +## Audience + +This script is intended for use by framework maintainers, +i.e. someone with commit rights on the CI4 repository. + +You will be prompted for your github credentials and +GPG-signing key as appropriate. + +## Workflow + +The repo has two branches of interest: "master" (stable) and "develop" (in progress). +There might be other feature branches, but they are not relevant to this process. + +Once "develop" is ready for a new release, the general workflow is to + +- create a "release" branch from develop +- update version dependencies or constants +- generate version(s) of the user guide +- move or ignore stuff, distinguishing release from development +- test that all is as it should be +- merge the release branch into "master" +- **manually** create the release & tag on github, based on master +- put everything back where it should be for development +- merge the post-release branch into "master" +- merge the post-release branch into "develop" +- **manually** delete the release branches in the repo +- **manually** post a sticky announcement thread on the forum +- **manually** tweet the release announcement + +Visually: + + develop -> release -> master + post-release -> master + post->release -> develop + +The `release` bash script does the first six workflow steps, +and the `post-release` script does the other three between +the manual steps. + +## Assumptions + +You (a maintainer) have forked the main CodeIgniter4 repo, +and the git alias `origin`, in your local clone, refers to your fork. +The script creates an additional alias, `upstream`, which refers to the +main repo. This separation keeps the release branch isolated +for any testing you want to do. + +The `develop` branch of the main repo should be "clean", and ready for +a release. This means that the changelog and upgrading instructions +have been suitably edited. + +This script is not intended to deal with hotfixes, i.e. PRs against +`master` that need to also be merged into `develop`, probably +as part of a bug fix minor release. + +## Usage + +Inside a shell prompt, in the project root: + + `admin/release version [qualifier]` + +Nothing is pushed to the repo. at this point - +the results are left inside +the release branch in your local clone. + +The "version" should follow semantic versioning, e.g. `4.0.6`, and the +version number should be higher than the current released one. + +The "qualifier" argument is a suffix to add to the version +for a pre-release, e.g. `beta.2` or `rc.41`. + +Examples: +- `admin/release 4.0.0 alpha.1` would prepare the "4.0.0-alpha.1" pre-release PR +- `admin/release 4.0.0` would prepare the "4.0.0" release PR +- `admin/release peanut butter banana` would complain and tell you to read these directions + +Complete the next few steps of the release manually: +- merge the release branch to "master" +- push that to the main repo +- on github.com, create an appropriate release (or pre-release) + +Once the release branch has been vetted, and you have +completed the manual steps, clean up with: + + `admin/post_release version [qualifier]` diff --git a/application/Config/App.php b/application/Config/App.php index da45f71b294e..d25b84c31b1c 100644 --- a/application/Config/App.php +++ b/application/Config/App.php @@ -52,6 +52,68 @@ class App extends BaseConfig */ public $uriProtocol = 'REQUEST_URI'; + /* + |-------------------------------------------------------------------------- + | Default Locale + |-------------------------------------------------------------------------- + | + | The Locale roughly represents the language and location that your visitor + | is viewing the site from. It affects the language strings and other + | strings (like currency markers, numbers, etc), that your program + | should run under for this request. + | + */ + public $defaultLocale = 'en'; + + /* + |-------------------------------------------------------------------------- + | Negotiate Locale + |-------------------------------------------------------------------------- + | + | If true, the current Request object will automatically determine the + | language to use based on the value of the Accept-Language header. + | + | If false, no automatic detection will be performed. + | + */ + public $negotiateLocale = false; + + /* + |-------------------------------------------------------------------------- + | Supported Locales + |-------------------------------------------------------------------------- + | + | If $negotiateLocale is true, this array lists the locales supported + | by the application in descending order of priority. If no match is + | found, the first locale will be used. + | + */ + public $supportedLocales = ['en']; + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | The default timezone that will be used in your application to display + | dates with the date helper, and can be retrieved through app_timezone() + | + */ + public $appTimezone = 'America/Chicago'; + + /* + |-------------------------------------------------------------------------- + | Default Character Set + |-------------------------------------------------------------------------- + | + | This determines which character set is used by default in various methods + | that require a character set to be provided. + | + | See http://php.net/htmlspecialchars for a list of supported charsets. + | + */ + public $charset = 'UTF-8'; + /* |-------------------------------------------------------------------------- | URI PROTOCOL @@ -119,13 +181,13 @@ class App extends BaseConfig | except for 'cookie_prefix' and 'cookie_httponly', which are ignored here. | */ - public $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'; - public $sessionCookieName = 'ci_session'; - public $sessionExpiration = 7200; - public $sessionSavePath = NULL; - public $sessionMatchIP = FALSE; - public $sessionTimeToUpdate = 300; - public $sessionRegenerateDestroy = FALSE; + public $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'; + public $sessionCookieName = 'ci_session'; + public $sessionExpiration = 7200; + public $sessionSavePath = WRITEPATH . 'session'; + public $sessionMatchIP = false; + public $sessionTimeToUpdate = 300; + public $sessionRegenerateDestroy = false; /* |-------------------------------------------------------------------------- @@ -153,7 +215,7 @@ class App extends BaseConfig | Reverse Proxy IPs |-------------------------------------------------------------------------- | - | If your getServer is behind a reverse proxy, you must whitelist the proxy + | If your server is behind a reverse proxy, you must whitelist the proxy | IP addresses from which CodeIgniter should trust headers such as | HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify | the visitor's IP address. @@ -178,14 +240,13 @@ class App extends BaseConfig | CSRFCookieName = The cookie name | CSRFExpire = The number in seconds the token should expire. | CSRFRegenerate = Regenerate token on every submission - | CSRFExcludeURIs = Array of URIs which ignore CSRF checks + | CSRFRedirect = Redirect to previous page with error on failure */ - public $CSRFProtection = false; public $CSRFTokenName = 'csrf_test_name'; public $CSRFCookieName = 'csrf_cookie_name'; public $CSRFExpire = 7200; public $CSRFRegenerate = true; - public $CSRFExcludeURIs = []; + public $CSRFRedirect = true; /* |-------------------------------------------------------------------------- @@ -210,10 +271,11 @@ class App extends BaseConfig | The Debug Toolbar provides a way to see information about the performance | and state of your application during that page display. By default it will | NOT be displayed under production environments, and will only display if - | CI_DEBIG is true, since if it's not, there's not much to display anyway. + | CI_DEBUG is true, since if it's not, there's not much to display anyway. + | + | toolbarMaxHistory = Number of history files, 0 for none or -1 for unlimited + | */ - public $toolbarEnabled = (ENVIRONMENT != 'production' && CI_DEBUG); - public $toolbarCollectors = [ 'CodeIgniter\Debug\Toolbar\Collectors\Timers', 'CodeIgniter\Debug\Toolbar\Collectors\Database', @@ -222,50 +284,9 @@ class App extends BaseConfig // 'CodeIgniter\Debug\Toolbar\Collectors\Cache', 'CodeIgniter\Debug\Toolbar\Collectors\Files', 'CodeIgniter\Debug\Toolbar\Collectors\Routes', + 'CodeIgniter\Debug\Toolbar\Collectors\Events', ]; - - /* - |-------------------------------------------------------------------------- - | Error Views Path - |-------------------------------------------------------------------------- - | This is the path to the directory that contains the 'cli' and 'html' - | directories that hold the views used to generate errors. - | - | Default: APPPATH.'Views/errors' - */ - public $errorViewPath = APPPATH.'Views/errors'; - - /* - |-------------------------------------------------------------------------- - | Composer auto-loading - |-------------------------------------------------------------------------- - | - | Enabling this setting will tell CodeIgniter to look for a Composer - | package auto-loader script in application/vendor/autoload.php. - | - | $composerAutoload = TRUE; - | - | Or if you have your vendor/ directory located somewhere else, you - | can opt to set a specific path as well: - | - | $composerAutoload = '/path/to/vendor/autoload.php'; - | - | For more information about Composer, please visit http://getcomposer.org/ - | - | Note: This will NOT disable or override the CodeIgniter-specific - | autoloading. - */ - public $composerAutoload = false; - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | If you use the Encryption class you must set - | an encryption key. See the user guide for more info. - */ - public $encryptionKey = ''; + public $toolbarMaxHistory = 20; /* |-------------------------------------------------------------------------- @@ -277,7 +298,6 @@ class App extends BaseConfig | and can be of any length, though the more random the characters | the better. | - | If you use the Model class' hashedID methods, this must be filled out. */ public $salt = ''; diff --git a/application/Config/Autoload.php b/application/Config/Autoload.php index 097844155456..7ac3add13e3b 100644 --- a/application/Config/Autoload.php +++ b/application/Config/Autoload.php @@ -1,6 +1,6 @@ APPPATH.'Config', - APP_NAMESPACE.'\Controllers' => APPPATH.'Controllers', - APP_NAMESPACE => realpath(APPPATH), + APP_NAMESPACE => APPPATH, // For custom namespace + 'App' => APPPATH, // To ensure filters, etc still found, + 'Tests\Support' => TESTPATH.'_support', // So custom migrations can run during testing ]; /** diff --git a/application/Config/Boot/development.php b/application/Config/Boot/development.php index 4e6be582e3c5..25f61516c87f 100644 --- a/application/Config/Boot/development.php +++ b/application/Config/Boot/development.php @@ -4,11 +4,10 @@ |-------------------------------------------------------------------------- | ERROR DISPLAY |-------------------------------------------------------------------------- +| In development, we want to show as many errors as possible to help +| make sure they don't make it to production. And save us hours of +| painful debugging. */ - -// In development, we want to show as many errors as possible to help -// make sure they don't make it to production. And save us hours of -// painful debugging. error_reporting(-1); ini_set('display_errors', 1); @@ -22,23 +21,13 @@ */ define('SHOW_DEBUG_BACKTRACE', true); -/* -|-------------------------------------------------------------------------- -| KINT -|-------------------------------------------------------------------------- -| If true, will enable the Kint PHP Debugging tool and make it available -| globally throughout your application to use while making sure things -| work the way you intend them to. -*/ -$useKint = true; - /* |-------------------------------------------------------------------------- | DEBUG MODE |-------------------------------------------------------------------------- | Debug mode is an experimental flag that can allow changes throughout -| the system. It's not widely used currently, and may not survive -| release of the framework. +| the system. This will control whether Kint is loaded, and a few other +| items. It can always be used within your own application too. */ -define('CI_DEBUG', 1); \ No newline at end of file +define('CI_DEBUG', 1); diff --git a/application/Config/Boot/production.php b/application/Config/Boot/production.php index 9f151ce973cc..bccc84b9e243 100644 --- a/application/Config/Boot/production.php +++ b/application/Config/Boot/production.php @@ -4,10 +4,9 @@ |-------------------------------------------------------------------------- | ERROR DISPLAY |-------------------------------------------------------------------------- +| Don't show ANY in production environments. Instead, let the system catch +| it and display a generic error message. */ - -// Don't show ANY in production environments. Instead, let the system catch -// it and display a generic error message. ini_set('display_errors', 0); error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); @@ -20,4 +19,4 @@ | release of the framework. */ -define('CI_DEBUG', 0); \ No newline at end of file +define('CI_DEBUG', 0); diff --git a/application/Config/Boot/testing.php b/application/Config/Boot/testing.php index 4e6be582e3c5..f3c7f1c7c6d2 100644 --- a/application/Config/Boot/testing.php +++ b/application/Config/Boot/testing.php @@ -4,11 +4,10 @@ |-------------------------------------------------------------------------- | ERROR DISPLAY |-------------------------------------------------------------------------- +| In development, we want to show as many errors as possible to help +| make sure they don't make it to production. And save us hours of +| painful debugging. */ - -// In development, we want to show as many errors as possible to help -// make sure they don't make it to production. And save us hours of -// painful debugging. error_reporting(-1); ini_set('display_errors', 1); @@ -22,16 +21,6 @@ */ define('SHOW_DEBUG_BACKTRACE', true); -/* -|-------------------------------------------------------------------------- -| KINT -|-------------------------------------------------------------------------- -| If true, will enable the Kint PHP Debugging tool and make it available -| globally throughout your application to use while making sure things -| work the way you intend them to. -*/ -$useKint = true; - /* |-------------------------------------------------------------------------- | DEBUG MODE @@ -41,4 +30,4 @@ | release of the framework. */ -define('CI_DEBUG', 1); \ No newline at end of file +define('CI_DEBUG', 1); diff --git a/application/Config/Cache.php b/application/Config/Cache.php new file mode 100644 index 000000000000..781432aae7ab --- /dev/null +++ b/application/Config/Cache.php @@ -0,0 +1,119 @@ + '127.0.0.1', + 'port' => 11211, + 'weight' => 1, + 'raw' => false, + ]; + + /* + | ------------------------------------------------------------------------- + | Redis settings + | ------------------------------------------------------------------------- + | Your Redis server can be specified below, if you are using + | the Redis or Predis drivers. + | + */ + public $redis = [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + ]; + + /* + |-------------------------------------------------------------------------- + | Available Cache Handlers + |-------------------------------------------------------------------------- + | + | This is an array of cache engine alias' and class names. Only engines + | that are listed here are allowed to be used. + | + */ + public $validHandlers = [ + 'dummy' => \CodeIgniter\Cache\Handlers\DummyHandler::class, + 'file' => \CodeIgniter\Cache\Handlers\FileHandler::class, + 'memcached' => \CodeIgniter\Cache\Handlers\MemcachedHandler::class, + 'predis' => \CodeIgniter\Cache\Handlers\PredisHandler::class, + 'redis' => \CodeIgniter\Cache\Handlers\RedisHandler::class, + 'wincache' => \CodeIgniter\Cache\Handlers\WincacheHandler::class, + ]; +} diff --git a/application/Config/Constants.php b/application/Config/Constants.php index 9cfb22ced281..a5c63d4b9901 100644 --- a/application/Config/Constants.php +++ b/application/Config/Constants.php @@ -13,6 +13,16 @@ // define('APP_NAMESPACE', 'App'); +/* +|-------------------------------------------------------------------------- +| Composer Path +|-------------------------------------------------------------------------- +| +| The path that Composer's autoload file is expected to live. By default, +| the vendor folder is in the Root directory, but you can customize that here. +*/ +define('COMPOSER_PATH', ROOTPATH.'vendor/autoload.php'); + /* |-------------------------------------------------------------------------- | Timing Constants @@ -21,13 +31,14 @@ | Provide simple ways to work with the myriad of PHP functions that | require information to be in seconds. */ -defined('SECOND') || define('SECOND', 1); -defined('MINUTE') || define('MINUTE', 60); -defined('HOUR') || define('HOUR', 3600); -defined('DAY') || define('DAY', 86400); -defined('WEEK') || define('WEEK', 604800); -defined('MONTH') || define('MONTH', 2592000); -defined('YEAR') || define('YEAR', 31536000); +defined('SECOND') || define('SECOND', 1); +defined('MINUTE') || define('MINUTE', 60); +defined('HOUR') || define('HOUR', 3600); +defined('DAY') || define('DAY', 86400); +defined('WEEK') || define('WEEK', 604800); +defined('MONTH') || define('MONTH', 2592000); +defined('YEAR') || define('YEAR', 31536000); +defined('DECADE') || define('DECADE', 315360000); /* |-------------------------------------------------------------------------- diff --git a/application/Config/ContentSecurityPolicy.php b/application/Config/ContentSecurityPolicy.php index 6a79136c1ebb..2cd502925820 100644 --- a/application/Config/ContentSecurityPolicy.php +++ b/application/Config/ContentSecurityPolicy.php @@ -23,7 +23,7 @@ class ContentSecurityPolicy extends BaseConfig public $imageSrc = 'self'; - public $base_uri = null; + public $baseURI = 'none'; public $childSrc = null; @@ -38,6 +38,8 @@ class ContentSecurityPolicy extends BaseConfig public $mediaSrc = null; public $objectSrc = null; + + public $manifestSrc = null; public $pluginTypes = null; diff --git a/application/Config/Database.php b/application/Config/Database.php index 266608cf5ada..f6c867c878c8 100644 --- a/application/Config/Database.php +++ b/application/Config/Database.php @@ -1,7 +1,5 @@ false, 'strictOn' => false, 'failover' => [], - 'saveQueries' => true, + 'port' => 3306 ]; /** @@ -64,7 +62,7 @@ class Database extends \CodeIgniter\Database\Config 'password' => '', 'database' => '', 'DBDriver' => '', - 'DBPrefix' => '', + 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE. 'pConnect' => false, 'DBDebug' => (ENVIRONMENT !== 'production'), 'cacheOn' => false, @@ -76,7 +74,7 @@ class Database extends \CodeIgniter\Database\Config 'compress' => false, 'strictOn' => false, 'failover' => [], - 'saveQueries' => true, + 'port' => 3306 ]; //-------------------------------------------------------------------- @@ -93,8 +91,8 @@ public function __construct() $this->defaultGroup = 'tests'; // Under Travis-CI, we can set an ENV var named 'DB_GROUP' - // so that we can test against multiple databases. - if ($group = getenv('DB_GROUP')) + // so that we can test against multiple databases. + if ($group = getenv('DB')) { if (is_file(TESTPATH.'travis/Database.php')) { diff --git a/application/Config/DocTypes.php b/application/Config/DocTypes.php new file mode 100755 index 000000000000..38ceaedfc905 --- /dev/null +++ b/application/Config/DocTypes.php @@ -0,0 +1,33 @@ + '', + 'xhtml1-strict' => '', + 'xhtml1-trans' => '', + 'xhtml1-frame' => '', + 'xhtml-basic11' => '', + 'html5' => '', + 'html4-strict' => '', + 'html4-trans' => '', + 'html4-frame' => '', + 'mathml1' => '', + 'mathml2' => '', + 'svg10' => '', + 'svg11' => '', + 'svg11-basic' => '', + 'svg11-tiny' => '', + 'xhtml-math-svg-xh' => '', + 'xhtml-math-svg-sh' => '', + 'xhtml-rdfa-1' => '', + 'xhtml-rdfa-2' => '' + ]; +} \ No newline at end of file diff --git a/application/Config/Email.php b/application/Config/Email.php new file mode 100644 index 000000000000..c5dfd13ae70e --- /dev/null +++ b/application/Config/Email.php @@ -0,0 +1,140 @@ + \App\Filters\CSRF::class, + 'toolbar' => \App\Filters\DebugToolbar::class, + 'honeypot' => \App\Filters\Honeypot::class + ]; + + // Always applied before every request + public $globals = [ + 'before' => [ + //'honeypot' + // 'csrf', + ], + 'after' => [ + 'toolbar', + //'honeypot' + ] + ]; + + // Works on all of a particular HTTP method + // (GET, POST, etc) as BEFORE filters only + // like: 'post' => ['CSRF', 'throttle'], + public $methods = []; + + // List filter aliases and any before/after uri patterns + // that they should run on, like: + // 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']], + public $filters = []; +} diff --git a/application/Config/ForeignCharacters.php b/application/Config/ForeignCharacters.php new file mode 100644 index 000000000000..8ee6f113d78e --- /dev/null +++ b/application/Config/ForeignCharacters.php @@ -0,0 +1,6 @@ + \CodeIgniter\Format\JSONFormatter::class, + 'application/xml' => \CodeIgniter\Format\XMLFormatter::class, + 'text/xml' => \CodeIgniter\Format\XMLFormatter::class, + ]; + + //-------------------------------------------------------------------- + + /** + * A Factory method to return the appropriate formatter for the given mime type. + * + * @param string $mime + * + * @return \CodeIgniter\Format\FormatterInterface + */ + public function getFormatter(string $mime) + { + if (! array_key_exists($mime, $this->formatters)) + { + throw new \InvalidArgumentException('No Formatter defined for mime type: '. $mime); + } + + $class = $this->formatters[$mime]; + + if (! class_exists($class)) + { + throw new \BadMethodCallException($class.' is not a valid Formatter.'); + } + + return new $class(); + } + + //-------------------------------------------------------------------- + +} diff --git a/application/Config/Honeypot.php b/application/Config/Honeypot.php new file mode 100644 index 000000000000..d35b0c7ee861 --- /dev/null +++ b/application/Config/Honeypot.php @@ -0,0 +1,31 @@ +{label}'; +} \ No newline at end of file diff --git a/application/Config/Hooks.php b/application/Config/Hooks.php deleted file mode 100644 index fd089239a694..000000000000 --- a/application/Config/Hooks.php +++ /dev/null @@ -1,20 +0,0 @@ - \CodeIgniter\Images\Handlers\GDHandler::class, + 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, + ]; +} diff --git a/application/Config/Logger.php b/application/Config/Logger.php index 4fc2192def57..8b64f88b586b 100644 --- a/application/Config/Logger.php +++ b/application/Config/Logger.php @@ -18,21 +18,21 @@ class Logger extends BaseConfig | 2 = Alert Messages - Action Must Be Taken Immediately | 3 = Critical Messages - Application component unavailable, unexpected exception. | 4 = Runtime Errors - Don't need immediate action, but should be monitored. - | 5 = Debug - Detailed debug information. - | 6 = Warnings - Exceptional occurrences that are not errors. - | 7 = Notices - Normal but significant events. - | 8 = Info - Interesting events, like user logging in, etc. + | 5 = Warnings - Exceptional occurrences that are not errors. + | 6 = Notices - Normal but significant events. + | 7 = Info - Interesting events, like user logging in, etc. + | 8 = Debug - Detailed debug information. | 9 = All Messages | | You can also pass an array with threshold levels to show individual error types | - | array(1, 2, 3, 5) = Emergency, Alert, Critical, and Debug messages + | array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages | | For a live site you'll usually enable Critical or higher (3) to be logged otherwise | your log files will fill up very fast. | */ - public $threshold = 0; + public $threshold = 3; /* |-------------------------------------------------------------------------- diff --git a/application/Config/Migrations.php b/application/Config/Migrations.php index 37f5dccd4833..c746a22cba1b 100644 --- a/application/Config/Migrations.php +++ b/application/Config/Migrations.php @@ -60,16 +60,4 @@ class Migrations extends BaseConfig */ public $currentVersion = 0; - /* - |-------------------------------------------------------------------------- - | Migrations Path - |-------------------------------------------------------------------------- - | - | Path to your migrations folder. - | Typically, it will be within your application path. - | Also, writing permission is required within the migrations path. - | - */ - public $path = APPPATH.'Database/Migrations/'; - } diff --git a/application/Config/Mimes.php b/application/Config/Mimes.php index 5dd958644519..25a9224f035c 100644 --- a/application/Config/Mimes.php +++ b/application/Config/Mimes.php @@ -280,7 +280,7 @@ class Mimes /** * Attempts to determine the best mime type for the given file extension. * - * @param string $ext + * @param string $extension * * @return string|null The mime type found, or none if unable to determine. */ diff --git a/application/Config/Modules.php b/application/Config/Modules.php new file mode 100644 index 000000000000..62069b34f91a --- /dev/null +++ b/application/Config/Modules.php @@ -0,0 +1,54 @@ +enabled) return false; + + $alias = strtolower($alias); + + return in_array($alias, $this->activeExplorers); + } +} diff --git a/application/Config/Pager.php b/application/Config/Pager.php new file mode 100644 index 000000000000..bfb8ec554e4b --- /dev/null +++ b/application/Config/Pager.php @@ -0,0 +1,34 @@ + 'CodeIgniter\Pager\Views\default_full', + 'default_simple' => 'CodeIgniter\Pager\Views\default_simple' + ]; + + /* + |-------------------------------------------------------------------------- + | Items Per Page + |-------------------------------------------------------------------------- + | + | The default number of results shown in a single page. + | + */ + public $perPage = 20; +} diff --git a/application/Config/Paths.php b/application/Config/Paths.php new file mode 100644 index 000000000000..f679dca50da8 --- /dev/null +++ b/application/Config/Paths.php @@ -0,0 +1,78 @@ +setDefaultNamespace(''); +$routes->setDefaultNamespace('App\Controllers'); $routes->setDefaultController('Home'); $routes->setDefaultMethod('index'); $routes->setTranslateURIDashes(false); diff --git a/application/Config/Services.php b/application/Config/Services.php index 9df755ef2b0e..58a93a0a4e5e 100644 --- a/application/Config/Services.php +++ b/application/Config/Services.php @@ -1,8 +1,9 @@ sessionDriver; - $driver = new $driverName($config); - $driver->setLogger($logger); - - $session = new \CodeIgniter\Session\Session($driver, $config); - $session->setLogger($logger); - - return $session; - } - - //-------------------------------------------------------------------- - - /** - * The Timer class provides a simple way to Benchmark portions of your - * application. - */ - public static function timer($getShared = true) - { - if ($getShared) - { - return self::getSharedInstance('timer'); - } - - return new \CodeIgniter\Debug\Timer(); - } - - //-------------------------------------------------------------------- - - public static function toolbar(App $config = null, $getShared = true) - { - if ($getShared) - { - return self::getSharedInstance('toolbar', $config); - } - - if (! is_object($config)) - { - $config = new App(); - } - - return new \CodeIgniter\Debug\Toolbar($config); - } - - //-------------------------------------------------------------------- - - /** - * The URI class provides a way to model and manipulate URIs. - */ - public static function uri($uri = null, $getShared = true) - { - if ($getShared) - { - return self::getSharedInstance('uri', $uri); - } - - return new \CodeIgniter\HTTP\URI($uri); - } - - //-------------------------------------------------------------------- - - - //-------------------------------------------------------------------- - // Utility Methods - DO NOT EDIT - //-------------------------------------------------------------------- - - /** - * Returns a shared instance of any of the class' services. - * - * $key must be a name matching a service. - * - * @param string $key - */ - protected static function getSharedInstance(string $key, ...$params) - { - if (! isset(static::$instances[$key])) - { - // Make sure $getShared is false - array_push($params, false); - - static::$instances[$key] = self::$key(...$params); - } - - return static::$instances[$key]; - } - - //-------------------------------------------------------------------- - - /** - * Provides the ability to perform case-insensitive calling of service - * names. - * - * @param string $name - * @param array $arguments - */ - public static function __callStatic(string $name, array $arguments) - { - $name = strtolower($name); - - if (method_exists(__CLASS__, $name)) - { - return Services::$name(...$arguments); - } - } - //-------------------------------------------------------------------- +// public static function example($getShared = true) +// { +// if ($getShared) +// { +// return self::getSharedInstance('example'); +// } +// +// return new \CodeIgniter\Example(); +// } + + public static function honeypot(BaseConfig $config = null, $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('honeypot', $config); + } + + if (is_null($config)) + { + $config = new \Config\Honeypot(); + } + + return new \CodeIgniter\Honeypot\Honeypot($config); + } } diff --git a/application/Config/UserAgents.php b/application/Config/UserAgents.php new file mode 100644 index 000000000000..f016a6e374d3 --- /dev/null +++ b/application/Config/UserAgents.php @@ -0,0 +1,219 @@ + 'Windows 10', + 'windows nt 6.3' => 'Windows 8.1', + 'windows nt 6.2' => 'Windows 8', + 'windows nt 6.1' => 'Windows 7', + 'windows nt 6.0' => 'Windows Vista', + 'windows nt 5.2' => 'Windows 2003', + 'windows nt 5.1' => 'Windows XP', + 'windows nt 5.0' => 'Windows 2000', + 'windows nt 4.0' => 'Windows NT 4.0', + 'winnt4.0' => 'Windows NT 4.0', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows phone' => 'Windows Phone', + 'windows' => 'Unknown Windows OS', + 'android' => 'Android', + 'blackberry' => 'BlackBerry', + 'iphone' => 'iOS', + 'ipad' => 'iOS', + 'ipod' => 'iOS', + 'os x' => 'Mac OS X', + 'ppc mac' => 'Power PC Mac', + 'freebsd' => 'FreeBSD', + 'ppc' => 'Macintosh', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'sunos' => 'Sun Solaris', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'gnu' => 'GNU/Linux', + 'unix' => 'Unknown Unix OS', + 'symbian' => 'Symbian OS', + ]; + + + // The order of this array should NOT be changed. Many browsers return + // multiple browser types so we want to identify the sub-type first. + public $browsers = [ + 'OPR' => 'Opera', + 'Flock' => 'Flock', + 'Edge' => 'Spartan', + 'Chrome' => 'Chrome', + // Opera 10+ always reports Opera/9.80 and appends Version/ to the user agent string + 'Opera.*?Version' => 'Opera', + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Trident.* rv' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Safari' => 'Safari', + 'Mozilla' => 'Mozilla', + 'Konqueror' => 'Konqueror', + 'icab' => 'iCab', + 'Lynx' => 'Lynx', + 'Links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', + 'Maxthon' => 'Maxthon', + 'Ubuntu' => 'Ubuntu Web Browser', + 'Vivaldi' => 'Vivaldi', + ]; + + public $mobiles = [ + // legacy array, old values commented out + 'mobileexplorer' => 'Mobile Explorer', + // 'openwave' => 'Open Wave', + // 'opera mini' => 'Opera Mini', + // 'operamini' => 'Opera Mini', + // 'elaine' => 'Palm', + 'palmsource' => 'Palm', + // 'digital paths' => 'Palm', + // 'avantgo' => 'Avantgo', + // 'xiino' => 'Xiino', + 'palmscape' => 'Palmscape', + // 'nokia' => 'Nokia', + // 'ericsson' => 'Ericsson', + // 'blackberry' => 'BlackBerry', + // 'motorola' => 'Motorola' + + // Phones and Manufacturers + 'motorola' => 'Motorola', + 'nokia' => 'Nokia', + 'palm' => 'Palm', + 'iphone' => 'Apple iPhone', + 'ipad' => 'iPad', + 'ipod' => 'Apple iPod Touch', + 'sony' => 'Sony Ericsson', + 'ericsson' => 'Sony Ericsson', + 'blackberry' => 'BlackBerry', + 'cocoon' => 'O2 Cocoon', + 'blazer' => 'Treo', + 'lg' => 'LG', + 'amoi' => 'Amoi', + 'xda' => 'XDA', + 'mda' => 'MDA', + 'vario' => 'Vario', + 'htc' => 'HTC', + 'samsung' => 'Samsung', + 'sharp' => 'Sharp', + 'sie-' => 'Siemens', + 'alcatel' => 'Alcatel', + 'benq' => 'BenQ', + 'ipaq' => 'HP iPaq', + 'mot-' => 'Motorola', + 'playstation portable' => 'PlayStation Portable', + 'playstation 3' => 'PlayStation 3', + 'playstation vita' => 'PlayStation Vita', + 'hiptop' => 'Danger Hiptop', + 'nec-' => 'NEC', + 'panasonic' => 'Panasonic', + 'philips' => 'Philips', + 'sagem' => 'Sagem', + 'sanyo' => 'Sanyo', + 'spv' => 'SPV', + 'zte' => 'ZTE', + 'sendo' => 'Sendo', + 'nintendo dsi' => 'Nintendo DSi', + 'nintendo ds' => 'Nintendo DS', + 'nintendo 3ds' => 'Nintendo 3DS', + 'wii' => 'Nintendo Wii', + 'open web' => 'Open Web', + 'openweb' => 'OpenWeb', + + // Operating Systems + 'android' => 'Android', + 'symbian' => 'Symbian', + 'SymbianOS' => 'SymbianOS', + 'elaine' => 'Palm', + 'series60' => 'Symbian S60', + 'windows ce' => 'Windows CE', + + // Browsers + 'obigo' => 'Obigo', + 'netfront' => 'Netfront Browser', + 'openwave' => 'Openwave Browser', + 'mobilexplorer' => 'Mobile Explorer', + 'operamini' => 'Opera Mini', + 'opera mini' => 'Opera Mini', + 'opera mobi' => 'Opera Mobile', + 'fennec' => 'Firefox Mobile', + + // Other + 'digital paths' => 'Digital Paths', + 'avantgo' => 'AvantGo', + 'xiino' => 'Xiino', + 'novarra' => 'Novarra Transcoder', + 'vodafone' => 'Vodafone', + 'docomo' => 'NTT DoCoMo', + 'o2' => 'O2', + + // Fallback + 'mobile' => 'Generic Mobile', + 'wireless' => 'Generic Mobile', + 'j2me' => 'Generic Mobile', + 'midp' => 'Generic Mobile', + 'cldc' => 'Generic Mobile', + 'up.link' => 'Generic Mobile', + 'up.browser' => 'Generic Mobile', + 'smartphone' => 'Generic Mobile', + 'cellphone' => 'Generic Mobile', + ]; + + // There are hundreds of bots but these are the most common. + public $robots = [ + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'baiduspider' => 'Baiduspider', + 'bingbot' => 'Bing', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'ask jeeves' => 'Ask Jeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', + 'yandex' => 'YandexBot', + 'mediapartners-google' => 'MediaPartners Google', + 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', + 'adsbot-google' => 'AdsBot Google', + 'feedfetcher-google' => 'Feedfetcher Google', + 'curious george' => 'Curious George', + 'ia_archiver' => 'Alexa Crawler', + 'MJ12bot' => 'Majestic-12', + 'Uptimebot' => 'Uptimebot', + ]; +} diff --git a/application/Config/Validation.php b/application/Config/Validation.php new file mode 100644 index 000000000000..1b45f4e90624 --- /dev/null +++ b/application/Config/Validation.php @@ -0,0 +1,36 @@ + 'CodeIgniter\Validation\Views\list', + 'single' => 'CodeIgniter\Validation\Views\single' + ]; + + //-------------------------------------------------------------------- + // Rules + //-------------------------------------------------------------------- +} diff --git a/application/Config/View.php b/application/Config/View.php new file mode 100644 index 000000000000..cacb57a97704 --- /dev/null +++ b/application/Config/View.php @@ -0,0 +1,34 @@ +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/application/Filters/DebugToolbar.php b/application/Filters/DebugToolbar.php new file mode 100644 index 000000000000..d28b45554a19 --- /dev/null +++ b/application/Filters/DebugToolbar.php @@ -0,0 +1,92 @@ +getPerformanceStats(); + $data = $toolbar->run( + $stats['startTime'], + $stats['totalTime'], + $request, + $response + ); + + helper('filesystem'); + + // Updated to time() so we can get history + $time = time(); + + if (! is_dir(WRITEPATH.'debugbar')) + { + mkdir(WRITEPATH.'debugbar', 0777); + } + + write_file(WRITEPATH .'debugbar/'.'debugbar_' . $time, $data, 'w+'); + + $format = $response->getHeaderLine('content-type'); + + // Non-HTML formats should not include the debugbar + // then we send headers saying where to find the debug data + // for this response + if ($request->isAJAX() || strpos($format, 'html') === false) + { + return $response->setHeader('Debugbar-Time', (string)$time) + ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}")) + ->getBody(); + } + + $script = PHP_EOL + . '' + . '' + . '' + . PHP_EOL; + + if (strpos($response->getBody(), '') !== false) + { + return $response->setBody(str_replace('', $script . '', + $response->getBody())); + } + + return $response->appendBody($script); + } + } + + //-------------------------------------------------------------------- +} diff --git a/application/Filters/Honeypot.php b/application/Filters/Honeypot.php new file mode 100644 index 000000000000..9e1e62d3318a --- /dev/null +++ b/application/Filters/Honeypot.php @@ -0,0 +1,51 @@ +hasContent($request)) + { + throw HoneypotException::isBot(); + } + + } + + /** + * Checks if Honeypot field is empty, if so + * then the requester is a bot,show a blank + * page + * + * @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request + * @param ResponseInterface|\CodeIgniter\HTTP\Response $response + * @return mixed + */ + + public function after (RequestInterface $request, ResponseInterface $response) + { + + $honeypot = Services::honeypot(new \Config\Honeypot()); + $honeypot->attachHoneypot($response); + } +} diff --git a/application/Filters/Throttle.php b/application/Filters/Throttle.php new file mode 100644 index 000000000000..02dda6032495 --- /dev/null +++ b/application/Filters/Throttle.php @@ -0,0 +1,46 @@ +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/application/Helpers/.gitkeep b/application/Helpers/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/application/Views/errors/cli/error_404.php b/application/Views/errors/cli/error_404.php index 28a39b3cafdf..26d4b42bff7c 100644 --- a/application/Views/errors/cli/error_404.php +++ b/application/Views/errors/cli/error_404.php @@ -1,8 +1,8 @@ - + Sorry! Cannot seem to find the page you were looking for. diff --git a/application/Views/errors/html/error_exception.php b/application/Views/errors/html/error_exception.php index 5bd77ba9f4e3..978e92c3bcf0 100644 --- a/application/Views/errors/html/error_exception.php +++ b/application/Views/errors/html/error_exception.php @@ -21,7 +21,7 @@

getCode() ? ' #'.$exception->getCode() : '') ?>

- getMessage(), ENT_SUBSTITUTE) ?> + getMessage() ?> getMessage())) ?>" rel="noreferrer" target="_blank">search →

@@ -86,12 +86,15 @@
- $value) : ?> - getParameters(); - ?> + getParameters(); + } + foreach ($row['args'] as $key => $value) : ?> @@ -142,7 +145,7 @@ - + @@ -373,7 +379,7 @@

Displayed at — PHP: — - CodeIgniter: + CodeIgniter:

diff --git a/application/Views/errors/html/production.php b/application/Views/errors/html/production.php index bb9bfa5ca072..b912a0152c44 100644 --- a/application/Views/errors/html/production.php +++ b/application/Views/errors/html/production.php @@ -1,12 +1,12 @@ - + - Whoops! + Whoops! - diff --git a/application/Views/form.php b/application/Views/form.php new file mode 100644 index 000000000000..5f7ad8ff97e4 --- /dev/null +++ b/application/Views/form.php @@ -0,0 +1,9 @@ +


+ + + + + + + + \ No newline at end of file diff --git a/application/Views/welcome_message.php b/application/Views/welcome_message.php index 1aaaec2d1b69..3bc26d89c9ce 100644 --- a/application/Views/welcome_message.php +++ b/application/Views/welcome_message.php @@ -27,7 +27,7 @@ } h1 { font-weight: lighter; - letter-spacing: 0.8; + letter-spacing: 0.8rem; font-size: 3rem; margin-top: 145px; margin-bottom: 0; @@ -99,7 +99,7 @@ -174 -24 -14 -43 -26 -43 -28 0 -2 24 4 53 14 241 83 427 271 482 486 19 76 19 202 -1 285 -35 152 -146 305 -299 412 l-70 49 -6 -33 c-8 -48 -26 -76 -59 -93 -45 -23 -103 -19 -138 10 -67 57 -78 146 -37 305 30 116 32 206 5 291 -27 - 89 -104 206 -162 247 -17 13 -18 12 -11 -15z"/> + 89 -104 206 -162 247 -17 13 -18 12 -11 -15z"> @@ -123,14 +123,14 @@ -

If you are exploring CodeIgniter for the very first time, you - should start by reading the (in progress) - User Guide.

+

If you are exploring CodeIgniter for the very first time, you + should start by reading the (in progress) + User Guide.

diff --git a/composer.json b/composer.json index 6726aa56c195..1fb81a127063 100644 --- a/composer.json +++ b/composer.json @@ -1,32 +1,35 @@ { - "description": "The CodeIgniter framework v4", - "name": "codeigniter/framework", - "type": "project", - "homepage": "https://codeigniter.com", - "license": "MIT", - "support": { - "forum": "http://forum.codeigniter.com/", - "wiki": "https://github.com/bcit-ci/CodeIgniter4/wiki", - "irc": "irc://irc.freenode.net/codeigniter", - "source": "https://github.com/bcit-ci/CodeIgniter4" - }, - "autoload": { - "psr-4": { - "CodeIgniter\\": "system/" - } - }, - "require": { - "php": ">=7.0", - "zendframework/zend-escaper": "^2.5" - }, - "require-dev": { - "phpunit/phpunit": "5.3.*", - "phpdocumentor/phpdocumentor": "^2.9" - }, - "scripts": { - "post-update-cmd": [ - "composer dump-autoload", - "CodeIgniter\\ComposerScripts::postUpdate" - ] - } + "description": "The CodeIgniter framework v4", + "name": "codeigniter4/framework", + "type": "project", + "homepage": "https://codeigniter.com", + "license": "MIT", + "support": { + "forum": "http://forum.codeigniter.com/", + "slack": "https://codeigniterchat.slack.com", + "source": "https://github.com/bcit-ci/CodeIgniter4" + }, + "autoload": { + "psr-4": { + "CodeIgniter\\": "system/", + "Psr\\Log\\": "system/ThirdParty/PSR/Log/" + } + }, + "require": { + "php": ">=7.1", + "zendframework/zend-escaper": "^2.5", + "kint-php/kint": "^2.1", + "ext-intl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7.0", + "mikey179/vfsStream": "1.6.*", + "codeigniter4/codeigniter4-standard": "^1.0" + }, + "scripts": { + "post-update-cmd": [ + "composer dump-autoload", + "CodeIgniter\\ComposerScripts::postUpdate" + ] + } } diff --git a/contributing.md b/contributing.md index 352f2eebadf8..6f69c39f0012 100644 --- a/contributing.md +++ b/contributing.md @@ -1,13 +1,11 @@ # Contributing to CodeIgniter4 -## Contributions (not) +## Contributions -We are not accepting contributions from the public until a stable enough base has been formed, and our plans fleshed out and things settle down a little bit. -At that point, we will welcome your comments and help creating the best framework for our community. - -Once the repository is opened to the community, we expect all contributions to conform to our style guide, be commented (inside the PHP source files), -be documented (in the user guide), and unit tested (in the test folder). +We expect all contributions to conform to our style guide, be commented (inside the PHP source files), +be documented (in the user guide), and unit tested (in the test folder). +There is a [Contributing to CodeIgniter](./contributing/index.rst) section in the repository which describes the contribution process; this page is an overview. ## Issues @@ -17,7 +15,7 @@ Issues are a quick way to point out a bug. If you find a bug or documentation er 2. The issue has already been fixed (check the develop branch, or look for closed Issues) 3. Is it something really obvious that you can fix yourself? -Reporting issues is helpful but an even better approach is to send a Pull Request, which is done by "Forking" the main repository and committing to your own copy. This will require you to use the version control system called Git. +Reporting issues is helpful but an even [better approach](https://bcit-ci.github.io/CodeIgniter4/contributing/workflow.html) is to send a Pull Request, which is done by "Forking" the main repository and committing to your own copy. This will require you to use the version control system called Git. ## Guidelines @@ -28,7 +26,7 @@ for us to maintain quality of the code-base. ### PHP Style -All code must meet the Style Guide, which will be an early part of the User Guide. +All code must meet the [Style Guide](https://bcit-ci.github.io/CodeIgniter4/contributing/styleguide.html). This makes certain that all code is the same format as the existing code and means it will be as readable as possible. ### Documentation @@ -37,7 +35,7 @@ If you change anything that requires a change to documentation then you will nee ### Compatibility -CodeIgniter4 requires PHP 7. +CodeIgniter4 requires PHP 7.1. ### Branching @@ -48,39 +46,20 @@ One thing at a time: A pull request should only contain one change. That does no ### Signing -You must sign your work, certifying that you either wrote the work or otherwise have the right to pass it on to an open source project. git makes this trivial as you merely have to use `--signoff` on your commits to your CodeIgniter fork. - -`git commit --signoff` - -or simply - -`git commit -s` - -This will sign your commits with the information setup in your git config, e.g. - -`Signed-off-by: John Q Public ` - -If you are using [Tower](http://www.git-tower.com/) there is a "Sign-Off" checkbox in the commit window. You could even alias git commit to use the `-s` flag so you don’t have to think about it. - -By signing your work in this manner, you certify to a "Developer's Certificate of Origin". The current version of this certificate is in the `DCO.txt` file in the root of this repository. - +You must [GPG-sign](https://bcit-ci.github.io/CodeIgniter4/contributing/signing.html) your work, certifying that you either wrote the work or otherwise have the right to pass it on to an open source project. This is *not* just a "signed-off-by" commit, but instead a digitally signed one. ## How-to Guide -There are two ways to make changes, the easy way and the hard way. Either way you will need to [create a GitHub account](https://github.com/signup/free). - -Easy way GitHub allows in-line editing of files for making simple typo changes and quick-fixes. This is not the best way as you are unable to test the code works. If you do this you could be introducing syntax errors, etc, but for a Git-phobic user this is good for a quick-fix. - -Hard way The best way to contribute is to "clone" your fork of CodeIgniter to your development area. That sounds like some jargon, but "forking" on GitHub means "making a copy of that repo to your account" and "cloning" means "copying that code to your environment so you can work on it". +The best way to contribute is to fork the CodeIgniter4 repository, and "clone" that to your development area. That sounds like some jargon, but "forking" on GitHub means "making a copy of that repo to your account" and "cloning" means "copying that code to your environment so you can work on it". 1. Set up Git (Windows, Mac & Linux) -2. Go to the CodeIgniter repo -3. Fork it -4. Clone your CodeIgniter repo: git@github.com:/CodeIgniter.git -5. Checkout the "develop" branch At this point you are ready to start making changes. +2. Go to the CodeIgniter4 repo +3. Fork it (to your Github account) +4. Clone your CodeIgniter repo: git@github.com:\/CodeIgniter4.git +5. Create a new branch in your project for each set of changes you want to make. 6. Fix existing bugs on the Issue tracker after taking a look to see nobody else is working on them. -7. Commit the files -8. Push your develop branch to your fork +7. Commit the changed files in your contribution branch +8. Push your contribution branch to your fork 9. Send a pull request [http://help.github.com/send-pull-requests/](http://help.github.com/send-pull-requests/) The codebase maintainers will now be alerted about the change and at least one of the team will respond. If your change fails to meet the guidelines it will be bounced, or feedback will be provided to help you improve it. @@ -97,4 +76,4 @@ If you are using command-line you can do the following: 2. `git pull codeigniter develop` 3. `git push origin develop` -Now your fork is up to date. This should be done regularly, or before you send a pull request at least. \ No newline at end of file +Now your fork is up to date. This should be done regularly, or before you send a pull request at least. diff --git a/user_guide_src/source/DCO.rst b/contributing/DCO.rst similarity index 100% rename from user_guide_src/source/DCO.rst rename to contributing/DCO.rst diff --git a/user_guide_src/source/contributing/ELDocs.tmbundle.zip b/contributing/ELDocs.tmbundle.zip similarity index 100% rename from user_guide_src/source/contributing/ELDocs.tmbundle.zip rename to contributing/ELDocs.tmbundle.zip diff --git a/user_guide_src/source/contributing/documentation.rst b/contributing/documentation.rst similarity index 94% rename from user_guide_src/source/contributing/documentation.rst rename to contributing/documentation.rst index d47d90a3daef..3baaf7f001b2 100644 --- a/user_guide_src/source/contributing/documentation.rst +++ b/contributing/documentation.rst @@ -18,7 +18,7 @@ It is created automatically by inserting the following: .. raw:: html -
+
.. contents:: :local: @@ -27,7 +27,7 @@ It is created automatically by inserting the following:
-The
that is inserted as raw HTML is a hook for the documentation's +The
that is inserted as raw HTML is a event for the documentation's JavaScript to dynamically add links to any function and method definitions contained in the current page. @@ -42,14 +42,12 @@ Pygments, so that code blocks can be properly highlighted. .. code-block:: bash - easy_install "sphinx==1.2.3" + easy_install "sphinx==1.4.5" easy_install sphinxcontrib-phpdomain Then follow the directions in the README file in the :samp:`cilexer` folder inside the documentation repository to install the CI Lexer. - - ***************************************** Page and Section Headings and Subheadings ***************************************** diff --git a/user_guide_src/source/contributing/guidelines.rst b/contributing/guidelines.rst similarity index 80% rename from user_guide_src/source/contributing/guidelines.rst rename to contributing/guidelines.rst index 8da9f901181f..78870d660732 100644 --- a/user_guide_src/source/contributing/guidelines.rst +++ b/contributing/guidelines.rst @@ -11,10 +11,10 @@ PHP Style ========= All code must conform to our `Style Guide -<./styleguide.html>`_, which is +<./styleguide.rst>`_, which is essentially the `Allman indent style `_, with -elaboration on naming and readable operators. +elaboration on naming and readable operators. This makes certain that all code is the same format as the existing code and means it will be as readable as possible. @@ -33,8 +33,8 @@ In the CodeIgniter project, there is a ``tests`` folder, with a structure that parallels that of ``system``. The normal practice would be to have a unit test class for each of the classes -in ``system``, named appropriately. For instance, the ``BananaTest`` -class would test the ``Banana`` class. There will be occasions when +in ``system``, named appropriately. For instance, the ``BananaTest`` +class would test the ``Banana`` class. There will be occasions when it is more convenient to have separate classes to test different functionality of a single CodeIgniter component. @@ -44,16 +44,12 @@ PHPdoc Comments =============== Source code should be commented using PHPdoc comments blocks. -Thie means implementation comments to explain potentially confusing sections +Thie means implementation comments to explain potentially confusing sections of code, and documentation comments before each public or protected class/interface/trait, method and variable. See the `phpDocumentor website `_ for more information. -We use ``phpDocumentor2`` to generate the API documentation for the -framework, with configuration details in ``phpdoc.dist.xml`` in the project -root. - Documentation ============= @@ -70,20 +66,14 @@ 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. -See the `CodeIgniter 3 change log -`_ +See the `CodeIgniter 3 change log +`_ for an example. PHP Compatibility ================= -CodeIgniter4 requires PHP 7. - -See the `CodeIgniter4-developer-setup `_ -repository for tips on setting this up on your system. - -That repository also contains tips for configuring your IDE or editor to work -better with PHP7 and CodeIgniter4. +CodeIgniter4 requires PHP 7.1. Backwards Compatibility ======================= @@ -91,7 +81,7 @@ Backwards Compatibility Generally, we aim to maintain backwards compatibility between minor versions of the framework. Any changes that break compatibility need a good reason to do so, and need to be pointed out in the -`Upgrading <../installation/upgrading.html>`_ guide. +`Upgrading `_ guide. CodeIgniter4 itself represents a significant backwards compatibility break with earlier versions of the framework. @@ -102,6 +92,7 @@ Mergeability Your PRs need to be mergeable before they will be considered. We suggest that you synchronize your repository's ``develop`` branch with -that in the main repository before submitting a PR. +that in the main repository, and then your feature branch and +your develop branch, before submitting a PR. You will need to resolve any merge conflicts introduced by changes incorporated since you started working on your contribution. diff --git a/user_guide_src/source/contributing/index.rst b/contributing/index.rst similarity index 83% rename from user_guide_src/source/contributing/index.rst rename to contributing/index.rst index d3d72890284d..13ade047248d 100644 --- a/user_guide_src/source/contributing/index.rst +++ b/contributing/index.rst @@ -2,21 +2,17 @@ Contributing to CodeIgniter ########################### -.. toctree:: - :titlesonly: - - guidelines - workflow - signing - roadmap - internals - documentation - PHP Style Guide - ../DCO +- `Contribution guidelines <./guidelines.rst>`_ +- `Contribution workflow <./workflow.rst>`_ +- `Contribution signing <./signing.rst>`_ +- `Framework internals <./internals.rst>`_ +- `CodeIgniter documentation <./documentation.rst>`_ +- `PHP Style Guide <./styleguide.rst>`_ +- `Developer's Certificate of Origin <../DCO.txt>`_ CodeIgniter is a community driven project and accepts contributions of code and documentation from the community. These contributions are made in the form -of Issues or `Pull Requests `_ +of Issues or `Pull Requests `_ on the `CodeIgniter4 repository `_ on GitHub. Issues are a quick way to point out a bug. If you find a bug or documentation @@ -52,7 +48,7 @@ Please *don't* disclose it publicly, but e-mail us at security@codeigniter.com, or report it via our page on `HackerOne `_. If you've found a critical vulnerability, we'd be happy to credit you in our -`ChangeLog <../changelog.html>`_. +`ChangeLog `_. **************************** Tips for a Good Issue Report @@ -62,7 +58,7 @@ Use a descriptive subject line (eg parser library chokes on commas) rather than Address a single issue in a report. -Identify the CodeIgniter version (eg 3.0-develop) and the component if you know it (eg. parser library) +Identify the CodeIgniter version (eg 4.0.1) and the component if you know it (eg. parser library) Explain what you expected to happen, and what did happen. Include error messages and stacktrace, if any. diff --git a/user_guide_src/source/contributing/internals.rst b/contributing/internals.rst similarity index 98% rename from user_guide_src/source/contributing/internals.rst rename to contributing/internals.rst index db48ffa0a56e..687ac77ac31e 100644 --- a/user_guide_src/source/contributing/internals.rst +++ b/contributing/internals.rst @@ -62,7 +62,7 @@ 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 -**system/Router/Router.php** respectively. +**system/Router/Router.php** respectively. Interfaces ---------- @@ -71,7 +71,7 @@ Most base classes should have an interface defined for them. At the very least t 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``. -The Router package mentioned above includes the +The Router package mentioned above includes the ``CodeIgniter\Router\RouterCollectionInterface`` and ``CodeIgniter\Router\RouterInterface`` interfaces to provide the abstractions for the two classes in the package. diff --git a/user_guide_src/source/contributing/signing.rst b/contributing/signing.rst similarity index 65% rename from user_guide_src/source/contributing/signing.rst rename to contributing/signing.rst index dfe64ddf4184..ef8e2c9f27d3 100644 --- a/user_guide_src/source/contributing/signing.rst +++ b/contributing/signing.rst @@ -11,7 +11,7 @@ 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 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 +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. @@ -31,51 +31,10 @@ contributions be securely signed. Read below to find out how to sign your commits :) -Basic Signing -============= -You must sign your work, certifying that you either wrote the work or -otherwise have the right to pass it on to an open source project. - -Setup your commit message user name and email address. See -`Setting your email in Git `_ -to set these up globally or for a single repository. - -.. code-block:: bash - - git config --global user.email "john.public@example.com" - git config --global user.name "John Q Public" - -Once in place, you merely have to use `--signoff` on your commits to your -CodeIgniter fork. - -.. code-block:: bash - - git commit --signoff - -or simply - -.. code-block:: bash - - git commit -s - -This will sign your commits with the information setup in your git config, e.g. - - Signed-off-by: John Q Public - -Your IDE may have a "Sign-Off" checkbox in the commit window, -or even an option to automatically sign-off all commits you make. You -could even alias git commit to use the -s flag so you don’t have to think about -it. - -By signing your work in this manner, you certify to a "Developer's Certificate -of Origin". The current version of this certificate is in the :doc:`/DCO` file -in the root of this documentation. - Secure Signing ============== -The "basic signing" described above cannot be verified, though it is a great start. -To verify your commits, you will need to +To verify your commits, you will need to setup a GPG key, and attach it to your github account. See the `git tools `_ @@ -85,18 +44,18 @@ page for directions on doing this. The complete story is part of The basic steps are - `generate your GPG key `_, and copy the ASCII representation of it. -- `Add your GPG key to your Github account `_. +- `Add your GPG key to your Github account `_. - `Tell Git `_ about your GPG key. - `Set default signing `_ to have all of your commits securely signed automatically. - Provide your GPG key passphrase, as prompted, when you do a commit. -Depending on your IDE, you may have to do your Git commits from your Git bash shell +Depending on your IDE, you may have to do your Git commits from your Git bash shell to use the **-S** option to force the secure signing. Commit Messages =============== -Regardless of how you sign a commit, commit messages are important too. +Regardless of how you sign a commit, commit messages are important too. They communicate the intent of a specific change, concisely. They make it easier to review code, and to find out why a change was made if the code history is examined later. @@ -105,5 +64,4 @@ The audience for your commit messages will be the codebase maintainers, any code reviewers, and debuggers trying to figure out when a bug might have been introduced. -Do try to make your commit messages meaningful. -. \ No newline at end of file +Make your commit messages meaningful. diff --git a/user_guide_src/source/contributing/styleguide.rst b/contributing/styleguide.rst similarity index 77% rename from user_guide_src/source/contributing/styleguide.rst rename to contributing/styleguide.rst index b88f023a1d8a..21d4d99e93ed 100644 --- a/user_guide_src/source/contributing/styleguide.rst +++ b/contributing/styleguide.rst @@ -20,14 +20,9 @@ PSR-2 is PHP-FIG's Coding Style Guide. We do not claim conformance with it, although there are a lot of similarities. The differences will be pointed out below. -.. note:: See the - `CodeIgniter4-developer-setup `_ - repository for tips on configuring your IDE or editor to help you conform - to the style guide.. - -*The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to -be interpreted as described in `RFC 2119 `_.* +be interpreted as described in `RFC 2119 `_. *Note: When used below, the term "class" refers to all kinds of classes, interfaces and traits.* @@ -52,13 +47,13 @@ Structure - `system/Debug/CustomExceptions` contains a number of CodeIgniter exceptions and errors, that we want to use for a consistent - experience across applications. - If we stick with the purist route, then each of the 13+/- custom - exceptions would require an additional file, which would have a - performance impact at times. + experience across applications. + If we stick with the purist route, then each of the 13+/- custom + exceptions would require an additional file, which would have a + performance impact at times. - `system/HTTP/Response` provides a RedirectException, used with the Response class. - - `system/Router/Router` similarly provides a RedirectException, used with + - `system/Router/Router` similarly provides a RedirectException, used with the Router class. - Files SHOULD either declare symbols (i.e. classes, functions, constants) @@ -69,20 +64,25 @@ Naming - File names MUST end with a ".php" name extension and MUST NOT have multiple name extensions. -- Files declaring classes MUST have names exactly matching the classes - that they declare (obviously excluding the ".php" name extension). +- Files declaring classes, interfaces or traits MUST have names exactly matching + the classes that they declare (obviously excluding the ".php" name extension). - Files declaring functions SHOULD be named in *snake_case.php*. ************************************* Whitespace, indentation and alignment ************************************* -- Indentation MUST use only tabs. -- Alignment MUST use only spaces. +- Best practice: indentation SHOULD use only tabs. +- Best practice: alignment SHOULD use only spaces. +- If using tabs for anything, you MUST set the tab spacing to 4. + +This will accommodate the widest range of developer environment options, +while maintaining consistency of code appearance. -The following code block would have a single tab at the beginning of +Following the "best practice" above, +the following code block would have a single tab at the beginning of each line containing braces, and two tabs at the beginning of the -nested statements. No alignment is implied.:: +nested statements. No alignment is implied:: { $first = 1; @@ -90,18 +90,18 @@ nested statements. No alignment is implied.:: $third = 3; } -The following code block would use spaces to have the assignment +Following the "best practice" above, +the following code block would use spaces to have the assignment operators line up with each other:: { - $first = 1; - $second = 2; - $third = 3; + $first = 1; + $second = 2; + $third = 3; } - .. note:: Our indenting and alignment convention differs from PSR-2, which - uses spaces for indenting and alignment. + **only** uses spaces for both indenting and alignment. - Unnecessary whitespace characters MUST NOT be present anywhere within a script. @@ -111,6 +111,11 @@ operators line up with each other:: well as any other whitespace usage that is not functionally required or explicitly described in this document. +.. note:: With conforming tab settings, alignment spacing should + be preserved in all development environments. + A pull request that deals only with tabs or spaces for alignment + will not be favorably considered. + **** Code **** @@ -132,7 +137,7 @@ PHP tags Namespaces and classes ====================== -- Class names and namespaces SHOULD be declared in `UpperCamelCase`, +- Class names and namespaces SHOULD be declared in `UpperCamelCase`, also called `StudlyCaps`, unless another form is *functionally* required. @@ -162,7 +167,6 @@ MUST be used for each such property "x" - Methods SHOULD use type hints and return type hints - Procedural code =============== @@ -210,29 +214,54 @@ Logical Operators higher precedence:: $result = true && false; // $result is false, expected - $result = true OR false; // $result is true, evaluated as "($result = true) OR false" - $result = (true OR false); // $result is false + $result = true AND false; // $result is true, evaluated as "($result = true) AND false" + $result = (true AND false); // $result is false - The logical negation operator MUST be separated from its argument by a single space, as in **! $result** instead of **!$result** - If there is potential confusion with a logical expression, then use parentheses for clarity, as shown above. +Control Structures +================== + +- Control structures, such as **if/else** statements, **for/foreach** statements, or + **while/do** statements, MUST use a brace-surrounded block for their body + segments. + + Good control structure examples:: + + if ( $foo ) + { + $bar += $baz; + } + else + { + $baz = 'bar'; + } + + Not-acceptable control structures:: + + if ( $foo ) $bar = $oneThing + $anotherThing + $yetAnotherThing + $evenMore; + + if ( $foo ) $bar += $baz; + else $baz = 'bar'; + Other ===== -- Argument separators (comma: `,`) MUST NOT be preceeded by a whitespace +- Argument separators (comma: `,`) MUST NOT be preceded by a whitespace character and MUST be followed by a space character or a newline (LF: `\n`). -- Semi-colons (i.e. `;`) MUST NOT be preceeded by a whitespace character +- Semi-colons (i.e. `;`) MUST NOT be preceded by a whitespace character and MUST be followed by a newline (LF: `\n`). - Opening parentheses SHOULD NOT be followed by a space character. -- Closing parentheses SHOULD NOT be preceeded by a space character. +- Closing parentheses SHOULD NOT be preceded by a space character. - Opening square brackets SHOULD NOT be followed by a space character, unless when using the "short array" declaration syntax. -- Closing square backets SHOULD NOT be preceeded by a space character, +- Closing square brackets SHOULD NOT be preceded by a space character, unless when using the "short array" declaration syntax. - A curly brace SHOULD be the only printable character on a line, unless: diff --git a/user_guide_src/source/contributing/workflow.rst b/contributing/workflow.rst similarity index 96% rename from user_guide_src/source/contributing/workflow.rst rename to contributing/workflow.rst index 4f04bae9be9a..4bb483b99796 100644 --- a/user_guide_src/source/contributing/workflow.rst +++ b/contributing/workflow.rst @@ -3,7 +3,7 @@ Contribution Workflow ===================== Much of the workflow for contributing to CodeIgniter (or any project) involves -understanding how `Git `_ is used to +understanding how `Git `_ is used to manage a shared repository and contributions to it. Examples below use the Git bash shell, to be as platform neutral as possible. Your IDE may make some of these easier. @@ -46,7 +46,7 @@ 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 -repository `_, +repository `_, click the **Fork** button in the top-right of the page, and choose which account or organization of yours should contain that fork. @@ -68,15 +68,15 @@ Clone your repository, leaving a local folder for you to work with:: Synching ======== -Within your local repository, Git will have created an alias, **origin**, for the +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 -includes any other contributions that have been merged by us into the shared repo.:: +includes any other contributions that have been merged by us into the shared repo:: git remote add upstream UPSTREAM_URL Then synchronizing is done by pulling from us and pushing to you. This is normally -done locally, so that you can resolve any merge conflicts. For instance, to +done locally, so that you can resolve any merge conflicts. For instance, to synchronize **develop** branches:: git checkout develop @@ -85,9 +85,9 @@ synchronize **develop** branches:: You might get merge conflicts when you pull from upstream. It is your responsibility to resolve those locally, so that you can continue collaborating with the shared -repository. Basically, the shared repository is updated in the order that contributions -are merged into it, not in the order that they might have been submitted. -If two PRs update the same piece of code, then the first one to be merged +repository. Basically, the shared repository is updated in the order that contributions +are merged into it, not in the order that they might have been submitted. +If two PRs update the same piece of code, then the first one to be merged will take precedence, even if it causes problems for other contributions. It is a good idea to synchronize repositories when the shared one changes. @@ -95,14 +95,14 @@ It is a good idea to synchronize repositories when the shared one changes. Branching Revisited =================== -The top of this page talked about the **master** and **develop** branches. +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". 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.:: +new feature branch, based on *develop*, for a new feature you are creating:: git checkout develop git checkout -b new/mind-reader @@ -155,7 +155,7 @@ Synchronize your repository:: git checkout develop git pull upstream develop git push origin develop - + Bring your feature branch up to date:: git checkout new/mind-reader @@ -188,12 +188,12 @@ If the unit tests fail, or if there are merge conflicts, your PR will not be mergeable until 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 +That will update the PR automatically, and re-run the CI tests. You don't need to raise a new PR. If your PR does not follow our contribution guidelines, or is incomplete, the codebase maintainers will comment on it, pointing out what -needs fixing. +needs fixing. Cleanup ======= diff --git a/application/env.example b/env similarity index 71% rename from application/env.example rename to env index 73c2384fa03e..d5559450fb3f 100644 --- a/application/env.example +++ b/env @@ -20,7 +20,7 @@ # app.sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler' # app.sessionCookieName = 'ci_session' # app.sessionSavePath = NULL -# app.sessionMachIP = false +# app.sessionMatchIP = false # app.sessionTimeToUpdate = 300 # app.sessionRegenerateDestroy = false @@ -39,6 +39,22 @@ # app.CSPEnabled = false +#-------------------------------------------------------------------- +# DATABASE +#-------------------------------------------------------------------- + +# database.default.hostname = localhost +# database.default.database = ci4 +# database.default.username = root +# database.default.password = root +# database.default.DBDriver = MySQLi + +# database.tests.hostname = localhost +# database.tests.database = ci4 +# database.tests.username = root +# database.tests.password = root +# database.tests.DBDriver = MySQLi + #-------------------------------------------------------------------- # CONTENT SECURITY POLICY #-------------------------------------------------------------------- @@ -60,3 +76,12 @@ # contentsecuritypolicy.reportURI = null # contentsecuritypolicy.sandbox = false # contentsecuritypolicy.upgradeInsecureRequests = false + +#-------------------------------------------------------------------- +# HONEYPOT +#-------------------------------------------------------------------- + +# honeypot.hidden = 'true' +# honeypot.label = 'Fill This Field' +# honeypot.name = 'honeypot' +# honeypot.template = '' diff --git a/license.txt b/license.txt index 555b5e6585db..b41fa19b9db1 100644 --- a/license.txt +++ b/license.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 - 2016, British Columbia Institute of Technology +Copyright (c) 2014-2018 British Columbia Institute of Technology Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/php_errors.log b/php_errors.log new file mode 100644 index 000000000000..f246f51007b5 --- /dev/null +++ b/php_errors.log @@ -0,0 +1 @@ +[30-Jan-2018 23:57:36 America/Chicago] PHP Fatal error: Class 'z' not found in /Users/kilishan/WebSites/CodeIgniter4/tests/system/Validation/RulesTest.php on line 5 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 34aba4712b04..c3738f0a7c06 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,6 @@ + + ./tests + ./tests/system + ./tests/system - + ./tests/system/Database + + + ./tests/system/Database @@ -19,12 +27,19 @@ ./system + ./system/Debug/Toolbar/Views + ./system/Pager/Views + ./system/ThirdParty + ./system/Validation/Views + ./system/bootstrap.php + ./system/Commands/Sessions/Views/migration.tpl.php ./system/ComposerScripts.php - ./system/View/Escaper.php - ./system/View/Exception - ./system/Debug/Toolbar/View - ./system/Debug/Kint + ./system/Config/Routes.php + + + + diff --git a/public/.htaccess b/public/.htaccess index a1628e9fb81a..5df48c54885b 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -3,7 +3,7 @@ # ---------------------------------------------------------------------- # Sets the environment that CodeIgniter runs under. -# SetEnv CI_ENV development + SetEnv CI_ENVIRONMENT development # ---------------------------------------------------------------------- # UTF-8 encoding @@ -33,7 +33,7 @@ AddDefaultCharset utf-8 RewriteBase / # Redirect Trailing Slashes... - RewriteRule ^(.*)/$ /public/$1 [L,R=301] + RewriteRule ^(.*)/$ /$1 [L,R=301] # Rewrite "www.example.com -> example.com" RewriteCond %{HTTPS} !=on @@ -94,4 +94,4 @@ AddDefaultCharset utf-8 text/x-component \ text/xml - \ No newline at end of file + diff --git a/public/index.php b/public/index.php index 1d581bfa9045..3137f3304780 100644 --- a/public/index.php +++ b/public/index.php @@ -1,272 +1,37 @@ load(); -unset($env); - -/* - * ------------------------------------------------------ - * Load the framework constants - * ------------------------------------------------------ - */ -if (file_exists(APPPATH.'Config/'.ENVIRONMENT.'/Constants.php')) -{ - require_once APPPATH.'Config/'.ENVIRONMENT.'/Constants.php'; -} - -require_once(APPPATH.'Config/Constants.php'); - -/* - * ------------------------------------------------------ - * Setup the autoloader - * ------------------------------------------------------ - */ -// The autoloader isn't initialized yet, so load the file manually. -require BASEPATH.'Autoloader/Autoloader.php'; -require APPPATH.'Config/Autoload.php'; -require APPPATH.'Config/Services.php'; - -// Use Config\Services as CodeIgniter\Services -class_alias('Config\Services', 'CodeIgniter\Services'); - -// The Autoloader class only handles namespaces -// and "legacy" support. -$loader = CodeIgniter\Services::autoloader(); -$loader->initialize(new Config\Autoload()); - -// The register function will prepend -// the psr4 loader. -$loader->register(); - -/* - * ------------------------------------------------------ - * Load the global functions - * ------------------------------------------------------ - */ - -require_once BASEPATH.'Common.php'; - -/* - * ------------------------------------------------------ - * Set custom exception handling - * ------------------------------------------------------ - */ -$config = new \Config\App(); - -CodeIgniter\Services::exceptions($config, true) - ->initialize(); - -//-------------------------------------------------------------------- -// Should we use a Composer autoloader? -//-------------------------------------------------------------------- - -if ($composer_autoload = $config->composerAutoload) -{ - if ($composer_autoload === TRUE) - { - file_exists(APPPATH.'vendor/autoload.php') - ? require_once(APPPATH.'vendor/autoload.php') - : log_message('error', '$config->\'composerAutoload\' is set to TRUE but '.APPPATH.'vendor/autoload.php was not found.'); - } - elseif (file_exists($composer_autoload)) - { - require_once($composer_autoload); - } - else - { - log_message('error', 'Could not find the specified $config->\'composerAutoload\' path: '.$composer_autoload); - } -} - -/* - * -------------------------------------------------------------------- - * LOAD THE BOOTSTRAP FILE - * -------------------------------------------------------------------- - * - * And away we go... + * Now that everything is setup, it's time to actually fire + * up the engines and make this app do it's thang. */ -$codeigniter = new CodeIgniter\CodeIgniter($startMemory, $startTime, $config); -$codeigniter->run(); +$app->run(); diff --git a/spark b/spark new file mode 100755 index 000000000000..087964bfc8a9 --- /dev/null +++ b/spark @@ -0,0 +1,58 @@ +#!/usr/bin/env php +publicDirectory, '/'); + +// Path to the front controller +define('FCPATH', realpath($public).DIRECTORY_SEPARATOR); + +// Ensure the current directory is pointing to the front controller's directory +chdir('public'); + +$app = require rtrim($paths->systemDirectory,'/ ').'/bootstrap.php'; + +// Grab our Console +$console = new \CodeIgniter\CLI\Console($app); + +// We want errors to be shown when using it from the CLI. +error_reporting(-1); +ini_set('display_errors', 1); + +// Show basic information before we do anything else. +$console->showHeader(); + +// fire off the command the main framework. +$console->run(); diff --git a/stale.yml b/stale.yml new file mode 100644 index 000000000000..897cc082a10e --- /dev/null +++ b/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be automatically closed in a week if no further activity occurs. + Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php new file mode 100644 index 000000000000..426e41a76e34 --- /dev/null +++ b/system/API/ResponseTrait.php @@ -0,0 +1,370 @@ + 201, + 'deleted' => 200, + 'invalid_request' => 400, + 'unsupported_response_type' => 400, + 'invalid_scope' => 400, + 'temporarily_unavailable' => 400, + 'invalid_grant' => 400, + 'invalid_credentials' => 400, + 'invalid_refresh' => 400, + 'no_data' => 400, + 'invalid_data' => 400, + 'access_denied' => 401, + 'unauthorized' => 401, + 'invalid_client' => 401, + 'forbidden' => 403, + 'resource_not_found' => 404, + 'not_acceptable' => 406, + 'resource_exists' => 409, + 'conflict' => 409, + 'resource_gone' => 410, + 'payload_too_large' => 413, + 'unsupported_media_type' => 415, + 'too_many_requests' => 429, + 'server_error' => 500, + 'unsupported_grant_type' => 501, + 'not_implemented' => 501, + ]; + + //-------------------------------------------------------------------- + + /** + * Provides a single, simple method to return an API response, formatted + * to match the requested format, with proper content-type and status code. + * + * @param null $data + * @param int $status + * @param string $message + * + * @return mixed + */ + public function respond($data = null, int $status = null, string $message = '') + { + // If data is null and status code not provided, exit and bail + if ($data === null && $status === null) + { + $status = 404; + + // Create the output var here in case of $this->response([]); + $output = null; + } // If data is null but status provided, keep the output empty. + elseif ($data === null && is_numeric($status)) + { + $output = null; + } + else + { + $status = empty($status) ? 200 : $status; + $output = $this->format($data); + } + + return $this->response->setBody($output) + ->setStatusCode($status, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used for generic failures that no custom methods exist for. + * + * @param string|array $messages + * @param int|null $status HTTP status code + * @param string|null $code Custom, API-specific, error code + * @param string $customMessage + * + * @return mixed + */ + public function fail($messages, int $status = 400, string $code = null, string $customMessage = '') + { + if ( ! is_array($messages)) + { + $messages = ['error' => $messages]; + } + + $response = [ + 'status' => $status, + 'error' => $code === null ? $status : $code, + 'messages' => $messages, + ]; + + return $this->respond($response, $status, $customMessage); + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + // Response Helpers + //-------------------------------------------------------------------- + + /** + * Used after successfully creating a new resource. + * + * @param mixed $data Data. + * @param string $message Message. + * + * @return mixed + */ + public function respondCreated($data = null, string $message = '') + { + return $this->respond($data, $this->codes['created'], $message); + } + + //-------------------------------------------------------------------- + + /** + * Used after a resource has been successfully deleted. + * + * @param mixed $data Data. + * @param string $message Message. + * + * @return mixed + */ + public function respondDeleted($data = null, string $message = '') + { + return $this->respond($data, $this->codes['deleted'], $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when the client is either didn't send authorization information, + * or had bad authorization credentials. User is encouraged to try again + * with the proper information. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failUnauthorized(string $description = 'Unauthorized', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['unauthorized'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when access is always denied to this resource and no amount + * of trying again will help. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failForbidden(string $description = 'Forbidden', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['forbidden'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when a specified resource cannot be found. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failNotFound(string $description = 'Not Found', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['resource_not_found'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when the data provided by the client cannot be validated. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failValidationError(string $description = 'Bad Request', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['invalid_data'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Use when trying to create a new resource and it already exists. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failResourceExists(string $description = 'Conflict', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['resource_exists'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Use when a resource was previously deleted. This is different than + * Not Found, because here we know the data previously existed, but is now gone, + * where Not Found means we simply cannot find any information about it. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failResourceGone(string $description = 'Gone', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['resource_gone'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when the user has made too many requests for the resource recently. + * + * @param string $description + * @param string $code + * @param string $message + * + * @return mixed + */ + public function failTooManyRequests(string $description = 'Too Many Requests', string $code = null, string $message = '') + { + return $this->fail($description, $this->codes['too_many_requests'], $code, $message); + } + + //-------------------------------------------------------------------- + + /** + * Used when there is a server error. + * + * @param string $description The error message to show the user. + * @param string|null $code A custom, API-specific, error code. + * @param string $message A custom "reason" message to return. + * + * @return Response The value of the Response's send() method. + */ + public function failServerError(string $description = 'Internal Server Error', string $code = null, string $message = ''): Response + { + return $this->fail($description, $this->codes['server_error'], $code, $message); + } + + //-------------------------------------------------------------------- + // Utility Methods + //-------------------------------------------------------------------- + + /** + * Handles formatting a response. Currently makes some heavy assumptions + * and needs updating! :) + * + * @param null $data + * + * @return null|string + */ + protected function format($data = null) + { + // If the data is a string, there's not much we can do to it... + if (is_string($data)) + { + // The content type should be text/... and not application/... + $contentType = $this->response->getHeaderLine('Content-Type'); + $contentType = str_replace('application/json', 'text/html', $contentType); + $contentType = str_replace('application/', 'text/', $contentType); + $this->response->setContentType($contentType); + + return $data; + } + + // Determine correct response type through content negotiation + $config = new Format(); + $format = $this->request->negotiate('media', $config->supportedResponseFormats, true); + + $this->response->setContentType($format); + + // if we don't have a formatter, make one + if ( ! isset($this->formatter)) + { + // if no formatter, use the default + $this->formatter = $config->getFormatter($format); + } + + if ($format !== 'application/json') + { + // Recursively convert objects into associative arrays + // Conversion not required for JSONFormatter + $data = json_decode(json_encode($data), true); + } + + return $this->formatter->format($data); + } + +} diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 95cef3c57b93..7c060e10b30a 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -97,7 +97,7 @@ class Autoloader * Reads in the configuration array (described above) and stores * the valid parts that we'll need. * - * @param $config + * @param \Config\Autoload $config */ public function initialize(\Config\Autoload $config) { @@ -143,17 +143,15 @@ public function register() // Now prepend another loader for the files in our class map. $config = is_array($this->classmap) ? $this->classmap : []; - spl_autoload_register(function ($class) use ($config) - { + spl_autoload_register(function ($class) use ($config) { if ( ! array_key_exists($class, $config)) { return false; } include_once $config[$class]; - }, - true, // Throw exception - true // Prepend + }, true, // Throw exception + true // Prepend ); } @@ -162,12 +160,12 @@ public function register() /** * Registers a namespace with the autoloader. * - * @param $namespace - * @param $path + * @param string $namespace + * @param string $path * - * @return $this + * @return Autoloader */ - public function addNamespace($namespace, $path) + public function addNamespace(string $namespace, string $path) { if (isset($this->prefixes[$namespace])) { @@ -192,11 +190,11 @@ public function addNamespace($namespace, $path) /** * Removes a single namespace from the psr4 settings. * - * @param $namespace + * @param string $namespace * - * @return $this + * @return Autoloader */ - public function removeNamespace($namespace) + public function removeNamespace(string $namespace) { unset($this->prefixes[$namespace]); @@ -213,7 +211,7 @@ public function removeNamespace($namespace) * @return mixed The mapped file on success, or boolean false * on failure. */ - public function loadClass($class) + public function loadClass(string $class) { $class = trim($class, '\\'); $class = str_ireplace('.php', '', $class); @@ -239,7 +237,7 @@ public function loadClass($class) * * @return mixed The mapped file name on success, or boolean false on fail */ - protected function loadInNamespace($class) + protected function loadInNamespace(string $class) { if (strpos($class, '\\') === false) { @@ -255,11 +253,15 @@ protected function loadInNamespace($class) foreach ($directories as $directory) { - if (strpos($class, $namespace) === 0) { + $directory = rtrim($directory, '/'); + + if (strpos($class, $namespace) === 0) + { $filePath = $directory . str_replace('\\', '/', substr($class, strlen($namespace))) . '.php'; $filename = $this->requireFile($filePath); - if ($filename) { + if ($filename) + { return $filename; } } @@ -277,30 +279,30 @@ protected function loadInNamespace($class) * version of CodeIgniter, namely 'application/libraries', and * 'application/Models'. * - * @param $class The class name. This typically should NOT have a namespace. + * @param string $class The class name. This typically should NOT have a namespace. * * @return mixed The mapped file name on success, or boolean false on failure */ - protected function loadLegacy($class) + protected function loadLegacy(string $class) { // If there is a namespace on this class, then // we cannot load it from traditional locations. - if (strpos('\\', $class) !== false) + if (strpos($class, '\\') !== false) { return false; } $paths = [ - APPPATH.'Controllers/', - APPPATH.'Libraries/', - APPPATH.'Models/', + APPPATH . 'Controllers/', + APPPATH . 'Libraries/', + APPPATH . 'Models/', ]; - $class = str_replace('\\', '/', $class).'.php'; + $class = str_replace('\\', '/', $class) . '.php'; foreach ($paths as $path) { - if ($file = $this->requireFile($path.$class)) + if ($file = $this->requireFile($path . $class)) { return $file; } @@ -317,11 +319,11 @@ protected function loadLegacy($class) * * @codeCoverageIgnore * - * @param $file + * @param string $file * * @return bool */ - protected function requireFile($file) + protected function requireFile(string $file) { $file = $this->sanitizeFilename($file); @@ -366,5 +368,4 @@ public function sanitizeFilename(string $filename): string } //-------------------------------------------------------------------- - } diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 53ab08612342..1a301aa03577 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use Config\Autoload; /** @@ -48,7 +47,8 @@ * * @package CodeIgniter */ -class FileLocator { +class FileLocator +{ /** * Stores our namespaces @@ -61,12 +61,12 @@ class FileLocator { /** * Constructor - * + * * @param Autoload $autoload */ public function __construct(Autoload $autoload) { - $this->namespaces = $autoload->psr4; + $this->namespaces = $autoload->psr4; unset($autoload); @@ -86,17 +86,15 @@ public function __construct(Autoload $autoload) * * @return string The path to the file if found, or an empty string. */ - public function locateFile(string $file, string $folder=null, string $ext = 'php'): string + public function locateFile(string $file, string $folder = null, string $ext = 'php'): string { // Ensure the extension is on the filename - $file = strpos($file, '.'.$ext) !== false - ? $file - : $file.'.'.$ext; + $file = strpos($file, '.' . $ext) !== false ? $file : $file . '.' . $ext; // Clean the folder name from the filename - if (! empty($folder)) + if ( ! empty($folder)) { - $file = str_replace($folder.'/', '', $file); + $file = str_replace($folder . '/', '', $file); } // No namespaceing? Try the application folder. @@ -111,24 +109,23 @@ public function locateFile(string $file, string $folder=null, string $ext = 'php $segments = explode('\\', $file); // The first segment will be empty if a slash started the filename. - if (empty($segments[0])) unset($segments[0]); + if (empty($segments[0])) + unset($segments[0]); - $path = ''; - $prefix = ''; + $path = ''; + $prefix = ''; $filename = ''; - while (! empty($segments)) + while ( ! empty($segments)) { - $prefix .= empty($prefix) - ? ucfirst(array_shift($segments)) - : '\\'. ucfirst(array_shift($segments)); + $prefix .= empty($prefix) ? ucfirst(array_shift($segments)) : '\\' . ucfirst(array_shift($segments)); - if (! array_key_exists($prefix, $this->namespaces)) + if ( ! array_key_exists($prefix, $this->namespaces)) { continue; } - $path = $this->namespaces[$prefix].'/'; + $path = $this->namespaces[$prefix] . '/'; $filename = implode('/', $segments); break; } @@ -137,14 +134,14 @@ public function locateFile(string $file, string $folder=null, string $ext = 'php // expects this file to be within that folder, like 'Views', // or 'libraries'. // @todo Allow it to check with and without the nested folder. - if (! empty($folder)) + if ( ! empty($folder) && strpos($filename, $folder) === false) { - $filename = $folder.'/'.$filename; + $filename = $folder . '/' . $filename; } $path .= $filename; - if (! $this->requireFile($path)) + if ( ! $this->requireFile($path)) { $path = ''; } @@ -154,25 +151,188 @@ public function locateFile(string $file, string $folder=null, string $ext = 'php //-------------------------------------------------------------------- + /** + * Examines a file and returns the fully qualified domain name. + * + * @param string $file + * + * @return string + */ + public function getClassname(string $file) : string + { + $php = file_get_contents($file); + $tokens = token_get_all($php); + $count = count($tokens); + $dlm = false; + $namespace = ''; + $class_name = ''; + + for ($i = 2; $i < $count; $i++) + { + if ((isset($tokens[$i-2][1]) && ($tokens[$i-2][1] == "phpnamespace" || $tokens[$i-2][1] == "namespace")) || ($dlm && $tokens[$i-1][0] == T_NS_SEPARATOR && $tokens[$i][0] == T_STRING)) + { + if (! $dlm) + { + $namespace = 0; + } + if (isset($tokens[$i][1])) + { + $namespace = $namespace ? $namespace."\\".$tokens[$i][1] : $tokens[$i][1]; + $dlm = true; + } + } + elseif ($dlm && ($tokens[$i][0] != T_NS_SEPARATOR) && ($tokens[$i][0] != T_STRING)) + { + $dlm = false; + } + if (($tokens[$i-2][0] == T_CLASS || (isset($tokens[$i-2][1]) && $tokens[$i-2][1] == "phpclass")) + && $tokens[$i-1][0] == T_WHITESPACE + && $tokens[$i][0] == T_STRING) + { + $class_name = $tokens[$i][1]; + break; + } + } + + if( empty( $class_name ) ) return ""; + + return $namespace .'\\'. $class_name; + } + + //-------------------------------------------------------------------- + + /** + * Searches through all of the defined namespaces looking for a file. + * Returns an array of all found locations for the defined file. + * + * Example: + * + * $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', + * ] + * + * @param string $path + * @param string $ext + * + * @return array + */ + public function search(string $path, string $ext = 'php'): array + { + $foundPaths = []; + + // Ensure the extension is on the filename + $path = strpos($path, '.' . $ext) !== false ? $path : $path . '.' . $ext; + + foreach ($this->namespaces as $name => $folder) + { + $folder = rtrim($folder, '/') . '/'; + + if (file_exists($folder . $path)) + { + $foundPaths[] = $folder . $path; + } + } + + // Remove any duplicates + $foundPaths = array_unique($foundPaths); + + return $foundPaths; + } + + //-------------------------------------------------------------------- + + /** + * Attempts to load a file and instantiate a new class by looking + * at its full path and comparing that to our existing psr4 namespaces + * in Autoloader config file. + * + * @param string $path + * + * @return string|void + */ + public function findQualifiedNameFromPath(string $path) + { + $path = realpath($path); + + if ( ! $path) + { + return; + } + + foreach ($this->namespaces as $namespace => $nsPath) + { + $nsPath = realpath($nsPath); + if (is_numeric($namespace) || empty($nsPath)) + continue; + + if (mb_strpos($path, $nsPath) === 0) + { + $className = '\\' . $namespace . '\\' . + ltrim(str_replace('/', '\\', mb_substr($path, mb_strlen($nsPath))), '\\'); + // Remove the file extension (.php) + $className = mb_substr($className, 0, -4); + + return $className; + } + } + } + + //-------------------------------------------------------------------- + + /** + * Scans the defined namespaces, returning a list of all files + * that are contained within the subpath specifed by $path. + * + * @param string $path + * + * @return array + */ + public function listFiles(string $path): array + { + if (empty($path)) + return []; + + $files = []; + helper('filesystem'); + + foreach ($this->namespaces as $namespace => $nsPath) + { + $fullPath = realpath(rtrim($nsPath, '/') . '/' . $path); + + if ( ! is_dir($fullPath)) + continue; + + $tempFiles = get_filenames($fullPath, true); + //CLI::newLine($tempFiles); + + if (! empty($tempFiles)) + $files = array_merge($files, $tempFiles); + } + + return $files; + } + /** * Checks the application folder to see if the file can be found. * Only for use with filenames that DO NOT include namespacing. * * @param string $file * @param string|null $folder - * @param string $ext * * @return string + * @internal param string $ext + * */ - protected function legacyLocate(string $file, string $folder=null): string + protected function legacyLocate(string $file, string $folder = null): string { $paths = [APPPATH, BASEPATH]; foreach ($paths as $path) { - $path .= empty($folder) - ? $file - : $folder.'/'.$file; + $path .= empty($folder) ? $file : $folder . '/' . $file; if ($this->requireFile($path) === true) { @@ -200,5 +360,4 @@ protected function requireFile(string $path): bool } //-------------------------------------------------------------------- - } diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php new file mode 100644 index 000000000000..0526b414be0d --- /dev/null +++ b/system/CLI/BaseCommand.php @@ -0,0 +1,241 @@ +logger = $logger; + $this->commands = $commands; + } + + //-------------------------------------------------------------------- + + abstract public function run(array $params); + + //-------------------------------------------------------------------- + + /** + * Can be used by a command to run other commands. + * + * @param string $command + * @param array $params + * + * @return mixed + */ + protected function call(string $command, array $params = []) + { + // The CommandRunner will grab the first element + // for the command name. + array_unshift($params, $command); + + return $this->commands->index($params); + } + + //-------------------------------------------------------------------- + + /** + * A simple method to display an error with line/file, + * in child commands. + * + * @param \Exception $e + */ + protected function showError(\Exception $e) + { + CLI::newLine(); + CLI::error($e->getMessage()); + CLI::write($e->getFile() . ' - ' . $e->getLine()); + CLI::newLine(); + } + + //-------------------------------------------------------------------- + + /** + * Makes it simple to access our protected properties. + * + * @param string $key + * + * @return mixed + */ + public function __get(string $key) + { + if (isset($this->$key)) + { + return $this->$key; + } + } + + //-------------------------------------------------------------------- + + /** + * show Help include (usage,arguments,description,options) + */ + public function showHelp() + { + // 4 spaces insted of tab + $tab = " "; + CLI::write(lang('CLI.helpDescription'), 'yellow'); + CLI::write($tab . $this->description); + CLI::newLine(); + + CLI::write(lang('CLI.helpUsage'), 'yellow'); + $usage = empty($this->usage) ? $this->name . " [arguments]" : $this->usage; + CLI::write($tab . $usage); + CLI::newLine(); + + $pad = max($this->getPad($this->options, 6), $this->getPad($this->arguments, 6)); + + if ( ! empty($this->arguments)) + { + CLI::write(lang('CLI.helpArguments'), 'yellow'); + foreach ($this->arguments as $argument => $description) + { + CLI::write($tab . CLI::color(str_pad($argument, $pad), 'green') . $description, 'yellow'); + } + CLI::newLine(); + } + + if ( ! empty($this->options)) + { + CLI::write(lang('CLI.helpOptions'), 'yellow'); + foreach ($this->options as $option => $description) + { + CLI::write($tab . CLI::color(str_pad($option, $pad), 'green') . $description, 'yellow'); + } + CLI::newLine(); + } + } + + //-------------------------------------------------------------------- + + /** + * Get pad for $key => $value array output + * + * @param array $array + * @param int $pad + * + * @return int + */ + public function getPad($array, int $pad) + { + $max = 0; + foreach ($array as $key => $value) + { + $max = max($max, strlen($key)); + } + return $max + $pad; + } + + //-------------------------------------------------------------------- +} diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index bc08e2f4c67d..d27ccf1e03e6 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,12 +29,13 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ +use CodeIgniter\CLI\Exceptions\CLIException; /** * Class CLI @@ -44,14 +45,24 @@ * * Portions of this code were initially from the FuelPHP Framework, * version 1.7.x, and used here under the MIT license they were - * originally made available under. + * originally made available under. Reference: http://fuelphp.com * - * http://fuelphp.com + * Some of the code in this class is Windows-specific, and not + * possible to test using travis-ci. It has been phpunit-annotated + * to prevent messing up code coverage. + * + * Some of the methods require keyboard input, and are not unit-testable + * as a result: input() and prompt(). + * validate() is internal, and not testable if prompt() isn't. + * The wait() method is mostly testable, as long as you don't give it + * an argument of "0". + * These have been flagged to ignore for code coverage purposes. * * @package CodeIgniter\HTTP */ class CLI { + /** * Is the readline library on the system? * @@ -73,35 +84,28 @@ class CLI */ protected static $initialized = false; - /** - * Used by the progress bar - * - * @var bool - */ - protected static $inProgress = false; - /** * Foreground color list * @var array */ protected static $foreground_colors = [ - 'black' => '0;30', - 'dark_gray' => '1;30', - 'blue' => '0;34', - 'dark_blue' => '1;34', - 'light_blue' => '1;34', - 'green' => '0;32', - 'light_green' => '1;32', - 'cyan' => '0;36', - 'light_cyan' => '1;36', - 'red' => '0;31', - 'light_red' => '1;31', - 'purple' => '0;35', - 'light_purple' => '1;35', - 'light_yellow' => '0;33', - 'yellow' => '1;33', - 'light_gray' => '0;37', - 'white' => '1;37', + 'black' => '0;30', + 'dark_gray' => '1;30', + 'blue' => '0;34', + 'dark_blue' => '1;34', + 'light_blue' => '1;34', + 'green' => '0;32', + 'light_green' => '1;32', + 'cyan' => '0;36', + 'light_cyan' => '1;36', + 'red' => '0;31', + 'light_red' => '1;31', + 'purple' => '0;35', + 'light_purple' => '1;35', + 'light_yellow' => '0;33', + 'yellow' => '1;33', + 'light_gray' => '0;37', + 'white' => '1;37', ]; /** @@ -109,16 +113,27 @@ class CLI * @var array */ protected static $background_colors = [ - 'black' => '40', - 'red' => '41', - 'green' => '42', - 'yellow' => '43', - 'blue' => '44', - 'magenta' => '45', - 'cyan' => '46', + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'yellow' => '43', + 'blue' => '44', + 'magenta' => '45', + 'cyan' => '46', 'light_gray' => '47', ]; + /** + * List of array segments. + * @var array + */ + protected static $segments = []; + + /** + * @var array + */ + protected static $options = []; + //-------------------------------------------------------------------- /** @@ -126,8 +141,17 @@ class CLI */ public static function init() { + // Readline is an extension for PHP that makes interactivity with PHP + // much more bash-like. + // http://www.php.net/manual/en/readline.installation.php static::$readline_support = extension_loaded('readline'); + // clear segments & options to keep testing clean + static::$segments = []; + static::$options = []; + + static::parseCommandLine(); + static::$initialized = true; } @@ -140,8 +164,9 @@ public static function init() * php index.php user -v --v -name=John --name=John * * @param string $prefix - * * @return string + * + * @codeCoverageIgnore */ public static function input(string $prefix = null): string { @@ -158,138 +183,122 @@ public static function input(string $prefix = null): string //-------------------------------------------------------------------- /** - * Asks the user for input. This can have either 1 or 2 arguments. + * Asks the user for input. * * Usage: * - * // Waits for any key press - * CLI::prompt(); - * * // Takes any input * $color = CLI::prompt('What is your favorite color?'); * * // Takes any input, but offers default * $color = CLI::prompt('What is your favourite color?', 'white'); * - * // Will only accept the options in the array - * $ready = CLI::prompt('Are you ready?', array('y','n')); + * // Will validate options with the in_list rule and accept only if one of the list + * $color = CLI::prompt('What is your favourite color?', array('red','blue')); + * + * // Do not provide options but requires a valid email + * $email = CLI::prompt('What is your email?', null, 'required|valid_email'); * - * @return string the user input + * @param string $field Output "field" question + * @param string|array $options String to a defaul value, array to a list of options (the first option will be the default value) + * @param string $validation Validation rules + * + * @return string The user input + * @codeCoverageIgnore */ - public static function prompt(): string + public static function prompt($field, $options = null, $validation = null): string { - $args = func_get_args(); - - $options = []; - $output = ''; - $default = null; - - // How many we got - $arg_count = count($args); + $extra_output = ''; + $default = ''; - // Is the last argument a boolean? True means required - $required = end($args) === true; - - // Reduce the argument count if required was passed, we don't care about that anymore - $required === true && --$arg_count; - - // This method can take a few crazy combinations of arguments, so lets work it out - switch ($arg_count) + if (is_string($options)) { - case 2: - - // E.g: $ready = CLI::prompt('Are you ready?', array('y','n')); - if (is_array($args[1])) - { - list($output, $options) = $args; - } - - // E.g: $color = CLI::prompt('What is your favourite color?', 'white'); - elseif (is_string($args[1])) - { - list($output, $default) = $args; - } - - break; - - case 1: - - // No question (probably been asked already) so just show options - // E.g: $ready = CLI::prompt(array('y','n')); - if (is_array($args[0])) - { - $options = $args[0]; - } - - // Question without options - // E.g: $ready = CLI::prompt('What did you do today?'); - elseif (is_string($args[0])) - { - $output = $args[0]; - } - - break; + $extra_output = ' [' . static::color($options, 'white') . ']'; + $default = $options; } - // If a question has been asked with the read - if ($output !== '') + if (is_array($options) && $options) { - $extra_output = ''; + $opts = $options; + $extra_output_default = static::color($opts[0], 'white'); + + unset($opts[0]); - if ($default !== null) + if (empty($opts)) { - $extra_output = ' [ Default: "'.$default.'" ]'; + $extra_output = $extra_output_default; } - - elseif ($options !== []) + else { - $extra_output = ' [ '.implode(', ', $options).' ]'; + $extra_output = ' [' . $extra_output_default . ', ' . implode(', ', $opts) . ']'; + $validation .= '|in_list[' . implode(',', $options) . ']'; + $validation = trim($validation, '|'); } - fwrite(STDOUT, $output.$extra_output.': '); + $default = $options[0]; } + fwrite(STDOUT, $field . $extra_output . ': '); + // Read the input from keyboard. - $input = trim(static::input()) ? : $default; + $input = trim(static::input()) ?: $default; - // No input provided and we require one (default will stop this being called) - if (empty($input) && $required === true) + if (isset($validation)) { - static::write('This is required.'); - static::newLine(); - - $input = forward_static_call_array([__CLASS__, 'prompt'], $args); + while ( ! static::validate($field, $input, $validation)) + { + $input = static::prompt($field, $options, $validation); + } } - // If options are provided and the choice is not in the array, tell them to try again - if ( ! empty($options) && ! in_array($input, $options)) + return empty($input) ? '' : $input; + } + + //-------------------------------------------------------------------- + + /** + * Validate one prompt "field" at a time + * + * @param string $field Prompt "field" output + * @param string $value Input value + * @param string $rules Validation rules + * + * @return boolean + * @codeCoverageIgnore + */ + protected static function validate($field, $value, $rules) + { + $validation = \Config\Services::validation(null, false); + $validation->setRule($field, null, $rules); + $validation->run([$field => $value]); + + if ($validation->hasError($field)) { - static::write('This is not a valid option. Please try again.'); - static::newLine(); + static::error($validation->getError($field)); - $input = forward_static_call_array([__CLASS__, 'prompt'], $args); + return false; } - return empty($input) ? '' : $input; + return true; } //-------------------------------------------------------------------- /** - * Outputs a string to the cli. + * Outputs a string to the cli. * - * @param string $text the text to output + * @param string $text The text to output * @param string $foreground * @param string $background */ - public static function write(string $text, string $foreground = null, string $background = null) + public static function write(string $text = '', string $foreground = null, string $background = null) { if ($foreground || $background) { $text = static::color($text, $foreground, $background); } - fwrite(STDOUT, $text.PHP_EOL); + fwrite(STDOUT, $text . PHP_EOL); } //-------------------------------------------------------------------- @@ -297,9 +306,9 @@ public static function write(string $text, string $foreground = null, string $ba /** * Outputs an error to the CLI using STDERR instead of STDOUT * - * @param string|array $text the text to output, or array of errors - * @param string $foreground - * @param string $background + * @param string|array $text The text to output, or array of errors + * @param string $foreground + * @param string $background */ public static function error(string $text, string $foreground = 'light_red', string $background = null) { @@ -308,7 +317,7 @@ public static function error(string $text, string $foreground = 'light_red', str $text = static::color($text, $foreground, $background); } - fwrite(STDERR, $text.PHP_EOL); + fwrite(STDERR, $text . PHP_EOL); } //-------------------------------------------------------------------- @@ -316,7 +325,7 @@ public static function error(string $text, string $foreground = 'light_red', str /** * Beeps a certain number of times. * - * @param int $num the number of times to beep + * @param int $num The number of times to beep */ public static function beep(int $num = 1) { @@ -329,8 +338,8 @@ public static function beep(int $num = 1) * Waits a certain number of seconds, optionally showing a wait message and * waiting for a key press. * - * @param int $seconds number of seconds - * @param bool $countdown show a countdown or not + * @param int $seconds Number of seconds + * @param bool $countdown Show a countdown or not */ public static function wait(int $seconds, bool $countdown = false) { @@ -340,13 +349,12 @@ public static function wait(int $seconds, bool $countdown = false) while ($time > 0) { - fwrite(STDOUT, $time.'... '); + fwrite(STDOUT, $time . '... '); sleep(1); - $time--; + $time --; } static::write(); } - else { if ($seconds > 0) @@ -355,13 +363,15 @@ public static function wait(int $seconds, bool $countdown = false) } else { + // this chunk cannot be tested because of keyboard input + // @codeCoverageIgnoreStart static::write(static::$wait_msg); static::input(); + // @codeCoverageIgnoreEnd } } } - //-------------------------------------------------------------------- /** @@ -369,7 +379,7 @@ public static function wait(int $seconds, bool $countdown = false) */ public static function isWindows() { - return 'win' === strtolower(substr(php_uname("s"), 0, 3)); + return stripos(PHP_OS, 'WIN') === 0; } //-------------------------------------------------------------------- @@ -384,9 +394,9 @@ public static function isWindows() public static function newLine(int $num = 1) { // Do it once or more, write with empty string gives us a new line - for ($i = 0; $i < $num; $i++) + for ($i = 0; $i < $num; $i ++) { - static::write(); + static::write(''); } } @@ -396,16 +406,17 @@ public static function newLine(int $num = 1) * Clears the screen of output * * @return void + * @codeCoverageIgnore */ public static function clearScreen() { static::isWindows() - // Windows is a bit crap at this, but their terminal is tiny so shove this in - ? static::newLine(40) + // Windows is a bit crap at this, but their terminal is tiny so shove this in + ? static::newLine(40) - // Anything with a flair of Unix will handle these magic characters - : fwrite(STDOUT, chr(27)."[H".chr(27)."[2J"); + // Anything with a flair of Unix will handle these magic characters + : fwrite(STDOUT, chr(27) . "[H" . chr(27) . "[2J"); } //-------------------------------------------------------------------- @@ -414,35 +425,37 @@ public static function clearScreen() * Returns the given text with the correct color codes for a foreground and * optionally a background color. * - * @param string $text the text to color - * @param string $foreground the foreground color - * @param string $background the background color - * @param string $format other formatting to apply. Currently only 'underline' is understood + * @param string $text The text to color + * @param string $foreground The foreground color + * @param string $background The background color + * @param string $format Other formatting to apply. Currently only 'underline' is understood * - * @return string the color coded string + * @return string The color coded string */ public static function color(string $text, string $foreground, string $background = null, string $format = null) { if (static::isWindows() && ! isset($_SERVER['ANSICON'])) { + // @codeCoverageIgnoreStart return $text; + // @codeCoverageIgnoreEnd } if ( ! array_key_exists($foreground, static::$foreground_colors)) { - throw new \RuntimeException('Invalid CLI foreground color: '.$foreground); + throw CLIException::forInvalidColor('foreground', $foreground); } if ($background !== null && ! array_key_exists($background, static::$background_colors)) { - throw new \RuntimeException('Invalid CLI background color: '.$background); + throw CLIException::forInvalidColor('background', $background); } - $string = "\033[".static::$foreground_colors[$foreground]."m"; + $string = "\033[" . static::$foreground_colors[$foreground] . "m"; if ($background !== null) { - $string .= "\033[".static::$background_colors[$background]."m"; + $string .= "\033[" . static::$background_colors[$background] . "m"; } if ($format === 'underline') @@ -450,7 +463,7 @@ public static function color(string $text, string $foreground, string $backgroun $string .= "\033[4m"; } - $string .= $text."\033[0m"; + $string .= $text . "\033[0m"; return $string; } @@ -468,12 +481,14 @@ public static function color(string $text, string $foreground, string $backgroun */ public static function getWidth(int $default = 80): int { - if (static::isWindows()) + if (static::isWindows() || (int) shell_exec('tput cols') == 0) { + // @codeCoverageIgnoreStart return $default; + // @codeCoverageIgnoreEnd } - return (int)shell_exec('tput cols'); + return (int) shell_exec('tput cols'); } //-------------------------------------------------------------------- @@ -491,10 +506,12 @@ public static function getHeight(int $default = 32): int { if (static::isWindows()) { + // @codeCoverageIgnoreStart return $default; + // @codeCoverageIgnoreEnd } - return (int)shell_exec('tput lines'); + return (int) shell_exec('tput lines'); } //-------------------------------------------------------------------- @@ -506,40 +523,34 @@ public static function getHeight(int $default = 32): int * @param int $thisStep * @param int $totalSteps */ - public static function showProgress(int $thisStep = 1, int $totalSteps = 10) + public static function showProgress($thisStep = 1, int $totalSteps = 10) { - // The first time through, save - // our position so the script knows where to go - // back to when writing the bar, and - // at the end of the script. - if ( ! static::$inProgress) + static $inProgress = false; + + // restore cursor position when progress is continuing. + if ($inProgress !== false && $inProgress <= $thisStep) { - fwrite(STDOUT, "\0337"); - static::$inProgress = true; + fwrite(STDOUT, "\033[1A"); } - - // Restore position - fwrite(STDERR, "\0338"); + $inProgress = $thisStep; if ($thisStep !== false) { // Don't allow div by zero or negative numbers.... - $thisStep = abs($thisStep); + $thisStep = abs($thisStep); $totalSteps = $totalSteps < 1 ? 1 : $totalSteps; $percent = intval(($thisStep / $totalSteps) * 100); - $step = (int)round($percent / 10); + $step = (int) round($percent / 10); // Write the progress bar - fwrite(STDOUT, "[\033[32m".str_repeat('#', $step).str_repeat('.', 10 - $step)."\033[0m]"); + fwrite(STDOUT, "[\033[32m" . str_repeat('#', $step) . str_repeat('.', 10 - $step) . "\033[0m]"); // Textual representation... - fwrite(STDOUT, " {$percent}% Complete".PHP_EOL); - // Move up, undo the PHP_EOL - fwrite(STDOUT, "\033[1A"); + fwrite(STDOUT, sprintf(" %3d%% Complete", $percent) . PHP_EOL); } else { - fwrite(STDERR, "\007"); + fwrite(STDOUT, "\007"); } } @@ -583,15 +594,14 @@ public static function wrap(string $string = null, int $max = 0, int $pad_left = if ($pad_left > 0) { - $lines = explode("\n", $lines); + $lines = explode(PHP_EOL, $lines); $first = true; - array_walk($lines, function (&$line, $index) use ($max, $pad_left, &$first) - { + array_walk($lines, function (&$line, $index) use ($pad_left, &$first) { if ( ! $first) { - $line = str_repeat(" ", $pad_left).$line; + $line = str_repeat(" ", $pad_left) . $line; } else { @@ -599,15 +609,290 @@ public static function wrap(string $string = null, int $max = 0, int $pad_left = } }); - $lines = implode("\n", $lines); + $lines = implode(PHP_EOL, $lines); } return $lines; } //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + // Command-Line 'URI' support + //-------------------------------------------------------------------- + + /** + * Parses the command line it was called from and collects all + * options and valid segments. + * + * I tried to use getopt but had it fail occassionally to find any + * options but argc has always had our back. We don't have all of the power + * of getopt but this does us just fine. + */ + protected static function parseCommandLine() + { + $optionsFound = false; + + // start picking segments off from #1, ignoring the invoking program + for ($i = 1; $i < $_SERVER['argc']; $i ++) + { + // If there's no '-' at the beginning of the argument + // then add it to our segments. + if ( ! $optionsFound && mb_strpos($_SERVER['argv'][$i], '-') === false) + { + static::$segments[] = $_SERVER['argv'][$i]; + continue; + } + + // We set $optionsFound here so that we know to + // skip the next argument since it's likely the + // value belonging to this option. + $optionsFound = true; + + $arg = str_replace('-', '', $_SERVER['argv'][$i]); + $value = null; + + // if there is a following segment, and it doesn't start with a dash, it's a value. + if (isset($_SERVER['argv'][$i + 1]) && mb_strpos($_SERVER['argv'][$i + 1], '-') !== 0) + { + $value = $_SERVER['argv'][$i + 1]; + $i ++; + } + + static::$options[$arg] = $value; + + // Reset $optionsFound so it can collect segments + // past any options. + $optionsFound = false; + } + } + + //-------------------------------------------------------------------- + + /** + * Returns the command line string portions of the arguments, minus + * any options, as a string. This is used to pass along to the main + * CodeIgniter application. + * + * @return string + */ + public static function getURI() + { + return implode('/', static::$segments); + } + + //-------------------------------------------------------------------- + + /** + * Returns an individual segment. + * + * This ignores any options that might have been dispersed between + * valid segments in the command: + * + * // segment(3) is 'three', not '-f' or 'anOption' + * > php spark one two -f anOption three + * + * @param int $index + * + * @return mixed|null + */ + public static function getSegment(int $index) + { + if ( ! isset(static::$segments[$index - 1])) + { + return null; + } + + return static::$segments[$index - 1]; + } + + //-------------------------------------------------------------------- + + /** + * Returns the raw array of segments found. + * + * @return array + */ + public static function getSegments() + { + return static::$segments; + } + + //-------------------------------------------------------------------- + + /** + * Gets a single command-line option. Returns TRUE if the option + * exists, but doesn't have a value, and is simply acting as a flag. + * + * @param string $name + * + * @return bool|mixed|null + */ + public static function getOption(string $name) + { + if ( ! array_key_exists($name, static::$options)) + { + return null; + } + + // If the option didn't have a value, simply return TRUE + // so they know it was set, otherwise return the actual value. + $val = static::$options[$name] === null ? true : static::$options[$name]; + + return $val; + } + + //-------------------------------------------------------------------- + + /** + * Returns the raw array of options found. + * + * @return array + */ + public static function getOptions() + { + return static::$options; + } + + //-------------------------------------------------------------------- + + /** + * Returns the options as a string, suitable for passing along on + * the CLI to other commands. + * + * @return string + */ + public static function getOptionString(): string + { + if (empty(static::$options)) + { + return ''; + } + + $out = ''; + + foreach (static::$options as $name => $value) + { + // If there's a space, we need to group + // so it will pass correctly. + if (mb_strpos($value, ' ') !== false) + { + $value = '"' . $value . '"'; + } + + $out .= "-{$name} $value "; + } + + return $out; + } + + //-------------------------------------------------------------------- + + /** + * Returns a well formated table + * + * @param array $tbody List of rows + * @param array $thead List of columns + * + * @return string + */ + public static function table(array $tbody, array $thead = []) + { + // All the rows in the table will be here until the end + $table_rows = []; + // We need only indexes and not keys + if ( ! empty($thead)) + { + $table_rows[] = array_values($thead); + } + + foreach ($tbody as $tr) + { + $table_rows[] = array_values($tr); + } + + // Yes, it really is necessary to know this count + $total_rows = count($table_rows); + + // Store all columns lengths + // $all_cols_lengths[row][column] = length + $all_cols_lengths = []; + + // Store maximum lengths by column + // $max_cols_lengths[column] = length + $max_cols_lengths = []; + + // Read row by row and define the longest columns + for ($row = 0; $row < $total_rows; $row ++ ) + { + $column = 0; // Current column index + foreach ($table_rows[$row] as $col) + { + // Sets the size of this column in the current row + $all_cols_lengths[$row][$column] = strlen($col); + + // If the current column does not have a value among the larger ones + // or the value of this is greater than the existing one + // then, now, this assumes the maximum length + if ( ! isset($max_cols_lengths[$column]) || $all_cols_lengths[$row][$column] > $max_cols_lengths[$column]) + { + $max_cols_lengths[$column] = $all_cols_lengths[$row][$column]; + } + + // We can go check the size of the next column... + $column ++; + } + } + + // Read row by row and add spaces at the end of the columns + // to match the exact column length + for ($row = 0; $row < $total_rows; $row ++ ) + { + $column = 0; + foreach ($table_rows[$row] as $col) + { + $diff = $max_cols_lengths[$column] - strlen($col); + if ($diff) + { + $table_rows[$row][$column] = $table_rows[$row][$column] . str_repeat(' ', $diff); + } + $column ++; + } + } + + $table = ''; + + // Joins columns and append the well formatted rows to the table + for ($row = 0; $row < $total_rows; $row ++ ) + { + // Set the table border-top + if ($row === 0) + { + $cols = '+'; + foreach ($table_rows[$row] as $col) + { + $cols .= str_repeat('-', strlen($col) + 2) . '+'; + } + $table .= $cols . PHP_EOL; + } + + // Set the columns borders + $table .= '| ' . implode(' | ', $table_rows[$row]) . ' |' . PHP_EOL; + + // Set the thead and table borders-bottom + if ($row === 0 && ! empty($thead) || $row + 1 === $total_rows) + { + $table .= $cols . PHP_EOL; + } + } + + fwrite(STDOUT, $table); + } + + //-------------------------------------------------------------------- } -// Ensure the class is initialized. +// Ensure the class is initialized. Done outside of code coverage +// @codeCoverageIgnoreStart CLI::init(); +// @codeCoverageIgnoreEnd diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php new file mode 100644 index 000000000000..46c2e18f68ff --- /dev/null +++ b/system/CLI/CommandRunner.php @@ -0,0 +1,185 @@ +index($params); + } + + //-------------------------------------------------------------------- + + + /** + * @param array $params + * + * @return mixed + */ + public function index(array $params) + { + $command = array_shift($params); + + $this->createCommandList(); + + if (is_null($command)) + { + $command = 'help'; + } + + return $this->runCommand($command, $params); + } + + //-------------------------------------------------------------------- + + /** + * Actually runs the command. + * + * @param string $command + * @param array $params + * + * @return mixed + */ + protected function runCommand(string $command, array $params) + { + if ( ! isset($this->commands[$command])) + { + CLI::error('Command \'' . $command . '\' not found'); + CLI::newLine(); + return; + } + + // The file would have already been loaded during the + // createCommandList function... + $className = $this->commands[$command]['class']; + $class = new $className($this->logger, $this); + + return $class->run($params); + } + + //-------------------------------------------------------------------- + + /** + * Scans all Commands directories and prepares a list + * of each command with it's group and file. + * + * @return null|void + */ + protected function createCommandList() + { + $files = service('locator')->listFiles("Commands/"); + + // If no matching command files were found, bail + if (empty($files)) + { + return; + } + + // Loop over each file checking to see if a command with that + // alias exists in the class. If so, return it. Otherwise, try the next. + foreach ($files as $file) + { + $className = service('locator')->findQualifiedNameFromPath($file); + + if (empty($className) || ! class_exists($className)) + { + continue; + } + + $class = new $className($this->logger, $this); + + // Store it! + if ($class->group !== null) + { + $this->commands[$class->name] = [ + 'class' => $className, + 'file' => $file, + 'group' => $class->group, + 'description' => $class->description + ]; + } + + $class = null; + unset($class); + } + + asort($this->commands); + } + + //-------------------------------------------------------------------- + + /** + * Allows access to the current commands that have been found. + * + * @return array + */ + public function getCommands() + { + return $this->commands; + } + + //-------------------------------------------------------------------- +} diff --git a/system/CLI/Console.php b/system/CLI/Console.php new file mode 100644 index 000000000000..7daccf9b0137 --- /dev/null +++ b/system/CLI/Console.php @@ -0,0 +1,99 @@ +app = $app; + } + + //-------------------------------------------------------------------- + + /** + * Runs the current command discovered on the CLI. + * + * @param bool $useSafeOutput + * + * @return \CodeIgniter\HTTP\RequestInterface|\CodeIgniter\HTTP\Response|\CodeIgniter\HTTP\ResponseInterface|mixed + * @throws \CodeIgniter\HTTP\RedirectException + */ + public function run(bool $useSafeOutput = false) + { + $path = CLI::getURI() ?: 'list'; + + // Set the path for the application to route to. + $this->app->setPath("ci{$path}"); + + return $this->app->useSafeOutput($useSafeOutput)->run(); + } + + //-------------------------------------------------------------------- + + /** + * Displays basic information about the Console. + */ + public function showHeader() + { + CLI::newLine(1); + + CLI::write(CLI::color('CodeIgniter CLI Tool', 'green') + . ' - Version ' . CodeIgniter::CI_VERSION + . ' - Server-Time: ' . date('Y-m-d H:i:sa')); + + CLI::newLine(1); + } + + //-------------------------------------------------------------------- +} diff --git a/system/CLI/Exceptions/CLIException.php b/system/CLI/Exceptions/CLIException.php new file mode 100644 index 000000000000..6e2477faa7b6 --- /dev/null +++ b/system/CLI/Exceptions/CLIException.php @@ -0,0 +1,15 @@ +validHandlers) || ! is_array($config->validHandlers)) + { + throw CacheException::forInvalidHandlers(); + } + + if ( ! isset($config->handler) || ! isset($config->backupHandler)) + { + throw CacheException::forNoBackup(); + } + + $handler = ! empty($handler) ? $handler : $config->handler; + $backup = ! empty($backup) ? $backup : $config->backupHandler; + + if ( ! array_key_exists($handler, $config->validHandlers) || ! array_key_exists($backup, $config->validHandlers)) + { + throw CacheException::forHandlerNotFound(); + } + + // Get an instance of our handler. + $adapter = new $config->validHandlers[$handler]($config); + + if ( ! $adapter->isSupported()) + { + $adapter = new $config->validHandlers[$backup]($config); + + if ( ! $adapter->isSupported()) + { + // Log stuff here, don't throw exception. No need to raise a fuss. + // Fall back to the dummy adapter. + $adapter = new $config->validHandlers['dummy'](); + } + } + + $adapter->initialize(); + + return $adapter; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Cache/CacheInterface.php b/system/Cache/CacheInterface.php new file mode 100644 index 000000000000..d5b5e6624c5a --- /dev/null +++ b/system/Cache/CacheInterface.php @@ -0,0 +1,147 @@ +prefix = $config->prefix ?: ''; + $this->path = ! empty($config->storePath) ? $config->storePath : WRITEPATH . 'cache'; + + $this->path = rtrim($this->path, '/') . '/'; + } + + //-------------------------------------------------------------------- + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + // Not to see here... + } + + //-------------------------------------------------------------------- + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $key = $this->prefix . $key; + + $data = $this->getItem($key); + + return is_array($data) ? $data['data'] : false; + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60) + { + $key = $this->prefix . $key; + + $contents = [ + 'time' => time(), + 'ttl' => $ttl, + 'data' => $value, + ]; + + if ($this->writeFile($this->path . $key, serialize($contents))) + { + chmod($this->path . $key, 0640); + + return true; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + $key = $this->prefix . $key; + + return file_exists($this->path . $key) ? unlink($this->path . $key) : false; + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $data = $this->getItem($key); + + if ($data === false) + { + $data = ['data' => 0, 'ttl' => 60]; + } + elseif ( ! is_int($data['data'])) + { + return false; + } + + $new_value = $data['data'] + $offset; + + return $this->save($key, $new_value, $data['ttl']) ? $new_value : false; + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $data = $this->getItem($key); + + if ($data === false) + { + $data = ['data' => 0, 'ttl' => 60]; + } + elseif ( ! is_int($data['data'])) + { + return false; + } + + $new_value = $data['data'] - $offset; + + return $this->save($key, $new_value, $data['ttl']) ? $new_value : false; + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + return $this->deleteFiles($this->path, false, true); + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return $this->getDirFileInfo($this->path); + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + $key = $this->prefix . $key; + + if ( ! file_exists($this->path . $key)) + { + return FALSE; + } + + $data = @unserialize(file_get_contents($this->path . $key)); + + if (is_array($data)) + { + $mtime = filemtime($this->path . $key); + + if ( ! isset($data['ttl'])) + { + return FALSE; + } + + return [ + 'expire' => $mtime + $data['ttl'], + 'mtime' => $mtime, + 'data' => $data['data'], + ]; + } + + return FALSE; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return is_writable($this->path); + } + + //-------------------------------------------------------------------- + + /** + * Does the heavy lifting of actually retrieving the file and + * verifying it's age. + * + * @param string $key + * + * @return bool|mixed + */ + protected function getItem(string $key) + { + if ( ! is_file($this->path . $key)) + { + return false; + } + + $data = unserialize(file_get_contents($this->path . $key)); + + if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl']) + { + unlink($this->path . $key); + + return false; + } + + return $data; + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + // SUPPORT METHODS FOR FILES + //-------------------------------------------------------------------- + + /** + * Writes a file to disk, or returns false if not successful. + * + * @param $path + * @param $data + * @param string $mode + * + * @return bool + */ + protected function writeFile($path, $data, $mode = 'wb') + { + try + { + if (($fp = @fopen($path, $mode)) === false) + { + return false; + } + } + catch (\ErrorException $e) + { + return false; + } + + flock($fp, LOCK_EX); + + for ($result = $written = 0, $length = strlen($data); $written < $length; $written += $result) + { + if (($result = fwrite($fp, substr($data, $written))) === false) + { + break; + } + } + + flock($fp, LOCK_UN); + fclose($fp); + + return is_int($result); + } + + //-------------------------------------------------------------------- + + /** + * Delete Files + * + * Deletes all files contained in the supplied directory path. + * Files must be writable or owned by the system in order to be deleted. + * If the second parameter is set to TRUE, any directories contained + * within the supplied base directory will be nuked as well. + * + * @param string $path File path + * @param bool $del_dir Whether to delete any directories found in the path + * @param bool $htdocs Whether to skip deleting .htaccess and index page files + * @param int $_level Current directory depth level (default: 0; internal use only) + * + * @return bool + */ + protected function deleteFiles($path, $del_dir = false, $htdocs = false, $_level = 0) + { + // Trim the trailing slash + $path = rtrim($path, '/\\'); + + if ( ! $current_dir = @opendir($path)) + { + return false; + } + + while (false !== ($filename = @readdir($current_dir))) + { + if ($filename !== '.' && $filename !== '..') + { + if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.') + { + $this->deleteFiles($path . DIRECTORY_SEPARATOR . $filename, $del_dir, $htdocs, $_level + 1); + } + elseif ($htdocs !== true || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename)) + { + @unlink($path . DIRECTORY_SEPARATOR . $filename); + } + } + } + + closedir($current_dir); + + return ($del_dir === true && $_level > 0) ? @rmdir($path) : true; + } + + //-------------------------------------------------------------------- + + /** + * Get Directory File Information + * + * Reads the specified directory and builds an array containing the filenames, + * filesize, dates, and permissions + * + * Any sub-folders contained within the specified path are read as well. + * + * @param string $source_dir Path to source + * @param bool $top_level_only Look only at the top level directory specified? + * @param bool $_recursion Internal variable to determine recursion status - do not use in calls + * + * @return array|false + */ + protected function getDirFileInfo($source_dir, $top_level_only = true, $_recursion = false) + { + static $_filedata = []; + $relative_path = $source_dir; + + if ($fp = @opendir($source_dir)) + { + // reset the array and make sure $source_dir has a trailing slash on the initial call + if ($_recursion === false) + { + $_filedata = []; + $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } + + // Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast + while (false !== ($file = readdir($fp))) + { + if (is_dir($source_dir . $file) && $file[0] !== '.' && $top_level_only === false) + { + $this->getDirFileInfo($source_dir . $file . DIRECTORY_SEPARATOR, $top_level_only, true); + } + elseif ($file[0] !== '.') + { + $_filedata[$file] = $this->getFileInfo($source_dir . $file); + $_filedata[$file]['relative_path'] = $relative_path; + } + } + + closedir($fp); + + return $_filedata; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Get File Info + * + * Given a file and path, returns the name, path, size, date modified + * Second parameter allows you to explicitly declare what information you want returned + * Options are: name, server_path, size, date, readable, writable, executable, fileperms + * Returns FALSE if the file cannot be found. + * + * @param string $file Path to file + * @param mixed $returned_values Array or comma separated string of information returned + * + * @return array|false + */ + protected function getFileInfo(string $file, array $returned_values = ['name', 'server_path', 'size', 'date']) + { + if ( ! file_exists($file)) + { + return false; + } + + foreach ($returned_values as $key) + { + switch ($key) + { + case 'name': + $fileinfo['name'] = basename($file); + break; + case 'server_path': + $fileinfo['server_path'] = $file; + break; + case 'size': + $fileinfo['size'] = filesize($file); + break; + case 'date': + $fileinfo['date'] = filemtime($file); + break; + case 'readable': + $fileinfo['readable'] = is_readable($file); + break; + case 'writable': + $fileinfo['writable'] = is_writable($file); + break; + case 'executable': + $fileinfo['executable'] = is_executable($file); + break; + case 'fileperms': + $fileinfo['fileperms'] = fileperms($file); + break; + } + } + + return $fileinfo; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php new file mode 100644 index 000000000000..36513248f6fe --- /dev/null +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -0,0 +1,317 @@ + '127.0.0.1', + 'port' => 11211, + 'weight' => 1, + 'raw' => false, + ]; + + //-------------------------------------------------------------------- + + public function __construct(array $config) + { + $this->prefix = $config['prefix'] ?? ''; + + if ( ! empty($config)) + { + $this->config = array_merge($this->config, $config); + } + } + + /** + * Class destructor + * + * Closes the connection to Memcache(d) if present. + */ + public function __destruct() + { + if ($this->memcached instanceof \Memcached) + { + $this->memcached->quit(); + } + elseif ($this->memcached instanceof \Memcache) + { + $this->memcached->close(); + } + } + + //-------------------------------------------------------------------- + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + if (class_exists('\Memcached')) + { + $this->memcached = new \Memcached(); + if ($this->config['raw']) + { + $this->memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + } + } + elseif (class_exists('\Memcache')) + { + $this->memcached = new \Memcache(); + } + else + { + throw new CriticalError('Cache: Not support Memcache(d) extension.'); + } + + if ($this->memcached instanceof \Memcached) + { + $this->memcached->addServer( + $this->config['host'], $this->config['port'], $this->config['weight'] + ); + } + elseif ($this->memcached instanceof \Memcache) + { + // Third parameter is persistance and defaults to TRUE. + $this->memcached->addServer( + $this->config['host'], $this->config['port'], true, $this->config['weight'] + ); + } + } + + //-------------------------------------------------------------------- + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $key = $this->prefix . $key; + + $data = $this->memcached->get($key); + + return is_array($data) ? $data[0] : $data; + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60) + { + $key = $this->prefix . $key; + + if ( ! $this->config['raw']) + { + $value = [$value, time(), $ttl]; + } + + if ($this->memcached instanceof \Memcached) + { + return $this->memcached->set($key, $value, $ttl); + } + elseif ($this->memcached instanceof \Memcache) + { + return $this->memcached->set($key, $value, 0, $ttl); + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + $key = $this->prefix . $key; + + return $this->memcached->delete($key); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + if ( ! $this->config['raw']) + { + return false; + } + + $key = $this->prefix . $key; + + return $this->memcached->increment($key, $offset, $offset, 60); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + if ( ! $this->config['raw']) + { + return false; + } + + $key = $this->prefix . $key; + + //FIXME: third parameter isn't other handler actions. + return $this->memcached->decrement($key, $offset, $offset, 60); + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + return $this->memcached->flush(); + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return $this->memcached->getStats(); + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + $key = $this->prefix . $key; + + $stored = $this->memcached->get($key); + + // if not an array, don't try to count for PHP7.2 + if (! is_array($stored) || count($stored) !== 3) + { + return FALSE; + } + + list($data, $time, $ttl) = $stored; + + return [ + 'expire' => $time + $ttl, + 'mtime' => $time, + 'data' => $data + ]; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return (extension_loaded('memcached') || extension_loaded('memcache')); + } + +} diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php new file mode 100644 index 000000000000..f0c09b4fbe4a --- /dev/null +++ b/system/Cache/Handlers/PredisHandler.php @@ -0,0 +1,289 @@ + 'tcp', + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + ]; + + /** + * Predis connection + * + * @var Predis + */ + protected $redis; + + //-------------------------------------------------------------------- + + public function __construct($config) + { + $this->prefix = $config->prefix ?: ''; + + if (isset($config->redis)) + { + $this->config = array_merge($this->config, $config->redis); + } + } + + //-------------------------------------------------------------------- + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + try + { + // Create a new instance of Predis\Client + $this->redis = new \Predis\Client($this->config, ['prefix' => $this->prefix]); + + // Check if the connection is valid by trying to get the time. + $this->redis->time(); + } catch (\Exception $e) + { + // thrown if can't connect to redis server. + throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ')'); + } + } + + //-------------------------------------------------------------------- + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $data = array_combine( + ['__ci_type', '__ci_value'], $this->redis->hmget($key, ['__ci_type', '__ci_value']) + ); + + if ( ! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) + { + return false; + } + + switch ($data['__ci_type']) + { + case 'array': + case 'object': + return unserialize($data['__ci_value']); + case 'boolean': + case 'integer': + case 'double': // Yes, 'double' is returned and NOT 'float' + case 'string': + case 'NULL': + return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : false; + case 'resource': + default: + return false; + } + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60) + { + switch ($data_type = gettype($value)) + { + case 'array': + case 'object': + $value = serialize($value); + break; + case 'boolean': + case 'integer': + case 'double': // Yes, 'double' is returned and NOT 'float' + case 'string': + case 'NULL': + break; + case 'resource': + default: + return false; + } + + if ( ! $this->redis->hmset($key, ['__ci_type' => $data_type, '__ci_value' => $value])) + { + return false; + } + + $this->redis->expireat($key, time() + $ttl); + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + return ($this->redis->del($key) === 1); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + return $this->redis->hincrby($key, 'data', $offset); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + return $this->redis->hincrby($key, 'data', -$offset); + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + return $this->redis->flushdb(); + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return $this->redis->info(); + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + $data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value'])); + + if (isset($data['__ci_value']) && $data['__ci_value'] !== false) + { + return [ + 'expire' => time() + $this->redis->ttl($key), + 'data' => $data['__ci_value'] + ]; + } + + return FALSE; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return class_exists('\Predis\Client'); + } + +} diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php new file mode 100644 index 000000000000..c4a5984942db --- /dev/null +++ b/system/Cache/Handlers/RedisHandler.php @@ -0,0 +1,325 @@ + '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + ]; + + /** + * Redis connection + * + * @var Redis + */ + protected $redis; + + //-------------------------------------------------------------------- + + public function __construct($config) + { + $config = (array)$config; + $this->prefix = $config['prefix'] ?? ''; + + if ( ! empty($config)) + { + $this->config = array_merge($this->config, $config['redis']); + } + } + + /** + * Class destructor + * + * Closes the connection to Memcache(d) if present. + */ + public function __destruct() + { + if ($this->redis) + { + $this->redis->close(); + } + } + + //-------------------------------------------------------------------- + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + $config = $this->config; + + $this->redis = new \Redis(); + + try + { + if ( ! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout']) + ) + { +// log_message('error', 'Cache: Redis connection failed. Check your configuration.'); + } + + if (isset($config['password']) && ! $this->redis->auth($config['password'])) + { +// log_message('error', 'Cache: Redis authentication failed.'); + } + } catch (\RedisException $e) + { + throw new CriticalError('Cache: Redis connection refused (' . $e->getMessage() . ')'); + } + } + + //-------------------------------------------------------------------- + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $key = $this->prefix . $key; + + $data = $this->redis->hMGet($key, ['__ci_type', '__ci_value']); + + if ( ! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) + { + return false; + } + + switch ($data['__ci_type']) + { + case 'array': + case 'object': + return unserialize($data['__ci_value']); + case 'boolean': + case 'integer': + case 'double': // Yes, 'double' is returned and NOT 'float' + case 'string': + case 'NULL': + return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : false; + case 'resource': + default: + return false; + } + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60) + { + $key = $this->prefix . $key; + + switch ($data_type = gettype($value)) + { + case 'array': + case 'object': + $value = serialize($value); + break; + case 'boolean': + case 'integer': + case 'double': // Yes, 'double' is returned and NOT 'float' + case 'string': + case 'NULL': + break; + case 'resource': + default: + return false; + } + + if ( ! $this->redis->hMSet($key, ['__ci_type' => $data_type, '__ci_value' => $value])) + { + return false; + } + elseif ($ttl) + { + $this->redis->expireAt($key, time() + $ttl); + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + $key = $this->prefix . $key; + + return ($this->redis->delete($key) === 1); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + return $this->redis->hIncrBy($key, 'data', $offset); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + return $this->redis->hIncrBy($key, 'data', -$offset); + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + return $this->redis->flushDB(); + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return $this->redis->info(); + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + $key = $this->prefix . $key; + + $value = $this->get($key); + + if ($value !== FALSE) + { + $time = time(); + return [ + 'expire' => $time + $this->redis->ttl($key), + 'mtime' => $time, + 'data' => $value + ]; + } + + return FALSE; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return extension_loaded('redis'); + } + + //-------------------------------------------------------------------- +} diff --git a/system/Cache/Handlers/WincacheHandler.php b/system/Cache/Handlers/WincacheHandler.php new file mode 100644 index 000000000000..466f90ea0f91 --- /dev/null +++ b/system/Cache/Handlers/WincacheHandler.php @@ -0,0 +1,231 @@ +prefix = $config->prefix ?: ''; + } + + //-------------------------------------------------------------------- + + /** + * Takes care of any handler-specific setup that must be done. + */ + public function initialize() + { + // Nothing to see here... + } + + //-------------------------------------------------------------------- + + /** + * Attempts to fetch an item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function get(string $key) + { + $key = $this->prefix . $key; + + $success = false; + $data = wincache_ucache_get($key, $success); + + // Success returned by reference from wincache_ucache_get() + return ($success) ? $data : false; + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60) + { + $key = $this->prefix . $key; + + return wincache_ucache_set($key, $value, $ttl); + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + $key = $this->prefix . $key; + + return wincache_ucache_delete($key); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $success = false; + $value = wincache_ucache_inc($key, $offset, $success); + + return ($success === true) ? $value : false; + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $success = false; + $value = wincache_ucache_dec($key, $offset, $success); + + return ($success === true) ? $value : false; + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + return wincache_ucache_clear(); + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return wincache_ucache_info(true); + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + $key = $this->prefix . $key; + + if ($stored = wincache_ucache_info(false, $key)) + { + $age = $stored['ucache_entries'][1]['age_seconds']; + $ttl = $stored['ucache_entries'][1]['ttl_seconds']; + $hitcount = $stored['ucache_entries'][1]['hitcount']; + + return [ + 'expire' => $ttl - $age, + 'hitcount' => $hitcount, + 'age' => $age, + 'ttl' => $ttl, + ]; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return (extension_loaded('wincache') && ini_get('wincache.ucenabled')); + } + + //-------------------------------------------------------------------- +} diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 6edf0b6d8690..4fbe9b638900 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,73 +27,79 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2018 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 * @filesource */ - - +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\Request; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Services; +use Config\Cache; +use CodeIgniter\HTTP\URI; +use CodeIgniter\Debug\Timer; +use CodeIgniter\Events\Events; +use CodeIgniter\HTTP\Response; +use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\Router\RouteCollectionInterface; -use Config\App; -use CodeIgniter\Services; -use CodeIgniter\Hooks\Hooks; +use CodeIgniter\Exceptions\PageNotFoundException; /** - * System Initialization Class - * - * Loads the base classes and executes the request. + * This class is the core of the framework, and will analyse the + * request, route it to a controller, and send back the response. + * Of course, there are variations to that flow, but this is the brains. */ class CodeIgniter { + /** * The current version of CodeIgniter Framework */ - const CI_VERSION = '4.0-dev'; + const CI_VERSION = '4.0.0-alpha.1'; /** - * UNIX timestamp for the start of script execution - * in seconds with microseconds. - * - * @var float + * App startup time. + * @var mixed */ - protected $startMemory; + protected $startTime; /** - * App start time - * + * Total app execution time * @var float */ - protected $startTime; + protected $totalTime; /** - * The application configuration object. - * + * Main application configuration * @var \Config\App */ protected $config; + /** + * Timer instance. + * @var Timer + */ + protected $benchmark; + /** * Current request. - * - * @var \CodeIgniter\HTTP\Request + * @var HTTP\Request|HTTP\IncomingRequest|CLIRequest */ protected $request; /** * Current response. - * - * @var \CodeIgniter\HTTP\Response + * @var HTTP\Response */ protected $response; /** * Router to use. - * - * @var \CodeIgniter\Router\Router + * @var Router\Router */ protected $router; @@ -105,7 +111,6 @@ class CodeIgniter /** * Controller method to invoke. - * * @var string */ protected $method; @@ -116,99 +121,287 @@ class CodeIgniter */ protected $output; - //-------------------------------------------------------------------- + /** + * Cache expiration time + * @var int + */ + protected static $cacheTTL = 0; /** - * CodeIgniter constructor. - * - * @param int $startMemory - * @param float $startTime - * @param App $config + * Request path to use. + * @var string + */ + protected $path; + + /** + * Should the Response instance "pretend" + * to keep from setting headers/cookies/etc + * @var bool */ - public function __construct(int $startMemory, float $startTime, App $config) + protected $useSafeOutput = false; + + //-------------------------------------------------------------------- + + public function __construct($config) { - $this->startMemory = $startMemory; - $this->startTime = $startTime; + $this->startTime = microtime(true); $this->config = $config; } //-------------------------------------------------------------------- /** - * The class entry point. This is where the magic happens and all - * of the framework pieces are pulled together and shown how to - * make beautiful music together. Or something like that. :) - * - * @param RouteCollectionInterface $routes + * Handles some basic app and environment setup. */ - public function run(RouteCollectionInterface $routes = null) + public function initialize() { - $this->startBenchmark(); + // Set default timezone on the server + date_default_timezone_set($this->config->appTimezone ?? 'UTC'); - //-------------------------------------------------------------------- - // Is there a "pre-system" hook? - //-------------------------------------------------------------------- - Hooks::trigger('pre_system'); + // Setup Exception Handling + Services::exceptions() + ->initialize(); - $this->getRequestObject(); - $this->getResponseObject(); - $this->forceSecureAccess(); + $this->detectEnvironment(); + $this->bootstrapEnvironment(); - try + if (CI_DEBUG) { - $this->tryToRouteIt($routes); + require_once BASEPATH . 'ThirdParty/Kint/kint.php'; + } + } + + //-------------------------------------------------------------------- + + /** + * Launch the application! + * + * This is "the loop" if you will. The main entry point into the script + * that gets the required class instances, fires off the filters, + * tries to route the response, loads the controller and generally + * makes all of the pieces work together. + * + * @param \CodeIgniter\Router\RouteCollectionInterface $routes + * @param bool $returnResponse + * + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function run(RouteCollectionInterface $routes = null, bool $returnResponse = false) + { + $this->startBenchmark(); - //-------------------------------------------------------------------- - // Are there any "pre-controller" hooks? - //-------------------------------------------------------------------- - Hooks::trigger('pre_controller'); + $this->getRequestObject(); + $this->getResponseObject(); - $this->startController(); + $this->forceSecureAccess(); - // Closure controller has run in startController(). - if ( ! is_callable($this->controller)) - { - $controller = $this->createController(); + $this->spoofRequestMethod(); - //-------------------------------------------------------------------- - // Is there a "post_controller_constructor" hook? - //-------------------------------------------------------------------- - Hooks::trigger('post_controller_constructor'); + Events::trigger('pre_system'); - $this->runController($controller); + // Check for a cached page. Execution will stop + // if the page has been cached. + $cacheConfig = new Cache(); + $response = $this->displayCache($cacheConfig); + if ($response instanceof ResponseInterface) + { + if ($returnResponse) + { + return $response; } - //-------------------------------------------------------------------- - // Is there a "post_controller" hook? - //-------------------------------------------------------------------- - Hooks::trigger('post_controller'); - - $this->gatherOutput(); - $this->sendResponse(); + $this->response->pretend($this->useSafeOutput)->send(); + $this->callExit(EXIT_SUCCESS); + } - //-------------------------------------------------------------------- - // Is there a post-system hook? - //-------------------------------------------------------------------- - Hooks::trigger('post_system'); + try + { + return $this->handleRequest($routes, $cacheConfig, $returnResponse); } catch (Router\RedirectException $e) { $logger = Services::logger(); - $logger->info('REDIRECTED ROUTE at '.$e->getMessage()); + $logger->info('REDIRECTED ROUTE at ' . $e->getMessage()); // If the route is a 'redirect' route, it throws // the exception with the $to as the message $this->response->redirect($e->getMessage(), 'auto', $e->getCode()); $this->callExit(EXIT_SUCCESS); } - // Catch Response::redirect() - catch (HTTP\RedirectException $e) + catch (PageNotFoundException $e) + { + $this->display404errors($e); + } + } + + //-------------------------------------------------------------------- + + /** + * Set our Response instance to "pretend" mode so that things like + * cookies and headers are not actually sent, allowing PHP 7.2+ to + * not complain when ini_set() function is used. + * + * @param bool $safe + * + * @return $this + */ + public function useSafeOutput(bool $safe = true) + { + $this->useSafeOutput = $safe; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Handles the main request logic and fires the controller. + * + * @param \CodeIgniter\Router\RouteCollectionInterface $routes + * @param $cacheConfig + * @param bool $returnResponse + * + * @return \CodeIgniter\HTTP\RequestInterface|\CodeIgniter\HTTP\Response|\CodeIgniter\HTTP\ResponseInterface|mixed + * @throws \CodeIgniter\Filters\Exceptions\FilterException + */ + protected function handleRequest(RouteCollectionInterface $routes = null, $cacheConfig, bool $returnResponse = false) + { + $routeFilter = $this->tryToRouteIt($routes); + + // Run "before" filters + $filters = Services::filters(); + + // If any filters were specified within the routes file, + // we need to ensure it's active for the current request (before only) + if (! is_null($routeFilter)) + { + $filters->enableFilter($routeFilter, 'before'); + } + + $uri = $this->request instanceof CLIRequest ? $this->request->getPath() : $this->request->uri->getPath(); + + $possibleRedirect = $filters->run($uri, 'before'); + if($possibleRedirect instanceof RedirectResponse) { + return $possibleRedirect; + } + // If a Response instance is returned, the Response will be sent back to the client and script execution will stop + if($possibleRedirect instanceof ResponseInterface) + { + return $possibleRedirect->send(); + } + + $returned = $this->startController(); + + // Closure controller has run in startController(). + if ( ! is_callable($this->controller)) + { + $controller = $this->createController(); + + // Is there a "post_controller_constructor" event? + Events::trigger('post_controller_constructor'); + + $returned = $this->runController($controller); + } + else + { + $this->benchmark->stop('controller_constructor'); + $this->benchmark->stop('controller'); + } + + // Handle any redirects + if ($returned instanceof RedirectResponse) + { + if ($returnResponse) + { + return $returned; + } + $this->callExit(EXIT_SUCCESS); } - catch (PageNotFoundException $e) + + // If $returned is a string, then the controller output something, + // probably a view, instead of echoing it directly. Send it along + // so it can be used with the output. + $this->gatherOutput($cacheConfig, $returned); + + // Run "after" filters + $response = $filters->run($uri, 'after'); + + if ($response instanceof Response) { - $this->display404errors($e); + $this->response = $response; + } + + // Save our current URI as the previous URI in the session + // for safer, more accurate use with `previous_url()` helper function. + $this->storePreviousURL($this->request->uri ?? $uri); + + unset($uri); + + if (! $returnResponse) + { + $this->sendResponse(); + } + + //-------------------------------------------------------------------- + // Is there a post-system event? + //-------------------------------------------------------------------- + Events::trigger('post_system'); + + return $this->response; + } + + //-------------------------------------------------------------------- + + /** + * You can load different configurations depending on your + * current environment. Setting the environment also influences + * things like logging and error reporting. + * + * This can be set to anything, but default usage is: + * + * development + * testing + * production + */ + protected function detectEnvironment() + { + // Make sure ENVIRONMENT isn't already set by other means. + if (! defined('ENVIRONMENT')) + { + // running under Continuous Integration server? + if (getenv('CI') !== false) + { + define('ENVIRONMENT', 'testing'); + } + else + { + define('ENVIRONMENT', $_SERVER['CI_ENVIRONMENT'] ?? 'production'); + } + } + } + + //-------------------------------------------------------------------- + + /** + * Load any custom boot files based upon the current environment. + * + * If no boot file exists, we shouldn't continue because something + * is wrong. At the very least, they should have error reporting setup. + */ + protected function bootstrapEnvironment() + { + if (file_exists(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) + { + require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'; + } + else + { + header('HTTP/1.1 503 Service Unavailable.', true, 503); + echo 'The application environment is not set correctly.'; + exit(1); // EXIT_ERROR } } @@ -216,7 +409,7 @@ public function run(RouteCollectionInterface $routes = null) /** * Start the Benchmark - * + * * The timer is used to display total script execution both in the * debug toolbar, and potentially on the displayed page. */ @@ -231,6 +424,23 @@ protected function startBenchmark() //-------------------------------------------------------------------- + /** + * Sets a Request object to be used for this request. + * Used when running certain tests. + * + * @param \CodeIgniter\HTTP\Request $request + * + * @return \CodeIgniter\CodeIgniter + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Get our Request object, (either IncomingRequest or CLIRequest) * and set the server protocol based on the information provided @@ -238,14 +448,20 @@ protected function startBenchmark() */ protected function getRequestObject() { - if (is_cli()) + if ($this->request instanceof Request) + { + return; + } + + if (is_cli() && ! (ENVIRONMENT == 'testing')) { $this->request = Services::clirequest($this->config); } else { $this->request = Services::request($this->config); - $this->request->setProtocolVersion($_SERVER['SERVER_PROTOCOL']); + // guess at protocol if needed + $this->request->setProtocolVersion($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'); } } @@ -259,7 +475,7 @@ protected function getResponseObject() { $this->response = Services::response($this->config); - if ( ! is_cli()) + if ( ! is_cli() || ENVIRONMENT == 'testing') { $this->response->setProtocolVersion($this->request->getProtocolVersion()); } @@ -293,19 +509,148 @@ protected function forceSecureAccess($duration = 31536000) //-------------------------------------------------------------------- /** - * CSRF Protection. Checks if it's enabled globally, and - * enforces the presence of CSRF tokens. + * Determines if a response has been cached for the given URI. + * + * @param \Config\Cache $config + * + * @throws \Exception + * + * @return bool */ - protected function CsrfProtection() + public function displayCache($config) { - if ($this->config->CSRFProtection !== true || is_cli()) + if ($cachedResponse = cache()->get($this->generateCacheName($config))) { - return; + $cachedResponse = unserialize($cachedResponse); + if ( ! is_array($cachedResponse) || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers'])) + { + throw new \Exception("Error unserializing page cache"); + } + + $headers = $cachedResponse['headers']; + $output = $cachedResponse['output']; + + // Clear all default headers + foreach ($this->response->getHeaders() as $key => $val) + { + $this->response->removeHeader($key); + } + + // Set cached headers + foreach ($headers as $name => $value) + { + $this->response->setHeader($name, $value); + } + + $output = $this->displayPerformanceMetrics($output); + $this->response->setBody($output); + + return $this->response; + }; + } + + //-------------------------------------------------------------------- + + /** + * Tells the app that the final output should be cached. + * + * @param int $time + * + * @return $this + */ + public static function cache(int $time) + { + self::$cacheTTL = $time; + } + + //-------------------------------------------------------------------- + + /** + * Caches the full response from the current request. Used for + * full-page caching for very high performance. + * + * @param \Config\Cache $config + * + * @return mixed + */ + public function cachePage(Cache $config) + { + $headers = []; + foreach ($this->response->getHeaders() as $header) + { + $headers[$header->getName()] = $header->getValueLine(); + } + + return cache()->save( + $this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), self::$cacheTTL + ); + } + + //-------------------------------------------------------------------- + + /** + * Returns an array with our basic performance stats collected. + * + * @return array + */ + public function getPerformanceStats() + { + return [ + 'startTime' => $this->startTime, + 'totalTime' => $this->totalTime, + ]; + } + + //-------------------------------------------------------------------- + + /** + * Generates the cache name to use for our full-page caching. + * + * @param $config + * + * @return string + */ + protected function generateCacheName($config): string + { + if (is_cli() && ! (ENVIRONMENT == 'testing')) + { + return md5($this->request->getPath()); } - $security = Services::security($this->config); + $uri = $this->request->uri; + + if ($config->cacheQueryString) + { + $name = URI::createURIString( + $uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery() + ); + } + else + { + $name = URI::createURIString( + $uri->getScheme(), $uri->getAuthority(), $uri->getPath() + ); + } - $security->CSRFVerify($this->request); + return md5($name); + } + + //-------------------------------------------------------------------- + + /** + * Replaces the memory_usage and elapsed_time tags. + * + * @param string $output + * + * @return string + */ + public function displayPerformanceMetrics(string $output): string + { + $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); + + $output = str_replace('{elapsed_time}', $this->totalTime, $output); + + return $output; } //-------------------------------------------------------------------- @@ -317,18 +662,20 @@ protected function CsrfProtection() * * @param RouteCollectionInterface $routes An collection interface to use in place * of the config file. + * + * @return array */ protected function tryToRouteIt(RouteCollectionInterface $routes = null) { if (empty($routes) || ! $routes instanceof RouteCollectionInterface) { - require APPPATH.'Config/Routes.php'; + require APPPATH . 'Config/Routes.php'; } // $routes is defined in Config/Routes.php $this->router = Services::router($routes); - $path = is_cli() ? $this->request->getPath() : $this->request->uri->getPath(); + $path = $this->determinePath(); $this->benchmark->stop('bootstrap'); $this->benchmark->start('routing'); @@ -336,9 +683,53 @@ protected function tryToRouteIt(RouteCollectionInterface $routes = null) ob_start(); $this->controller = $this->router->handle($path); - $this->method = $this->router->methodName(); + $this->method = $this->router->methodName(); + + // If a {locale} segment was matched in the final route, + // then we need to set the correct locale on our Request. + if ($this->router->hasLocale()) + { + $this->request->setLocale($this->router->getLocale()); + } $this->benchmark->stop('routing'); + + return $this->router->getFilter(); + } + + //-------------------------------------------------------------------- + + /** + * Determines the path to use for us to try to route to, based + * on user input (setPath), or the CLI/IncomingRequest path. + */ + protected function determinePath() + { + if ( ! empty($this->path)) + { + return $this->path; + } + + return (is_cli() && ! (ENVIRONMENT == 'testing')) ? $this->request->getPath() : $this->request->uri->getPath(); + } + + //-------------------------------------------------------------------- + + /** + * Allows the request path to be set from outside the class, + * instead of relying on CLIRequest or IncomingRequest for the path. + * + * This is primarily used by the Console. + * + * @param string $path + * + * @return $this + */ + public function setPath(string $path) + { + $this->path = $path; + + return $this; } //-------------------------------------------------------------------- @@ -354,31 +745,28 @@ protected function startController() $this->benchmark->start('controller_constructor'); // Is it routed to a Closure? - if (is_callable($this->controller)) + if (is_object($this->controller) && (get_class($this->controller) == 'Closure')) { $controller = $this->controller; - echo $controller(...$this->router->params()); + return $controller(...$this->router->params()); } - else + + // No controller specified - we don't know what to do now. + if (empty($this->controller)) { - if (empty($this->controller)) - { - throw new PageNotFoundException('Controller is empty.'); - } - else - { - // Try to autoload the class - if ( ! class_exists($this->controller, true) || $this->method[0] === '_') - { - throw new PageNotFoundException('Controller or its method is not found.'); - } - else if ( ! method_exists($this->controller, '_remap') && - ! is_callable([$this->controller, $this->method], false) - ) - { - throw new PageNotFoundException('Controller method is not found.'); - } - } + throw PageNotFoundException::forEmptyController(); + } + + // Try to autoload the class + if ( ! class_exists($this->controller, true) || $this->method[0] === '_') + { + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); + } + else if ( ! method_exists($this->controller, '_remap') && + ! is_callable([$this->controller, $this->method], false) + ) + { + throw PageNotFoundException::forMethodNotFound($this->method); } } @@ -391,7 +779,8 @@ protected function startController() */ protected function createController() { - $class = new $this->controller($this->request, $this->response); + $class = new $this->controller(); + $class->initController($this->request, $this->response, Services::logger()); $this->benchmark->stop('controller_constructor'); @@ -404,19 +793,23 @@ protected function createController() * Runs the controller, allowing for _remap methods to function. * * @param mixed $class + * + * @return mixed */ protected function runController($class) { if (method_exists($class, '_remap')) { - $class->_remap($this->method, ...$this->router->params()); + $output = $class->_remap($this->method, ...$this->router->params()); } else { - $class->{$this->method}(...$this->router->params()); + $output = $class->{$this->method}(...$this->router->params()); } $this->benchmark->stop('controller'); + + return $output; } //-------------------------------------------------------------------- @@ -442,7 +835,7 @@ protected function display404errors(PageNotFoundException $e) $this->benchmark->start('controller_constructor'); $this->controller = $override[0]; - $this->method = $override[1]; + $this->method = $override[1]; unset($override); @@ -457,83 +850,132 @@ protected function display404errors(PageNotFoundException $e) } // Display 404 Errors - $this->response->setStatusCode(404); + $this->response->setStatusCode($e->getCode()); - if (ENVIRONMENT !== 'testing') { - if (ob_get_level() > 0) { + if (ENVIRONMENT !== 'testing') + { + if (ob_get_level() > 0) + { ob_end_flush(); } } else { // When testing, one is for phpunit, another is for test case. - if (ob_get_level() > 2) { + if (ob_get_level() > 2) + { ob_end_flush(); } } - ob_start(); + throw PageNotFoundException::forPageNotFound($e->getMessage()); + } - // These might show as unused here - but don't delete! - // They are used within the view files. - $heading = 'Page Not Found'; - $message = $e->getMessage(); + //-------------------------------------------------------------------- - // Show the 404 error page - if (is_cli()) + /** + * Gathers the script output from the buffer, replaces some execution + * time tag in the output and displays the debug toolbar, if required. + * + * @param null $cacheConfig + * @param null $returned + */ + protected function gatherOutput($cacheConfig = null, $returned = null) + { + $this->output = ob_get_contents(); + // If buffering is not null. + // Clean (erase) the output buffer and turn off output buffering + if (ob_get_length()) { - require APPPATH.'Views/errors/cli/error_404.php'; + ob_end_clean(); } - else + + // If the controller returned a response object, + // we need to grab the body from it so it can + // be added to anything else that might have been + // echoed already. + // We also need to save the instance locally + // so that any status code changes, etc, take place. + if ($returned instanceof Response) { - require APPPATH.'Views/errors/html/error_404.php'; + $this->response = $returned; + $returned = $returned->getBody(); } - $buffer = ob_get_contents(); - ob_end_clean(); + if (is_string($returned)) + { + $this->output .= $returned; + } - echo $buffer; - $this->callExit(EXIT_UNKNOWN_FILE); // Unknown file + // Cache it without the performance metrics replaced + // so that we can have live speed updates along the way. + if (self::$cacheTTL > 0) + { + $this->cachePage($cacheConfig); + } + + $this->output = $this->displayPerformanceMetrics($this->output); + + $this->response->setBody($this->output); } //-------------------------------------------------------------------- /** - * Gathers the script output from the buffer, replaces some execution - * time tag in the output and displays the debug toolbar, if required. + * If we have a session object to use, store the current URI + * as the previous URI. This is called just prior to sending the + * response to the client, and will make it available next request. + * + * This helps provider safer, more reliable previous_url() detection. + * + * @param \CodeIgniter\HTTP\URI $uri */ - protected function gatherOutput() + public function storePreviousURL($uri) { - $this->output = ob_get_contents(); - ob_end_clean(); - - $totalTime = $this->benchmark->getElapsedTime('total_execution'); - - $this->output = str_replace('{elapsed_time}', $totalTime, $this->output); + // This is mainly needed during testing... + if (is_string($uri)) + { + $uri = new URI($uri); + } - //-------------------------------------------------------------------- - // Display the Debug Toolbar? - //-------------------------------------------------------------------- - if ( ! is_cli() && ENVIRONMENT != 'production' && $this->config->toolbarEnabled) + if (isset($_SESSION)) { - $toolbar = Services::toolbar($this->config); - $this->output .= $toolbar->run($this->startTime, $totalTime, - $this->startMemory, $this->request, - $this->response); + $_SESSION['_ci_previous_url'] = (string) $uri; } } //-------------------------------------------------------------------- + /** + * Modifies the Request Object to use a different method if a POST + * variable called _method is found. + * + * Does not work on CLI commands. + */ + public function spoofRequestMethod() + { + if (is_cli()) + return; + + // Only works with POSTED forms + if ($this->request->getMethod() !== 'post') + return; + + $method = $this->request->getPost('_method'); + + if (empty($method)) + return; + + $this->request = $this->request->setMethod($method); + } + /** * Sends the output of this request back to the client. * This is what they've been waiting for! */ protected function sendResponse() { - $this->response->setBody($this->output); - - $this->response->send(); + $this->response->pretend($this->useSafeOutput)->send(); } //-------------------------------------------------------------------- diff --git a/system/Commands/Database/CreateMigration.php b/system/Commands/Database/CreateMigration.php new file mode 100644 index 000000000000..0717fd7be630 --- /dev/null +++ b/system/Commands/Database/CreateMigration.php @@ -0,0 +1,178 @@ + 'The migration file name' + ]; + + /** + * the Command's Options + * + * @var array + */ + protected $options = [ + '-n' => 'Set migration namespace' + ]; + + /** + * Creates a new migration file with the current timestamp. + * + * @todo Have this check the settings and see what type of file it should create (timestamp or sequential) + * + * @param array $params + */ + public function run(array $params = []) + { + + $name = array_shift($params); + + if (empty($name)) + { + $name = CLI::prompt(lang('Migrations.nameMigration')); + } + + if (empty($name)) + { + CLI::error(lang('Migrations.badCreateName')); + return; + } + $ns = CLI::getOption('n'); + $homepath = APPPATH; + + if ( ! empty($ns)) + { + // Get all namespaces form PSR4 paths. + $config = new Autoload(); + $namespaces = $config->psr4; + + foreach ($namespaces as $namespace => $path) + { + + if ($namespace == $ns) + { + $homepath = realpath($path); + break; + } + } + } + else + { + $ns = "App"; + } + + $path = $homepath . '/Database/Migrations/' . date('YmdHis_') . $name . '.php'; + + $template = << 'Set database group', + ]; + + /** + * Migrates us up or down to the version specified as $currentVersion + * in the migrations config file. + * + * @param array $params + */ + public function run(array $params = []) + { + $runner = Services::migrations(); + + CLI::write(lang('Migrations.toVersion'), 'yellow'); + + $group = CLI::getOption('g'); + try + { + $runner->current($group); + $messages = $runner->getCliMessages(); + foreach ($messages as $message) + { + CLI::write($message); + } + + CLI::write('Done'); + + } catch (\Exception $e) + { + $this->showError($e); + } + + + } + +} diff --git a/system/Commands/Database/MigrateLatest.php b/system/Commands/Database/MigrateLatest.php new file mode 100644 index 000000000000..bf738a89fcd2 --- /dev/null +++ b/system/Commands/Database/MigrateLatest.php @@ -0,0 +1,136 @@ + 'Set migration namespace', + '-g' => 'Set database group', + '-all' => 'Set latest for all namespace, will ignore (-n) option', + ]; + + /** + * Ensures that all migrations have been run. + * + * @param array $params + */ + public function run(array $params = []) + { + $runner = Services::migrations(); + + CLI::write(lang('Migrations.toLatest'), 'yellow'); + + $namespace = CLI::getOption('n'); + $group = CLI::getOption('g'); + + try + { + if ( ! is_null(CLI::getOption('all'))) + { + $runner->latestAll($group); + } + else + { + $runner->latest($namespace, $group); + } + $messages = $runner->getCliMessages(); + foreach ($messages as $message) + { + CLI::write($message); + } + + CLI::write('Done'); + + } catch (\Exception $e) + { + $this->showError($e); + } + + + } + +} diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php new file mode 100644 index 000000000000..18ce467fbd3f --- /dev/null +++ b/system/Commands/Database/MigrateRefresh.php @@ -0,0 +1,108 @@ + 'Set migration namespace', + '-g' => 'Set database group', + '-all' => 'Set latest for all namespace, will ignore (-n) option' + ]; + + /** + * Does a rollback followed by a latest to refresh the current state + * of the database. + * + * @param array $params + */ + public function run(array $params = []) + { + $this->call('migrate:rollback'); + $this->call('migrate:latest'); + } + +} diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php new file mode 100644 index 000000000000..f82dfd4ea156 --- /dev/null +++ b/system/Commands/Database/MigrateRollback.php @@ -0,0 +1,154 @@ + 'Set migration namespace', + '-g' => 'Set database group', + '-all' => 'Set latest for all namespace, will ignore (-n) option', + ]; + + /** + * Runs all of the migrations in reverse order, until they have + * all been un-applied. + * + * @param array $params + */ + public function run(array $params = []) + { + $runner = Services::migrations(); + + CLI::write(lang('Migrations.rollingBack'), 'yellow'); + $group = CLI::getOption('g'); + if ( ! is_null($group)) + { + $runner->setGroup($group); + } + try + { + if (is_null(CLI::getOption('all'))) + { + $namespace = CLI::getOption('n'); + $runner->version(0, $namespace); + } + else + { + // Get all namespaces form PSR4 paths. + $config = new Autoload(); + $namespaces = $config->psr4; + foreach ($namespaces as $namespace => $path) + { + $runner->setNamespace($namespace); + $migrations = $runner->findMigrations(); + if (empty($migrations)) + { + continue; + } + $runner->version(0, $namespace, $group); + } + } + $messages = $runner->getCliMessages(); + foreach ($messages as $message) + { + CLI::write($message); + } + + CLI::write('Done'); + + } catch (\Exception $e) + { + $this->showError($e); + } + + + } + +} diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php new file mode 100644 index 000000000000..b51dfb7a3a80 --- /dev/null +++ b/system/Commands/Database/MigrateStatus.php @@ -0,0 +1,171 @@ + 'Set database group', + ]; + + protected $ignoredNamespaces = [ + 'CodeIgniter', + 'Config', + 'Tests\Support' + ]; + + /** + * Displays a list of all migrations and whether they've been run or not. + * + * @param array $params + */ + public function run(array $params = []) + { + $runner = Services::migrations(); + + if ( ! is_null(CLI::getOption('g'))) + { + $runner->setGroup(CLI::getOption('g')); + } + + // Get all namespaces form PSR4 paths. + $config = new Autoload(); + $namespaces = $config->psr4; + + // Loop for all $namespaces + foreach ($namespaces as $namespace => $path) + { + if (in_array($namespace, $this->ignoredNamespaces)) + { + continue; + } + + $runner->setNamespace($namespace); + $migrations = $runner->findMigrations(); + $history = $runner->getHistory(); + + CLI::write($namespace); + + if (empty($migrations)) + { + CLI::error(lang('Migrations.noneFound')); + continue; + } + + ksort($migrations); + + $max = 0; + foreach ($migrations as $version => $migration) + { + $file = substr($migration->name, strpos($migration->name, $version . '_')); + $migrations[$version]->name = $file; + + $max = max($max, strlen($file)); + } + + CLI::write(' '. str_pad(lang('Migrations.filename'), $max + 4) . lang('Migrations.on'), 'yellow'); + + + foreach ($migrations as $version => $migration) + { + $date = ''; + foreach ($history as $row) + { + if ($row['version'] != $version) + { + continue; + } + + $date = date("Y-m-d H:i:s", $row['time']); + } + CLI::write(str_pad(' '.$migration->name, $max + 6) . ($date ? $date : '---')); + } + } + } + +} diff --git a/system/Commands/Database/MigrateVersion.php b/system/Commands/Database/MigrateVersion.php new file mode 100644 index 000000000000..68b7d2324dce --- /dev/null +++ b/system/Commands/Database/MigrateVersion.php @@ -0,0 +1,137 @@ + 'The version number to migrate', + ]; + + /** + * the Command's Options + * + * @var array + */ + protected $options = [ + '-n' => 'Set migration namespace', + '-g' => 'Set database group', + ]; + + /** + * Migrates the database up or down to get to the specified version. + * + * @param array $params + */ + public function run(array $params = []) + { + $runner = Services::migrations(); + + // Get the version number + $version = array_shift($params); + + if (is_null($version)) + { + $version = CLI::prompt(lang('Migrations.version')); + } + + if (is_null($version)) + { + CLI::error(lang('Migrations.invalidVersion')); + exit(); + } + + CLI::write(sprintf(lang('Migrations.toVersionPH'), $version), 'yellow'); + + $namespace = CLI::getOption('n'); + $group = CLI::getOption('g'); + try + { + $runner->version($version, $namespace, $group); + CLI::write('Done'); + } catch (\Exception $e) + { + $this->showError($e); + } + + + } + +} diff --git a/system/Commands/Database/Seed.php b/system/Commands/Database/Seed.php new file mode 100644 index 000000000000..c6b39566f14a --- /dev/null +++ b/system/Commands/Database/Seed.php @@ -0,0 +1,128 @@ + 'The seeder name to run' + ]; + + /** + * the Command's Options + * + * @var array + */ + protected $options = []; + + /** + * Runs all of the migrations in reverse order, until they have + * all been un-applied. + * + * @param array $params + */ + public function run(array $params = []) + { + $seeder = new Seeder(new \Config\Database()); + + $seedName = array_shift($params); + + if (empty($seedName)) + { + $seedName = CLI::prompt(lang('Migrations.migSeeder'), 'DatabaseSeeder'); + } + + if (empty($seedName)) + { + CLI::error(lang('Migrations.migMissingSeeder')); + return; + } + + try + { + $seeder->call($seedName); + } catch (\Exception $e) + { + $this->showError($e); + } + } + +} diff --git a/system/Commands/Help.php b/system/Commands/Help.php new file mode 100644 index 000000000000..2e39bb18561c --- /dev/null +++ b/system/Commands/Help.php @@ -0,0 +1,118 @@ + 'The command name [default: "help"]' + ]; + + /** + * the Command's Options + * + * @var array + */ + protected $options = []; + + //-------------------------------------------------------------------- + + /** + * Displays the help for the spark cli script itself. + * + * @param array $params + */ + public function run(array $params) + { + $command = array_shift($params); + if (is_null($command)) + { + $command = 'help'; + } + + $commands = $this->commands->getCommands(); + $class = new $commands[$command]['class']($this->logger, $this->commands); + + $class->showHelp(); + } + +} diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php new file mode 100644 index 000000000000..d17c349cfe00 --- /dev/null +++ b/system/Commands/ListCommands.php @@ -0,0 +1,193 @@ +commands->getCommands(); + + $this->describeCommands($commands); + + CLI::newLine(); + } + + //-------------------------------------------------------------------- + + /** + * Displays the commands on the CLI. + * + * @param array $commands + */ + protected function describeCommands(array $commands = []) + { + ksort($commands); + + // Sort into buckets by group + $sorted = []; + $maxTitleLength = 0; + + foreach ($commands as $title => $command) + { + if (! isset($sorted[$command['group']])) + { + $sorted[$command['group']] = []; + } + + $sorted[$command['group']][$title] = $command; + + $maxTitleLength = max($maxTitleLength, strlen($title)); + } + + ksort($sorted); + + // Display it all... + foreach ($sorted as $group => $items) + { + CLI::newLine(); + CLI::write($group); + + foreach ($items as $title => $item) + { + $title = $this->padTitle($title, $maxTitleLength, 2, 2); + + $out = CLI::color($title, 'yellow'); + + if (isset($item['description'])) + { + $out .= CLI::wrap($item['description'], 125, strlen($title)); + } + + CLI::write($out); + } + } + } + + //-------------------------------------------------------------------- + + /** + * Pads our string out so that all titles are the same length to nicely line up descriptions. + * + * @param string $item + * @param $max + * @param int $extra // How many extra spaces to add at the end + * @param int $indent + * + * @return array + */ + protected function padTitle(string $item, $max, $extra = 2, $indent = 0) + { + $max += $extra + $indent; + + $item = str_repeat(' ', $indent) . $item; + $item = str_pad($item, $max); + + return $item; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Commands/MigrationsCommand.php b/system/Commands/MigrationsCommand.php deleted file mode 100644 index 49a0d8db5f3d..000000000000 --- a/system/Commands/MigrationsCommand.php +++ /dev/null @@ -1,281 +0,0 @@ -runner = Services::migrations(); - } - - //-------------------------------------------------------------------- - - /** - * Provides a list of available commands. - */ - public function index() - { - CLI::write('Migration Commands', 'white'); - CLI::write(CLI::color('latest', 'yellow'). "\t\tMigrates database to latest available migration."); - CLI::write(CLI::color('current', 'yellow'). "\t\tMigrates database to version set as 'current' in configuration."); - CLI::write(CLI::color('version [v]', 'yellow'). "\tMigrates database to version {v}."); - CLI::write(CLI::color('rollback', 'yellow'). "\tRuns all migrations 'down' to version 0."); - CLI::write(CLI::color('refresh', 'yellow'). "\t\tUninstalls and re-runs all migrations to freshen database."); - CLI::write(CLI::color('seed [name]', 'yellow'). "\tRuns the seeder named [name]."); - } - - //-------------------------------------------------------------------- - - - /** - * Ensures that all migrations have been run. - */ - public function latest() - { - CLI::write('Migrating to latest version...', 'yellow'); - - try { - $this->runner->latest(); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Migrates the database up or down to get to the specified version. - * - * @param int $version - */ - public function version(int $version = null) - { - if (is_null($version)) - { - $version = CLI::prompt('Version'); - } - - if (is_null($version)) - { - CLI::error('Invalid version number provided.'); - exit(); - } - - CLI::write("Migrating to version {$version}...", 'yellow'); - - try { - $this->runner->version($version); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Migrates us up or down to the version specified as $currentVersion - * in the migrations config file. - */ - public function current() - { - CLI::write("Migrating to current version...", 'yellow'); - - try { - $this->runner->current(); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Runs all of the migrations in reverse order, until they have - * all been un-applied. - */ - public function rollback() - { - CLI::write("Rolling back all migrations...", 'yellow'); - - try { - $this->runner->version(0); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Does a rollback followed by a latest to refresh the current state - * of the database. - */ - public function refresh() - { - $this->rollback(); - $this->latest(); - } - - //-------------------------------------------------------------------- - - /** - * Displays a list of all migrations and whether they've been run or not. - */ - public function status() - { - $migrations = $this->runner->findMigrations(); - $history = $this->runner->getHistory(); - - if (empty($migrations)) - { - return CLI::error('No migrations were found.'); - } - - $max = 0; - - foreach ($migrations as $version => $file) - { - $file = substr($file, strpos($file, $version.'_')); - $migrations[$version] = $file; - - $max = max($max, strlen($file)); - } - - CLI::write(str_pad('Filename', $max+4).'Migrated On', 'yellow'); - - foreach ($migrations as $version => $file) - { - $date = ''; - foreach ($history as $row) - { - if ($row['version'] != $version) continue; - - $date = $row['time']; - } - - CLI::write(str_pad($file, $max+4). ($date ? $date : '---')); - } - } - - //-------------------------------------------------------------------- - - /** - * Runs the specified Seeder file to populate the database - * with some data. - * - * @param string $seedName - */ - public function seed(string $seedName = null) - { - $seeder = new Seeder(new \Config\Database()); - - if (empty($seedName)) - { - $seedName = CLI::prompt('Seeder name'); - } - - if (empty($seedName)) - { - CLI::error('You must provide a seeder name.'); - return; - } - - try - { - $seeder->call($seedName); - } - catch (\Exception $e) - { - $this->showError($e); - } - } - - //-------------------------------------------------------------------- - - /** - * Displays a caught exception. - * - * @param \Exception $e - */ - protected function showError(\Exception $e) - { - CLI::error($e->getMessage()); - CLI::write($e->getFile().' - '.$e->getLine(), 'white'); - } - - //-------------------------------------------------------------------- - -} diff --git a/system/Commands/Server/Serve.php b/system/Commands/Server/Serve.php new file mode 100644 index 000000000000..849abef0fdf4 --- /dev/null +++ b/system/Commands/Server/Serve.php @@ -0,0 +1,85 @@ + 'The PHP Binary [default: "PHP_BINARY"]', + '-host' => 'The HTTP Host [default: "localhost"]', + '-port' => 'The HTTP Host Port [default: "8080"]', + ]; + + public function run(array $params) + { + // Collect any user-supplied options and apply them + $php = CLI::getOption('php') ?? PHP_BINARY; + $host = CLI::getOption('host') ?? 'localhost'; + $port = CLI::getOption('port') ?? '8080'; + + // Get the party started + CLI::write("CodeIgniter development server started on http://{$host}:{$port}", 'green'); + CLI::write('Press Control-C to stop.'); + + // Set the Front Controller path as Document Root + $docroot = FCPATH; + + // Mimic Apache's mod_rewrite functionality with user settings + $rewrite = __DIR__ . '/rewrite.php'; + + // Call PHP's built-in webserver, making sure to set our + // base path to the public folder, and to use the rewrite file + // to ensure our environment is set and it simulates basic mod_rewrite. + passthru("{$php} -S {$host}:{$port} -t {$docroot} {$rewrite}"); + } + +} diff --git a/system/Commands/Server/rewrite.php b/system/Commands/Server/rewrite.php new file mode 100644 index 000000000000..aedc5deef350 --- /dev/null +++ b/system/Commands/Server/rewrite.php @@ -0,0 +1,40 @@ + 'Set migration namespace', + '-g' => 'Set database group', + '-t' => 'Set table name', + ]; + + /** + * Creates a new migration file with the current timestamp. + * + * @param array $params + */ + public function run(array $params = []) + { + $config = new App(); + + $tableName = CLI::getOption('t') ?? 'ci_sessions'; + + $path = APPPATH . 'Database/Migrations/' . date('YmdHis_') . 'create_' . $tableName . '_table' . '.php'; + + $data = [ + 'namespace' => CLI::getOption('n') ?? APP_NAMESPACE ?? 'App', + 'DBGroup' => CLI::getOption('g'), + 'tableName' => $tableName, + 'matchIP' => $config->sessionMatchIP ?? false, + ]; + + $template = view('\CodeIgniter\Commands\Sessions\Views\migration.tpl', $data, ['debug' => false]); + $template = str_replace('@php', '\Database\Migrations; + +use CodeIgniter\Database\Migration; + +class Migration_create__table extends Migration +{ + + protected $DBGroup = ''; + + + public function up() + { + $this->forge->addField([ + 'id' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => false + ], + 'ip_address' => [ + 'type' => 'VARCHAR', + 'constraint' => 45, + 'null' => false + ], + 'timestamp' => [ + 'type' => 'INT', + 'constraint' => 10, + 'unsigned' => true, + 'null' => false, + 'default' => 0 + ], + 'data' => [ + 'type' => 'TEXT', + 'null' => false, + 'default' => '' + ], + ]); + + $this->forge->addKey(['id', 'ip_address'], true); + + $this->forge->addKey('id', true); + + $this->forge->addKey('timestamp'); + $this->forge->createTable('', true); + } + + //-------------------------------------------------------------------- + + public function down() + { + $this->forge->dropTable('', true); + } +} diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php new file mode 100644 index 000000000000..8210ead6dce6 --- /dev/null +++ b/system/Commands/Utilities/Namespaces.php @@ -0,0 +1,123 @@ +psr4 as $ns => $path) + { + $path = realpath($path) ?? $path; + + $tbody[] = [ + $ns, + realpath($path) ?? $path, + is_dir($path) ? "Yes" : "MISSING" + ]; + } + + $thead = ['Namespace', 'Path', 'Found?']; + + CLI::table($tbody, $thead); + } + +} diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php new file mode 100644 index 000000000000..2de935ece651 --- /dev/null +++ b/system/Commands/Utilities/Routes.php @@ -0,0 +1,125 @@ +getRoutes($method); + + foreach ($routes as $from => $to) + $tbody[] = [ + $from, + $method, + $to + ]; + } + + $thead = ['Route', 'Method', 'Command']; + + CLI::table($tbody, $thead); + } + +} diff --git a/system/Common.php b/system/Common.php index bfaeb8ae1257..62bd6032826d 100644 --- a/system/Common.php +++ b/system/Common.php @@ -1,4 +1,5 @@ save('foo', 'bar'); + * $foo = cache('bar'); + * + * @param string|null $key + * + * @return \CodeIgniter\Cache\CacheInterface|mixed + */ + function cache(string $key = null) + { + $cache = \Config\Services::cache(); + + // No params - return cache object + if (is_null($key)) + { + return $cache; + } + + // Still here? Retrieve the value. + return $cache->get($key); + } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('config')) +{ + /** + * More simple way of getting config instances + * + * @param string $name + * @param bool $getShared + * + * @return mixed + */ + function config(string $name, bool $getShared = true) + { + return \CodeIgniter\Config\Config::get($name, $getShared); + } +} + +//-------------------------------------------------------------------- + if ( ! function_exists('view')) { + /** - * Grabs the current RenderableInterface-compatible class + * Grabs the current RendererInterface-compatible class * and tells it to render the specified view. Simply provides * a convenience method that can be used in Controllers, * libraries, and routed closures. @@ -77,22 +128,96 @@ function view(string $name, array $data = [], array $options = []) */ $renderer = Services::renderer(); - $saveData = false; + $saveData = null; if (array_key_exists('saveData', $options) && $options['saveData'] === true) { - $saveData = (bool)$options['saveData']; + $saveData = (bool) $options['saveData']; unset($options['saveData']); } return $renderer->setData($data, 'raw') - ->render($name, $options, $saveData); + ->render($name, $options, $saveData); } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('view_cell')) +{ + + /** + * View cells are used within views to insert HTML chunks that are managed + * by other classes. + * + * @param string $library + * @param null $params + * @param int $ttl + * @param string|null $cacheName + * + * @return string + */ + function view_cell(string $library, $params = null, int $ttl = 0, string $cacheName = null) + { + return Services::viewcell() + ->render($library, $params, $ttl, $cacheName); + } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('env')) +{ + + /** + * Allows user to retrieve values from the environment + * variables that have been set. Especially useful for + * retrieving values set from the .env file for + * use in config files. + * + * @param string $key + * @param null $default + * + * @return mixed + */ + function env(string $key, $default = null) + { + $value = getenv($key); + if ($value === false) + { + $value = $_ENV[$key] ?? $_SERVER[$key] ?? false; + } + + // Not found? Return the default value + if ($value === false) + { + return $default; + } + + // Handle any boolean values + switch (strtolower($value)) + { + case 'true': + return true; + case 'false': + return false; + case 'empty': + return ''; + case 'null': + return; + } + + return $value; + } + } //-------------------------------------------------------------------- if ( ! function_exists('esc')) { + /** * Performs simple auto-escaping of data for security reasons. * Might consider making this more complex at a later date. @@ -107,9 +232,9 @@ function view(string $name, array $data = [], array $options = []) * @param string $context * @param string $encoding * - * @return $data + * @return string|array */ - function esc($data, $context = 'html', $encoding=null) + function esc($data, $context = 'html', $encoding = null) { if (is_array($data)) { @@ -142,22 +267,33 @@ function esc($data, $context = 'html', $encoding=null) } else { - $method = 'escape'.ucfirst($context); + $method = 'escape' . ucfirst($context); } - $escaper = new \Zend\Escaper\Escaper($encoding); + static $escaper; + if (! $escaper) + { + $escaper = new \Zend\Escaper\Escaper($encoding); + } + + if ($encoding && $escaper->getEncoding() !== $encoding) + { + $escaper = new \Zend\Escaper\Escaper($encoding); + } - $data = $escaper->$method($data); + $data = $escaper->$method($data); } return $data; } + } //-------------------------------------------------------------------- -if (! function_exists('session')) +if ( ! function_exists('session')) { + /** * A convenience method for accessing the session instance, * or an item that has been set in the session. @@ -166,26 +302,30 @@ function esc($data, $context = 'html', $encoding=null) * session()->set('foo', 'bar'); * $foo = session('bar'); * - * @param null $val + * @param string $val * - * @return \CodeIgniter\Session\Session|null|void + * @return \CodeIgniter\Session\Session|mixed|null */ function session($val = null) { + $session = \Config\Services::session(); + // Returning a single item? if (is_string($val)) { - return $_SESSION[$val] ?: null; + return $session->get($val); } - return \Config\Services::session(); + return $session; } + } //-------------------------------------------------------------------- -if (! function_exists('timer')) +if ( ! function_exists('timer')) { + /** * A convenience method for working with the timer. * If no parameter is passed, it will return the timer instance, @@ -193,7 +333,7 @@ function session($val = null) * * @param string|null $name * - * @return $this|\CodeIgniter\Debug\Timer|mixed + * @return \CodeIgniter\Debug\Timer|mixed */ function timer(string $name = null) { @@ -211,58 +351,89 @@ function timer(string $name = null) return $timer->start($name); } + } //-------------------------------------------------------------------- -if (! function_exists('service')) +if ( ! function_exists('service')) { + /** * Allows cleaner access to the Services Config file. + * Always returns a SHARED instance of the class, so + * calling the function multiple times should always + * return the same instance. * * These are equal: * - $timer = service('timer') - * - $timer = \CodeIgniter\Services::timer(); + * - $timer = \CodeIgniter\Config\Services::timer(); * * @param string $name - * @param ...$params + * @param array ...$params * * @return mixed */ function service(string $name, ...$params) { - // Ensure it's not a shared instance - array_push($params, false); - return Services::$name(...$params); } + } //-------------------------------------------------------------------- -if (! function_exists('shared_service')) +if ( ! function_exists('single_service')) { + /** - * Allow cleaner access to shared services - * - * @param string $name + * Allow cleaner access to a Service. + * Always returns a new instance of the class. + * + * @param string $name * @param array|null $params - * @return type + * + * @return mixed */ - function shared_service(string $name, ...$params) + function single_service(string $name, ...$params) { + // Ensure it's NOT a shared instance + array_push($params, false); + return Services::$name(...$params); } + } //-------------------------------------------------------------------- +if ( ! function_exists('lang')) +{ + + /** + * A convenience method to translate a string and format it + * with the intl extension's MessageFormatter object. + * + * @param string $line + * @param array $args + * @param string $locale + * + * @return string + */ + function lang(string $line, array $args = [], string $locale = null) + { + return Services::language($locale) + ->getLine($line, $args); + } +} +//-------------------------------------------------------------------- if ( ! function_exists('log_message')) { + /** * A convenience/compatibility method for logging events through * the Log system. @@ -277,9 +448,9 @@ function shared_service(string $name, ...$params) * - info * - debug * - * @param string $level - * @param string $message - * @param array|null $context + * @param string $level + * @param string $message + * @param array|null $context * * @return mixed */ @@ -290,13 +461,17 @@ function log_message(string $level, string $message, array $context = []) // for asserting that logs were called in the test code. if (ENVIRONMENT == 'testing') { - $logger = new \CodeIgniter\Log\TestLogger(new \Config\Logger()); + $logger = new \Tests\Support\Log\TestLogger(new \Config\Logger()); + return $logger->log($level, $message, $context); } + // @codeCoverageIgnoreStart return Services::logger(true) - ->log($level, $message, $context); + ->log($level, $message, $context); + // @codeCoverageIgnoreEnd } + } //-------------------------------------------------------------------- @@ -315,12 +490,14 @@ function is_cli() { return (PHP_SAPI === 'cli' || defined('STDIN')); } + } //-------------------------------------------------------------------- if ( ! function_exists('route_to')) { + /** * Given a controller/method string and any params, * will attempt to build the relative URL to the @@ -330,9 +507,9 @@ function is_cli() * have a route defined in the routes Config file. * * @param string $method - * @param ...$params + * @param array ...$params * - * @return \CodeIgniter\Router\string + * @return false|string */ function route_to(string $method, ...$params): string { @@ -340,80 +517,114 @@ function route_to(string $method, ...$params): string return $routes->reverseRoute($method, ...$params); } + } //-------------------------------------------------------------------- if ( ! function_exists('remove_invisible_characters')) { + /** * Remove Invisible Characters * * This prevents sandwiching null characters * between ascii characters, like Java\0script. * - * @param string - * @param bool - * @return string + * @param string $str + * @param bool $url_encoded + * + * @return string */ - function remove_invisible_characters($str, $url_encoded = TRUE) + function remove_invisible_characters($str, $url_encoded = true) { - $non_displayables = array(); + $non_displayables = []; // every control character except newline (dec 10), // carriage return (dec 13) and horizontal tab (dec 09) if ($url_encoded) { - $non_displayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15 - $non_displayables[] = '/%1[0-9a-f]/'; // url encoded 16-31 + $non_displayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15 + $non_displayables[] = '/%1[0-9a-f]/'; // url encoded 16-31 } - $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 + $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 do { $str = preg_replace($non_displayables, '', $str, -1, $count); - } - while ($count); + } while ($count); return $str; } + } //-------------------------------------------------------------------- -if (! function_exists('helper')) +if ( ! function_exists('helper')) { + /** * Loads a helper file into memory. Supports namespaced helpers, * both in and out of the 'helpers' directory of a namespaced directory. * - * @param string $filename - * - * @return string + * @param string|array $filenames */ - function helper(string $filename)//: string + function helper($filenames) { $loader = Services::locator(true); - if (strpos($filename, '_helper') === false) + if ( ! is_array($filenames)) { - $filename .= '_helper'; + $filenames = [$filenames]; } - $path = $loader->locateFile($filename, 'Helpers'); - - if (! empty($path)) + foreach ($filenames as $filename) { - include $path; + if (strpos($filename, '_helper') === false) + { + $filename .= '_helper'; + } + + $path = $loader->locateFile($filename, 'Helpers'); + + if ( ! empty($path)) + { + include_once $path; + } } } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('app_timezone')) +{ + + /** + * Returns the timezone the application has been set to display + * dates in. This might be different than the timezone set + * at the server level, as you often want to stores dates in UTC + * and convert them on the fly for the user. + * + * @return string + */ + function app_timezone() + { + $config = config(\Config\App::class); + + return $config->appTimezone; + } + } //-------------------------------------------------------------------- -if (! function_exists('csrf_token')) +if ( ! function_exists('csrf_token')) { + /** * Returns the CSRF token name. * Can be used in Views when building hidden inputs manually, @@ -423,16 +634,18 @@ function helper(string $filename)//: string */ function csrf_token() { - $config = new \Config\App(); + $config = config(\Config\App::class); return $config->CSRFTokenName; } + } //-------------------------------------------------------------------- -if (! function_exists('csrf_hash')) +if ( ! function_exists('csrf_hash')) { + /** * Returns the current hash value for the CSRF protection. * Can be used in Views when building hidden inputs manually, @@ -446,12 +659,31 @@ function csrf_hash() return $security->getCSRFHash(); } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('csrf_field')) +{ + + /** + * Generates a hidden input field for use within manually generated forms. + * + * @return string + */ + function csrf_field() + { + return ''; + } + } //-------------------------------------------------------------------- -if (! function_exists('force_https')) +if ( ! function_exists('force_https')) { + /** * Used to force a page to be accessed in via HTTPS. * Uses a standard redirect, plus will set the HSTS header @@ -460,17 +692,28 @@ function csrf_hash() * * @see https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security * - * @param int $duration How long should the SSL header be set for? (in seconds) - * Defaults to 1 year. - * @param RequestInterface $request + * @param int $duration How long should the SSL header be set for? (in seconds) + * Defaults to 1 year. + * @param RequestInterface $request * @param ResponseInterface $response + * + * Not testable, as it will exit! + * + * @throws \CodeIgniter\HTTP\RedirectException + * @codeCoverageIgnore */ function force_https(int $duration = 31536000, RequestInterface $request = null, ResponseInterface $response = null) { - if (is_null($request)) $request = Services::request(null, true); - if (is_null($response)) $response = Services::response(null, true); + if (is_null($request)) + { + $request = Services::request(null, true); + } + if (is_null($response)) + { + $response = Services::response(null, true); + } - if ($request->isSecure()) + if (is_cli() || $request->isSecure()) { return; } @@ -479,31 +722,68 @@ function force_https(int $duration = 31536000, RequestInterface $request = null, // the session ID for safety sake. if (class_exists('Session', false)) { - Services::session(null, true)->regenerate(); + Services::session(null, true) + ->regenerate(); } $uri = $request->uri; $uri->setScheme('https'); $uri = \CodeIgniter\HTTP\URI::createURIString( - $uri->getScheme(), - $uri->getAuthority(true), - $uri->getPath(), // Absolute URIs should use a "/" for an empty path - $uri->getQuery(), - $uri->getFragment() + $uri->getScheme(), $uri->getAuthority(true), $uri->getPath(), // Absolute URIs should use a "/" for an empty path + $uri->getQuery(), $uri->getFragment() ); // Set an HSTS header - $response->setHeader('Strict-Transport-Security', 'max-age='.$duration); + $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration); $response->redirect($uri); exit(); } + } //-------------------------------------------------------------------- -if (! function_exists('redirect')) +if (! function_exists('old')) { + /** + * Provides access to "old input" that was set in the session + * during a redirect()->withInput(). + * + * @param string $key + * @param null $default + * @param string|bool $escape + * + * @return mixed|null + */ + function old(string $key, $default = null, $escape = 'html') + { + $request = Services::request(); + + $value = $request->getOldInput($key); + + // Return the default value if nothing + // found in the old input. + if (is_null($value)) + { + return $default; + } + + // If the result was serialized array or string, then unserialize it for use... + if (strpos($value, 'a:') === 0 || strpos($value, 's:') === 0) + { + $value = unserialize($value); + } + + return $escape === false ? $value : esc($value, $escape); + } +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('redirect')) +{ + /** * Convenience method that works with the current global $request and * $router instances to redirect using named/reverse-routed routes @@ -513,22 +793,215 @@ function force_https(int $duration = 31536000, RequestInterface $request = null, * * If more control is needed, you must use $response->redirect explicitly. * - * @param string $uri - * @param $params + * @param string $uri + * + * @return \CodeIgniter\HTTP\RedirectResponse */ - function redirect(string $uri, ...$params) + function redirect(string $uri=null) { - $response = Services::response(null, true); - $routes = Services::routes(true); + $response = Services::redirectResponse(null, true); - if ($route = $routes->reverseRoute($uri, ...$params)) + if (! empty($uri)) { - $uri = $route; + return $response->to($uri); } - $response->redirect($uri); + return $response; + } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('stringify_attributes')) +{ + + /** + * Stringify attributes for use in HTML tags. + * + * Helper function used to convert a string, array, or object + * of attributes to a string. + * + * @param mixed $attributes string, array, object + * @param bool $js + * + * @return string + */ + function stringify_attributes($attributes, $js = false): string + { + $atts = ''; + + if (empty($attributes)) + { + return $atts; + } + + if (is_string($attributes)) + { + return ' ' . $attributes; + } + + $attributes = (array) $attributes; + + foreach ($attributes as $key => $val) + { + $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . esc($val, 'attr') . '"'; + } + + return rtrim($atts, ','); } + } //-------------------------------------------------------------------- +if ( ! function_exists('is_really_writable')) +{ + + /** + * Tests for file writability + * + * is_writable() returns TRUE on Windows servers when you really can't write to + * the file, based on the read-only attribute. is_writable() is also unreliable + * on Unix servers if safe_mode is on. + * + * @link https://bugs.php.net/bug.php?id=54709 + * + * @param string $file + * + * @return bool + * + * @codeCoverageIgnore Not practical to test, as travis runs on linux + */ + function is_really_writable($file) + { + // If we're on a Unix server with safe_mode off we call is_writable + if (DIRECTORY_SEPARATOR === '/' || ! ini_get('safe_mode')) + { + return is_writable($file); + } + + /* For Windows servers and safe_mode "on" installations we'll actually + * write a file then read it. Bah... + */ + if (is_dir($file)) + { + $file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16)); + if (($fp = @fopen($file, 'ab')) === false) + { + return false; + } + + fclose($fp); + @chmod($file, 0777); + @unlink($file); + + return true; + } + elseif ( ! is_file($file) || ( $fp = @fopen($file, 'ab')) === false) + { + return false; + } + + fclose($fp); + + return true; + } + +} + +//-------------------------------------------------------------------- + +if ( ! function_exists('slash_item')) +{ + + //Unlike CI3, this function is placed here because + //it's not a config, or part of a config. + /** + * Fetch a config file item with slash appended (if not empty) + * + * @param string $item Config item name + * + * @return string|null The configuration item or NULL if + * the item doesn't exist + */ + function slash_item($item) + { + $config = config(\Config\App::class); + $configItem = $config->{$item}; + + if ( ! isset($configItem) || empty(trim($configItem))) + { + return $configItem; + } + + return rtrim($configItem, '/') . '/'; + } + +} +//-------------------------------------------------------------------- + +if ( ! function_exists('function_usable')) +{ + + /** + * Function usable + * + * Executes a function_exists() check, and if the Suhosin PHP + * extension is loaded - checks whether the function that is + * checked might be disabled in there as well. + * + * This is useful as function_exists() will return FALSE for + * functions disabled via the *disable_functions* php.ini + * setting, but not for *suhosin.executor.func.blacklist* and + * *suhosin.executor.disable_eval*. These settings will just + * terminate script execution if a disabled function is executed. + * + * The above described behavior turned out to be a bug in Suhosin, + * but even though a fix was commited for 0.9.34 on 2012-02-12, + * that version is yet to be released. This function will therefore + * be just temporary, but would probably be kept for a few years. + * + * @link http://www.hardened-php.net/suhosin/ + * @param string $function_name Function to check for + * @return bool TRUE if the function exists and is safe to call, + * FALSE otherwise. + * + * @codeCoverageIgnore This is too exotic + */ + function function_usable($function_name) + { + static $_suhosin_func_blacklist; + + if (function_exists($function_name)) + { + if ( ! isset($_suhosin_func_blacklist)) + { + $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : []; + } + + return ! in_array($function_name, $_suhosin_func_blacklist, TRUE); + } + + return FALSE; + } + +} + +//-------------------------------------------------------------------- + +if (! function_exists('dd')) +{ + /** + * Prints a Kint debug report and exits. + * + * @param array ...$vars + * + * @codeCoverageIgnore Can't be tested ... exits + */ + function dd(...$vars) + { + Kint::dump(...$vars); + exit; + } +} diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index 6d8a6bf3814e..77629ab9d334 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,12 +27,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2018 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 * @filesource */ @@ -49,6 +49,8 @@ */ class ComposerScripts { + protected static $basePath = 'system/ThirdParty/'; + /** * After composer install/update, this is called to move * the bare-minimum required files for our dependencies @@ -56,45 +58,18 @@ class ComposerScripts */ public static function postUpdate() { - /* - * Zend/Escaper - */ - if (class_exists('\\Zend\\Escaper\\Escaper') && file_exists(self::getClassFilePath('\\Zend\\Escaper\\Escaper'))) - { - $base = 'system/ThirdParty/ZendEscaper'; - - foreach ([$base, $base.'/Exception'] as $path) - { - if (! is_dir($path)) - { - mkdir($path, 0755); - } - } - - $files = [ - self::getClassFilePath('\\Zend\\Escaper\\Exception\\ExceptionInterface') => $base.'/Exception/ExceptionInterface.php', - self::getClassFilePath('\\Zend\\Escaper\\Exception\\InvalidArgumentException') => $base.'/Exception/InvalidArgumentException.php', - self::getClassFilePath('\\Zend\\Escaper\\Exception\\RuntimeException') => $base.'/Exception/RuntimeException.php', - self::getClassFilePath('\\Zend\\Escaper\\Escaper') => $base.'/Escaper.php' - ]; - - foreach ($files as $source => $dest) - { - if (! self::moveFile($source, $dest)) - { - die('Error moving: '. $source); - } - } - } + static::moveEscaper(); + static::moveKint(); } //-------------------------------------------------------------------- /** * Move a file. - * + * * @param string $source * @param string $destination + * * @return boolean */ protected static function moveFile(string $source, string $destination) @@ -111,23 +86,121 @@ protected static function moveFile(string $source, string $destination) return false; } - return rename($source, $destination); + return copy($source, $destination); } //-------------------------------------------------------------------- - + /** * Determine file path of a class. - * + * * @param string $class - * @return type + * + * @return string */ - protected static function getClassFilePath( string $class ) + protected static function getClassFilePath(string $class) { $reflector = new \ReflectionClass($class); + return $reflector->getFileName(); } - + + //-------------------------------------------------------------------- + + /** + * A recursive remove directory method. + * + * @param $dir + */ + protected static function removeDir($dir) + { + if (is_dir($dir)) + { + $objects = scandir($dir); + foreach ($objects as $object) + { + if ($object != "." && $object != "..") + { + if (filetype($dir."/".$object) == "dir") + { + static::removeDir($dir."/".$object); + } + else + { + unlink($dir."/".$object); + } + } + } + reset($objects); + rmdir($dir); + } + } + + /** + * Moves the Zend Escaper files into our base repo so that it's + * available for packaged releases where the users don't user Composer. + */ + public static function moveEscaper() + { + if (class_exists('\\Zend\\Escaper\\Escaper') && file_exists(self::getClassFilePath('\\Zend\\Escaper\\Escaper'))) + { + $base = static::$basePath.'ZendEscaper'; + + foreach ([$base, $base.'/Exception'] as $path) + { + if (! is_dir($path)) + { + mkdir($path, 0755); + } + } + + $files = [ + self::getClassFilePath('\\Zend\\Escaper\\Exception\\ExceptionInterface') => $base.'/Exception/ExceptionInterface.php', + self::getClassFilePath('\\Zend\\Escaper\\Exception\\InvalidArgumentException') => $base.'/Exception/InvalidArgumentException.php', + self::getClassFilePath('\\Zend\\Escaper\\Exception\\RuntimeException') => $base.'/Exception/RuntimeException.php', + self::getClassFilePath('\\Zend\\Escaper\\Escaper') => $base.'/Escaper.php', + ]; + + foreach ($files as $source => $dest) + { + if (! self::moveFile($source, $dest)) + { + die('Error moving: '.$source); + } + } + } + } + //-------------------------------------------------------------------- + /** + * Moves the Kint file into our base repo so that it's + * available for packaged releases where the users don't user Composer. + */ + public static function moveKint() + { + $filename = 'vendor/kint-php/kint/build/kint-aante-light.php'; + + if (file_exists($filename)) + { + $base = static::$basePath.'Kint'; + + // Remove the contents of the previous Kint folder, if any. + if (is_dir($base)) + { + static::removeDir($base); + } + + // Create Kint if it doesn't exist already + if (! is_dir($base)) + { + mkdir($base, 0755); + } + + if (! self::moveFile($filename, $base.'/kint.php')) + { + die('Error moving: '.$filename); + } + } + } } diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index d06770b2dfb2..96b2787809e2 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,31 +29,31 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - /** * AUTO-LOADER - * + * * This file defines the namespaces and class maps so the Autoloader * can find the files as needed. */ class AutoloadConfig { + /** * Array of namespaces for autoloading. - * @var type + * @var array */ public $psr4 = []; /** * Map of class names and locations - * @var type + * @var array */ public $classmap = []; @@ -89,9 +89,9 @@ public function __construct() 'CodeIgniter' => realpath(BASEPATH) ]; - if (ENVIRONMENT == 'testing') + if (isset($_SERVER['CI_ENVIRONMENT']) && $_SERVER['CI_ENVIRONMENT'] === 'testing') { - $this->psr4['Tests\Support'] = BASEPATH.'../tests/_support/'; + $this->psr4['Tests\Support'] = BASEPATH . '../tests/_support'; } /** @@ -111,67 +111,77 @@ public function __construct() * ]; */ $this->classmap = [ - 'CodeIgniter\CodeIgniter' => BASEPATH.'CodeIgniter.php', - 'CodeIgniter\CLI\CLI' => BASEPATH.'CLI/CLI.php', - 'CodeIgniter\Loader' => BASEPATH.'Loader.php', - 'CodeIgniter\Controller' => BASEPATH.'Controller.php', - 'CodeIgniter\Config\AutoloadConfig' => BASEPATH.'Config/Autoload.php', - 'CodeIgniter\Config\BaseConfig' => BASEPATH.'Config/BaseConfig.php', - 'CodeIgniter\Config\Database' => BASEPATH.'Config/Database.php', - 'CodeIgniter\Config\Database\Connection' => BASEPATH.'Config/Database/Connection.php', - 'CodeIgniter\Config\Database\Connection\MySQLi' => BASEPATH.'Config/Database/Connection/MySQLi.php', - 'CodeIgniter\Config\DotEnv' => BASEPATH.'Config/DotEnv.php', - 'CodeIgniter\Database\BaseBuilder' => BASEPATH.'Database/BaseBuilder.php', - 'CodeIgniter\Database\BaseConnection' => BASEPATH.'Database/BaseConnection.php', - 'CodeIgniter\Database\BaseResult' => BASEPATH.'Database/BaseResult.php', - 'CodeIgniter\Database\Config' => BASEPATH.'Database/Config.php', - 'CodeIgniter\Database\ConnectionInterface' => BASEPATH.'Database/ConnectionInterface.php', - 'CodeIgniter\Database\Database' => BASEPATH.'Database/Database.php', - 'CodeIgniter\Database\Query' => BASEPATH.'Database/Query.php', - 'CodeIgniter\Database\QueryInterface' => BASEPATH.'Database/QueryInterface.php', - 'CodeIgniter\Database\ResultInterface' => BASEPATH.'Database/ResultInterface.php', - 'CodeIgniter\Database\Migration' => BASEPATH.'Database/Migration.php', - 'CodeIgniter\Database\MigrationRunner' => BASEPATH.'Database/MigrationRunner.php', - 'CodeIgniter\Debug\Exceptions' => BASEPATH.'Debug/Exceptions.php', - 'CodeIgniter\Debug\Timer' => BASEPATH.'Debug/Timer.php', - 'CodeIgniter\Debug\Iterator' => BASEPATH.'Debug/Iterator.php', - 'CodeIgniter\Hooks\Hooks' => BASEPATH.'Hooks/Hooks.php', - 'CodeIgniter\HTTP\CLIRequest' => BASEPATH.'HTTP/CLIRequest.php', - 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH.'HTTP/ContentSecurityPolicy.php', - 'CodeIgniter\HTTP\CURLRequest' => BASEPATH.'HTTP/CURLRequest.php', - 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH.'HTTP/IncomingRequest.php', - 'CodeIgniter\HTTP\Message' => BASEPATH.'HTTP/Message.php', - 'CodeIgniter\HTTP\Negotiate' => BASEPATH.'HTTP/Negotiate.php', - 'CodeIgniter\HTTP\Request' => BASEPATH.'HTTP/Request.php', - 'CodeIgniter\HTTP\RequestInterface' => BASEPATH.'HTTP/RequestInterface.php', - 'CodeIgniter\HTTP\Response' => BASEPATH.'HTTP/Response.php', - 'CodeIgniter\HTTP\ResponseInterface' => BASEPATH.'HTTP/ResponseInterface.php', - 'CodeIgniter\HTTP\URI' => BASEPATH.'HTTP/URI.php', - 'CodeIgniter\Log\Logger' => BASEPATH.'Log/Logger.php', - 'Psr\Log\LoggerAwareInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareInterface.php', - 'Psr\Log\LoggerAwareTrait' => BASEPATH.'ThirdParty/PSR/Log/LoggerAwareTrait.php', - 'Psr\Log\LoggerInterface' => BASEPATH.'ThirdParty/PSR/Log/LoggerInterface.php', - 'Psr\Log\LogLevel' => BASEPATH.'ThirdParty/PSR/Log/LogLevel.php', - 'CodeIgniter\Log\Handlers\BaseHandler' => BASEPATH.'Log/Handlers/BaseHandler.php', - 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => BASEPATH.'Log/Handlers/ChromeLoggerHandler.php', - 'CodeIgniter\Log\Handlers\FileHandler' => BASEPATH.'Log/Handlers/FileHandler.php', - 'CodeIgniter\Log\Handlers\HandlerInterface' => BASEPATH.'Log/Handlers/HandlerInterface.php', - 'CodeIgniter\Router\RouteCollection' => BASEPATH.'Router/RouteCollection.php', - 'CodeIgniter\Router\RouteCollectionInterface' => BASEPATH.'Router/RouteCollectionInterface.php', - 'CodeIgniter\Router\Router' => BASEPATH.'Router/Router.php', - 'CodeIgniter\Router\RouterInterface' => BASEPATH.'Router/RouterInterface.php', - 'CodeIgniter\Security\Security' => BASEPATH.'Security/Security.php', - 'CodeIgniter\Session\Session' => BASEPATH.'Session/Session.php', - 'CodeIgniter\Session\SessionInterface' => BASEPATH.'Session/SessionInterface.php', - 'CodeIgniter\Session\Handlers\BaseHandler' => BASEPATH.'Session/Handlers/BaseHandler.php', - 'CodeIgniter\Session\Handlers\FileHandler' => BASEPATH.'Session/Handlers/FileHandler.php', - 'CodeIgniter\Session\Handlers\MemcachedHandler' => BASEPATH.'Session/Handlers/MemcachedHandler.php', - 'CodeIgniter\Session\Handlers\RedisHandler' => BASEPATH.'Session/Handlers/RedisHandler.php', - 'CodeIgniter\View\RenderableInterface' => BASEPATH.'View/RenderableInterface.php', - 'CodeIgniter\View\View' => BASEPATH.'View/View.php', - 'Zend\Escaper\Escaper' => BASEPATH.'ThirdParty/ZendEscaper/Escaper.php', - 'CodeIgniter\Log\TestLogger' => BASEPATH.'../tests/_support/Log/TestLogger.php', - 'CIDatabaseTestCase' => BASEPATH.'../tests/_support/CIDatabaseTestCase.php' + 'CodeIgniter\CodeIgniter' => BASEPATH . 'CodeIgniter.php', + 'CodeIgniter\CLI\CLI' => BASEPATH . 'CLI/CLI.php', + 'CodeIgniter\Loader' => BASEPATH . 'Loader.php', + 'CodeIgniter\Cache\CacheFactory' => BASEPATH . 'Cache/CacheFactory.php', + 'CodeIgniter\Cache\CacheInterface' => BASEPATH . 'Cache/CacheInterface.php', + 'CodeIgniter\Cache\Handlers\DummyHandler' => BASEPATH . 'Cache/Handlers/DummyHandler.php', + 'CodeIgniter\Cache\Handlers\FileHandler' => BASEPATH . 'Cache/Handlers/FileHandler.php', + 'CodeIgniter\Cache\Handlers\MemcachedHandler' => BASEPATH . 'Cache/Handlers/MemcachedHandler.php', + 'CodeIgniter\Cache\Handlers\PredisHandler' => BASEPATH . 'Cache/Handlers/PredisHandler.php', + 'CodeIgniter\Cache\Handlers\RedisHandler' => BASEPATH . 'Cache/Handlers/RedisHandler.php', + 'CodeIgniter\Cache\Handlers\WincacheHandler' => BASEPATH . 'Cache/Handlers/WincacheHandler.php', + 'CodeIgniter\Controller' => BASEPATH . 'Controller.php', + 'CodeIgniter\Config\AutoloadConfig' => BASEPATH . 'Config/Autoload.php', + 'CodeIgniter\Config\BaseConfig' => BASEPATH . 'Config/BaseConfig.php', + 'CodeIgniter\Config\Database' => BASEPATH . 'Config/Database.php', + 'CodeIgniter\Config\Database\Connection' => BASEPATH . 'Config/Database/Connection.php', + 'CodeIgniter\Config\Database\Connection\MySQLi' => BASEPATH . 'Config/Database/Connection/MySQLi.php', + 'CodeIgniter\Config\DotEnv' => BASEPATH . 'Config/DotEnv.php', + 'CodeIgniter\Database\BaseBuilder' => BASEPATH . 'Database/BaseBuilder.php', + 'CodeIgniter\Database\BaseConnection' => BASEPATH . 'Database/BaseConnection.php', + 'CodeIgniter\Database\BaseResult' => BASEPATH . 'Database/BaseResult.php', + 'CodeIgniter\Database\Config' => BASEPATH . 'Database/Config.php', + 'CodeIgniter\Database\ConnectionInterface' => BASEPATH . 'Database/ConnectionInterface.php', + 'CodeIgniter\Database\Database' => BASEPATH . 'Database/Database.php', + 'CodeIgniter\Database\Query' => BASEPATH . 'Database/Query.php', + 'CodeIgniter\Database\QueryInterface' => BASEPATH . 'Database/QueryInterface.php', + 'CodeIgniter\Database\ResultInterface' => BASEPATH . 'Database/ResultInterface.php', + 'CodeIgniter\Database\Migration' => BASEPATH . 'Database/Migration.php', + 'CodeIgniter\Database\MigrationRunner' => BASEPATH . 'Database/MigrationRunner.php', + 'CodeIgniter\Debug\Exceptions' => BASEPATH . 'Debug/Exceptions.php', + 'CodeIgniter\Debug\Timer' => BASEPATH . 'Debug/Timer.php', + 'CodeIgniter\Debug\Iterator' => BASEPATH . 'Debug/Iterator.php', + 'CodeIgniter\Events\Events' => BASEPATH . 'Events/Events.php', + 'CodeIgniter\HTTP\CLIRequest' => BASEPATH . 'HTTP/CLIRequest.php', + 'CodeIgniter\HTTP\ContentSecurityPolicy' => BASEPATH . 'HTTP/ContentSecurityPolicy.php', + 'CodeIgniter\HTTP\CURLRequest' => BASEPATH . 'HTTP/CURLRequest.php', + 'CodeIgniter\HTTP\IncomingRequest' => BASEPATH . 'HTTP/IncomingRequest.php', + 'CodeIgniter\HTTP\Message' => BASEPATH . 'HTTP/Message.php', + 'CodeIgniter\HTTP\Negotiate' => BASEPATH . 'HTTP/Negotiate.php', + 'CodeIgniter\HTTP\Request' => BASEPATH . 'HTTP/Request.php', + 'CodeIgniter\HTTP\RequestInterface' => BASEPATH . 'HTTP/RequestInterface.php', + 'CodeIgniter\HTTP\Response' => BASEPATH . 'HTTP/Response.php', + 'CodeIgniter\HTTP\ResponseInterface' => BASEPATH . 'HTTP/ResponseInterface.php', + 'CodeIgniter\HTTP\URI' => BASEPATH . 'HTTP/URI.php', + 'CodeIgniter\Log\Logger' => BASEPATH . 'Log/Logger.php', + 'Psr\Log\LoggerAwareInterface' => BASEPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php', + 'Psr\Log\LoggerAwareTrait' => BASEPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php', + 'Psr\Log\LoggerInterface' => BASEPATH . 'ThirdParty/PSR/Log/LoggerInterface.php', + 'Psr\Log\LogLevel' => BASEPATH . 'ThirdParty/PSR/Log/LogLevel.php', + 'CodeIgniter\Log\Handlers\BaseHandler' => BASEPATH . 'Log/Handlers/BaseHandler.php', + 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => BASEPATH . 'Log/Handlers/ChromeLoggerHandler.php', + 'CodeIgniter\Log\Handlers\FileHandler' => BASEPATH . 'Log/Handlers/FileHandler.php', + 'CodeIgniter\Log\Handlers\HandlerInterface' => BASEPATH . 'Log/Handlers/HandlerInterface.php', + 'CodeIgniter\Router\RouteCollection' => BASEPATH . 'Router/RouteCollection.php', + 'CodeIgniter\Router\RouteCollectionInterface' => BASEPATH . 'Router/RouteCollectionInterface.php', + 'CodeIgniter\Router\Router' => BASEPATH . 'Router/Router.php', + 'CodeIgniter\Router\RouterInterface' => BASEPATH . 'Router/RouterInterface.php', + 'CodeIgniter\Security\Security' => BASEPATH . 'Security/Security.php', + 'CodeIgniter\Session\Session' => BASEPATH . 'Session/Session.php', + 'CodeIgniter\Session\SessionInterface' => BASEPATH . 'Session/SessionInterface.php', + 'CodeIgniter\Session\Handlers\BaseHandler' => BASEPATH . 'Session/Handlers/BaseHandler.php', + 'CodeIgniter\Session\Handlers\FileHandler' => BASEPATH . 'Session/Handlers/FileHandler.php', + 'CodeIgniter\Session\Handlers\MemcachedHandler' => BASEPATH . 'Session/Handlers/MemcachedHandler.php', + 'CodeIgniter\Session\Handlers\RedisHandler' => BASEPATH . 'Session/Handlers/RedisHandler.php', + 'CodeIgniter\View\RendererInterface' => BASEPATH . 'View/RendererInterface.php', + 'CodeIgniter\View\View' => BASEPATH . 'View/View.php', + 'CodeIgniter\View\Parser' => BASEPATH . 'View/Parser.php', + 'CodeIgniter\View\Cell' => BASEPATH . 'View/Cell.php', + 'Zend\Escaper\Escaper' => BASEPATH . 'ThirdParty/ZendEscaper/Escaper.php', + 'CodeIgniter\Log\TestLogger' => BASEPATH . '../tests/_support/Log/TestLogger.php', + 'CIDatabaseTestCase' => BASEPATH . '../tests/_support/CIDatabaseTestCase.php' ]; } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 28cc3f7e0dfb..1f43f232f2d5 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -47,15 +47,33 @@ */ class BaseConfig { + + /** + * An optional array of classes that will act as Registrars + * for rapidly setting config class properties. + * + * @var array + */ + public static $registrars = []; + + protected static $didDiscovery = false; + + protected static $moduleConfig; + /** * Will attempt to get environment variables with names * that match the properties of the child class. + * + * The "shortPrefix" is the lowercase-only config class name. */ public function __construct() { - $properties = array_keys(get_object_vars($this)); - $prefix = get_class($this); - $shortPrefix = strtolower(substr($prefix, strrpos($prefix, '\\') + 1)); + static::$moduleConfig = config('Modules'); + + $properties = array_keys(get_object_vars($this)); + $prefix = get_class($this); + $slashAt = strrpos($prefix, '\\'); + $shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt+1 )); foreach ($properties as $property) { @@ -65,10 +83,19 @@ public function __construct() { if ($value = $this->getEnvValue("{$property}.{$key}", $prefix, $shortPrefix)) { - if (is_null($value)) continue; + if (is_null($value)) + { + continue; + } - if ($value === 'false') $value = false; - elseif ($value === 'true') $value = true; + if ($value === 'false') + { + $value = false; + } + elseif ($value === 'true') + { + $value = true; + } $this->$property[$key] = $value; } @@ -76,46 +103,119 @@ public function __construct() } else { - if (($value = $this->getEnvValue($property, $prefix, $shortPrefix)) !== false ) + if (($value = $this->getEnvValue($property, $prefix, $shortPrefix)) !== false) { - if (is_null($value)) continue; + if (is_null($value)) + { + continue; + } - if ($value === 'false') $value = false; - elseif ($value === 'true') $value = true; + if ($value === 'false') + { + $value = false; + } + elseif ($value === 'true') + { + $value = true; + } - $this->$property = $value; + $this->$property = is_bool($value) + ? $value + : trim($value, '\'"'); } } } + + if (defined('ENVIRONMENT') && ENVIRONMENT != 'testing') + { + $this->registerProperties(); + } } //-------------------------------------------------------------------- /** * Retrieve an environment-specific configuration setting + * * @param string $property * @param string $prefix * @param string $shortPrefix - * @return type + * + * @return mixed */ protected function getEnvValue(string $property, string $prefix, string $shortPrefix) { - if (($value = getenv("{$shortPrefix}.{$property}")) !== false) + $shortPrefix = ltrim( $shortPrefix, '\\' ); + switch (true) { - return $value; + case array_key_exists( "{$shortPrefix}.{$property}", $_ENV ): + return $_ENV["{$shortPrefix}.{$property}"]; + break; + case array_key_exists( "{$shortPrefix}.{$property}", $_SERVER ): + return $_SERVER["{$shortPrefix}.{$property}"]; + break; + case array_key_exists( "{$prefix}.{$property}", $_ENV ): + return $_ENV["{$prefix}.{$property}"]; + break; + case array_key_exists( "{$prefix}.{$property}", $_SERVER ): + return $_SERVER["{$prefix}.{$property}"]; + break; + default: + $value = getenv( $property ); + return $value === false ? null : $value; } - elseif (($value = getenv("{$prefix}.{$property}")) !== false) + } + + //-------------------------------------------------------------------- + + /** + * Provides external libraries a simple way to register one or more + * options into a config file. + */ + protected function registerProperties() + { + if (! static::$moduleConfig->shouldDiscover('registrars')) { - return $value; + return; } - elseif (($value = getenv($property)) !== false) + + if (! static::$didDiscovery) { - return $value; + $locator = \Config\Services::locator(); + static::$registrars = $locator->search('Config/Registrar.php'); } - return null; + $shortName = (new \ReflectionClass($this))->getShortName(); + + // Check the registrar class for a method named after this class' shortName + foreach (static::$registrars as $callable) + { + // ignore non-applicable registrars + if ( ! method_exists($callable, $shortName)) + { + continue; + } + + $properties = $callable::$shortName(); + + if ( ! is_array($properties)) + { + throw new \RuntimeException('Registrars must return an array of properties and their values.'); + } + + foreach ($properties as $property => $value) + { + if (isset($this->$property) && is_array($this->$property) && is_array($value)) + { + $this->$property = array_merge($this->$property, $value); + } + else + { + $this->$property = $value; + } + } + } } //-------------------------------------------------------------------- - } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php new file mode 100644 index 000000000000..88ff74d29c5c --- /dev/null +++ b/system/Config/BaseService.php @@ -0,0 +1,246 @@ +shouldDiscover('services')) + { + $locator = static::locator(); + $files = $locator->search('Config/Services'); + + if (empty($files)) + { + return; + } + + // Get instances of all service classes and cache them locally. + foreach ($files as $file) + { + $classname = $locator->getClassname($file); + + if (! in_array($classname, ['CodeIgniter\\Config\\Services'])) + { + static::$services[] = new $classname(); + } + } + } + + static::$discovered = true; + } + + if (! static::$services) + { + return; + } + + // Try to find the desired service method + foreach (static::$services as $class) + { + if (method_exists(get_class($class), $name)) + { + return $class::$name(...$arguments); + } + } + } +} diff --git a/system/Config/Config.php b/system/Config/Config.php new file mode 100644 index 000000000000..533123c01b2e --- /dev/null +++ b/system/Config/Config.php @@ -0,0 +1,133 @@ +locateFile($name, 'Config'); + + if (empty($file)) + { + return null; + } + + $name = $locator->getClassname($file); + + if (empty($name)) + { + return null; + } + + return new $name(); + } + + //-------------------------------------------------------------------- +} diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index a95c7645f214..950b2838e53e 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -39,9 +39,9 @@ /** * Environment-specific configuration */ - class DotEnv { + /** * The directory where the .env file can be located. * @@ -57,14 +57,9 @@ class DotEnv * @param string $path * @param string $file */ - public function __construct(string $path, $file = '.env') + public function __construct(string $path, string $file = '.env') { - if ( ! is_string($file)) - { - $file = '.env'; - } - - $this->path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$file; + $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file; } //-------------------------------------------------------------------- @@ -73,6 +68,8 @@ public function __construct(string $path, $file = '.env') * The main entry point, will load the .env file and process it * so that we end up with all settings in the PHP environment vars * (i.e. getenv(), $_ENV, and $_SERVER) + * + * @return bool */ public function load() { @@ -106,6 +103,8 @@ public function load() $this->setVariable($line); } } + + return true; // for success } //-------------------------------------------------------------------- @@ -122,9 +121,18 @@ protected function setVariable(string $name, string $value = '') { list($name, $value) = $this->normaliseVariable($name, $value); - putenv("$name=$value"); - $_ENV[$name] = $value; - $_SERVER[$name] = $value; + if ( ! getenv($name, true)) + { + putenv("$name=$value"); + } + if (empty($_ENV[$name])) + { + $_ENV[$name] = $value; + } + if (empty($_SERVER[$name])) + { + $_SERVER[$name] = $value; + } } //-------------------------------------------------------------------- @@ -135,6 +143,7 @@ protected function setVariable(string $name, string $value = '') * * @param string $name * @param string $value + * * @return array */ public function normaliseVariable(string $name, string $value = ''): array @@ -145,7 +154,7 @@ public function normaliseVariable(string $name, string $value = ''): array list($name, $value) = explode('=', $name, 2); } - $name = trim($name); + $name = trim($name); $value = trim($value); // Sanitize the name @@ -183,25 +192,24 @@ protected function sanitizeValue(string $value): string if (strpbrk($value[0], '"\'') !== false) { // value starts with a quote - $quote = $value[0]; + $quote = $value[0]; $regexPattern = sprintf( - '/^ - %1$s # match a quote at the start of the value - ( # capturing sub-pattern used - (?: # we do not need to capture this - [^%1$s\\\\] # any character other than a quote or backslash - |\\\\\\\\ # or two backslashes together - |\\\\%1$s # or an escaped quote e.g \" - )* # as many characters that match the previous rules - ) # end of the capturing sub-pattern - %1$s # and the closing quote - .*$ # and discard any string after the closing quote - /mx', - $quote + '/^ + %1$s # match a quote at the start of the value + ( # capturing sub-pattern used + (?: # we do not need to capture this + [^%1$s\\\\] # any character other than a quote or backslash + |\\\\\\\\ # or two backslashes together + |\\\\%1$s # or an escaped quote e.g \" + )* # as many characters that match the previous rules + ) # end of the capturing sub-pattern + %1$s # and the closing quote + .*$ # and discard any string after the closing quote + /mx', $quote ); - $value = preg_replace($regexPattern, '$1', $value); - $value = str_replace("\\$quote", $quote, $value); - $value = str_replace('\\\\', '\\', $value); + $value = preg_replace($regexPattern, '$1', $value); + $value = str_replace("\\$quote", $quote, $value); + $value = str_replace('\\\\', '\\', $value); } else { @@ -241,21 +249,16 @@ protected function resolveNestedVariables(string $value): string $loader = $this; $value = preg_replace_callback( - '/\${([a-zA-Z0-9_]+)}/', - function ($matchedPatterns) use ($loader) + '/\${([a-zA-Z0-9_]+)}/', function ($matchedPatterns) use ($loader) { + $nestedVariable = $loader->getVariable($matchedPatterns[1]); + + if (is_null($nestedVariable)) { - $nestedVariable = $loader->getVariable($matchedPatterns[1]); - - if (is_null($nestedVariable)) - { - return $matchedPatterns[0]; - } - else - { - return $nestedVariable; - } - }, - $value + return $matchedPatterns[0]; + } + + return $nestedVariable; + }, $value ); } @@ -293,5 +296,4 @@ protected function getVariable(string $name) } //-------------------------------------------------------------------- - } diff --git a/system/Config/ForeignCharacters.php b/system/Config/ForeignCharacters.php new file mode 100644 index 000000000000..e3fc5a599a8c --- /dev/null +++ b/system/Config/ForeignCharacters.php @@ -0,0 +1,141 @@ + 'ae', + '/ö|œ/' => 'oe', + '/ü/' => 'ue', + '/Ä/' => 'Ae', + '/Ü/' => 'Ue', + '/Ö/' => 'Oe', + '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A', + '/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č/' => 'C', + '/ç|ć|ĉ|ċ|č/' => 'c', + '/Д/' => 'D', + '/д/' => 'd', + '/Ð|Ď|Đ|Δ/' => 'Dj', + '/ð|ď|đ|δ/' => 'dj', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e', + '/Ф/' => 'F', + '/ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G', + '/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g', + '/Ĥ|Ħ/' => 'H', + '/ĥ|ħ/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i', + '/Ĵ/' => 'J', + '/ĵ/' => 'j', + '/Ķ|Κ|К/' => 'K', + '/ķ|κ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n', + '/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O', + '/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R', + '/ŕ|ŗ|ř|ρ|р/' => 'r', + '/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S', + '/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's', + '/Ț|Ţ|Ť|Ŧ|τ|Т/' => 'T', + '/ț|ţ|ť|ŧ|т/' => 't', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u', + '/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y', + '/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y', + '/В/' => 'V', + '/в/' => 'v', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Ź|Ż|Ž|Ζ|З/' => 'Z', + '/ź|ż|ž|ζ|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/' => 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/ƒ/' => 'f', + '/ξ/' => 'ks', + '/π/' => 'p', + '/β/' => 'v', + '/μ/' => 'm', + '/ψ/' => 'ps', + '/Ё/' => 'Yo', + '/ё/' => 'yo', + '/Є/' => 'Ye', + '/є/' => 'ye', + '/Ї/' => 'Yi', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + '/Х/' => 'Kh', + '/х/' => 'kh', + '/Ц/' => 'Ts', + '/ц/' => 'ts', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ъ|ъ|Ь|ь/' => '', + '/Ю/' => 'Yu', + '/ю/' => 'yu', + '/Я/' => 'Ya', + '/я/' => 'ya' + ]; + +} diff --git a/system/Config/Routes.php b/system/Config/Routes.php index afe10a4bee9b..8834db5a9225 100644 --- a/system/Config/Routes.php +++ b/system/Config/Routes.php @@ -1,5 +1,4 @@ cli('migrations/(:segment)/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1/$2'); -$routes->cli('migrations/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1'); -$routes->cli('migrations', '\CodeIgniter\Commands\MigrationsCommand::index'); +$routes->cli('migrations/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1'); +$routes->cli('migrations', '\CodeIgniter\Commands\MigrationsCommand::index'); + +// CLI Catchall - uses a _remap to +$routes->cli('ci(:any)', '\CodeIgniter\CLI\CommandRunner::index/$1'); + diff --git a/system/Config/Services.php b/system/Config/Services.php new file mode 100644 index 000000000000..f99c88c7650a --- /dev/null +++ b/system/Config/Services.php @@ -0,0 +1,842 @@ +setLogger(self::logger(true)); + + return $email; + } + + //-------------------------------------------------------------------- + + /** + * The Exceptions class holds the methods that handle: + * + * - set_exception_handler + * - set_error_handler + * - register_shutdown_function + * + * @param \Config\Exceptions $config + * @param \CodeIgniter\HTTP\IncomingRequest $request + * @param \CodeIgniter\HTTP\Response $response + * @param bool $getShared + * + * @return \CodeIgniter\Debug\Exceptions + */ + public static function exceptions( + \Config\Exceptions $config = null, + \CodeIgniter\HTTP\IncomingRequest $request = null, + \CodeIgniter\HTTP\Response $response = null, + $getShared = true + ) { + if ($getShared) + { + return self::getSharedInstance('exceptions', $config, $request, $response); + } + + if (empty($config)) + { + $config = new \Config\Exceptions(); + } + + if (empty($request)) + { + $request = static::request(); + } + + if (empty($response)) + { + $response = static::response(); + } + + return (new \CodeIgniter\Debug\Exceptions($config, $request, $response)); + } + + //-------------------------------------------------------------------- + + /** + * Filters allow you to run tasks before and/or after a controller + * is executed. During before filters, the request can be modified, + * and actions taken based on the request, while after filters can + * act on or modify the response itself before it is sent to the client. + * + * @param mixed $config + * @param bool $getShared + * + * @return \CodeIgniter\Filters\Filters + */ + public static function filters($config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('filters', $config); + } + + if (empty($config)) + { + $config = new \Config\Filters(); + } + + return new \CodeIgniter\Filters\Filters($config, self::request(), self::response()); + } + + //-------------------------------------------------------------------- + + /** + * Acts as a factory for ImageHandler classes and returns an instance + * of the handler. Used like Services::image()->withFile($path)->rotate(90)->save(); + * + * @param string $handler + * @param mixed $config + * @param bool $getShared + * + * @return \CodeIgniter\Images\Handlers\BaseHandler + */ + public static function image(string $handler = null, $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('image', $handler, $config); + } + + if (empty($config)) + { + $config = new \Config\Images(); + } + + $handler = is_null($handler) ? $config->defaultHandler : $handler; + + $class = $config->handlers[$handler]; + + return new $class($config); + } + + //-------------------------------------------------------------------- + + /** + * The Iterator class provides a simple way of looping over a function + * and timing the results and memory usage. Used when debugging and + * optimizing applications. + * + * @param bool $getShared + * + * @return \CodeIgniter\Debug\Iterator + */ + public static function iterator($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('iterator'); + } + + return new \CodeIgniter\Debug\Iterator(); + } + + //-------------------------------------------------------------------- + + /** + * Responsible for loading the language string translations. + * + * @param string $locale + * @param bool $getShared + * + * @return \CodeIgniter\Language\Language + */ + public static function language(string $locale = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('language', $locale) + ->setLocale($locale); + } + + $locale = ! empty($locale) + ? $locale + : self::request() + ->getLocale(); + + return new \CodeIgniter\Language\Language($locale); + } + + //-------------------------------------------------------------------- + + /** + * The Logger class is a PSR-3 compatible Logging class that supports + * multiple handlers that process the actual logging. + * + * @param bool $getShared + * + * @return \CodeIgniter\Log\Logger + */ + public static function logger($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('logger'); + } + + return new \CodeIgniter\Log\Logger(new \Config\Logger()); + } + + //-------------------------------------------------------------------- + + /** + * @param \CodeIgniter\Config\BaseConfig $config + * @param \CodeIgniter\Database\ConnectionInterface $db + * @param bool $getShared + * + * @return \CodeIgniter\Database\MigrationRunner + */ + public static function migrations(BaseConfig $config = null, ConnectionInterface $db = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('migrations', $config, $db); + } + + $config = empty($config) ? new \Config\Migrations() : $config; + + return new MigrationRunner($config, $db); + } + + //-------------------------------------------------------------------- + + /** + * The Negotiate class provides the content negotiation features for + * working the request to determine correct language, encoding, charset, + * and more. + * + * @param \CodeIgniter\HTTP\RequestInterface $request + * @param bool $getShared + * + * @return \CodeIgniter\HTTP\Negotiate + */ + public static function negotiator(\CodeIgniter\HTTP\RequestInterface $request = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('negotiator', $request); + } + + if (is_null($request)) + { + $request = self::request(); + } + + return new \CodeIgniter\HTTP\Negotiate($request); + } + + //-------------------------------------------------------------------- + + + /** + * @param mixed $config + * @param \CodeIgniter\View\RendererInterface $view + * @param bool $getShared + * + * @return \CodeIgniter\Pager\Pager + */ + public static function pager($config = null, RendererInterface $view = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('pager', $config, $view); + } + + if (empty($config)) + { + $config = new \Config\Pager(); + } + + if (! $view instanceof RendererInterface) + { + $view = self::renderer(); + } + + return new \CodeIgniter\Pager\Pager($config, $view); + } + + //-------------------------------------------------------------------- + + /** + * The Parser is a simple template parser. + * + * @param string $viewPath + * @param mixed $config + * @param bool $getShared + * + * @return \CodeIgniter\View\Parser + */ + public static function parser($viewPath = APPPATH.'Views/', $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('parser', $viewPath, $config); + } + + if (is_null($config)) + { + $config = new \Config\View(); + } + + return new \CodeIgniter\View\Parser($config, $viewPath, self::locator(true), CI_DEBUG, self::logger(true)); + } + + //-------------------------------------------------------------------- + + /** + * The Renderer class is the class that actually displays a file to the user. + * The default View class within CodeIgniter is intentionally simple, but this + * service could easily be replaced by a template engine if the user needed to. + * + * @param string $viewPath + * @param mixed $config + * @param bool $getShared + * + * @return \CodeIgniter\View\View + */ + public static function renderer($viewPath = APPPATH.'Views/', $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('renderer', $viewPath, $config); + } + + if (is_null($config)) + { + $config = new \Config\View(); + } + + return new \CodeIgniter\View\View($config, $viewPath, self::locator(true), CI_DEBUG, self::logger(true)); + } + + //-------------------------------------------------------------------- + + /** + * The Request class models an HTTP request. + * + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\HTTP\IncomingRequest + */ + public static function request(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('request', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + return new \CodeIgniter\HTTP\IncomingRequest( + $config, + new \CodeIgniter\HTTP\URI(), + 'php://input', + new \CodeIgniter\HTTP\UserAgent() + ); + } + + //-------------------------------------------------------------------- + + /** + * The Response class models an HTTP response. + * + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\HTTP\Response + */ + public static function response(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('response', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + return new \CodeIgniter\HTTP\Response($config); + } + + //-------------------------------------------------------------------- + + /** + * The Redirect class provides nice way of working with redirects. + * + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\HTTP\Response + */ + public static function redirectResponse(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('redirectResponse', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + $response = new \CodeIgniter\HTTP\RedirectResponse($config); + $response->setProtocolVersion(self::request() + ->getProtocolVersion()); + + return $response; + } + + //-------------------------------------------------------------------- + + /** + * The Routes service is a class that allows for easily building + * a collection of routes. + * + * @param bool $getShared + * + * @return \CodeIgniter\Router\RouteCollection + */ + public static function routes($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('routes'); + } + + return new \CodeIgniter\Router\RouteCollection(self::locator(), config('Modules')); + } + + //-------------------------------------------------------------------- + + /** + * The Router class uses a RouteCollection's array of routes, and determines + * the correct Controller and Method to execute. + * + * @param \CodeIgniter\Router\RouteCollectionInterface $routes + * @param bool $getShared + * + * @return \CodeIgniter\Router\Router + */ + public static function router(\CodeIgniter\Router\RouteCollectionInterface $routes = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('router', $routes); + } + + if (empty($routes)) + { + $routes = self::routes(true); + } + + return new \CodeIgniter\Router\Router($routes); + } + + //-------------------------------------------------------------------- + + /** + * The Security class provides a few handy tools for keeping the site + * secure, most notably the CSRF protection tools. + * + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\Security\Security + */ + public static function security(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('security', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + return new \CodeIgniter\Security\Security($config); + } + + //-------------------------------------------------------------------- + + /** + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\Session\Session + */ + public static function session(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('session', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + $logger = self::logger(true); + + $driverName = $config->sessionDriver; + $driver = new $driverName($config); + $driver->setLogger($logger); + + $session = new \CodeIgniter\Session\Session($driver, $config); + $session->setLogger($logger); + + if (session_status() == PHP_SESSION_NONE) + { + $session->start(); + } + + return $session; + } + + //-------------------------------------------------------------------- + + /** + * The Throttler class provides a simple method for implementing + * rate limiting in your applications. + * + * @param bool $getShared + * + * @return \CodeIgniter\Throttle\Throttler + */ + public static function throttler($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('throttler'); + } + + return new \CodeIgniter\Throttle\Throttler(self::cache()); + } + + //-------------------------------------------------------------------- + + /** + * The Timer class provides a simple way to Benchmark portions of your + * application. + * + * @param bool $getShared + * + * @return \CodeIgniter\Debug\Timer + */ + public static function timer($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('timer'); + } + + return new \CodeIgniter\Debug\Timer(); + } + + //-------------------------------------------------------------------- + + /** + * @param \Config\App $config + * @param bool $getShared + * + * @return \CodeIgniter\Debug\Toolbar + */ + public static function toolbar(\Config\App $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('toolbar', $config); + } + + if (! is_object($config)) + { + $config = config(App::class); + } + + return new \CodeIgniter\Debug\Toolbar($config); + } + + //-------------------------------------------------------------------- + + /** + * The URI class provides a way to model and manipulate URIs. + * + * @param string $uri + * @param bool $getShared + * + * @return \CodeIgniter\HTTP\URI + */ + public static function uri($uri = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('uri', $uri); + } + + return new \CodeIgniter\HTTP\URI($uri); + } + + //-------------------------------------------------------------------- + + /** + * The Validation class provides tools for validating input data. + * + * @param \Config\Validation $config + * @param bool $getShared + * + * @return \CodeIgniter\Validation\Validation + */ + public static function validation(\Config\Validation $config = null, bool $getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('validation', $config); + } + + if (is_null($config)) + { + $config = new \Config\Validation(); + } + + return new \CodeIgniter\Validation\Validation($config, self::renderer()); + } + + //-------------------------------------------------------------------- + + /** + * View cells are intended to let you insert HTML into view + * that has been generated by any callable in the system. + * + * @param bool $getShared + * + * @return \CodeIgniter\View\Cell + */ + public static function viewcell($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('viewcell'); + } + + return new \CodeIgniter\View\Cell(self::cache()); + } + + //-------------------------------------------------------------------- + + /** + * The Typography class provides a way to format text in semantically relevant ways. + * + * @param bool $getShared + * + * @return \CodeIgniter\Typography\Typography + */ + public static function typography($getShared = true) + { + if ($getShared) + { + return self::getSharedInstance('typography'); + } + + return new \CodeIgniter\Typography\Typography(); + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Config/View.php b/system/Config/View.php new file mode 100644 index 000000000000..17969643aa47 --- /dev/null +++ b/system/Config/View.php @@ -0,0 +1,83 @@ + '\abs', + 'capitalize' => '\CodeIgniter\View\Filters::capitalize', + 'date' => '\CodeIgniter\View\Filters::date', + 'date_modify' => '\CodeIgniter\View\Filters::date_modify', + 'default' => '\CodeIgniter\View\Filters::default', + 'esc' => '\CodeIgniter\View\Filters::esc', + 'excerpt' => '\CodeIgniter\View\Filters::excerpt', + 'highlight' => '\CodeIgniter\View\Filters::highlight', + 'highlight_code' => '\CodeIgniter\View\Filters::highlight_code', + 'limit_words' => '\CodeIgniter\View\Filters::limit_words', + 'limit_chars' => '\CodeIgniter\View\Filters::limit_chars', + 'local_currency' => '\CodeIgniter\View\Filters::local_currency', + 'local_number' => '\CodeIgniter\View\Filters::local_number', + 'lower' => '\strtolower', + 'nl2br' => '\CodeIgniter\View\Filters::nl2br', + 'number_format' => '\number_format', + 'prose' => '\CodeIgniter\View\Filters::prose', + 'round' => '\CodeIgniter\View\Filters::round', + 'strip_tags' => '\strip_tags', + 'title' => '\CodeIgniter\View\Filters::title', + 'upper' => '\strtoupper', + ]; + protected $corePlugins = [ + 'current_url' => '\CodeIgniter\View\Plugins::currentURL', + 'previous_url' => '\CodeIgniter\View\Plugins::previousURL', + 'mailto' => '\CodeIgniter\View\Plugins::mailto', + 'safe_mailto' => '\CodeIgniter\View\Plugins::safeMailto', + 'lang' => '\CodeIgniter\View\Plugins::lang', + 'validation_errors' => '\CodeIgniter\View\Plugins::validationErrors', + 'route' => '\CodeIgniter\View\Plugins::route', + 'siteURL' => '\CodeIgniter\View\Plugins::siteURL', + ]; + + public function __construct() + { + $this->filters = array_merge($this->filters, $this->coreFilters); + $this->plugins = array_merge($this->plugins, $this->corePlugins); + + parent::__construct(); + } + +} diff --git a/system/Controller.php b/system/Controller.php index e001e1f32461..ef1ba0448b33 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,17 +29,17 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\Services; use CodeIgniter\Log\Logger; +use CodeIgniter\Validation\Validation; +use Psr\Log\LoggerInterface; /** * Class Controller @@ -48,6 +48,7 @@ */ class Controller { + /** * An array of helpers to be automatically loaded * upon class instantiation. @@ -61,14 +62,14 @@ class Controller /** * Instance of the main Request object. * - * @var RequestInterface + * @var HTTP\IncomingRequest */ protected $request; /** * Instance of the main response object. * - * @var ResponseInterface + * @var HTTP\Response */ protected $response; @@ -86,24 +87,31 @@ class Controller */ protected $forceHTTPS = 0; + /** + * Once validation has been run, + * will hold the Validation instance. + * + * @var Validation + */ + protected $validator; + //-------------------------------------------------------------------- /** * Constructor. - * - * @param RequestInterface $request - * @param ResponseInterface $response - * @param Logger $logger + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param \Psr\Log\LoggerInterface $logger + * + * @throws \CodeIgniter\HTTP\RedirectException */ - public function __construct(RequestInterface $request, ResponseInterface $response, Logger $logger = null) + public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) { - $this->request = $request; - + $this->request = $request; $this->response = $response; - - $this->logger = is_null($logger) ? Services::logger(true) : $logger; - - $this->logger->info('Controller "'.get_class($this).'" loaded.'); + $this->logger = $logger; + $this->logger->info('Controller "' . get_class($this) . '" loaded.'); if ($this->forceHTTPS > 0) { @@ -112,7 +120,7 @@ public function __construct(RequestInterface $request, ResponseInterface $respon $this->loadHelpers(); } - + //-------------------------------------------------------------------- /** @@ -124,10 +132,25 @@ public function __construct(RequestInterface $request, ResponseInterface $respon * @param int $duration The number of seconds this link should be * considered secure for. Only with HSTS header. * Default value is 1 year. + * + * @throws \CodeIgniter\HTTP\RedirectException */ public function forceHTTPS(int $duration = 31536000) { - force_https($duration, $this->request, $this->response); + force_https($duration, $this->request, $this->response); + } + + //-------------------------------------------------------------------- + + /** + * Provides a simple way to tie into the main CodeIgniter class + * and tell it how long to cache the current page for. + * + * @param int $time + */ + public function cachePage(int $time) + { + CodeIgniter::cache($time); } //-------------------------------------------------------------------- @@ -137,7 +160,8 @@ public function forceHTTPS(int $duration = 31536000) */ protected function loadHelpers() { - if (empty($this->helpers)) return; + if (empty($this->helpers)) + return; foreach ($this->helpers as $helper) { @@ -147,5 +171,26 @@ protected function loadHelpers() //-------------------------------------------------------------------- + /** + * 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 + * + * @return bool + */ + public function validate($rules, array $messages = []): bool + { + $this->validator = Services::validation(); + + $success = $this->validator + ->withRequest($this->request) + ->setRules($rules, $messages) + ->run(); + return $success; + } + + //-------------------------------------------------------------------- } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index ae8c92192e12..77809da67554 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2,12 +2,12 @@ /** * CodeIgniter - * + * * An open source application development framework for PHP * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,14 +29,13 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\DatabaseException; +use \CodeIgniter\Database\Exceptions\DatabaseException; /** * Class BaseBuilder @@ -46,9 +45,11 @@ * certain methods to make them work. * * @package CodeIgniter\Database + * @mixin \CodeIgniter\Model */ class BaseBuilder { + /** * Reset DELETE data flag * @@ -140,13 +141,6 @@ class BaseBuilder */ protected $QBSet = []; - /** - * QB aliased tables list - * - * @var array - */ - protected $QBAliasedTables = []; - /** * QB WHERE group started flag * @@ -164,7 +158,7 @@ class BaseBuilder /** * A reference to the database connection. * - * @var ConnectionInterface + * @var BaseConnection */ protected $db; @@ -194,12 +188,27 @@ class BaseBuilder */ protected $binds = []; + /** + * Some databases, like SQLite, do not by default + * allow limiting of delete clauses. + * + * @var bool + */ + protected $canLimitDeletes = true; + + /** + * Some databases do not by default + * allow limit update queries with WHERE. + * @var bool + */ + protected $canLimitWhereUpdates = true; + //-------------------------------------------------------------------- /** * Constructor - * - * @param type $tableName + * + * @param string|array $tableName * @param \CodeIgniter\Database\ConnectionInterface $db * @param array $options * @throws DatabaseException @@ -213,10 +222,9 @@ public function __construct($tableName, ConnectionInterface &$db, array $options $this->db = $db; - $this->trackAliases($tableName); $this->from($tableName); - if (count($options)) + if (! empty($options)) { foreach ($options as $key => $value) { @@ -245,8 +253,8 @@ public function getBinds(): array * * Generates the SELECT portion of the query * - * @param string|array - * @param mixed + * @param string|array $select + * @param mixed $escape * * @return BaseBuilder */ @@ -266,7 +274,20 @@ public function select($select = '*', $escape = null) if ($val !== '') { - $this->QBSelect[] = $val; + $this->QBSelect[] = $val; + + /* + * When doing 'SELECT NULL as field_alias FROM table' + * null gets taken as a field, and therefore escaped + * with backticks. + * This prevents NULL being escaped + * @see https://github.com/bcit-ci/CodeIgniter4/issues/1169 + */ + if ( strtoupper(mb_substr(trim($val), 0, 4)) == 'NULL') + { + $escape = false; + } + $this->QBNoEscape[] = $escape; } } @@ -281,8 +302,8 @@ public function select($select = '*', $escape = null) * * Generates a SELECT MAX(field) portion of a query * - * @param string the field - * @param string an alias + * @param string $select The field + * @param string $alias An alias * * @return BaseBuilder */ @@ -298,8 +319,8 @@ public function selectMax($select = '', $alias = '') * * Generates a SELECT MIN(field) portion of a query * - * @param string the field - * @param string an alias + * @param string $select The field + * @param string $alias An alias * * @return BaseBuilder */ @@ -315,8 +336,8 @@ public function selectMin($select = '', $alias = '') * * Generates a SELECT AVG(field) portion of a query * - * @param string the field - * @param string an alias + * @param string $select The field + * @param string $alias An alias * * @return BaseBuilder */ @@ -332,8 +353,8 @@ public function selectAvg($select = '', $alias = '') * * Generates a SELECT SUM(field) portion of a query * - * @param string the field - * @param string an alias + * @param string $select The field + * @param string $alias An alias * * @return BaseBuilder */ @@ -357,6 +378,7 @@ public function selectSum($select = '', $alias = '') * @param string $type * * @return BaseBuilder + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ protected function maxMinAvgSum($select = '', $alias = '', $type = 'MAX') { @@ -369,7 +391,7 @@ protected function maxMinAvgSum($select = '', $alias = '', $type = 'MAX') if ( ! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM'])) { - throw new DatabaseException('Invalid function type: '.$type); + throw new DatabaseException('Invalid function type: ' . $type); } if ($alias === '') @@ -377,9 +399,9 @@ protected function maxMinAvgSum($select = '', $alias = '', $type = 'MAX') $alias = $this->createAliasFromTable(trim($select)); } - $sql = $type.'('.$this->db->protectIdentifiers(trim($select)).') AS '.$this->db->escapeIdentifiers(trim($alias)); + $sql = $type . '(' . $this->db->protectIdentifiers(trim($select)) . ') AS ' . $this->db->escapeIdentifiers(trim($alias)); - $this->QBSelect[] = $sql; + $this->QBSelect[] = $sql; $this->QBNoEscape[] = null; return $this; @@ -440,11 +462,11 @@ public function from($from, $overwrite = false) { if ($overwrite === true) { - $this->QBFrom = []; - $this->QBAliasedTables = []; + $this->QBFrom = []; + $this->db->setAliasedTables([]); } - foreach ((array)$from as $val) + foreach ((array) $from as $val) { if (strpos($val, ',') !== false) { @@ -478,10 +500,10 @@ public function from($from, $overwrite = false) * * Generates the JOIN portion of the query * - * @param string - * @param string the join condition - * @param string the type of join - * @param string whether not to try to escape identifiers + * @param string $table + * @param string $cond The join condition + * @param string $type The type of join + * @param string $escape Whether not to try to escape identifiers * * @return BaseBuilder */ @@ -509,11 +531,11 @@ public function join($table, $cond, $type = '', $escape = null) if ( ! $this->hasOperator($cond)) { - $cond = ' USING ('.($escape ? $this->db->escapeIdentifiers($cond) : $cond).')'; + $cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')'; } elseif ($escape === false) { - $cond = ' ON '.$cond; + $cond = ' ON ' . $cond; } else { @@ -521,31 +543,29 @@ public function join($table, $cond, $type = '', $escape = null) if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) { $conditions = []; - $joints = $joints[0]; + $joints = $joints[0]; array_unshift($joints, ['', 0]); - for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) + for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i -- ) { $joints[$i][1] += strlen($joints[$i][0]); // offset $conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]); - $pos = $joints[$i][1] - strlen($joints[$i][0]); - $joints[$i] = $joints[$i][0]; + $pos = $joints[$i][1] - strlen($joints[$i][0]); + $joints[$i] = $joints[$i][0]; } } else { $conditions = [$cond]; - $joints = ['']; + $joints = ['']; } $cond = ' ON '; - for ($i = 0, $c = count($conditions); $i < $c; $i++) + for ($i = 0, $c = count($conditions); $i < $c; $i ++ ) { $operator = $this->getOperator($conditions[$i]); $cond .= $joints[$i]; - $cond .= preg_match("/(\(*)?([\[\]\w\.'-]+)".preg_quote($operator)."(.*)/i", $conditions[$i], $match) - ? $match[1].$this->db->protectIdentifiers($match[2]).$operator.$this->db->protectIdentifiers($match[3]) - : $conditions[$i]; + $cond .= preg_match("/(\(*)?([\[\]\w\.'-]+)" . preg_quote($operator) . "(.*)/i", $conditions[$i], $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $conditions[$i]; } } @@ -556,7 +576,7 @@ public function join($table, $cond, $type = '', $escape = null) } // Assemble the JOIN statement - $this->QBJoin[] = $join = $type.'JOIN '.$table.$cond; + $this->QBJoin[] = $join = $type . 'JOIN ' . $table . $cond; return $this; } @@ -569,9 +589,9 @@ public function join($table, $cond, $type = '', $escape = null) * Generates the WHERE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed - * @param mixed - * @param bool + * @param mixed $key + * @param mixed $value + * @param bool $escape * * @return BaseBuilder */ @@ -588,9 +608,9 @@ public function where($key, $value = null, $escape = null) * Generates the WHERE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed - * @param mixed - * @param bool + * @param mixed $key + * @param mixed $value + * @param bool $escape * * @return BaseBuilder */ @@ -629,14 +649,12 @@ protected function whereHaving($qb_key, $key, $value = null, $type = 'AND ', $es foreach ($key as $k => $v) { - $prefix = (count($this->$qb_key) === 0) - ? $this->groupGetType('') - : $this->groupGetType($type); + $prefix = empty($this->$qb_key) ? $this->groupGetType('') : $this->groupGetType($type); if ($v !== null) { $op = $this->getOperator($k); - $k = trim(str_replace($op, '', $k)); + $k = trim(str_replace($op, '', $k)); $bind = $this->setBind($k, $v); @@ -656,12 +674,12 @@ protected function whereHaving($qb_key, $key, $value = null, $type = 'AND ', $es } elseif (preg_match('/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE)) { - $k = substr($k, 0, $match[0][1]).($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL'); + $k = substr($k, 0, $match[0][1]) . ($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL'); } - $v = ! is_null($v) ? ' :'.$bind : $v; + $v = ! is_null($v) ? " :$bind:" : $v; - $this->{$qb_key}[] = ['condition' => $prefix.$k.$v, 'escape' => $escape]; + $this->{$qb_key}[] = ['condition' => $prefix . $k . $v, 'escape' => $escape]; } return $this; @@ -763,7 +781,7 @@ public function orWhereNotIn($key = null, $values = null, $escape = null) */ protected function _whereIn($key = null, $values = null, $not = false, $type = 'AND ', $escape = null) { - if ($key === null OR $values === null) + if ($key === null || $values === null) { return $this; } @@ -784,16 +802,14 @@ protected function _whereIn($key = null, $values = null, $not = false, $type = ' $not = ($not) ? ' NOT' : ''; - $where_in = array_values($values); + $where_in = array_values($values); $this->binds[$ok] = $where_in; - $prefix = (count($this->QBWhere) === 0) - ? $this->groupGetType('') - : $this->groupGetType($type); + $prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type); $where_in = [ - 'condition' => $prefix.$key.$not.' IN :'.$ok, - 'escape' => false, + 'condition' => $prefix . $key . $not . " IN :{$ok}:", + 'escape' => false, ]; $this->QBWhere[] = $where_in; @@ -813,12 +829,13 @@ protected function _whereIn($key = null, $values = null, $not = false, $type = ' * @param string $match * @param string $side * @param bool $escape + * @param bool $insensitiveSearch IF true, will force a case-insensitive search * * @return BaseBuilder */ - public function like($field, $match = '', $side = 'both', $escape = null) + public function like($field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false) { - return $this->_like($field, $match, 'AND ', $side, '', $escape); + return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch); } //-------------------------------------------------------------------- @@ -833,12 +850,13 @@ public function like($field, $match = '', $side = 'both', $escape = null) * @param string $match * @param string $side * @param bool $escape + * @param bool $insensitiveSearch IF true, will force a case-insensitive search * * @return BaseBuilder */ - public function notLike($field, $match = '', $side = 'both', $escape = null) + public function notLike($field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false) { - return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape); + return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch); } //-------------------------------------------------------------------- @@ -853,12 +871,13 @@ public function notLike($field, $match = '', $side = 'both', $escape = null) * @param string $match * @param string $side * @param bool $escape + * @param bool $insensitiveSearch IF true, will force a case-insensitive search * * @return BaseBuilder */ - public function orLike($field, $match = '', $side = 'both', $escape = null) + public function orLike($field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false) { - return $this->_like($field, $match, 'OR ', $side, '', $escape); + return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch); } //-------------------------------------------------------------------- @@ -873,12 +892,13 @@ public function orLike($field, $match = '', $side = 'both', $escape = null) * @param string $match * @param string $side * @param bool $escape + * @param bool $insensitiveSearch IF true, will force a case-insensitive search * * @return BaseBuilder */ - public function orNotLike($field, $match = '', $side = 'both', $escape = null) + public function orNotLike($field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false) { - return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape); + return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch); } //-------------------------------------------------------------------- @@ -897,10 +917,11 @@ public function orNotLike($field, $match = '', $side = 'both', $escape = null) * @param string $side * @param string $not * @param bool $escape + * @param bool $insensitiveSearch IF true, will force a case-insensitive search * * @return BaseBuilder */ - protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $escape = null) + protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $escape = null, $insensitiveSearch = false) { if ( ! is_array($field)) { @@ -914,8 +935,12 @@ protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $n foreach ($field as $k => $v) { - $prefix = (count($this->QBWhere) === 0) - ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type); + + if ($insensitiveSearch === true) + { + $v = strtolower($v); + } if ($side === 'none') { @@ -934,7 +959,7 @@ protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $n $bind = $this->setBind($k, "%$v%"); } - $like_statement = "{$prefix} {$k} {$not} LIKE :{$bind}"; + $like_statement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch); // some platforms require an escape sequence definition for LIKE wildcards if ($escape === true && $this->db->likeEscapeStr !== '') @@ -950,6 +975,31 @@ protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $n //-------------------------------------------------------------------- + /** + * Platform independent LIKE statement builder. + * + * @param string|null $prefix + * @param string $column + * @param string|null $not + * @param string $bind + * @param bool $insensitiveSearch + * + * @return string $like_statement + */ + public function _like_statement(string $prefix = null, string $column, string $not = null, string $bind, bool $insensitiveSearch = false): string + { + $like_statement = "{$prefix} {$column} {$not} LIKE :{$bind}:"; + + if ($insensitiveSearch === true) + { + $like_statement = "{$prefix} LOWER({$column}) {$not} LIKE :{$bind}:"; + } + + return $like_statement; + } + + //-------------------------------------------------------------------- + /** * Starts a query group. * @@ -963,11 +1013,10 @@ public function groupStart($not = '', $type = 'AND ') $type = $this->groupGetType($type); $this->QBWhereGroupStarted = true; - $prefix = count($this->QBWhere) === 0 ? '' - : $type; - $where = [ - 'condition' => $prefix.$not.str_repeat(' ', ++$this->QBWhereGroupCount).' (', - 'escape' => false, + $prefix = empty($this->QBWhere) ? '' : $type; + $where = [ + 'condition' => $prefix . $not . str_repeat(' ', ++ $this->QBWhereGroupCount) . ' (', + 'escape' => false, ]; $this->QBWhere[] = $where; @@ -1021,9 +1070,9 @@ public function orNotGroupStart() public function groupEnd() { $this->QBWhereGroupStarted = false; - $where = [ - 'condition' => str_repeat(' ', $this->QBWhereGroupCount--).')', - 'escape' => false, + $where = [ + 'condition' => str_repeat(' ', $this->QBWhereGroupCount -- ) . ')', + 'escape' => false, ]; $this->QBWhere[] = $where; @@ -1049,7 +1098,7 @@ protected function groupGetType($type) { if ($this->QBWhereGroupStarted) { - $type = ''; + $type = ''; $this->QBWhereGroupStarted = false; } @@ -1072,9 +1121,7 @@ public function groupBy($by, $escape = null) if (is_string($by)) { - $by = ($escape === true) - ? explode(',', $by) - : [$by]; + $by = ($escape === true) ? explode(',', $by) : [$by]; } foreach ($by as $val) @@ -1148,9 +1195,7 @@ public function orderBy($orderby, $direction = '', $escape = null) $direction = ''; // Do we have a seed value? - $orderby = ctype_digit((string)$orderby) - ? sprintf($this->randomKeyword[1], $orderby) - : $this->randomKeyword[0]; + $orderby = ctype_digit((string) $orderby) ? sprintf($this->randomKeyword[1], $orderby) : $this->randomKeyword[0]; } elseif (empty($orderby)) { @@ -1158,7 +1203,7 @@ public function orderBy($orderby, $direction = '', $escape = null) } elseif ($direction !== '') { - $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' '.$direction : ''; + $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : ''; } is_bool($escape) || $escape = $this->db->protectIdentifiers; @@ -1173,13 +1218,11 @@ public function orderBy($orderby, $direction = '', $escape = null) foreach (explode(',', $orderby) as $field) { $qb_orderby[] = ($direction === '' && - preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) - ? [ - 'field' => ltrim(substr($field, 0, $match[0][1])), - 'direction' => ' '.$match[1][0], - 'escape' => true, - ] - : ['field' => trim($field), 'direction' => $direction, 'escape' => true]; + preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) ? [ + 'field' => ltrim(substr($field, 0, $match[0][1])), + 'direction' => ' ' . $match[1][0], + 'escape' => true, + ] : ['field' => trim($field), 'direction' => $direction, 'escape' => true]; } } @@ -1198,16 +1241,16 @@ public function orderBy($orderby, $direction = '', $escape = null) * * @return BaseBuilder */ - public function limit($value, $offset = 0) + public function limit(int $value = null, int $offset = 0) { - if (! is_null($value)) + if ( ! is_null($value)) { - $this->QBLimit = (int)$value; + $this->QBLimit = $value; } - if (! empty($offset)) + if ( ! empty($offset)) { - $this->QBOffset = (int)$offset; + $this->QBOffset = $offset; } return $this; @@ -1224,9 +1267,9 @@ public function limit($value, $offset = 0) */ public function offset($offset) { - if (! empty($offset)) + if ( ! empty($offset)) { - $this->QBOffset = (int)$offset; + $this->QBOffset = (int) $offset; } return $this; @@ -1245,7 +1288,7 @@ public function offset($offset) */ protected function _limit($sql) { - return $sql.' LIMIT '.($this->QBOffset ? $this->QBOffset.', ' : '').$this->QBLimit; + return $sql . ' LIMIT ' . ($this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit; } //-------------------------------------------------------------------- @@ -1253,11 +1296,11 @@ protected function _limit($sql) /** * The "set" function. * - * Allows key/value pairs to be set for inserting or updating + * Allows key/value pairs to be set for insert(), update() or replace(). * - * @param mixed - * @param string - * @param bool + * @param string|array|object $key Field name, or an array of field/value pairs + * @param string $value Field value, if $key is a single field + * @param bool $escape Whether to escape values and identifiers * * @return BaseBuilder */ @@ -1274,8 +1317,15 @@ public function set($key, $value = '', $escape = null) foreach ($key as $k => $v) { - $this->binds[$k] = $v; - $this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = ':'.$k; + if ($escape) + { + $bind = $this->setBind($k, $v); + $this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = ":$bind:"; + } + else + { + $this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = $v; + } } return $this; @@ -1283,12 +1333,34 @@ public function set($key, $value = '', $escape = null) //-------------------------------------------------------------------- + /** + * Returns the previously set() data, alternatively resetting it + * if needed. + * + * @param bool $clean + * + * @return array + */ + public function getSetData(bool $clean = false) + { + $data = $this->QBSet; + + if ($clean) + { + $this->QBSet = []; + } + + return $data; + } + + //-------------------------------------------------------------------- + /** * Get SELECT query string * * Compiles a SELECT query string and returns the sql. * - * @param bool TRUE: resets QB values; FALSE: leave QB values alone + * @param bool $reset TRUE: resets QB values; FALSE: leave QB values alone * * @return string */ @@ -1312,21 +1384,19 @@ public function getCompiledSelect($reset = true) * Compiles the select statement based on the other functions called * and runs the query * - * @param string the limit clause - * @param string the offset clause - * @param bool If true, returns the generate SQL, otherwise executes the query. + * @param int $limit The limit clause + * @param int $offset The offset clause + * @param bool $returnSQL If true, returns the generate SQL, otherwise executes the query. * - * @return CI_DB_result + * @return ResultInterface */ - public function get($limit = null, $offset = null, $returnSQL = false) + public function get(int $limit = null, int $offset = 0, $returnSQL = false) { - if ( ! empty($limit)) + if ( ! is_null($limit)) { $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); $this->resetSelect(); @@ -1349,8 +1419,8 @@ public function countAll($test = false) { $table = $this->QBFrom[0]; - $sql = $this->countString.$this->db->escapeIdentifiers('numrows').' FROM '. - $this->db->protectIdentifiers($table, true, null, false); + $sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . + $this->db->protectIdentifiers($table, true, null, false); if ($test) { @@ -1358,7 +1428,7 @@ public function countAll($test = false) } $query = $this->db->query($sql); - if (count($query->getResult()) === 0) + if (empty($query->getResult())) { return 0; } @@ -1366,7 +1436,7 @@ public function countAll($test = false) $query = $query->getRow(); $this->resetSelect(); - return (int)$query->numrows; + return (int) $query->numrows; } //-------------------------------------------------------------------- @@ -1377,8 +1447,8 @@ public function countAll($test = false) * Generates a platform-specific query string that counts all records * returned by an Query Builder query. * - * @param string - * @param bool the reset clause + * @param bool $reset + * @param bool $test The reset clause * * @return int */ @@ -1389,14 +1459,12 @@ public function countAllResults($reset = true, $test = false) // for selecting COUNT(*) ... if ( ! empty($this->QBOrderBy)) { - $orderby = $this->QBOrderBy; + $orderby = $this->QBOrderBy; $this->QBOrderBy = null; } - $sql = ($this->QBDistinct === true) - ? $this->countString.$this->db->protectIdentifiers('numrows')."\nFROM (\n". - $this->compileSelect()."\n) CI_count_all_results" - : $this->compileSelect($this->countString.$this->db->protectIdentifiers('numrows')); + $sql = ($this->QBDistinct === true) ? $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . + $this->compileSelect() . "\n) CI_count_all_results" : $this->compileSelect($this->countString . $this->db->protectIdentifiers('numrows')); if ($test) { @@ -1415,14 +1483,16 @@ public function countAllResults($reset = true, $test = false) $this->QBOrderBy = $orderby; } - $row = $result->getRow(); + $row = (! $result instanceof ResultInterface) + ? null + : $result->getRow(); if (empty($row)) { return 0; } - return (int)$row->numrows; + return (int) $row->numrows; } //-------------------------------------------------------------------- @@ -1436,7 +1506,7 @@ public function countAllResults($reset = true, $test = false) * @param int $limit * @param int $offset * - * @return CI_DB_result + * @return ResultInterface */ public function getWhere($where = null, $limit = null, $offset = null) { @@ -1466,13 +1536,13 @@ public function getWhere($where = null, $limit = null, $offset = null) * @param array $set An associative array of insert values * @param bool $escape Whether to escape values and identifiers * - * @param int $batch_size + * @param int $batchSize * @param bool $testing * * @return int Number of rows inserted or FALSE on failure * @throws DatabaseException */ - public function insertBatch($set = null, $escape = null, $batch_size = 100, $testing = false) + public function insertBatch($set = null, $escape = null, $batchSize = 100, $testing = false) { if ($set === null) { @@ -1505,14 +1575,13 @@ public function insertBatch($set = null, $escape = null, $batch_size = 100, $tes // Batch this baby $affected_rows = 0; - for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batch_size) + for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize) { - $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, $escape, false), $this->QBKeys, - array_slice($this->QBSet, $i, $batch_size)); + $sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, $escape, false), $this->QBKeys, array_slice($this->QBSet, $i, $batchSize)); if ($testing) { - ++$affected_rows; + ++ $affected_rows; } else { @@ -1544,7 +1613,7 @@ public function insertBatch($set = null, $escape = null, $batch_size = 100, $tes */ protected function _insertBatch($table, $keys, $values) { - return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES '.implode(', ', $values); + return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values); } //-------------------------------------------------------------------- @@ -1552,9 +1621,9 @@ protected function _insertBatch($table, $keys, $values) /** * The "setInsertBatch" function. Allows key/value pairs to be set for batch inserts * - * @param mixed - * @param string - * @param bool + * @param mixed $key + * @param string $value + * @param bool $escape * * @return BaseBuilder */ @@ -1588,12 +1657,12 @@ public function setInsertBatch($key, $value = '', $escape = null) $clean = []; foreach ($row as $k => $value) { - $clean[] = ':'.$this->setBind($k, $value); + $clean[] = ':' . $this->setBind($k, $value) .':'; } $row = $clean; - $this->QBSet[] = '('.implode(',', $row).')'; + $this->QBSet[] = '(' . implode(',', $row) . ')'; } foreach ($keys as $k) @@ -1611,7 +1680,7 @@ public function setInsertBatch($key, $value = '', $escape = null) * * Compiles an insert query and returns the sql * - * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @param bool $reset TRUE: reset QB values; FALSE: leave QB values alone * * @return string */ @@ -1623,11 +1692,9 @@ public function getCompiledInsert($reset = true) } $sql = $this->_insert( - $this->db->protectIdentifiers( - $this->QBFrom[0], true, null, false - ), - array_keys($this->QBSet), - array_values($this->QBSet) + $this->db->protectIdentifiers( + $this->QBFrom[0], true, null, false + ), array_keys($this->QBSet), array_values($this->QBSet) ); if ($reset === true) @@ -1645,9 +1712,9 @@ public function getCompiledInsert($reset = true) * * Compiles an insert string and runs the query * - * @param array an associative array of insert values - * @param bool $escape Whether to escape values and identifiers - * @param bool $test Used when running tests + * @param array $set An associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * @param bool $test Used when running tests * * @return bool TRUE on success, FALSE on failure */ @@ -1664,11 +1731,9 @@ public function insert($set = null, $escape = null, $test = false) } $sql = $this->_insert( - $this->db->protectIdentifiers( - $this->QBFrom[0], true, $escape, false - ), - array_keys($this->QBSet), - array_values($this->QBSet) + $this->db->protectIdentifiers( + $this->QBFrom[0], true, $escape, false + ), array_keys($this->QBSet), array_values($this->QBSet) ); if ($test === false) @@ -1688,13 +1753,12 @@ public function insert($set = null, $escape = null, $test = false) * validate that the there data is actually being set and that table * has been chosen to be inserted into. * - * @param string the table to insert data into - * * @return string + * @throws DatabaseException */ protected function validateInsert() { - if (count($this->QBSet) === 0) + if (empty($this->QBSet)) { if (CI_DEBUG) { @@ -1714,15 +1778,15 @@ protected function validateInsert() * * Generates a platform-specific insert string from the supplied data * - * @param string the table name - * @param array the insert keys - * @param array the insert values + * @param string $table The table name + * @param array $keys The insert keys + * @param array $unescapedKeys The insert values * * @return string */ protected function _insert($table, array $keys, array $unescapedKeys) { - return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $unescapedKeys).')'; + return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')'; } //-------------------------------------------------------------------- @@ -1732,13 +1796,11 @@ protected function _insert($table, array $keys, array $unescapedKeys) * * Compiles an replace into string and runs the query * - * @param array an associative array of insert values - * @param bool $returnSQL + * @param array $set An associative array of insert values + * @param bool $returnSQL * * @return bool TRUE on success, FALSE on failure * @throws DatabaseException - * @internal param true $bool returns the generated SQL, false executes the query. - * */ public function replace($set = null, $returnSQL = false) { @@ -1747,7 +1809,7 @@ public function replace($set = null, $returnSQL = false) $this->set($set); } - if (count($this->QBSet) === 0) + if (empty($this->QBSet)) { if (CI_DEBUG) { @@ -1758,8 +1820,7 @@ public function replace($set = null, $returnSQL = false) $table = $this->QBFrom[0]; - $sql = $this->_replace($table, array_keys($this->QBSet), - array_values($this->QBSet)); + $sql = $this->_replace($table, array_keys($this->QBSet), array_values($this->QBSet)); $this->resetWrite(); @@ -1773,15 +1834,15 @@ public function replace($set = null, $returnSQL = false) * * Generates a platform-specific replace string from the supplied data * - * @param string the table name - * @param array the insert keys - * @param array the insert values + * @param string $table The table name + * @param array $keys The insert keys + * @param array $values The insert values * * @return string */ protected function _replace($table, $keys, $values) { - return 'REPLACE INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')'; + return 'REPLACE INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')'; } //-------------------------------------------------------------------- @@ -1808,7 +1869,7 @@ protected function _fromTables() * * Compiles an update query and returns the sql * - * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @param bool $reset TRUE: reset QB values; FALSE: leave QB values alone * * @return string */ @@ -1843,7 +1904,7 @@ public function getCompiledUpdate($reset = true) * * @return bool TRUE on success, FALSE on failure */ - public function update($set = null, $where = null, $limit = null, $test = false) + public function update($set = null, $where = null, int $limit = null, $test = false) { if ($set !== null) { @@ -1862,6 +1923,11 @@ public function update($set = null, $where = null, $limit = null, $test = false) if ( ! empty($limit)) { + if (! $this->canLimitWhereUpdates) + { + throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.'); + } + $this->limit($limit); } @@ -1875,7 +1941,11 @@ public function update($set = null, $where = null, $limit = null, $test = false) { return true; } + + return false; } + + return true; } //-------------------------------------------------------------------- @@ -1885,8 +1955,8 @@ public function update($set = null, $where = null, $limit = null, $test = false) * * Generates a platform-specific update string from the supplied data * - * @param string the table name - * @param array the update data + * @param string $table the Table name + * @param array $values the Update data * * @return string */ @@ -1894,13 +1964,13 @@ protected function _update($table, $values) { foreach ($values as $key => $val) { - $valstr[] = $key.' = '.$val; + $valstr[] = $key . ' = ' . $val; } - return 'UPDATE '.$table.' SET '.implode(', ', $valstr) - .$this->compileWhereHaving('QBWhere') - .$this->compileOrderBy() - .($this->QBLimit ? $this->_limit(' ') : ''); + return 'UPDATE ' . $table . ' SET ' . implode(', ', $valstr) + . $this->compileWhereHaving('QBWhere') + . $this->compileOrderBy() + . ($this->QBLimit ? $this->_limit(' ') : ''); } //-------------------------------------------------------------------- @@ -1914,10 +1984,11 @@ protected function _update($table, $values) * * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ protected function validateUpdate() { - if (count($this->QBSet) === 0) + if (empty($this->QBSet)) { if (CI_DEBUG) { @@ -1937,14 +2008,15 @@ protected function validateUpdate() * * Compiles an update string and runs the query * - * @param array an associative array of update values - * @param string the where key - * @param int The size of the batch to run - * @param bool true means SQL is returned, false will execute the query + * @param array $set An associative array of update values + * @param string $index The where key + * @param int $batchSize The size of the batch to run + * @param bool $returnSQL True means SQL is returned, false will execute the query * - * @return int number of rows affected or FALSE on failure + * @return mixed Number of rows affected or FALSE on failure + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function updateBatch($set = null, $index = null, $batch_size = 100, $returnSQL = false) + public function updateBatch($set = null, $index = null, $batchSize = 100, $returnSQL = false) { if ($index === null) { @@ -1986,11 +2058,9 @@ public function updateBatch($set = null, $index = null, $batch_size = 100, $retu // Batch this baby $affected_rows = 0; $savedSQL = []; - for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batch_size) + for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize) { - $sql = $this->_updateBatch($table, - array_slice($this->QBSet, $i, $batch_size), - $this->db->protectIdentifiers($index) + $sql = $this->_updateBatch($table, array_slice($this->QBSet, $i, $batchSize), $this->db->protectIdentifiers($index) ); if ($returnSQL) @@ -2035,7 +2105,7 @@ protected function _updateBatch($table, $values, $index) { if ($field !== $index) { - $final[$field][] = 'WHEN '.$index.' = '.$val[$index].' THEN '.$val[$field]; + $final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field]; } } } @@ -2043,14 +2113,14 @@ protected function _updateBatch($table, $values, $index) $cases = ''; foreach ($final as $k => $v) { - $cases .= $k." = CASE \n" - .implode("\n", $v)."\n" - .'ELSE '.$k.' END, '; + $cases .= $k . " = CASE \n" + . implode("\n", $v) . "\n" + . 'ELSE ' . $k . ' END, '; } - $this->where($index.' IN('.implode(',', $ids).')', null, false); + $this->where($index . ' IN(' . implode(',', $ids) . ')', null, false); - return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->compileWhereHaving('QBWhere'); + return 'UPDATE ' . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); } //-------------------------------------------------------------------- @@ -2058,11 +2128,12 @@ protected function _updateBatch($table, $values, $index) /** * The "setUpdateBatch" function. Allows key/value pairs to be set for batch updating * - * @param array - * @param string - * @param bool + * @param array $key + * @param string $index + * @param bool $escape * * @return BaseBuilder + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function setUpdateBatch($key, $index = '', $escape = null) { @@ -2078,7 +2149,7 @@ public function setUpdateBatch($key, $index = '', $escape = null) foreach ($key as $k => $v) { $index_set = false; - $clean = []; + $clean = []; foreach ($v as $k2 => $v2) { if ($k2 === $index) @@ -2088,7 +2159,7 @@ public function setUpdateBatch($key, $index = '', $escape = null) $bind = $this->setBind($k2, $v2); - $clean[$this->db->protectIdentifiers($k2, false, $escape)] = ':'.$bind; + $clean[$this->db->protectIdentifiers($k2, false, $escape)] = ":$bind:"; } if ($index_set === false) @@ -2137,7 +2208,7 @@ public function emptyTable($test = false) * If the database does not support the truncate() command * This function maps to "DELETE FROM table" * - * @param bool Whether we're in test mode or not. + * @param bool $test Whether we're in test mode or not. * * @return bool TRUE on success, FALSE on failure */ @@ -2167,13 +2238,13 @@ public function truncate($test = false) * If the database does not support the truncate() command, * then this method maps to 'DELETE FROM table' * - * @param string the table name + * @param string $table The table name * * @return string */ protected function _truncate($table) { - return 'TRUNCATE '.$table; + return 'TRUNCATE ' . $table; } //-------------------------------------------------------------------- @@ -2183,8 +2254,7 @@ protected function _truncate($table) * * Compiles a delete query string and returns the sql * - * @param string the table to delete from - * @param bool TRUE: reset QB values; FALSE: leave QB values alone + * @param bool $reset TRUE: reset QB values; FALSE: leave QB values alone * * @return string */ @@ -2193,7 +2263,7 @@ public function getCompiledDelete($reset = true) $table = $this->QBFrom[0]; $this->returnDeleteSQL = true; - $sql = $this->delete($table, '', null, $reset); + $sql = $this->delete($table, '', null, $reset); $this->returnDeleteSQL = false; return $sql; @@ -2206,12 +2276,13 @@ public function getCompiledDelete($reset = true) * * Compiles a delete string and runs the query * - * @param mixed $where the where clause - * @param mixed $limit the limit clause + * @param mixed $where The where clause + * @param mixed $limit The limit clause * @param bool $reset_data * @param bool $returnSQL * * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function delete($where = '', $limit = null, $reset_data = true, $returnSQL = false) { @@ -2222,7 +2293,7 @@ public function delete($where = '', $limit = null, $reset_data = true, $returnSQ $this->where($where); } - if (count($this->QBWhere) === 0) + if (empty($this->QBWhere)) { if (CI_DEBUG) { @@ -2239,11 +2310,16 @@ public function delete($where = '', $limit = null, $reset_data = true, $returnSQ $this->QBLimit = $limit; } - if (! empty($this->QBLimit)) + if ( ! empty($this->QBLimit)) { + if (! $this->canLimitDeletes) + { + throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.'); + } + $sql = $this->_limit($sql); } - + if ($reset_data) { $this->resetWrite(); @@ -2297,14 +2373,14 @@ public function decrement(string $column, int $value = 1) * * Generates a platform-specific delete string from the supplied data * - * @param string the table name + * @param string $table The table name * * @return string */ protected function _delete($table) { - return 'DELETE FROM '.$table.$this->compileWhereHaving('QBWhere') - .($this->QBLimit ? ' LIMIT '.$this->QBLimit : ''); + return 'DELETE FROM ' . $table . $this->compileWhereHaving('QBWhere') + . ($this->QBLimit ? ' LIMIT ' . $this->QBLimit : ''); } //-------------------------------------------------------------------- @@ -2314,7 +2390,7 @@ protected function _delete($table) * * Used to track SQL statements written with aliased tables. * - * @param string The table to inspect + * @param string $table The table to inspect * * @return string */ @@ -2347,10 +2423,7 @@ protected function trackAliases($table) $table = trim(strrchr($table, ' ')); // Store the alias, if it doesn't already exist - if ( ! in_array($table, $this->QBAliasedTables)) - { - $this->QBAliasedTables[] = $table; - } + $this->db->addTableAlias($table); } } @@ -2377,7 +2450,7 @@ protected function compileSelect($select_override = false) { $sql = ( ! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT '; - if (count($this->QBSelect) === 0) + if (empty($this->QBSelect)) { $sql .= '*'; } @@ -2388,7 +2461,7 @@ protected function compileSelect($select_override = false) // is because until the user calls the from() function we don't know if there are aliases foreach ($this->QBSelect as $key => $val) { - $no_escape = isset($this->QBNoEscape[$key]) ? $this->QBNoEscape[$key] : null; + $no_escape = $this->QBNoEscape[$key] ?? null; $this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $no_escape); } @@ -2397,26 +2470,25 @@ protected function compileSelect($select_override = false) } // Write the "FROM" portion of the query - if (count($this->QBFrom) > 0) + if (! empty($this->QBFrom)) { - $sql .= "\nFROM ".$this->_fromTables(); + $sql .= "\nFROM " . $this->_fromTables(); } // Write the "JOIN" portion of the query - if (count($this->QBJoin) > 0) + if (! empty($this->QBJoin)) { - $sql .= "\n".implode("\n", $this->QBJoin); + $sql .= "\n" . implode("\n", $this->QBJoin); } $sql .= $this->compileWhereHaving('QBWhere') - .$this->compileGroupBy() - .$this->compileWhereHaving('QBHaving') - .$this->compileOrderBy(); // ORDER BY - + . $this->compileGroupBy() + . $this->compileWhereHaving('QBHaving') + . $this->compileOrderBy(); // ORDER BY // LIMIT if ($this->QBLimit) { - return $this->_limit($sql."\n"); + return $this->_limit($sql . "\n"); } return $sql; @@ -2439,9 +2511,9 @@ protected function compileSelect($select_override = false) */ protected function compileWhereHaving($qb_key) { - if (count($this->$qb_key) > 0) + if (! empty($this->$qb_key)) { - for ($i = 0, $c = count($this->$qb_key); $i < $c; $i++) + for ($i = 0, $c = count($this->$qb_key); $i < $c; $i ++ ) { // Is this condition already compiled? if (is_string($this->{$qb_key}[$i])) @@ -2456,18 +2528,13 @@ protected function compileWhereHaving($qb_key) // Split multiple conditions $conditions = preg_split( - '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i', - $this->{$qb_key}[$i]['condition'], - -1, - PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY + '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i', $this->{$qb_key}[$i]['condition'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); - for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci++) + for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci ++ ) { if (($op = $this->getOperator($conditions[$ci])) === false - OR - ! preg_match('/^(\(?)(.*)('.preg_quote($op, '/').')\s*(.*(?isLiteral($matches[4]) OR $matches[4] = $this->db->protectIdentifiers(trim($matches[4])); - $matches[4] = ' '.$matches[4]; + $matches[4] = ' ' . $matches[4]; } - $conditions[$ci] = $matches[1].$this->db->protectIdentifiers(trim($matches[2])) - .' '.trim($matches[3]).$matches[4].$matches[5]; + $conditions[$ci] = $matches[1] . $this->db->protectIdentifiers(trim($matches[2])) + . ' ' . trim($matches[3]) . $matches[4] . $matches[5]; } $this->{$qb_key}[$i] = implode('', $conditions); } return ($qb_key === 'QBHaving' ? "\nHAVING " : "\nWHERE ") - .implode("\n", $this->$qb_key); + . implode("\n", $this->$qb_key); } return ''; @@ -2517,9 +2584,9 @@ protected function compileWhereHaving($qb_key) */ protected function compileGroupBy() { - if (count($this->QBGroupBy) > 0) + if (! empty($this->QBGroupBy)) { - for ($i = 0, $c = count($this->QBGroupBy); $i < $c; $i++) + for ($i = 0, $c = count($this->QBGroupBy); $i < $c; $i ++ ) { // Is it already compiled? if (is_string($this->QBGroupBy[$i])) @@ -2528,12 +2595,10 @@ protected function compileGroupBy() } $this->QBGroupBy[$i] = ($this->QBGroupBy[$i]['escape'] === false OR - $this->isLiteral($this->QBGroupBy[$i]['field'])) - ? $this->QBGroupBy[$i]['field'] - : $this->db->protectIdentifiers($this->QBGroupBy[$i]['field']); + $this->isLiteral($this->QBGroupBy[$i]['field'])) ? $this->QBGroupBy[$i]['field'] : $this->db->protectIdentifiers($this->QBGroupBy[$i]['field']); } - return "\nGROUP BY ".implode(', ', $this->QBGroupBy); + return "\nGROUP BY " . implode(', ', $this->QBGroupBy); } return ''; @@ -2554,19 +2619,19 @@ protected function compileGroupBy() */ protected function compileOrderBy() { - if (is_array($this->QBOrderBy) && count($this->QBOrderBy) > 0) + if (is_array($this->QBOrderBy) && ! empty($this->QBOrderBy)) { - for ($i = 0, $c = count($this->QBOrderBy); $i < $c; $i++) + for ($i = 0, $c = count($this->QBOrderBy); $i < $c; $i ++ ) { if ($this->QBOrderBy[$i]['escape'] !== false && ! $this->isLiteral($this->QBOrderBy[$i]['field'])) { $this->QBOrderBy[$i]['field'] = $this->db->protectIdentifiers($this->QBOrderBy[$i]['field']); } - $this->QBOrderBy[$i] = $this->QBOrderBy[$i]['field'].$this->QBOrderBy[$i]['direction']; + $this->QBOrderBy[$i] = $this->QBOrderBy[$i]['field'] . $this->QBOrderBy[$i]['direction']; } - return $this->QBOrderBy = "\nORDER BY ".implode(', ', $this->QBOrderBy); + return $this->QBOrderBy = "\nORDER BY " . implode(', ', $this->QBOrderBy); } elseif (is_string($this->QBOrderBy)) { @@ -2583,7 +2648,7 @@ protected function compileOrderBy() * * Takes an object as input and converts the class variables to array key/vals * - * @param object + * @param object $object * * @return array */ @@ -2614,7 +2679,7 @@ protected function objectToArray($object) * * Takes an object as input and converts the class variables to array key/vals * - * @param object + * @param object $object * * @return array */ @@ -2625,8 +2690,8 @@ protected function batchObjectToArray($object) return $object; } - $array = []; - $out = get_object_vars($object); + $array = []; + $out = get_object_vars($object); $fields = array_keys($out); foreach ($fields as $val) @@ -2637,7 +2702,7 @@ protected function batchObjectToArray($object) $i = 0; foreach ($out[$val] as $data) { - $array[$i++][$val] = $data; + $array[$i ++][$val] = $data; } } } @@ -2660,8 +2725,8 @@ protected function isLiteral($str) { $str = trim($str); - if (empty($str) || ctype_digit($str) || (string)(float)$str === $str || - in_array(strtoupper($str), ['TRUE', 'FALSE'], true) + if (empty($str) || ctype_digit($str) || (string) (float) $str === $str || + in_array(strtoupper($str), ['TRUE', 'FALSE'], true) ) { return true; @@ -2671,8 +2736,7 @@ protected function isLiteral($str) if (empty($_str)) { - $_str = ($this->db->escapeChar !== '"') - ? ['"', "'"] : ["'"]; + $_str = ($this->db->escapeChar !== '"') ? ['"', "'"] : ["'"]; } return in_array($str[0], $_str, true); @@ -2700,9 +2764,7 @@ public function resetQuery() /** * Resets the query builder values. Called by the get() function * - * @param array An array of fields to reset - * - * @return void + * @param array $qb_reset_items An array of fields to reset */ protected function resetRun($qb_reset_items) { @@ -2716,24 +2778,26 @@ protected function resetRun($qb_reset_items) /** * Resets the query builder values. Called by the get() function - * - * @return void */ protected function resetSelect() { $this->resetRun([ - 'QBSelect' => [], - 'QBJoin' => [], - 'QBWhere' => [], - 'QBGroupBy' => [], - 'QBHaving' => [], - 'QBOrderBy' => [], - 'QBAliasedTables' => [], - 'QBNoEscape' => [], - 'QBDistinct' => false, - 'QBLimit' => false, - 'QBOffset' => false, + 'QBSelect' => [], + 'QBJoin' => [], + 'QBWhere' => [], + 'QBGroupBy' => [], + 'QBHaving' => [], + 'QBOrderBy' => [], + 'QBNoEscape' => [], + 'QBDistinct' => false, + 'QBLimit' => false, + 'QBOffset' => false, ]); + + if (! empty($this->db)) + { + $this->db->setAliasedTables([]); + } } //-------------------------------------------------------------------- @@ -2742,18 +2806,16 @@ protected function resetSelect() * Resets the query builder "write" values. * * Called by the insert() update() insertBatch() updateBatch() and delete() functions - * - * @return void */ protected function resetWrite() { $this->resetRun([ - 'QBSet' => [], - 'QBJoin' => [], - 'QBWhere' => [], - 'QBOrderBy' => [], - 'QBKeys' => [], - 'QBLimit' => false, + 'QBSet' => [], + 'QBJoin' => [], + 'QBWhere' => [], + 'QBOrderBy' => [], + 'QBKeys' => [], + 'QBLimit' => false, ]); } @@ -2762,14 +2824,13 @@ protected function resetWrite() /** * Tests whether the string has an SQL operator * - * @param string + * @param string $str * * @return bool */ protected function hasOperator($str) { - return (bool)preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', - trim($str)); + return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str)); } // -------------------------------------------------------------------- @@ -2777,7 +2838,7 @@ protected function hasOperator($str) /** * Returns the SQL string operator * - * @param string + * @param string $str * * @return string */ @@ -2787,27 +2848,24 @@ protected function getOperator($str) if (empty($_operators)) { - $_les = ($this->db->likeEscapeStr !== '') - ? '\s+'.preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/') - : ''; + $_les = ($this->db->likeEscapeStr !== '') ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/') : ''; $_operators = [ - '\s*(?:<|>|!)?=\s*', // =, <=, >=, != - '\s*<>?\s*', // <, <> - '\s*>\s*', // > - '\s+IS NULL', // IS NULL - '\s+IS NOT NULL', // IS NOT NULL - '\s+EXISTS\s*\(.*\)', // EXISTS(sql) - '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql) - '\s+BETWEEN\s+', // BETWEEN value AND value - '\s+IN\s*\(.*\)', // IN(list) - '\s+NOT IN\s*\(.*\)', // NOT IN (list) - '\s+LIKE\s+\S.*('.$_les.')?', // LIKE 'expr'[ ESCAPE '%s'] - '\s+NOT LIKE\s+\S.*('.$_les.')?' // NOT LIKE 'expr'[ ESCAPE '%s'] + '\s*(?:<|>|!)?=\s*', // =, <=, >=, != + '\s*<>?\s*', // <, <> + '\s*>\s*', // > + '\s+IS NULL', // IS NULL + '\s+IS NOT NULL', // IS NOT NULL + '\s+EXISTS\s*\(.*\)', // EXISTS(sql) + '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql) + '\s+BETWEEN\s+', // BETWEEN value AND value + '\s+IN\s*\(.*\)', // IN(list) + '\s+NOT IN\s*\(.*\)', // NOT IN (list) + '\s+LIKE\s+\S.*(' . $_les . ')?', // LIKE 'expr'[ ESCAPE '%s'] + '\s+NOT LIKE\s+\S.*(' . $_les . ')?' // NOT LIKE 'expr'[ ESCAPE '%s'] ]; } - return preg_match('/'.implode('|', $_operators).'/i', $str, $match) - ? $match[0] : false; + return preg_match('/' . implode('|', $_operators) . '/i', $str, $match) ? $match[0] : false; } // -------------------------------------------------------------------- @@ -2831,16 +2889,15 @@ protected function setBind(string $key, $value = null) $count = 0; - while (array_key_exists($key.$count, $this->binds)) + while (array_key_exists($key . $count, $this->binds)) { - ++$count; + ++ $count; } - $this->binds[$key.$count] = $value; + $this->binds[$key . $count] = $value; - return $key.$count; + return $key . $count; } //-------------------------------------------------------------------- - } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 146e4acdc167..555d06b76a10 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,68 +29,69 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\DatabaseException; +use CodeIgniter\Events\Events; +use CodeIgniter\Database\Exceptions\DatabaseException; /** * Class BaseConnection */ abstract class BaseConnection implements ConnectionInterface { + /** * Data Source Name / Connect string * * @var string */ - public $DSN; + protected $DSN; /** * Database port * * @var int */ - public $port = ''; + protected $port = ''; /** * Hostname * * @var string */ - public $hostname; + protected $hostname; /** * Username * * @var string */ - public $username; + protected $username; /** * Password * * @var string */ - public $password; + protected $password; /** * Database name * * @var string */ - public $database; + protected $database; /** * Database driver * * @var string */ - public $DBDriver = 'MySQLi'; + protected $DBDriver = 'MySQLi'; /** * Sub-driver @@ -98,21 +99,21 @@ abstract class BaseConnection implements ConnectionInterface * @used-by CI_DB_pdo_driver * @var string */ - public $subdriver; + protected $subdriver; /** * Table prefix * * @var string */ - public $DBPrefix = ''; + protected $DBPrefix = ''; /** * Persistent connection flag * * @var bool */ - public $pConnect = false; + protected $pConnect = false; /** * Debug flag @@ -121,56 +122,56 @@ abstract class BaseConnection implements ConnectionInterface * * @var bool */ - public $DBDebug = false; + protected $DBDebug = false; /** * Should we cache results? * * @var bool */ - public $cacheOn = false; + protected $cacheOn = false; /** * Path to store cache files. * * @var string */ - public $cacheDir; + protected $cacheDir; /** * Character set * * @var string */ - public $charset = 'utf8'; + protected $charset = 'utf8'; /** * Collation * * @var string */ - public $DBCollat = 'utf8_general_ci'; + protected $DBCollat = 'utf8_general_ci'; /** * Swap Prefix * * @var string */ - public $swapPre = ''; + protected $swapPre = ''; /** * Encryption flag/data * * @var mixed */ - public $encrypt = false; + protected $encrypt = false; /** * Compression flag * * @var bool */ - public $compress = false; + protected $compress = false; /** * Strict ON flag @@ -179,32 +180,24 @@ abstract class BaseConnection implements ConnectionInterface * * @var bool */ - public $strictOn; + protected $strictOn; /** * Settings for a failover connection. * * @var array */ - public $failover = []; - - /** - * Whether to keep an in-memory history of queries - * for debugging and timeline purposes. - * - * @var bool - */ - public $saveQueries = false; + protected $failover = []; //-------------------------------------------------------------------- /** - * Array of query objects that have executed + * The last query object that was executed * on this connection. * * @var array */ - protected $queries = []; + protected $lastQuery; /** * Connection ID @@ -232,7 +225,7 @@ abstract class BaseConnection implements ConnectionInterface * * Identifiers that must NOT be escaped. * - * @var string[] + * @var array */ protected $reservedIdentifiers = ['*']; @@ -263,7 +256,7 @@ abstract class BaseConnection implements ConnectionInterface * * @var array */ - protected $dataCache = []; + public $dataCache = []; /** * Microtime when connection was made @@ -278,6 +271,60 @@ abstract class BaseConnection implements ConnectionInterface */ protected $connectDuration; + /** + * If true, no queries will actually be + * ran against the database. + * + * @var bool + */ + protected $pretend = false; + + /** + * Transaction enabled flag + * + * @var bool + */ + public $transEnabled = true; + + /** + * Strict transaction mode flag + * + * @var bool + */ + public $transStrict = true; + + /** + * Transaction depth level + * + * @var int + */ + protected $transDepth = 0; + + /** + * Transaction status flag + * + * Used with transactions to determine if a rollback should occur. + * + * @var bool + */ + protected $transStatus = true; + + /** + * Transaction failure flag + * + * Used with transactions to determine if a transaction has failed. + * + * @var bool + */ + protected $transFailure = false; + + /** + * Array of table aliases. + * + * @var array + */ + protected $aliasedTables = []; + //-------------------------------------------------------------------- /** @@ -287,19 +334,19 @@ abstract class BaseConnection implements ConnectionInterface */ public function __construct(array $params) { - foreach ($params as $key => $value) - { - $this->$key = $value; - } + foreach ($params as $key => $value) + { + $this->$key = $value; + } } //-------------------------------------------------------------------- - /** * Initializes the database connection/settings. * * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function initialize() { @@ -369,6 +416,29 @@ abstract public function connect($persistent = false); //-------------------------------------------------------------------- + /** + * Close the database connection. + */ + public function close() + { + if ($this->connID) + { + $this->_close(); + $this->connID = FALSE; + } + } + + //-------------------------------------------------------------------- + + /** + * Platform dependent way method for closing the connection. + * + * @return mixed + */ + abstract protected function _close(); + + //-------------------------------------------------------------------- + /** * Create a persistent database connection. * @@ -439,6 +509,7 @@ public function getDatabase(): string */ public function getError() { + } //-------------------------------------------------------------------- @@ -465,19 +536,41 @@ abstract public function getVersion(); //-------------------------------------------------------------------- /** - * Specifies whether this connection should keep queries objects or not. + * Sets the Table Aliases to use. These are typically + * collected during use of the Builder, and set here + * so queries are built correctly. + * + * @param array $aliases * - * @param bool $save + * @return $this */ - public function saveQueries($save = false) + public function setAliasedTables(array $aliases) { - $this->saveQueries = $save; + $this->aliasedTables = $aliases; return $this; } //-------------------------------------------------------------------- + /** + * Add a table alias to our list. + * + * @param string $table + * + * @return $this + */ + public function addTableAlias(string $table) + { + if ( ! in_array($table, $this->aliasedTables)) + { + $this->aliasedTables[] = $table; + } + + return $this; + } + + /** * Executes the query against the database. * @@ -489,7 +582,6 @@ abstract protected function execute($sql); //-------------------------------------------------------------------- - /** * Orchestrates a query against the database. Queries must use * Database\Statement objects to store the query and build it. @@ -500,8 +592,8 @@ abstract protected function execute($sql); * * @param string $sql * @param array ...$binds - * @param $queryClass - * @return mixed + * @param string $queryClass + * @return BaseResult|Query|false */ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Database\\Query') { @@ -512,27 +604,66 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da $resultClass = str_replace('Connection', 'Result', get_class($this)); + /** + * @var Query $query + */ $query = new $queryClass($this); $query->setQuery($sql, $binds); - if (! empty($this->swapPre) && ! empty($this->DBPrefix)) + if ( ! empty($this->swapPre) && ! empty($this->DBPrefix)) { $query->swapPrefix($this->DBPrefix, $this->swapPre); } $startTime = microtime(true); - // Run the query - if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) + // Always save the last query so we can use + // the getLastQuery() method. + $this->lastQuery = $query; + + // Run the query for real + if ( ! $this->pretend && false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { $query->setDuration($startTime, $startTime); + // This will trigger a rollback if transactions are being used + if ($this->transDepth !== 0) + { + $this->transStatus = false; + } + // @todo deal with errors - if ($this->saveQueries) + if ($this->DBDebug) { - $this->queries[] = $query; + // We call this function in order to roll-back queries + // if transactions are enabled. If we don't call this here + // the error message will trigger an exit, causing the + // transactions to remain in limbo. + while ($this->transDepth !== 0) + { + $transDepth = $this->transDepth; + $this->transComplete(); + + if ($transDepth === $this->transDepth) + { + // @todo log + // log_message('error', 'Database: Failure during an automated transaction commit/rollback!'); + break; + } + } + + // display the errors.... + // @todo display the error... + + return false; + } + + if ( ! $this->pretend) + { + // Let others do something with this query. + Events::trigger('DBQuery', $query); } return new $resultClass($this->connID, $this->resultID); @@ -540,12 +671,16 @@ public function query(string $sql, $binds = null, $queryClass = 'CodeIgniter\\Da $query->setDuration($startTime); - if ($this->saveQueries) + if ( ! $this->pretend) { - $this->queries[] = $query; + // Let others do somethign with this query + Events::trigger('DBQuery', $query); } - return new $resultClass($this->connID, $this->resultID); + // If $pretend is true, then we just want to return + // the actual query object here. There won't be + // any results to return. + return $this->pretend ? $query : new $resultClass($this->connID, $this->resultID); } //-------------------------------------------------------------------- @@ -571,6 +706,218 @@ public function simpleQuery(string $sql) //-------------------------------------------------------------------- + /** + * Disable Transactions + * + * This permits transactions to be disabled at run-time. + */ + public function transOff() + { + $this->transEnabled = FALSE; + } + + //-------------------------------------------------------------------- + + /** + * Enable/disable Transaction Strict Mode + * + * When strict mode is enabled, if you are running multiple groups of + * transactions, if one group fails all subsequent groups will be + * rolled back. + * + * If strict mode is disabled, each group is treated autonomously, + * meaning a failure of one group will not affect any others + * + * @param bool $mode = true + * + * @return $this + */ + public function transStrict(bool $mode = true) + { + $this->transStrict = $mode; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Start Transaction + * + * @param bool $test_mode = FALSE + * @return bool + */ + public function transStart($test_mode = false) + { + if ( ! $this->transEnabled) + { + return false; + } + + return $this->transBegin($test_mode); + } + + //-------------------------------------------------------------------- + + /** + * Complete Transaction + * + * @return bool + */ + public function transComplete() + { + if ( ! $this->transEnabled) + { + return false; + } + + // The query() function will set this flag to FALSE in the event that a query failed + if ($this->transStatus === false || $this->transFailure === true) + { + $this->transRollback(); + + // If we are NOT running in strict mode, we will reset + // the _trans_status flag so that subsequent groups of + // transactions will be permitted. + if ($this->transStrict === false) + { + $this->transStatus = true; + } + + // log_message('debug', 'DB Transaction Failure'); + return FALSE; + } + + return $this->transCommit(); + } + + //-------------------------------------------------------------------- + + /** + * Lets you retrieve the transaction flag to determine if it has failed + * + * @return bool + */ + public function transStatus(): bool + { + return $this->transStatus; + } + + //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @param bool $test_mode + * @return bool + */ + public function transBegin(bool $test_mode = false): bool + { + if ( ! $this->transEnabled) + { + return false; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->transDepth > 0) + { + $this->transDepth ++; + return true; + } + + if (empty($this->connID)) + { + $this->initialize(); + } + + // Reset the transaction failure flag. + // If the $test_mode flag is set to TRUE transactions will be rolled back + // even if the queries produce a successful result. + $this->transFailure = ($test_mode === true); + + if ($this->_transBegin()) + { + $this->transDepth ++; + return true; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + public function transCommit(): bool + { + if (! $this->transEnabled || $this->transDepth === 0) + { + return false; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->transDepth > 1 || $this->_transCommit()) + { + $this->transDepth --; + return true; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + public function transRollback(): bool + { + if (! $this->transEnabled || $this->transDepth === 0) + { + return false; + } + // When transactions are nested we only begin/commit/rollback the outermost ones + elseif ($this->transDepth > 1 || $this->_transRollback()) + { + $this->transDepth --; + return true; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + abstract protected function _transBegin(): bool; + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + abstract protected function _transCommit(): bool; + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + abstract protected function _transRollback(): bool; + + //-------------------------------------------------------------------- + /** * Returns an instance of the query builder for this connection. * @@ -594,26 +941,44 @@ public function table($tableName) //-------------------------------------------------------------------- /** - * Returns an array containing all of the + * Creates a prepared statement with the database that can then + * be used to execute multiple statements against. Within the + * closure, you would build the query in any normal way, though + * the Query Builder is the expected manner. + * + * Example: + * $stmt = $db->prepare(function($db) + * { + * return $db->table('users') + * ->where('id', 1) + * ->get(); + * }) * - * @return array + * @param \Closure $func + * @param array $options Passed to the prepare() method + * + * @return BasePreparedQuery|null */ - public function getQueries(): array + public function prepare(\Closure $func, array $options = []) { - return $this->queries; - } + $this->pretend(true); - //-------------------------------------------------------------------- + $sql = $func($this); - /** - * Returns the total number of queries that have been performed - * on this connection. - * - * @return mixed - */ - public function getQueryCount() - { - return count($this->queries); + $this->pretend(false); + + if ($sql instanceof QueryInterface) + { + $sql = $sql->getOriginalQuery(); + } + + $class = str_ireplace('Connection', 'PreparedQuery', get_class($this)); + /** + * @var BasePreparedQuery $class + */ + $class = new $class($this); + + return $class->prepare($sql, $options); } //-------------------------------------------------------------------- @@ -625,7 +990,7 @@ public function getQueryCount() */ public function getLastQuery() { - return end($this->queries); + return $this->lastQuery; } //-------------------------------------------------------------------- @@ -637,13 +1002,11 @@ public function getLastQuery() */ public function showLastQuery() { - return (string)end($this->queries); + return (string) $this->lastQuery; } //-------------------------------------------------------------------- - - /** * Returns the time we started to connect to this database in * seconds with microseconds. @@ -654,7 +1017,7 @@ public function showLastQuery() */ public function getConnectStart() { - return $this->connectTime; + return $this->connectTime; } //-------------------------------------------------------------------- @@ -671,7 +1034,7 @@ public function getConnectStart() */ public function getConnectDuration($decimals = 6) { - return number_format($this->connectDuration, $decimals); + return number_format($this->connectDuration, $decimals); } //-------------------------------------------------------------------- @@ -701,7 +1064,7 @@ public function getConnectDuration($decimals = 6) * @param mixed * @param bool * - * @return string + * @return string|array */ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentifiers = null, $fieldExists = true) { @@ -715,8 +1078,7 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif $escaped_array = []; foreach ($item as $k => $v) { - $escaped_array[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, - $protectIdentifiers, $fieldExists); + $escaped_array[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists); } return $escaped_array; @@ -741,17 +1103,13 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif // Note: strripos() is used in order to support spaces in table names if ($offset = strripos($item, ' AS ')) { - $alias = ($protectIdentifiers) - ? substr($item, $offset, 4).$this->escapeIdentifiers(substr($item, $offset + 4)) - : substr($item, $offset); - $item = substr($item, 0, $offset); + $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset); + $item = substr($item, 0, $offset); } elseif ($offset = strrpos($item, ' ')) { - $alias = ($protectIdentifiers) - ? ' '.$this->escapeIdentifiers(substr($item, $offset + 1)) - : substr($item, $offset); - $item = substr($item, 0, $offset); + $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset); + $item = substr($item, 0, $offset); } else { @@ -771,7 +1129,7 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif // // NOTE: The ! empty() condition prevents this method // from breaking when QB isn't enabled. - if ( ! empty($this->qb_aliased_tables) && in_array($parts[0], $this->qb_aliased_tables)) + if ( ! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables)) { if ($protectIdentifiers === true) { @@ -786,7 +1144,7 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif $item = implode('.', $parts); } - return $item.$alias; + return $item . $alias; } // Is there a table prefix defined in the config file? If not, no need to do anything @@ -816,18 +1174,18 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif // This can happen when this function is being called from a JOIN. if ($fieldExists === false) { - $i++; + $i ++; } // Verify table prefix and replace if necessary if ($this->swapPre !== '' && strpos($parts[$i], $this->swapPre) === 0) { - $parts[$i] = preg_replace('/^'.$this->swapPre.'(\S+?)/', $this->DBPrefix.'\\1', $parts[$i]); + $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]); } // We only add the table prefix if it does not already exist elseif (strpos($parts[$i], $this->DBPrefix) !== 0) { - $parts[$i] = $this->DBPrefix.$parts[$i]; + $parts[$i] = $this->DBPrefix . $parts[$i]; } // Put the parts back together @@ -839,21 +1197,26 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif $item = $this->escapeIdentifiers($item); } - return $item.$alias; + return $item . $alias; } + // In some cases, especially 'from', we end up running through + // protect_identifiers twice. This algorithm won't work when + // it contains the escapeChar so strip it out. + $item = trim($item, $this->escapeChar); + // Is there a table prefix? If not, no need to insert it if ($this->DBPrefix !== '') { // Verify table prefix and replace if necessary if ($this->swapPre !== '' && strpos($item, $this->swapPre) === 0) { - $item = preg_replace('/^'.$this->swapPre.'(\S+?)/', $this->DBPrefix.'\\1', $item); + $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item); } // Do we prefix an item with no segments? elseif ($prefixSingle === true && strpos($item, $this->DBPrefix) !== 0) { - $item = $this->DBPrefix.$item; + $item = $this->DBPrefix . $item; } } @@ -862,7 +1225,7 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif $item = $this->escapeIdentifiers($item); } - return $item.$alias; + return $item . $alias; } //-------------------------------------------------------------------- @@ -878,7 +1241,7 @@ public function protectIdentifiers($item, $prefixSingle = false, $protectIdentif */ public function escapeIdentifiers($item) { - if ($this->escapeChar === '' OR empty($item) OR in_array($item, $this->reservedIdentifiers)) + if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers)) { return $item; } @@ -892,8 +1255,8 @@ public function escapeIdentifiers($item) return $item; } // Avoid breaking functions and literal values inside queries - elseif (ctype_digit($item) OR $item[0] === "'" OR ($this->escapeChar !== '"' && $item[0] === '"') OR - strpos($item, '(') !== false + elseif (ctype_digit($item) || $item[0] === "'" OR ( $this->escapeChar !== '"' && $item[0] === '"') OR + strpos($item, '(') !== false ) { return $item; @@ -921,15 +1284,13 @@ public function escapeIdentifiers($item) foreach ($this->reservedIdentifiers as $id) { - if (strpos($item, '.'.$id) !== false) + if (strpos($item, '.' . $id) !== false) { - return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?\./i', - $preg_ec[2].'$1'.$preg_ec[3].'.', $item); + return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?\./i', $preg_ec[2] . '$1' . $preg_ec[3] . '.', $item); } } - return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?(\.)?/i', - $preg_ec[2].'$1'.$preg_ec[3].'$2', $item); + return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?(\.)?/i', $preg_ec[2] . '$1' . $preg_ec[3] . '$2', $item); } //-------------------------------------------------------------------- @@ -939,9 +1300,10 @@ public function escapeIdentifiers($item) * * Prepends a database prefix if one exists in configuration * - * @param string the table + * @param string $table the table * - * @return string + * @return string + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function prefixTable($table = '') { @@ -950,7 +1312,7 @@ public function prefixTable($table = '') throw new DatabaseException('A table name is required for that operation.'); } - return $this->DBPrefix.$table; + return $this->DBPrefix . $table; } //-------------------------------------------------------------------- @@ -960,7 +1322,7 @@ public function prefixTable($table = '') * * Set's the DB Prefix to something new without needing to reconnect * - * @param string the prefix + * @param string $prefix The prefix * * @return string */ @@ -998,14 +1360,18 @@ public function escape($str) return $str; } - else if (is_string($str) OR (is_object($str) && method_exists($str, '__toString'))) + else if (is_string($str) || ( is_object($str) && method_exists($str, '__toString'))) { - return "'".$this->escapeString($str)."'"; + return "'" . $this->escapeString($str) . "'"; } else if (is_bool($str)) { return ($str === false) ? 0 : 1; } + else if (is_numeric($str) && $str < 0) + { + return "'{$str}'"; + } else if ($str === null) { return 'NULL'; @@ -1041,9 +1407,7 @@ public function escapeString($str, $like = FALSE) if ($like === true) { return str_replace( - [$this->likeEscapeChar, '%', '_'], - [$this->likeEscapeChar.$this->likeEscapeChar, $this->likeEscapeChar.'%', $this->likeEscapeChar.'_'], - $str + [$this->likeEscapeChar, '%', '_'], [$this->likeEscapeChar . $this->likeEscapeChar, $this->likeEscapeChar . '%', $this->likeEscapeChar . '_'], $str ); } @@ -1068,7 +1432,6 @@ public function escapeLikeString($str) //-------------------------------------------------------------------- - /** * Platform independent string escape. * @@ -1080,7 +1443,7 @@ public function escapeLikeString($str) */ protected function _escapeString(string $str): string { - return str_replace("'", "\\'", remove_invisible_characters($str)); + return str_replace("'", "''", remove_invisible_characters($str, false)); } //-------------------------------------------------------------------- @@ -1097,11 +1460,11 @@ protected function _escapeString(string $str): string */ public function callFunction(string $functionName, ...$params) { - $driver = ($this->DBDriver === 'postgre' ? 'pg' : strtolower($this->DBDriver)).'_'; + $driver = ($this->DBDriver === 'postgre' ? 'pg' : strtolower($this->DBDriver)) . '_'; if (FALSE === strpos($driver, $functionName)) { - $functionName = $driver.$functionName; + $functionName = $driver . $functionName; } if ( ! function_exists($functionName)) @@ -1118,7 +1481,6 @@ public function callFunction(string $functionName, ...$params) } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // META Methods //-------------------------------------------------------------------- @@ -1126,13 +1488,14 @@ public function callFunction(string $functionName, ...$params) /** * Returns an array of table names * - * @param string $constrain_by_prefix = FALSE - * @return array + * @param bool $constrain_by_prefix = FALSE + * @return bool|array + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function listTables($constrain_by_prefix = FALSE) { // Is there a cached result? - if (isset($this->dataCache['table_names'])) + if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) { return $this->dataCache['table_names']; } @@ -1146,7 +1509,7 @@ public function listTables($constrain_by_prefix = FALSE) return false; } - $this->dataCache['table_names'] = array(); + $this->dataCache['table_names'] = []; $query = $this->query($sql); foreach ($query->getResultArray() as $row) @@ -1200,7 +1563,7 @@ public function tableExists($table_name) * * @param string $table Table name * - * @return array + * @return array|false * @throws DatabaseException */ public function getFieldNames($table) @@ -1211,6 +1574,11 @@ public function getFieldNames($table) return $this->dataCache['field_names'][$table]; } + if (empty($this->connID)) + { + $this->initialize(); + } + if (FALSE === ($sql = $this->_listColumns($table))) { if ($this->DBDebug) @@ -1221,7 +1589,7 @@ public function getFieldNames($table) } $query = $this->query($sql); - $this->dataCache['field_names'][$table] = array(); + $this->dataCache['field_names'][$table] = []; foreach ($query->getResultArray() as $row) { @@ -1260,7 +1628,7 @@ public function getFieldNames($table) */ public function fieldExists($fieldName, $tableName) { - return in_array($fieldName, $this->listFields($tableName)); + return in_array($fieldName, $this->getFieldNames($tableName)); } //-------------------------------------------------------------------- @@ -1269,12 +1637,62 @@ public function fieldExists($fieldName, $tableName) * Returns an object with field data * * @param string $table the table name - * @return array + * @return array|false */ public function getFieldData(string $table) { - $query = $this->query($this->_fieldData($this->protect_identifiers($table, TRUE, NULL, FALSE))); - return ($query) ? $query->field_data() : FALSE; + $fields = $this->_fieldData($this->protectIdentifiers($table, true, false, false)); + + return $fields ?? false; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with key data + * + * @param string $table the table name + * @return array + */ + public function getIndexData(string $table) + { + $fields = $this->_indexData($this->protectIdentifiers($table, true, false, false)); + + return $fields ?? false; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with foreign key data + * + * @param string $table the table name + * @return array + */ + public function getForeignKeyData(string $table) + { + $fields = $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false)); + + return $fields ?? false; + } + + //-------------------------------------------------------------------- + + /** + * Allows the engine to be set into a mode where queries are not + * actually executed, but they are still generated, timed, etc. + * + * This is primarily used by the prepared query functionality. + * + * @param bool $pretend + * + * @return $this + */ + public function pretend(bool $pretend = true) + { + $this->pretend = $pretend; + + return $this; } //-------------------------------------------------------------------- @@ -1323,5 +1741,14 @@ abstract protected function _listColumns(string $table = ''): string; //-------------------------------------------------------------------- + public function __get($key) + { + if (property_exists($this, $key)) + { + return $this->$key; + } + } + + //-------------------------------------------------------------------- } diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php new file mode 100644 index 000000000000..867b95400fb0 --- /dev/null +++ b/system/Database/BasePreparedQuery.php @@ -0,0 +1,260 @@ +db = & $db; + } + + //-------------------------------------------------------------------- + + /** + * Prepares the query against the database, and saves the connection + * info necessary to execute the query later. + * + * NOTE: This version is based on SQL code. Child classes should + * override this method. + * + * @param string $sql + * @param array $options Passed to the connection's prepare statement. + * @param string $queryClass + * + * @return mixed + */ + public function prepare(string $sql, array $options = [], $queryClass = 'CodeIgniter\\Database\\Query') + { + // We only supports positional placeholders (?) + // in order to work with the execute method below, so we + // need to replace our named placeholders (:name) + $sql = preg_replace('/:[^\s,)]+/', '?', $sql); + + /** + * @var \CodeIgniter\Database\Query $query + */ + $query = new $queryClass($this->db); + + $query->setQuery($sql); + + if ( ! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) + { + $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre); + } + + $this->query = $query; + + return $this->_prepare($query->getOriginalQuery(), $options); + } + + //-------------------------------------------------------------------- + + /** + * The database-dependent portion of the prepare statement. + * + * @param string $sql + * @param array $options Passed to the connection's prepare statement. + * + * @return mixed + */ + abstract public function _prepare(string $sql, array $options = []); + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return ResultInterface + */ + public function execute(...$data) + { + // Execute the Query. + $startTime = microtime(true); + + $result = $this->_execute($data); + + // Update our query object + $query = clone $this->query; + $query->setBinds($data); + + $query->setDuration($startTime); + + // Let others do something with this query + Events::trigger('DBQuery', $query); + + // Return a result object + $resultClass = str_replace('PreparedQuery', 'Result', get_class($this)); + + $resultID = $this->_getResult(); + + return new $resultClass($this->db->connID, $resultID); + } + + //-------------------------------------------------------------------- + + /** + * The database dependant version of the execute method. + * + * @param array $data + * + * @return ResultInterface + */ + abstract public function _execute($data); + + //-------------------------------------------------------------------- + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + abstract public function _getResult(); + + //-------------------------------------------------------------------- + + /** + * Explicity closes the statement. + */ + public function close() + { + if ( ! is_object($this->statement)) + { + return; + } + + $this->statement->close(); + } + + //-------------------------------------------------------------------- + + /** + * Returns the SQL that has been prepared. + * + * @return string + */ + public function getQueryString(): string + { + if ( ! $this->query instanceof QueryInterface) + { + throw new \BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.'); + } + + return $this->query->getQuery(); + } + + //-------------------------------------------------------------------- + + /** + * A helper to determine if any error exists. + * + * @return bool + */ + public function hasError() + { + return ! empty($this->errorString); + } + + //-------------------------------------------------------------------- + + /** + * Returns the error code created while executing this statement. + * + * @return int + */ + public function getErrorCode(): int + { + return $this->errorCode; + } + + //-------------------------------------------------------------------- + + /** + * Returns the error message created while executing this statement. + * + * @return string + */ + public function getErrorMessage(): string + { + return $this->errorString; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 8459b974072b..16387e3825ec 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -102,13 +102,13 @@ abstract class BaseResult implements ResultInterface /** * Constructor - * - * @param type $connID - * @param type $resultID + * + * @param object|resource $connID + * @param object|resource $resultID */ public function __construct(&$connID, &$resultID) { - $this->connID = $connID; + $this->connID = $connID; $this->resultID = $resultID; } @@ -152,7 +152,7 @@ public function getCustomResultObject(string $className) { return $this->customResultObject[$className]; } - elseif ( ! $this->resultID OR $this->numRows === 0) + elseif ( ! $this->resultID || $this->numRows === 0) { return []; } @@ -170,7 +170,7 @@ public function getCustomResultObject(string $className) if ($_data !== null) { - for ($i = 0; $i < $c; $i++) + for ($i = 0; $i < $c; $i ++ ) { $this->customResultObject[$className][$i] = new $className(); @@ -183,7 +183,7 @@ public function getCustomResultObject(string $className) return $this->customResultObject[$className]; } - is_null($this->rowData) OR $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(0); $this->customResultObject[$className] = []; while ($row = $this->fetchObject($className)) @@ -205,7 +205,7 @@ public function getCustomResultObject(string $className) */ public function getResultArray(): array { - if (count($this->resultArray) > 0) + if (! empty($this->resultArray)) { return $this->resultArray; } @@ -213,22 +213,22 @@ public function getResultArray(): array // In the event that query caching is on, the result_id variable // will not be a valid resource so we'll simply return an empty // array. - if ( ! $this->resultID OR $this->numRows === 0) + if ( ! $this->resultID || $this->numRows === 0) { return []; } if (($c = count($this->resultObject)) > 0) { - for ($i = 0; $i < $c; $i++) + for ($i = 0; $i < $c; $i ++ ) { - $this->resultArray[$i] = (array)$this->resultObject[$i]; + $this->resultArray[$i] = (array) $this->resultObject[$i]; } return $this->resultArray; } - is_null($this->rowData) OR $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(0); while ($row = $this->fetchAssoc()) { $this->resultArray[] = $row; @@ -248,7 +248,7 @@ public function getResultArray(): array */ public function getResultObject(): array { - if (count($this->resultObject) > 0) + if (! empty($this->resultObject)) { return $this->resultObject; } @@ -256,22 +256,22 @@ public function getResultObject(): array // In the event that query caching is on, the result_id variable // will not be a valid resource so we'll simply return an empty // array. - if ( ! $this->resultID OR $this->numRows === 0) + if ( ! $this->resultID || $this->numRows === 0) { return []; } if (($c = count($this->resultArray)) > 0) { - for ($i = 0; $i < $c; $i++) + for ($i = 0; $i < $c; $i ++ ) { - $this->resultObject[$i] = (object)$this->resultArray[$i]; + $this->resultObject[$i] = (object) $this->resultArray[$i]; } return $this->resultObject; } - is_null($this->rowData) OR $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(0); while ($row = $this->fetchObject()) { $this->resultObject[] = $row; @@ -298,10 +298,10 @@ public function getRow($n = 0, $type = 'object') if ( ! is_numeric($n)) { // We cache the row data for subsequent uses - is_array($this->rowData) OR $this->row_data = $this->getRowArray(0); + is_array($this->rowData) || $this->row_data = $this->getRowArray(0); // array_key_exists() instead of isset() to allow for NULL values - if (empty($this->rowData) OR ! array_key_exists($n, $this->rowData)) + if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) { return null; } @@ -317,10 +317,8 @@ public function getRow($n = 0, $type = 'object') { return $this->getRowArray($n); } - else - { - return $this->getCustomRowObject($n, $type); - } + + return $this->getCustomRowObject($n, $type); } //-------------------------------------------------------------------- @@ -337,9 +335,9 @@ public function getRow($n = 0, $type = 'object') */ public function getCustomRowObject($n, string $className) { - isset($this->customResultObject[$className]) OR $this->customResultObject($className); + isset($this->customResultObject[$className]) || $this->getCustomResultObject($className); - if (count($this->customResultObject[$className]) === 0) + if (empty($this->customResultObject[$className])) { return null; } @@ -366,7 +364,7 @@ public function getCustomRowObject($n, string $className) public function getRowArray($n = 0) { $result = $this->getResultArray(); - if (count($result) === 0) + if (empty($result)) { return null; } @@ -393,7 +391,7 @@ public function getRowArray($n = 0) public function getRowObject($n = 0) { $result = $this->getResultObject(); - if (count($result) === 0) + if (empty($result)) { return null; } @@ -453,7 +451,7 @@ public function getFirstRow($type = 'object') { $result = $this->getResult($type); - return (count($result) === 0) ? null : $result[0]; + return (empty($result)) ? null : $result[0]; } //-------------------------------------------------------------------- @@ -469,7 +467,7 @@ public function getLastRow($type = 'object') { $result = $this->getResult($type); - return (count($result) === 0) ? null : $result[count($result) - 1]; + return (empty($result)) ? null : $result[count($result) - 1]; } //-------------------------------------------------------------------- @@ -484,14 +482,12 @@ public function getLastRow($type = 'object') public function getNextRow($type = 'object') { $result = $this->getResult($type); - if (count($result) === 0) + if (empty($result)) { return null; } - return isset($result[$this->currentRow + 1]) - ? $result[++$this->currentRow] - : null; + return isset($result[$this->currentRow + 1]) ? $result[++ $this->currentRow] : null; } //-------------------------------------------------------------------- @@ -506,14 +502,14 @@ public function getNextRow($type = 'object') public function getPreviousRow($type = 'object') { $result = $this->getResult($type); - if (count($result) === 0) + if (empty($result)) { return null; } if (isset($result[$this->currentRow - 1])) { - --$this->currentRow; + -- $this->currentRow; } return $result[$this->currentRow]; @@ -616,5 +612,4 @@ abstract protected function fetchAssoc(); abstract protected function fetchObject($className = 'stdClass'); //-------------------------------------------------------------------- - } diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index 0461bb84b7ec..cab813d6c10d 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,20 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\DatabaseException; +use \CodeIgniter\Database\Exceptions\DatabaseException; /** * Class BaseUtils */ abstract class BaseUtils { + /** * Database object * @@ -57,21 +57,21 @@ abstract class BaseUtils * * @var string */ - protected $listDatabases = FALSE; + protected $listDatabases = FALSE; /** * OPTIMIZE TABLE statement * * @var string */ - protected $optimizeTable = FALSE; + protected $optimizeTable = FALSE; /** * REPAIR TABLE statement * * @var string */ - protected $repairTable = FALSE; + protected $repairTable = FALSE; //-------------------------------------------------------------------- @@ -82,7 +82,7 @@ abstract class BaseUtils */ public function __construct(ConnectionInterface &$db) { - $this->db =& $db; + $this->db = & $db; } //-------------------------------------------------------------------- @@ -90,7 +90,8 @@ public function __construct(ConnectionInterface &$db) /** * List databases * - * @return array + * @return array|bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function listDatabases() { @@ -108,7 +109,7 @@ public function listDatabases() return false; } - $this->db->dataCache['db_names'] = array(); + $this->db->dataCache['db_names'] = []; $query = $this->db->query($this->listDatabases); if ($query === FALSE) @@ -116,7 +117,7 @@ public function listDatabases() return $this->db->dataCache['db_names']; } - for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i++) + for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i ++ ) { $this->db->dataCache['db_names'][] = current($query[$i]); } @@ -143,7 +144,8 @@ public function databaseExists($database_name) * Optimize Table * * @param string $table_name - * @return mixed + * @return bool|mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function optimizeTable($table_name) { @@ -171,7 +173,8 @@ public function optimizeTable($table_name) /** * Optimize Database * - * @return mixed + * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function optimizeDatabase() { @@ -184,7 +187,7 @@ public function optimizeDatabase() return false; } - $result = array(); + $result = []; foreach ($this->db->listTables() as $table_name) { $res = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($table_name))); @@ -196,7 +199,7 @@ public function optimizeDatabase() // Build the result array... $res = $res->getResultArray(); $res = current($res); - $key = str_replace($this->db->database.'.', '', current($res)); + $key = str_replace($this->db->database . '.', '', current($res)); $keys = array_keys($res); unset($res[$keys[0]]); @@ -213,6 +216,7 @@ public function optimizeDatabase() * * @param string $table_name * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function repairTable($table_name) { @@ -240,11 +244,12 @@ public function repairTable($table_name) /** * Generate CSV from a query result object * - * @param object $query Query result object - * @param string $delim Delimiter (default: ,) - * @param string $newline Newline character (default: \n) - * @param string $enclosure Enclosure (default: ") - * @return string + * @param ResultInterface $query Query result object + * @param string $delim Delimiter (default: ,) + * @param string $newline Newline character (default: \n) + * @param string $enclosure Enclosure (default: ") + * + * @return string */ public function getCSVFromResult(ResultInterface $query, $delim = ',', $newline = "\n", $enclosure = '"') { @@ -252,20 +257,20 @@ public function getCSVFromResult(ResultInterface $query, $delim = ',', $newline // First generate the headings from the table column names foreach ($query->getFieldNames() as $name) { - $out .= $enclosure.str_replace($enclosure, $enclosure.$enclosure, $name).$enclosure.$delim; + $out .= $enclosure . str_replace($enclosure, $enclosure . $enclosure, $name) . $enclosure . $delim; } - $out = substr($out, 0, -strlen($delim)).$newline; + $out = substr($out, 0, -strlen($delim)) . $newline; // Next blast through the result array and build out the rows while ($row = $query->getUnbufferedRow('array')) { - $line = array(); + $line = []; foreach ($row as $item) { - $line[] = $enclosure.str_replace($enclosure, $enclosure.$enclosure, $item).$enclosure; + $line[] = $enclosure . str_replace($enclosure, $enclosure . $enclosure, $item) . $enclosure; } - $out .= implode($delim, $line).$newline; + $out .= implode($delim, $line) . $newline; } return $out; @@ -276,14 +281,15 @@ public function getCSVFromResult(ResultInterface $query, $delim = ',', $newline /** * Generate XML data from a query result object * - * @param object $query Query result object - * @param array $params Any preferences - * @return string + * @param ResultInterface $query Query result object + * @param array $params Any preferences + * + * @return string */ - public function getXMLFromResult(ResultInterface $query, $params = array()) + public function getXMLFromResult(ResultInterface $query, $params = []) { // Set our default values - foreach (array('root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t") as $key => $val) + foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) { if ( ! isset($params[$key])) { @@ -296,20 +302,19 @@ public function getXMLFromResult(ResultInterface $query, $params = array()) // Load the xml helper // get_instance()->load->helper('xml'); - // Generate the result - $xml = '<'.$root.'>'.$newline; + $xml = '<' . $root . '>' . $newline; while ($row = $query->getUnbufferedRow()) { - $xml .= $tab.'<'.$element.'>'.$newline; + $xml .= $tab . '<' . $element . '>' . $newline; foreach ($row as $key => $val) { - $xml .= $tab.$tab.'<'.$key.'>'.xml_convert($val).''.$newline; + $xml .= $tab . $tab . '<' . $key . '>' . xml_convert($val) . '' . $newline; } - $xml .= $tab.''.$newline; + $xml .= $tab . '' . $newline; } - return $xml.''.$newline; + return $xml . '' . $newline; } //-------------------------------------------------------------------- @@ -318,32 +323,33 @@ public function getXMLFromResult(ResultInterface $query, $params = array()) * Database Backup * * @param array $params - * @return string + * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function backup($params = array()) + public function backup($params = []) { // If the parameters have not been submitted as an // array then we know that it is simply the table // name, which is a valid short cut. if (is_string($params)) { - $params = array('tables' => $params); + $params = ['tables' => $params]; } // Set up our default preferences - $prefs = array( - 'tables' => array(), - 'ignore' => array(), - 'filename' => '', - 'format' => 'gzip', // gzip, zip, txt - 'add_drop' => TRUE, - 'add_insert' => TRUE, - 'newline' => "\n", - 'foreign_key_checks' => TRUE - ); + $prefs = [ + 'tables' => [], + 'ignore' => [], + 'filename' => '', + 'format' => 'gzip', // gzip, zip, txt + 'add_drop' => TRUE, + 'add_insert' => TRUE, + 'newline' => "\n", + 'foreign_key_checks' => TRUE + ]; // Did the user submit any preferences? If so set them.... - if (count($params) > 0) + if (! empty($params)) { foreach ($prefs as $key => $val) { @@ -356,13 +362,13 @@ public function backup($params = array()) // Are we backing up a complete database or individual tables? // If no table names were submitted we'll fetch the entire table list - if (count($prefs['tables']) === 0) + if (empty($prefs['tables'])) { $prefs['tables'] = $this->db->listTables(); } // Validate the format - if ( ! in_array($prefs['format'], array('gzip', 'zip', 'txt'), TRUE)) + if ( ! in_array($prefs['format'], ['gzip', 'zip', 'txt'], TRUE)) { $prefs['format'] = 'txt'; } @@ -370,7 +376,7 @@ public function backup($params = array()) // Is the encoder supported? If not, we'll either issue an // error or use plain text depending on the debug settings if (($prefs['format'] === 'gzip' && ! function_exists('gzencode')) - OR ($prefs['format'] === 'zip' && ! function_exists('gzcompress'))) + OR ( $prefs['format'] === 'zip' && ! function_exists('gzcompress'))) { if ($this->db->DBDebug) { @@ -387,7 +393,7 @@ public function backup($params = array()) if ($prefs['filename'] === '') { $prefs['filename'] = (count($prefs['tables']) === 1 ? $prefs['tables'] : $this->db->database) - .date('Y-m-d_H-i', time()).'.sql'; + . date('Y-m-d_H-i', time()) . '.sql'; } else { @@ -434,5 +440,4 @@ public function backup($params = array()) abstract public function _backup(array $prefs = null); //-------------------------------------------------------------------- - } diff --git a/system/Database/Config.php b/system/Database/Config.php index 4afec32ddc51..0a97742b1c28 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Config\BaseConfig; use Config\Database; @@ -44,6 +43,7 @@ */ class Config extends BaseConfig { + /** * Cache for instance of any connections that * have been requested as a "shared" instance. @@ -69,9 +69,9 @@ class Config extends BaseConfig * or an array of configuration settings. * @param bool $getShared Whether to return a shared instance of the connection. * - * @return mixed + * @return BaseConnection */ - public static function connect($group = null, $getShared = true) + public static function connect($group = null, bool $getShared = true) { if (is_array($group)) { @@ -79,13 +79,6 @@ public static function connect($group = null, $getShared = true) $group = 'custom'; } - if ($getShared && isset(self::$instances[$group])) - { - return self::$instances[$group]; - } - - self::ensureFactory(); - $config = $config ?? new \Config\Database(); if (empty($group)) @@ -95,9 +88,16 @@ public static function connect($group = null, $getShared = true) if (is_string($group) && ! isset($config->$group) && $group != 'custom') { - throw new \InvalidArgumentException($group.' is not a valid database connection group.'); + throw new \InvalidArgumentException($group . ' is not a valid database connection group.'); + } + + if ($getShared && isset(self::$instances[$group])) + { + return self::$instances[$group]; } + self::ensureFactory(); + if (isset($config->$group)) { $config = $config->$group; @@ -105,7 +105,7 @@ public static function connect($group = null, $getShared = true) $connection = self::$factory->load($config, $group); - self::$instances[$group] =& $connection; + self::$instances[$group] = & $connection; return $connection; } @@ -119,7 +119,7 @@ public static function connect($group = null, $getShared = true) */ public static function getConnections() { - return self::$instances; + return self::$instances; } //-------------------------------------------------------------------- @@ -129,6 +129,8 @@ public static function getConnections() * database group, and loads the group if it hasn't been loaded yet. * * @param string|null $group + * + * @return Forge */ public static function forge(string $group = null) { @@ -141,20 +143,20 @@ public static function forge(string $group = null) $group = ENVIRONMENT == 'testing' ? 'tests' : $config->defaultGroup; } - if (! isset($config->$group)) + if ( ! isset($config->$group)) { - throw new \InvalidArgumentException($group.' is not a valid database connection group.'); + throw new \InvalidArgumentException($group . ' is not a valid database connection group.'); } - if (! isset(self::$instances[$group])) + if ( ! isset(self::$instances[$group])) { $db = self::connect($group); } - else + else { $db = self::$instances[$group]; } - + return self::$factory->loadForge($db); } @@ -162,14 +164,14 @@ public static function forge(string $group = null) /** * Returns a new instance of the Database Utilities class. - * + * * @param string|null $group * - * @return mixed + * @return BaseUtils */ public static function utils(string $group = null) { - $config = new \Config\Database(); + $config = new \Config\Database(); self::ensureFactory(); @@ -178,12 +180,12 @@ public static function utils(string $group = null) $group = $config->defaultGroup; } - if (! isset($config->group)) + if ( ! isset($config->group)) { - throw new \InvalidArgumentException($group.' is not a valid database connection group.'); + throw new \InvalidArgumentException($group . ' is not a valid database connection group.'); } - if (! isset(self::$instances[$group])) + if ( ! isset(self::$instances[$group])) { $db = self::connect($group); } @@ -213,8 +215,6 @@ public static function seeder(string $group = null) //-------------------------------------------------------------------- - - /** * Ensures the database Connection Manager/Factory is loaded and ready to use. */ @@ -229,5 +229,4 @@ protected static function ensureFactory() } //-------------------------------------------------------------------- - } diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 06a125553ee5..3bd3c262d620 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -43,6 +43,7 @@ */ interface ConnectionInterface { + /** * Initializes the database connection/settings. * @@ -55,7 +56,7 @@ public function initialize(); /** * Connect to the database. * - * @param $persistent + * @param bool $persistent * @return mixed */ public function connect($persistent = false); @@ -91,7 +92,7 @@ public function reconnect(); * * @return mixed */ - public function getConnection(string $alias=null); + public function getConnection(string $alias = null); //-------------------------------------------------------------------- @@ -142,15 +143,6 @@ public function getVersion(); //-------------------------------------------------------------------- - /** - * Specifies whether this connection should keep queries objects or not. - * - * @param bool $doLog - */ - public function saveQueries($doLog = false); - - //-------------------------------------------------------------------- - /** * Orchestrates a query against the database. Queries must use * Database\Statement objects to store the query and build it. @@ -184,33 +176,14 @@ public function simpleQuery(string $sql); /** * Returns an instance of the query builder for this connection. * - * @param string|array $tableName + * @param string|array $tableName Table name. * - * @return QueryBuilder + * @return BaseBuilder Builder. */ public function table($tableName); //-------------------------------------------------------------------- - /** - * Returns an array containing all of the - * - * @return array - */ - public function getQueries(): array; - - //-------------------------------------------------------------------- - - /** - * Returns the total number of queries that have been performed - * on this connection. - * - * @return mixed - */ - public function getQueryCount(); - - //-------------------------------------------------------------------- - /** * Returns the last query's statement object. * @@ -226,7 +199,7 @@ public function getLastQuery(); * Escapes data based on type. * Sets boolean and null types. * - * @param $str + * @param string $str * * @return mixed */ @@ -246,5 +219,4 @@ public function escape($str); public function callFunction(string $functionName, ...$params); //-------------------------------------------------------------------- - } diff --git a/system/Database/Database.php b/system/Database/Database.php index 820c80e0f404..345c0228e67f 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -45,6 +45,7 @@ */ class Database { + /** * Maintains an array of the instances of all connections * that have been created. Helps to keep track of all open @@ -74,10 +75,10 @@ public function load(array $params = [], string $alias) { throw new \InvalidArgumentException('You have not selected a database type to connect to.'); } - + $className = strpos($params['DBDriver'], '\\') === false - ? '\CodeIgniter\Database\\'.$params['DBDriver'].'\\Connection' - : $params['DBDriver'].'\\Connection'; + ? '\CodeIgniter\Database\\' . $params['DBDriver'] . '\\Connection' + : $params['DBDriver'] . '\\Connection'; $class = new $className($params); @@ -92,18 +93,16 @@ public function load(array $params = [], string $alias) /** * Creates a new Forge instance for the current database type. * - * @param ConnectionInterface $db + * @param ConnectionInterface|BaseConnection $db * * @return mixed */ public function loadForge(ConnectionInterface $db) { - $className = strpos($db->DBDriver, '\\') === false - ? '\CodeIgniter\Database\\'.$db->DBDriver.'\\Forge' - : $db->DBDriver.'\\Connection'; + $className = strpos($db->DBDriver, '\\') === false ? '\CodeIgniter\Database\\' . $db->DBDriver . '\\Forge' : $db->DBDriver . '\\Connection'; // Make sure a connection exists - if (! $db->connID) + if ( ! $db->connID) { $db->initialize(); } @@ -118,18 +117,16 @@ public function loadForge(ConnectionInterface $db) /** * Loads the Database Utilities class. * - * @param ConnectionInterface $db + * @param ConnectionInterface|BaseConnection $db * * @return mixed */ public function loadUtils(ConnectionInterface $db) { - $className = strpos($db->DBDriver, '\\') === false - ? '\CodeIgniter\Database\\'.$db->DBDriver.'\\Utils' - : $db->DBDriver.'\\Utils'; + $className = strpos($db->DBDriver, '\\') === false ? '\CodeIgniter\Database\\' . $db->DBDriver . '\\Utils' : $db->DBDriver . '\\Utils'; // Make sure a connection exists - if (! $db->connID) + if ( ! $db->connID) { $db->initialize(); } @@ -140,6 +137,4 @@ public function loadUtils(ConnectionInterface $db) } //-------------------------------------------------------------------- - - } diff --git a/system/Database/Exceptions/DataException.php b/system/Database/Exceptions/DataException.php new file mode 100644 index 000000000000..806139211e98 --- /dev/null +++ b/system/Database/Exceptions/DataException.php @@ -0,0 +1,48 @@ +db =& $db; + $this->db = &$db; } //-------------------------------------------------------------------- @@ -173,18 +187,18 @@ public function __construct(ConnectionInterface $db) */ public function getConnection() { - return $this->db; + return $this->db; } //-------------------------------------------------------------------- - /** * Create database * * @param string $db_name * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function createDatabase($db_name) { @@ -197,8 +211,7 @@ public function createDatabase($db_name) return false; } - elseif ( ! $this->db->query(sprintf($this->createDatabaseStr, $db_name, $this->db->charset, - $this->db->DBCollat)) + elseif (! $this->db->query(sprintf($this->createDatabaseStr, $db_name, $this->db->charset, $this->db->DBCollat)) ) { if ($this->db->DBDebug) @@ -209,7 +222,7 @@ public function createDatabase($db_name) return false; } - if ( ! empty($this->db->dataCache['db_names'])) + if (! empty($this->db->dataCache['db_names'])) { $this->db->dataCache['db_names'][] = $db_name; } @@ -225,6 +238,7 @@ public function createDatabase($db_name) * @param string $db_name * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function dropDatabase($db_name) { @@ -237,7 +251,7 @@ public function dropDatabase($db_name) return false; } - elseif ( ! $this->db->query(sprintf($this->dropDatabaseStr, $db_name))) + elseif (! $this->db->query(sprintf($this->dropDatabaseStr, $db_name))) { if ($this->db->DBDebug) { @@ -247,7 +261,7 @@ public function dropDatabase($db_name) return false; } - if ( ! empty($this->db->dataCache['db_names'])) + if (! empty($this->db->dataCache['db_names'])) { $key = array_search(strtolower($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); if ($key !== false) @@ -264,30 +278,28 @@ public function dropDatabase($db_name) /** * Add Key * - * @param string $key - * @param bool $primary + * @param string|array $key + * @param bool $primary + * @param bool $unique * - * @return CI_DB_forge + * @return Forge */ - public function addKey($key, $primary = false) + public function addKey($key, bool $primary = false, bool $unique = false) { - if (is_array($key)) + if ($primary === true) { - foreach ($key as $one) + foreach ((array)$key as $one) { - $this->addKey($one, $primary); + $this->primaryKeys[] = $one; } - - return $this; - } - - if ($primary === true) - { - $this->primaryKeys[] = $key; } else { $this->keys[] = $key; + if ($unique === true) + { + $this->uniqueKeys[] = ($c = count($this->keys)) ? $c - 1 : 0; + } } return $this; @@ -295,12 +307,41 @@ public function addKey($key, $primary = false) //-------------------------------------------------------------------- + /** + * Add Primary Key + * + * @param string|array $key + * + * @return Forge + */ + public function addPrimaryKey($key) + { + return $this->addKey($key, true); + } + + //-------------------------------------------------------------------- + + + /** + * Add Unique Key + * + * @param string|array $key + * + * @return Forge + */ + public function addUniqueKey($key) + { + return $this->addKey($key, false, true); + } + + //-------------------------------------------------------------------- + /** * Add Field * * @param array $field * - * @return CI_DB_forge + * @return Forge */ public function addField($field) { @@ -338,6 +379,67 @@ public function addField($field) //-------------------------------------------------------------------- + /** + * Add Foreign Key + * + * @param string $fieldName + * @param string $tableName + * @param string $tableField + * @param bool $onUpdate + * @param bool $onDelete + * + * @return \CodeIgniter\Database\Forge + * @throws \CodeIgniter\Database\Exceptions\DatabaseException + */ + public function addForeignKey($fieldName = '', $tableName = '', $tableField = '', $onUpdate = false, $onDelete = false) + { + if (! isset($this->fields[$fieldName])) + { + throw new DatabaseException(lang('Database.fieldNotExists', [$fieldName])); + } + + $this->foreignKeys[$fieldName] = [ + 'table' => $tableName, + 'field' => $tableField, + 'onDelete' => strtoupper($onDelete), + 'onUpdate' => strtoupper($onUpdate), + ]; + + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Foreign Key Drop + * + * @param string $table Table name + * @param string $foreign_name Foreign name + * + * @return bool|\CodeIgniter\Database\BaseResult|\CodeIgniter\Database\Query|false|mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException + */ + public function dropForeignKey($table, $foreign_name) + { + $sql = sprintf($this->dropConstraintStr, $this->db->escapeIdentifiers($this->db->DBPrefix.$table), + $this->db->escapeIdentifiers($this->db->DBPrefix.$foreign_name)); + + if ($sql === false) + { + if ($this->db->DBDebug) + { + throw new DatabaseException('This feature is not available for the database you are using.'); + } + + return false; + } + + return $this->db->query($sql); + } + + //-------------------------------------------------------------------- + /** * Create Table * @@ -345,7 +447,8 @@ public function addField($field) * @param bool $if_not_exists Whether to add IF NOT EXISTS condition * @param array $attributes Associative array of table attributes * - * @return bool + * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function createTable($table, $if_not_exists = false, array $attributes = []) { @@ -353,10 +456,8 @@ public function createTable($table, $if_not_exists = false, array $attributes = { throw new \InvalidArgumentException('A table name is required for that operation.'); } - else - { - $table = $this->db->DBPrefix.$table; - } + + $table = $this->db->DBPrefix . $table; if (count($this->fields) === 0) { @@ -381,10 +482,10 @@ public function createTable($table, $if_not_exists = false, array $attributes = if (($result = $this->db->query($sql)) !== false) { - empty($this->db->dataCache['table_names']) OR $this->db->dataCache['table_names'][] = $table; + empty($this->db->dataCache['table_names']) || $this->db->dataCache['table_names'][] = $table; // Most databases don't support creating indexes from within the CREATE TABLE statement - if ( ! empty($this->keys)) + if (! empty($this->keys)) { for ($i = 0, $sqls = $this->_processIndexes($table), $c = count($sqls); $i < $c; $i++) { @@ -411,32 +512,31 @@ public function createTable($table, $if_not_exists = false, array $attributes = */ protected function _createTable($table, $if_not_exists, $attributes) { + // For any platforms that don't support Create If Not Exists... if ($if_not_exists === true && $this->createTableIfStr === false) { if ($this->db->tableExists($table)) { return true; } - else - { - $if_not_exists = false; - } + + $if_not_exists = false; } - $sql = ($if_not_exists) - ? sprintf($this->createTableIfStr, $this->db->escapeIdentifiers($table)) + $sql = ($if_not_exists) ? sprintf($this->createTableIfStr, $this->db->escapeIdentifiers($table)) : 'CREATE TABLE'; $columns = $this->_processFields(true); for ($i = 0, $c = count($columns); $i < $c; $i++) { - $columns[$i] = ($columns[$i]['_literal'] !== false) - ? "\n\t".$columns[$i]['_literal'] + $columns[$i] = ($columns[$i]['_literal'] !== false) ? "\n\t".$columns[$i]['_literal'] : "\n\t".$this->_processColumn($columns[$i]); } - $columns = implode(',', $columns) - .$this->_processPrimaryKeys($table); + $columns = implode(',', $columns); + + $columns .= $this->_processPrimaryKeys($table); + $columns .= $this->_processForeignKeys($table); // Are indexes created from within the CREATE TABLE statement? (e.g. in MySQL) if ($this->createTableKeys === true) @@ -445,12 +545,8 @@ protected function _createTable($table, $if_not_exists, $attributes) } // createTableStr will usually have the following format: "%s %s (%s\n)" - $sql = sprintf($this->createTableStr.'%s', - $sql, - $this->db->escapeIdentifiers($table), - $columns, - $this->_createTableAttributes($attributes) - ); + $sql = sprintf($this->createTableStr.'%s', $sql, $this->db->escapeIdentifiers($table), $columns, + $this->_createTableAttributes($attributes)); return $sql; } @@ -472,7 +568,7 @@ protected function _createTableAttributes($attributes) { if (is_string($key)) { - $sql .= ' '.strtoupper($key).' '.$attributes[$key]; + $sql .= ' ' . strtoupper($key) . ' ' . $this->db->escape($attributes[$key]); } } @@ -486,10 +582,12 @@ protected function _createTableAttributes($attributes) * * @param string $table_name Table name * @param bool $if_exists Whether to add an IF EXISTS condition + * @param bool $cascade Whether to add an CASCADE condition * - * @return bool + * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function dropTable($table_name, $if_exists = false) + public function dropTable($table_name, $if_exists = false, $cascade = false) { if ($table_name === '') { @@ -501,7 +599,14 @@ public function dropTable($table_name, $if_exists = false) return false; } - if (($query = $this->_dropTable($this->db->DBPrefix.$table_name, $if_exists)) === true) + + // If the prefix is already starting the table name, remove it... + if (! empty($this->db->DBPrefix) && strpos($table_name, $this->db->DBPrefix) === 0) + { + $table_name = substr($table_name, strlen($this->db->DBPrefix)); + } + + if (($query = $this->_dropTable($this->db->DBPrefix.$table_name, $if_exists, $cascade)) === true) { return true; } @@ -531,10 +636,11 @@ public function dropTable($table_name, $if_exists = false) * * @param string $table Table name * @param bool $if_exists Whether to add an IF EXISTS condition + * @param bool $cascade Whether to add an CASCADE condition * * @return string */ - protected function _dropTable($table, $if_exists) + protected function _dropTable($table, $if_exists, $cascade) { $sql = 'DROP TABLE'; @@ -542,7 +648,7 @@ protected function _dropTable($table, $if_exists) { if ($this->dropTableIfStr === false) { - if ( ! $this->db->tableExists($table)) + if (! $this->db->tableExists($table)) { return true; } @@ -553,7 +659,9 @@ protected function _dropTable($table, $if_exists) } } - return $sql.' '.$this->db->escapeIdentifiers($table); + $sql = $sql.' '.$this->db->escapeIdentifiers($table); + + return $sql; } //-------------------------------------------------------------------- @@ -564,11 +672,12 @@ protected function _dropTable($table, $if_exists) * @param string $table_name Old table name * @param string $new_table_name New table name * - * @return bool + * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function renameTable($table_name, $new_table_name) { - if ($table_name === '' OR $new_table_name === '') + if ($table_name === '' || $new_table_name === '') { throw new \InvalidArgumentException('A table name is required for that operation.'); } @@ -605,16 +714,16 @@ public function renameTable($table_name, $new_table_name) /** * Column Add * - * @param string $table Table name - * @param array $field Column definition - * @param string $_after Column for AFTER clause (deprecated) + * @param string $table Table name + * @param array $field Column definition * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function addColumn($table, $field, $_after = null) + public function addColumn($table, $field) { // Work-around for literal column definitions - is_array($field) OR $field = [$field]; + is_array($field) || $field = [$field]; foreach (array_keys($field) as $k) { @@ -653,6 +762,7 @@ public function addColumn($table, $field, $_after = null) * @param string $column_name Column name * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function dropColumn($table, $column_name) { @@ -679,11 +789,12 @@ public function dropColumn($table, $column_name) * @param string $field Column definition * * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function modifyColumn($table, $field) { // Work-around for literal column definitions - is_array($field) OR $field = [$field]; + is_array($field) || $field = [$field]; foreach (array_keys($field) as $k) { @@ -739,9 +850,7 @@ protected function _alterTable($alter_type, $table, $field) return $sql.'DROP COLUMN '.$this->db->escapeIdentifiers($field); } - $sql .= ($alter_type === 'ADD') - ? 'ADD ' - : $alter_type.' COLUMN '; + $sql .= ($alter_type === 'ADD') ? 'ADD ' : $alter_type.' COLUMN '; $sqls = []; for ($i = 0, $c = count($field); $i < $c; $i++) @@ -843,14 +952,12 @@ protected function _processFields($create_table = false) case 'ENUM': case 'SET': $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']); - $field['length'] = is_array($attributes['CONSTRAINT']) - ? "('".implode("','", $attributes['CONSTRAINT'])."')" - : '('.$attributes['CONSTRAINT'].')'; + $field['length'] = is_array($attributes['CONSTRAINT']) ? "('".implode("','", + $attributes['CONSTRAINT'])."')" : '('.$attributes['CONSTRAINT'].')'; break; default: - $field['length'] = is_array($attributes['CONSTRAINT']) - ? '('.implode(',', $attributes['CONSTRAINT']).')' - : '('.$attributes['CONSTRAINT'].')'; + $field['length'] = is_array($attributes['CONSTRAINT']) ? '('.implode(',', + $attributes['CONSTRAINT']).')' : '('.$attributes['CONSTRAINT'].')'; break; } } @@ -918,7 +1025,7 @@ protected function _attributeType(&$attributes) */ protected function _attributeUnsigned(&$attributes, &$field) { - if (empty($attributes['UNSIGNED']) OR $attributes['UNSIGNED'] !== true) + if (empty($attributes['UNSIGNED']) || $attributes['UNSIGNED'] !== true) { return; } @@ -996,7 +1103,7 @@ protected function _attributeDefault(&$attributes, &$field) */ protected function _attributeUnique(&$attributes, &$field) { - if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) + if (! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) { $field['unique'] = ' UNIQUE'; } @@ -1014,8 +1121,8 @@ protected function _attributeUnique(&$attributes, &$field) */ protected function _attributeAutoIncrement(&$attributes, &$field) { - if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && - stripos($field['type'], 'int') !== false + if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + && stripos($field['type'], 'int') !== false ) { $field['auto_increment'] = ' AUTO_INCREMENT'; @@ -1037,7 +1144,7 @@ protected function _processPrimaryKeys($table) for ($i = 0, $c = count($this->primaryKeys); $i < $c; $i++) { - if ( ! isset($this->fields[$this->primaryKeys[$i]])) + if (! isset($this->fields[$this->primaryKeys[$i]])) { unset($this->primaryKeys[$i]); } @@ -1059,7 +1166,7 @@ protected function _processPrimaryKeys($table) * * @param string $table * - * @return string + * @return array */ protected function _processIndexes($table) { @@ -1067,24 +1174,27 @@ protected function _processIndexes($table) for ($i = 0, $c = count($this->keys); $i < $c; $i++) { - if (is_array($this->keys[$i])) + $this->keys[$i] = (array)$this->keys[$i]; + + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) { - for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + if (! isset($this->fields[$this->keys[$i][$i2]])) { - if ( ! isset($this->fields[$this->keys[$i][$i2]])) - { - unset($this->keys[$i][$i2]); - continue; - } + unset($this->keys[$i][$i2]); } } - elseif ( ! isset($this->fields[$this->keys[$i]])) + if (count($this->keys[$i]) <= 0) { - unset($this->keys[$i]); continue; } - is_array($this->keys[$i]) OR $this->keys[$i] = [$this->keys[$i]]; + if (in_array($i, $this->uniqueKeys)) + { + $sqls[] = 'ALTER TABLE '.$this->db->escapeIdentifiers($table) + .' ADD CONSTRAINT '.$this->db->escapeIdentifiers($table.'_'.implode('_', $this->keys[$i])) + .' UNIQUE ('.implode(', ', $this->db->escapeIdentifiers($this->keys[$i])).');'; + continue; + } $sqls[] = 'CREATE INDEX '.$this->db->escapeIdentifiers($table.'_'.implode('_', $this->keys[$i])) .' ON '.$this->db->escapeIdentifiers($table) @@ -1095,6 +1205,40 @@ protected function _processIndexes($table) } //-------------------------------------------------------------------- + + /** + * Process foreign keys + * + * @param string $table Table name + * + * @return string + */ + protected function _processForeignKeys($table) { + $sql = ''; + + $allowActions = ['CASCADE','SET NULL','NO ACTION','RESTRICT','SET DEFAULT']; + + if (count($this->foreignKeys) > 0){ + foreach ($this->foreignKeys as $field => $fkey) { + $name_index = $table.'_'.$field.'_foreign'; + + $sql .= ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers($name_index) + . ' FOREIGN KEY(' . $this->db->escapeIdentifiers($field) . ') REFERENCES '.$this->db->escapeIdentifiers($this->db->DBPrefix.$fkey['table']).' ('.$this->db->escapeIdentifiers($fkey['field']).')'; + + if($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions)){ + $sql .= " ON DELETE ".$fkey['onDelete']; + } + + if($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions)){ + $sql .= " ON UPDATE ".$fkey['onUpdate']; + } + + } + } + + return $sql; + } + //-------------------------------------------------------------------- /** @@ -1106,7 +1250,7 @@ protected function _processIndexes($table) */ protected function _reset() { - $this->fields = $this->keys = $this->primaryKeys = []; + $this->fields = $this->keys = $this->uniqueKeys = $this->primaryKeys = $this->foreignKeys = []; } } diff --git a/system/Database/Migration.php b/system/Database/Migration.php index 5ac601c09a85..2e497d904054 100644 --- a/system/Database/Migration.php +++ b/system/Database/Migration.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -41,6 +41,7 @@ */ abstract class Migration { + /** * The name of the database group to use. * @var string @@ -58,23 +59,21 @@ abstract class Migration * @var Forge */ protected $forge; - + //-------------------------------------------------------------------- - + /** * Constructor. * * @param \CodeIgniter\Database\Forge $forge */ - public function __construct(Forge $forge = null) + public function __construct(Forge $forge = null) { - $this->forge = ! is_null($forge) - ? $forge - : \Config\Database::forge($this->DBGroup); + $this->forge = ! is_null($forge) ? $forge : \Config\Database::forge($this->DBGroup); $this->db = $this->forge->getConnection(); } - + //-------------------------------------------------------------------- /** @@ -84,7 +83,7 @@ public function __construct(Forge $forge = null) */ public function getDBGroup() { - return $this->DBGroup; + return $this->DBGroup; } //-------------------------------------------------------------------- @@ -102,5 +101,4 @@ abstract public function up(); abstract public function down(); //-------------------------------------------------------------------- - } diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 00e2ba68afae..30e779c31064 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,23 +27,25 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2018 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 * @filesource */ - +use Config\Autoload; +use CodeIgniter\CLI\CLI; use CodeIgniter\Config\BaseConfig; -use CodeIgniter\ConfigException; +use CodeIgniter\Exceptions\ConfigException; /** * Class MigrationRunner */ class MigrationRunner { + /** * Whether or not migrations are allowed to run. * @@ -73,11 +75,25 @@ class MigrationRunner protected $currentVersion = 0; /** - * The location where migrations can be found. + * The Namespace where migrations can be found. * * @var string */ - protected $path; + protected $namespace; + + /** + * The database Group to migrate. + * + * @var string + */ + protected $group; + + /** + * The migration name. + * + * @var string + */ + protected $name; /** * The pattern used to locate migration file versions. @@ -89,6 +105,7 @@ class MigrationRunner /** * The main database connection. Used to store * migration information in. + * * @var ConnectionInterface */ protected $db; @@ -96,49 +113,59 @@ class MigrationRunner /** * If true, will continue instead of throwing * exceptions. + * * @var bool */ protected $silent = false; + /** + * used to return messages for CLI. + * + * @var bool + */ + protected $cliMessages = []; + //-------------------------------------------------------------------- /** * Constructor. - * - * @param BaseConfig $config + * + * @param BaseConfig $config * @param \CodeIgniter\Database\ConnectionInterface $db + * * @throws ConfigException */ public function __construct(BaseConfig $config, ConnectionInterface $db = null) { - $this->enabled = $config->enabled ?? false; - $this->type = $config->type ?? 'timestamp'; - $this->table = $config->table ?? 'migrations'; + $this->enabled = $config->enabled ?? false; + $this->type = $config->type ?? 'timestamp'; + $this->table = $config->table ?? 'migrations'; $this->currentVersion = $config->currentVersion ?? 0; - $this->path = $config->path ?? APPPATH.'Database/Migrations/'; - $this->path = rtrim($this->path, '/').'/'; + // Default name space is the app namespace + $this->namespace = APP_NAMESPACE; + + // get default database group + $config = new \Config\Database(); + $this->group = $config->defaultGroup; + unset($config); if (empty($this->table)) { - throw new ConfigException('Migrations table must be set.'); + throw ConfigException::forMissingMigrationsTable(); } if ( ! in_array($this->type, ['sequential', 'timestamp'])) { - throw new ConfigException('An invalid migration numbering type was specified: '.$this->type); + throw ConfigException::forInvalidMigrationType($this->type); } // Migration basename regex - $this->regex = ($this->type === 'timestamp') - ? '/^\d{14}_(\w+)$/' - : '/^\d{3}_(\w+)$/'; + $this->regex = ($this->type === 'timestamp') ? '/^\d{14}_(\w+)$/' : '/^\d{3}_(\w+)$/'; // If no db connection passed in, use // default database group. - $this->db = ! empty($db) - ? $db - : \Config\Database::connect(); + $this->db = ! empty($db) ? $db : \Config\Database::connect(); $this->ensureTable(); } @@ -151,41 +178,46 @@ public function __construct(BaseConfig $config, ConnectionInterface $db = null) * Calls each migration step required to get to the schema version of * choice * - * @param string $targetVersion Target schema version - * @param $group + * @param string $targetVersion Target schema version + * @param string $namespace + * @param string $group + * * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure * @throws ConfigException */ - public function version(string $targetVersion, $group='default') + public function version(string $targetVersion, $namespace = null, $group = null) { - if (! $this->enabled) + if ( ! $this->enabled) { - throw new ConfigException('Migrations have been loaded but are disabled or setup incorrectly.'); + throw ConfigException::forDisabledMigrations(); } - - // Note: We use strings, so that timestamp versions work on 32-bit systems - $currentVersion = $this->getVersion($group); - - if ($this->type === 'sequential') + // Set Namespace if not null + if ( ! is_null($namespace)) { - $targetVersion = sprintf('%03d', $targetVersion); + $this->setNamespace($namespace); } - else + + // Set database group if not null + if ( ! is_null($group)) { - $targetVersion = (string)$targetVersion; + $this->setGroup($group); } $migrations = $this->findMigrations(); - if ($targetVersion > 0 && ! isset($migrations[$targetVersion])) + if (empty($migrations)) { - throw new \RuntimeException('Migration file not found: '.$targetVersion); + return true; } + // Get Namespace current version + // Note: We use strings, so that timestamp versions work on 32-bit systems + $currentVersion = $this->getVersion(); if ($targetVersion > $currentVersion) { // Moving Up $method = 'up'; + ksort($migrations); } else { @@ -194,53 +226,51 @@ public function version(string $targetVersion, $group='default') krsort($migrations); } - if (empty($migrations)) - { - return true; - } + // Check Migration consistency + $this->CheckMigrations($migrations, $method, $targetVersion); - // Validate all available migrations, and run the ones within our target range - foreach ($migrations as $number => $file) + // loop migration for each namespace (module) + foreach ($migrations as $version => $migration) { - // Check for sequence gaps - if ($this->type === 'sequential' && $previous !== false && abs($number - $previous) > 1) + + // Only include migrations within the scoop + if (($method === 'up' && $version > $currentVersion && $version <= $targetVersion) || ( $method === 'down' && $version <= $currentVersion && $version > $targetVersion) + ) { - throw new \RuntimeException('There is a gap in the migration sequence near version number: '.$number); - } - include_once $file; - $class = 'Migration_'.($this->getMigrationName(basename($file, '.php'))); + include_once $migration->path; + // Get namespaced class name + $class = $this->namespace . '\Database\Migrations\Migration_' . ($migration->name); - // Validate the migration file structure - if ( ! class_exists($class, false)) - { - throw new \RuntimeException(sprintf('The migration class "%s" could not be found.', $class)); - } + $this->setName($migration->name); - $previous = $number; + // Validate the migration file structure + if ( ! class_exists($class, false)) + { + throw new \RuntimeException(sprintf(lang('Migrations.classNotFound'), $class)); + } - // Run migrations that are inside the target range - if ( - ($method === 'up' && $number > $currentVersion && $number <= $targetVersion) OR - ($method === 'down' && $number <= $currentVersion && $number > $targetVersion) - ) - { - $instance = new $class(); + // Forcing migration to selected database group + $instance = new $class(\Config\Database::forge($this->group)); if ( ! is_callable([$instance, $method])) { - throw new \RuntimeException("The migration class is missing an \"{$method}\" method."); + throw new \RuntimeException(sprintf(lang('Migrations.missingMethod'), $method)); } - call_user_func([$instance, $method]); - - $currentVersion = $number; - if ($method === 'up') $this->addHistory($currentVersion, $instance->getDBGroup()); - elseif ($method === 'down') $this->removeHistory($currentVersion, $instance->getDBGroup()); + $instance->{$method}(); + if ($method === 'up') + { + $this->addHistory($migration->version); + } + elseif ($method === 'down') + { + $this->removeHistory($migration->version); + } } } - return $currentVersion; + return true; } //-------------------------------------------------------------------- @@ -248,35 +278,98 @@ public function version(string $targetVersion, $group='default') /** * Sets the schema to the latest migration * + * @param string $namespace + * @param string $group + * * @return mixed Current version string on success, FALSE on failure */ - public function latest() + public function latest($namespace = null, $group = null) { - $migrations = $this->findMigrations(); - if (empty($migrations)) + // Set Namespace if not null + if ( ! is_null($namespace)) { - if ($this->silent) return false; - - throw new \RuntimeException('No migrations were found.'); + $this->setNamespace($namespace); + } + // Set database group if not null + if ( ! is_null($group)) + { + $this->setGroup($group); } - $lastMigration = basename(end($migrations)); + $migrations = $this->findMigrations(); + + $lastMigration = end($migrations)->version ?? 0; // Calculate the last migration step from existing migration // filenames and proceed to the standard version migration - return $this->version($this->getMigrationNumber($lastMigration)); + return $this->version($lastMigration); + } + + //-------------------------------------------------------------------- + + /** + * Sets the schema to the latest migration for all namespaces + * + * @param string $group + * + * @return bool + */ + public function latestAll($group = null) + { + // Set database group if not null + if ( ! is_null($group)) + { + $this->setGroup($group); + } + + // Get all namespaces form PSR4 paths. + $config = new Autoload(); + $namespaces = $config->psr4; + + foreach ($namespaces as $namespace => $path) + { + + $this->setNamespace($namespace); + $migrations = $this->findMigrations(); + + if (empty($migrations)) + { + continue; + } + + $lastMigration = end($migrations)->version; + // No New migrations to add + if ($lastMigration == $this->getVersion()) + { + continue; + } + + // Calculate the last migration step from existing migration + // filenames and proceed to the standard version migration + $this->version($lastMigration); + } + + return true; } //-------------------------------------------------------------------- /** - * Sets the schema to the migration version set in config + * Sets the (APP_NAMESPACE) schema to $currentVersion in migration config file + * + * @param string $group * * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure */ - public function current() + public function current($group = null) { + // Set database group if not null + if ( ! is_null($group)) + { + $this->setGroup($group); + } + return $this->version($this->currentVersion); } @@ -285,69 +378,172 @@ public function current() /** * Retrieves list of available migration scripts * - * @return array list of migration file paths sorted by version + * @return array list of migrations as $version for one namespace */ public function findMigrations() { $migrations = []; + // Get namespace location form PSR4 paths. + $config = new Autoload(); + + $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($this->path.'*_*.php') as $file) + foreach (glob($dir . '*_*.php') as $file) { $name = basename($file, '.php'); - // Filter out non-migration files if (preg_match($this->regex, $name)) { - $number = $this->getMigrationNumber($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; + + // Add to migrations[version] + $migrations[$migration->version] = $migration; + } + } - // There cannot be duplicate migration numbers - if (isset($migrations[$number])) - { - throw new \RuntimeException('There are multiple migrations with the same version number: '.$number); - } + return $migrations; + } + + //-------------------------------------------------------------------- + + /** + * checks if the list of available migration scripts list are consistent + * if sequential check if no gaps and check if all consistent with migrations table if downgrading + * if timestamp check if consistent with migrations table if downgrading + * + * @param array $migrations + * @param string $method + * @param int $targetversion + * + * @return bool + */ + protected function CheckMigrations($migrations, $method, $targetversion) + { + // Check if no migrations found + if (empty($migrations)) + { + if ($this->silent) + { + return false; + } + throw new \RuntimeException(lang('Migrations.empty')); + } - $migrations[$number] = $file; + // Check if $targetversion file is found + if ($targetversion != 0 && ! array_key_exists($targetversion, $migrations)) + { + if ($this->silent) + { + return false; } + throw new \RuntimeException(lang('Migrations.notFound') . $targetversion); } ksort($migrations); - return $migrations; + if ($method === 'down') + { + $history_migrations = $this->getHistory($this->group); + $history_size = count($history_migrations) - 1; + } + // Check for sequence gaps + $loop = 0; + foreach ($migrations as $migration) + { + if ($this->type === 'sequential' && abs($migration->version - $loop) > 1) + { + throw new \RuntimeException(lang('Migration.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); + } + } + $loop ++; + } + + return true; } //-------------------------------------------------------------------- /** - * Updates the expected location of the migration files. + * Set namespace. * Allows other scripts to modify on the fly as needed. * - * @param string $path + * @param string $namespace * - * @return $this + * @return MigrationRunner */ - public function setPath(string $path) + public function setNamespace(string $namespace) { - $this->path = rtrim($path, '/').'/'; + $this->namespace = $namespace; return $this; } //-------------------------------------------------------------------- + /** + * Set database Group. + * Allows other scripts to modify on the fly as needed. + * + * @param string $group + * + * @return MigrationRunner + */ + public function setGroup(string $group) + { + $this->group = $group; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set migration Name. + * + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + //-------------------------------------------------------------------- + /** * Grabs the full migration history from the database. * - * @param $group - * @return mixed + * @param string $group + * + * @return array */ public function getHistory($group = 'default') { - $query = $this->db->table($this->table) - ->where('group', $group) - ->get(); + $query = $this->db->table($this->table) + ->where('group', $group) + ->where('namespace', $this->namespace) + ->orderBy('version', 'ASC') + ->get(); - if (! $query) return []; + if ( ! $query) + { + return []; + } return $query->getResultArray(); } @@ -360,19 +556,17 @@ public function getHistory($group = 'default') * * @param bool $silent * - * @return $this + * @return MigrationRunner */ public function setSilent(bool $silent) { - $this->silent = $silent; + $this->silent = $silent; return $this; } //-------------------------------------------------------------------- - - /** * Extracts the migration number from a filename * @@ -382,8 +576,7 @@ public function setSilent(bool $silent) */ protected function getMigrationNumber($migration) { - return sscanf($migration, '%[0-9]+', $number) - ? $number : '0'; + return sscanf($migration, '%[0-9]+', $number) ? $number : '0'; } //-------------------------------------------------------------------- @@ -408,18 +601,31 @@ protected function getMigrationName($migration) /** * Retrieves current schema version * - * @param $group * @return string Current migration version */ - protected function getVersion($group = 'default') + protected function getVersion() { $row = $this->db->table($this->table) - ->select('version') - ->where('group', $group) - ->get() - ->getRow(); + ->select('version') + ->where('group', $this->group) + ->where('namespace', $this->namespace) + ->orderBy('version', 'DESC') + ->get(); - return $row ? $row->version : '0'; + return $row && ! is_null($row->getRow()) ? $row->getRow()->version : '0'; + } + + //-------------------------------------------------------------------- + + /** + * Retrieves current schema version + * + * @return string Current migration version + */ + public function getCliMessages() + { + + return $this->cliMessages; } //-------------------------------------------------------------------- @@ -428,26 +634,24 @@ protected function getVersion($group = 'default') * Stores the current schema version. * * @param string $version - * @param string $group The database group * * @internal param string $migration Migration reached * */ - protected function addHistory($version, $group = 'default') + protected function addHistory($version) { - if (empty($group)) + $this->db->table($this->table) + ->insert([ + 'version' => $version, + 'name' => $this->name, + 'group' => $this->group, + 'namespace' => $this->namespace, + 'time' => time(), + ]); + if (is_cli()) { - $config = new \Config\Database(); - $group = $config->defaultGroup; - unset($config); + $this->cliMessages[] = "\t" . CLI::color(lang('Migrations.added'), 'yellow') . "($this->namespace) " . $version . '_' . $this->name; } - - $this->db->table($this->table) - ->insert([ - 'version' => $version, - 'group' => $group, - 'time' => date('Y-m-d H:i:s') - ]); } //-------------------------------------------------------------------- @@ -456,14 +660,18 @@ protected function addHistory($version, $group = 'default') * Removes a single history * * @param string $version - * @param string $group The database group */ - protected function removeHistory($version, $group = 'default') + protected function removeHistory($version) { $this->db->table($this->table) - ->where('version', $version) - ->where('group', $group) - ->delete(); + ->where('version', $version) + ->where('group', $this->group) + ->where('namespace', $this->namespace) + ->delete(); + if (is_cli()) + { + $this->cliMessages[] = "\t" . CLI::color(lang('Migrations.removed'), 'yellow') . "($this->namespace) " . $version . '_' . $this->name; + } } //-------------------------------------------------------------------- @@ -482,25 +690,35 @@ protected function ensureTable() $forge = \Config\Database::forge(); $forge->addField([ - 'version' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'null' => false + 'version' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'group' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, ], - 'group' => [ - 'type' => 'varchar', - 'constraint' => 255, - 'null' => false + 'namespace' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'time' => [ + 'type' => 'INT', + 'constraint' => 11, + 'null' => false, ], - 'time' => [ - 'type' => 'timestamp', - 'null' => false - ] ]); $forge->createTable($this->table, true); } //-------------------------------------------------------------------- - } diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index c0eabf4032d0..8a13e01cc2bd 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseBuilder; /** @@ -43,10 +42,12 @@ */ class Builder extends BaseBuilder { + /** * Identifier escape character * * @var string */ protected $escapeChar = '`'; + } diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 8f20826f1315..a7f26a494288 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,22 +29,22 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; -use CodeIgniter\DatabaseException; +use \CodeIgniter\Database\Exceptions\DatabaseException; /** * Connection for MySQLi */ class Connection extends BaseConnection implements ConnectionInterface { + /** * Database driver * @@ -79,9 +79,9 @@ class Connection extends BaseConnection implements ConnectionInterface * * Has to be preserved without being assigned to $conn_id. * - * @var MySQLi + * @var \MySQLi */ - protected $mysqli; + public $mysqli; //-------------------------------------------------------------------- @@ -89,7 +89,9 @@ class Connection extends BaseConnection implements ConnectionInterface * Connect to the database. * * @param bool $persistent + * * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function connect($persistent = false) { @@ -97,42 +99,40 @@ public function connect($persistent = false) if ($this->hostname[0] === '/') { $hostname = null; - $port = null; - $socket = $this->hostname; + $port = null; + $socket = $this->hostname; } else { - $hostname = ($persistent === true) - ? 'p:'.$this->hostname - : $this->hostname; - $port = empty($this->port) ? null : $this->port; - $socket = null; + $hostname = ($persistent === true) ? 'p:' . $this->hostname : $this->hostname; + $port = empty($this->port) ? null : $this->port; + $socket = null; } $client_flags = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0; $this->mysqli = mysqli_init(); + mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX); + $this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10); if (isset($this->strictOn)) { if ($this->strictOn) { - $this->mysqli->options(MYSQLI_INIT_COMMAND, - 'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); + $this->mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")'); } else { - $this->mysqli->options(MYSQLI_INIT_COMMAND, - 'SET SESSION sql_mode = - REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( - @@sql_mode, - "STRICT_ALL_TABLES,", ""), - ",STRICT_ALL_TABLES", ""), - "STRICT_ALL_TABLES", ""), - "STRICT_TRANS_TABLES,", ""), - ",STRICT_TRANS_TABLES", ""), - "STRICT_TRANS_TABLES", "")' + $this->mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + @@sql_mode, + "STRICT_ALL_TABLES,", ""), + ",STRICT_ALL_TABLES", ""), + "STRICT_ALL_TABLES", ""), + "STRICT_TRANS_TABLES,", ""), + ",STRICT_TRANS_TABLES", ""), + "STRICT_TRANS_TABLES", "")' ); } } @@ -140,11 +140,11 @@ public function connect($persistent = false) if (is_array($this->encrypt)) { $ssl = []; - empty($this->encrypt['ssl_key']) OR $ssl['key'] = $this->encrypt['ssl_key']; - empty($this->encrypt['ssl_cert']) OR $ssl['cert'] = $this->encrypt['ssl_cert']; - empty($this->encrypt['ssl_ca']) OR $ssl['ca'] = $this->encrypt['ssl_ca']; - empty($this->encrypt['ssl_capath']) OR $ssl['capath'] = $this->encrypt['ssl_capath']; - empty($this->encrypt['ssl_cipher']) OR $ssl['cipher'] = $this->encrypt['ssl_cipher']; + empty($this->encrypt['ssl_key']) || $ssl['key'] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) || $ssl['cert'] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) || $ssl['ca'] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) || $ssl['capath'] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) || $ssl['cipher'] = $this->encrypt['ssl_cipher']; if ( ! empty($ssl)) { @@ -153,7 +153,7 @@ public function connect($persistent = false) if ($this->encrypt['ssl_verify']) { defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT') && - $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true); + $this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, true); } // Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT // to FALSE didn't do anything, so PHP 5.6.16 introduced yet another @@ -169,32 +169,25 @@ public function connect($persistent = false) $client_flags |= MYSQLI_CLIENT_SSL; $this->mysqli->ssl_set( - isset($ssl['key']) ? $ssl['key'] : null, - isset($ssl['cert']) ? $ssl['cert'] : null, - isset($ssl['ca']) ? $ssl['ca'] : null, - isset($ssl['capath']) ? $ssl['capath'] : null, - isset($ssl['cipher']) ? $ssl['cipher'] : null + $ssl['key'] ?? null, $ssl['cert'] ?? null, $ssl['ca'] ?? null, $ssl['capath'] ?? null, $ssl['cipher'] ?? null ); } } - if ($this->mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, - $client_flags) + if ($this->mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, $client_flags) ) { // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails if ( - ($client_flags & MYSQLI_CLIENT_SSL) - && version_compare($this->mysqli->client_info, '5.7.3', '<=') - && empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'") - ->fetch_object()->Value) + ($client_flags & MYSQLI_CLIENT_SSL) && version_compare($this->mysqli->client_info, '5.7.3', '<=') && empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'") + ->fetch_object()->Value) ) { $this->mysqli->close(); $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!'; log_message('error', $message); - if ($this->db->db_debug) + if ($this->DBDebug) { throw new DatabaseException($message); } @@ -208,7 +201,7 @@ public function connect($persistent = false) if ($this->db->debug) { - throw new DatabaseException('Unable to set client connection character set: '.$this->charset); + throw new DatabaseException('Unable to set client connection character set: ' . $this->charset); } return false; } @@ -225,14 +218,22 @@ public function connect($persistent = false) * Keep or establish the connection if no queries have been sent for * a length of time exceeding the server's idle timeout. * - * @return mixed + * @return void */ public function reconnect() { - if ($this->connID !== false && $this->connID->ping() === false) - { - $this->connID = false; - } + $this->close(); + $this->initialize(); + } + + //-------------------------------------------------------------------- + + /** + * Close the database connection. + */ + protected function _close() + { + $this->connID->close(); } //-------------------------------------------------------------------- @@ -251,6 +252,11 @@ public function setDatabase(string $databaseName) $databaseName = $this->database; } + if (empty($this->connID)) + { + $this->initialize(); + } + if ($this->connID->select_db($databaseName)) { $this->database = $databaseName; @@ -275,6 +281,11 @@ public function getVersion() return $this->dataCache['version']; } + if (empty($this->mysqli)) + { + $this->initialize(); + } + return $this->dataCache['version'] = $this->mysqli->server_info; } @@ -283,12 +294,21 @@ public function getVersion() /** * Executes the query against the database. * - * @param $sql + * @param string $sql * * @return mixed */ public function execute($sql) { + while($this->connID->more_results()) + { + $this->connID->next_result(); + if($res = $this->connID->store_result()) + { + $res->free(); + } + } + return $this->connID->query($this->prepQuery($sql)); } @@ -309,7 +329,7 @@ protected function prepQuery($sql) // modifies the query so that it a proper number of affected rows is returned. if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) { - return trim($sql).' WHERE 1=1'; + return trim($sql) . ' WHERE 1=1'; } return $sql; @@ -337,6 +357,16 @@ public function affectedRows(): int */ protected function _escapeString(string $str): string { + if (is_bool($str)) + { + return $str; + } + + if (! $this->connID) + { + $this->initialize(); + } + return $this->connID->real_escape_string($str); } @@ -351,11 +381,11 @@ protected function _escapeString(string $str): string */ protected function _listTables($prefixLimit = false): string { - $sql = 'SHOW TABLES FROM '.$this->escapeIdentifiers($this->database); + $sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database); if ($prefixLimit !== FALSE && $this->DBPrefix !== '') { - return $sql." LIKE '".$this->escapeLikeStr($this->DBPrefix)."%'"; + return $sql . " LIKE '" . $this->escapeLikeStr($this->DBPrefix) . "%'"; } return $sql; @@ -372,7 +402,7 @@ protected function _listTables($prefixLimit = false): string */ protected function _listColumns(string $table = ''): string { - return 'SHOW COLUMNS FROM '.$this->protectIdentifiers($table, TRUE, NULL, FALSE); + return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, TRUE, NULL, FALSE); } //-------------------------------------------------------------------- @@ -383,27 +413,127 @@ protected function _listColumns(string $table = ''): string * @param string $table * @return array */ - public function fieldData(string $table) + public function _fieldData(string $table) { - if (($query = $this->query('SHOW COLUMNS FROM '.$this->protectIdentifiers($table, TRUE, NULL, FALSE))) === FALSE) + if (($query = $this->query('SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, TRUE, NULL, FALSE))) === FALSE) { return FALSE; } $query = $query->getResultObject(); - $retval = array(); - for ($i = 0, $c = count($query); $i < $c; $i++) + $retval = []; + for ($i = 0, $c = count($query); $i < $c; $i ++ ) { - $retval[$i] = new \stdClass(); - $retval[$i]->name = $query[$i]->Field; + $retval[$i] = new \stdClass(); + $retval[$i]->name = $query[$i]->Field; - sscanf($query[$i]->Type, '%[a-z](%d)', - $retval[$i]->type, - $retval[$i]->max_length + sscanf($query[$i]->Type, '%[a-z](%d)', $retval[$i]->type, $retval[$i]->max_length ); - $retval[$i]->default = $query[$i]->Default; - $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + $retval[$i]->default = $query[$i]->Default; + $retval[$i]->primary_key = (int) ($query[$i]->Key === 'PRI'); + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with index data + * + * @param string $table + * @return array + */ + public function _indexData(string $table) + { + if (($query = $this->query('SHOW CREATE TABLE ' . $this->protectIdentifiers($table, TRUE, NULL, FALSE))) === FALSE) + { + return FALSE; + } + $row = $query->getRowArray(); + if ( ! $row) + { + return FALSE; + } + + $retval = []; + foreach (explode("\n", $row['Create Table']) as $line) + { + $line = trim($line); + if (strpos($line, 'PRIMARY KEY') === 0) + { + $obj = new \stdClass(); + $obj->name = 'PRIMARY KEY'; + $_fields = explode(',', preg_replace('/^.*\((.+)\).*$/', '$1', $line)); + $obj->fields = array_map(function($v) { + return trim($v, '`'); + }, $_fields); + $obj->type = 'PRIMARY'; + + $retval[] = $obj; + } + elseif (($unique = strpos($line, 'UNIQUE KEY') === 0) || strpos($line, 'KEY') === 0) + { + if (preg_match('/KEY `([^`]+)` \((.+)\)/', $line, $matches)) + { + $obj = new \stdClass(); + $obj->name = $matches[1]; + $obj->fields = array_map(function($v) { + return trim($v, '`'); + }, explode(',', $matches[2])); + $obj->type = $unique ? 'UNIQUE' : 'INDEX'; + + $retval[] = $obj; + } + else + { + throw new \LogicException('parsing key string failed.'); + } + } + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with Foreign key data + * + * @param string $table + * @return array + */ + public function _foreignKeyData(string $table) + { + $sql = ' + SELECT + tc.CONSTRAINT_NAME, + tc.TABLE_NAME, + rc.REFERENCED_TABLE_NAME + FROM information_schema.TABLE_CONSTRAINTS AS tc + INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc + ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE + tc.CONSTRAINT_TYPE = '.$this->escape('FOREIGN KEY').' AND + tc.TABLE_SCHEMA = '.$this->escape($this->database).' AND + tc.TABLE_NAME = '.$this->escape($table); + + if (($query = $this->query($sql)) === false) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->constraint_name = $row->CONSTRAINT_NAME; + $obj->table_name = $row->TABLE_NAME; + $obj->foreign_table_name = $row->REFERENCED_TABLE_NAME; + + $retval[] = $obj; } return $retval; @@ -424,13 +554,13 @@ public function error() { if ( ! empty($this->mysqli->connect_errno)) { - return array( - 'code' => $this->mysqli->connect_errno, - 'message' => $this->_mysqli->connect_error - ); + return [ + 'code' => $this->mysqli->connect_errno, + 'message' => $this->_mysqli->connect_error + ]; } - return array('code' => $this->connID->errno, 'message' => $this->connID->error); + return ['code' => $this->connID->errno, 'message' => $this->connID->error]; } //-------------------------------------------------------------------- @@ -447,5 +577,53 @@ public function insertID() //-------------------------------------------------------------------- + /** + * Begin Transaction + * + * @return bool + */ + protected function _transBegin(): bool + { + $this->connID->autocommit(false); + + return $this->connID->begin_transaction(); + } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _transCommit(): bool + { + if ($this->connID->commit()) + { + $this->connID->autocommit(true); + return true; + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _transRollback(): bool + { + if ($this->connID->rollback()) + { + $this->connID->autocommit(true); + return true; + } + + return false; + } + //-------------------------------------------------------------------- } diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index 1c310ed72f7e..c24ca2e64572 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -41,6 +41,7 @@ */ class Forge extends \CodeIgniter\Database\Forge { + /** * CREATE DATABASE statement * @@ -48,6 +49,13 @@ class Forge extends \CodeIgniter\Database\Forge */ protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s'; + /** + * DROP CONSTRAINT statement + * + * @var string + */ + protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s'; + /** * CREATE TABLE keys flag * @@ -101,18 +109,18 @@ protected function _createTableAttributes($attributes) { if (is_string($key)) { - $sql .= ' '.strtoupper($key).' = '.$attributes[$key]; + $sql .= ' ' . strtoupper($key) . ' = ' . $this->db->escape($attributes[$key]); } } if ( ! empty($this->db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) { - $sql .= ' DEFAULT CHARACTER SET = '.$this->db->charset; + $sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escape($this->db->charset); } if ( ! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE')) { - $sql .= ' COLLATE = '.$this->db->DBCollat; + $sql .= ' COLLATE = ' . $this->db->escape($this->db->DBCollat); } return $sql; @@ -135,14 +143,12 @@ protected function _alterTable($alter_type, $table, $field) return parent::_alterTable($alter_type, $table, $field); } - $sql = 'ALTER TABLE '.$this->db->escapeIdentifiers($table); - for ($i = 0, $c = count($field); $i < $c; $i++) + $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); + for ($i = 0, $c = count($field); $i < $c; $i ++ ) { if ($field[$i]['_literal'] !== FALSE) { - $field[$i] = ($alter_type === 'ADD') - ? "\n\tADD ".$field[$i]['_literal'] - : "\n\tMODIFY ".$field[$i]['_literal']; + $field[$i] = ($alter_type === 'ADD') ? "\n\tADD " . $field[$i]['_literal'] : "\n\tMODIFY " . $field[$i]['_literal']; } else { @@ -155,11 +161,11 @@ protected function _alterTable($alter_type, $table, $field) $field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; } - $field[$i] = $field[$i]['_literal'].$this->_processColumn($field[$i]); + $field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]); } } - return array($sql.implode(',', $field)); + return [$sql . implode(',', $field)]; } //-------------------------------------------------------------------- @@ -172,8 +178,7 @@ protected function _alterTable($alter_type, $table, $field) */ protected function _processColumn($field) { - $extra_clause = isset($field['after']) - ? ' AFTER '.$this->db->escapeIdentifiers($field['after']) : ''; + $extra_clause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : ''; if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE) { @@ -181,15 +186,15 @@ protected function _processColumn($field) } return $this->db->escapeIdentifiers($field['name']) - .(empty($field['new_name']) ? '' : ' '.$this->db->escapeIdentifiers($field['new_name'])) - .' '.$field['type'].$field['length'] - .$field['unsigned'] - .$field['null'] - .$field['default'] - .$field['auto_increment'] - .$field['unique'] - .(empty($field['comment']) ? '' : ' COMMENT '.$field['comment']) - .$extra_clause; + . (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name'])) + . ' ' . $field['type'] . $field['length'] + . $field['unsigned'] + . $field['null'] + . $field['default'] + . $field['auto_increment'] + . $field['unique'] + . (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment']) + . $extra_clause; } //-------------------------------------------------------------------- @@ -204,11 +209,11 @@ protected function _processIndexes($table) { $sql = ''; - for ($i = 0, $c = count($this->keys); $i < $c; $i++) + for ($i = 0, $c = count($this->keys); $i < $c; $i ++ ) { if (is_array($this->keys[$i])) { - for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2 ++ ) { if ( ! isset($this->fields[$this->keys[$i][$i2]])) { @@ -223,17 +228,18 @@ protected function _processIndexes($table) continue; } - is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]); + is_array($this->keys[$i]) || $this->keys[$i] = [$this->keys[$i]]; - $sql .= ",\n\tKEY ".$this->db->escapeIdentifiers(implode('_', $this->keys[$i])) - .' ('.implode(', ', $this->db->escapeIdentifiers($this->keys[$i])).')'; + $unique = in_array($i, $this->uniqueKeys) ? 'UNIQUE ' : ''; + + $sql .= ",\n\t{$unique}KEY " . $this->db->escapeIdentifiers(implode('_', $this->keys[$i])) + . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ')'; } - $this->keys = array(); + $this->keys = []; return $sql; } //-------------------------------------------------------------------- - } diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php new file mode 100644 index 000000000000..c80bae0714b6 --- /dev/null +++ b/system/Database/MySQLi/PreparedQuery.php @@ -0,0 +1,130 @@ +statement = $this->db->mysqli->prepare($sql)) + { + $this->errorCode = $this->db->mysqli->errno; + $this->errorString = $this->db->mysqli->error; + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return \CodeIgniter\Database\ResultInterface + */ + public function _execute($data) + { + if (is_null($this->statement)) + { + throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + // First off -bind the parameters + $bindTypes = ''; + + // Determine the type string + foreach ($data as $item) + { + if (is_integer($item)) + { + $bindTypes .= 'i'; + } + elseif (is_numeric($item)) + { + $bindTypes .= 'd'; + } + else + { + $bindTypes .= 's'; + } + } + + // Bind it + $this->statement->bind_param($bindTypes, ...$data); + + $success = $this->statement->execute(); + + return $success; + } + + //-------------------------------------------------------------------- + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->statement->get_result(); + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/MySQLi/Result.php b/system/Database/MySQLi/Result.php index 989d24c3e4f0..490c6af3550e 100644 --- a/system/Database/MySQLi/Result.php +++ b/system/Database/MySQLi/Result.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\ResultInterface; @@ -44,6 +43,7 @@ */ class Result extends BaseResult implements ResultInterface { + /** * Gets the number of fields in the result set. * @@ -82,17 +82,17 @@ public function getFieldNames(): array */ public function getFieldData(): array { - $retval = []; + $retval = []; $fieldData = $this->resultID->fetch_fields(); - for ($i = 0, $c = count($fieldData); $i < $c; $i++) + for ($i = 0, $c = count($fieldData); $i < $c; $i ++ ) { - $retval[$i] = new \stdClass(); - $retval[$i]->name = $fieldData[$i]->name; - $retval[$i]->type = $fieldData[$i]->type; - $retval[$i]->max_length = $fieldData[$i]->max_length; - $retval[$i]->primary_key = (int)($fieldData[$i]->flags & 2); - $retval[$i]->default = $fieldData[$i]->def; + $retval[$i] = new \stdClass(); + $retval[$i]->name = $fieldData[$i]->name; + $retval[$i]->type = $fieldData[$i]->type; + $retval[$i]->max_length = $fieldData[$i]->max_length; + $retval[$i]->primary_key = (int) ($fieldData[$i]->flags & 2); + $retval[$i]->default = $fieldData[$i]->def; } return $retval; @@ -102,8 +102,6 @@ public function getFieldData(): array /** * Frees the current result. - * - * @return mixed */ public function freeResult() { diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index a88cc1327b38..55e777ded06b 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,21 +29,21 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseBuilder; -use CodeIgniter\DatabaseException; +use \CodeIgniter\Database\Exceptions\DatabaseException; /** * Builder for Postgre */ class Builder extends BaseBuilder { + /** * ORDER BY random keyword * @@ -133,7 +133,7 @@ public function decrement(string $column, int $value = 1) * we simply do a DELETE and an INSERT on the first key/value * combo, assuming that it's either the primary key or a unique key. * - * @param array an associative array of insert values + * @param array $set An associative array of insert values * @param bool $returnSQL * * @return bool TRUE on success, FALSE on failure @@ -148,7 +148,7 @@ public function replace($set = null, $returnSQL = false) $this->set($set); } - if (count($this->QBSet) === 0) + if (! $this->QBSet) { if (CI_DEBUG) { @@ -168,7 +168,7 @@ public function replace($set = null, $returnSQL = false) if (empty($exists)) { - $result = $builder->insert($set, false); + $result = $builder->insert($set); } else { @@ -203,7 +203,7 @@ public function replace($set = null, $returnSQL = false) */ public function delete($where = '', $limit = null, $reset_data = true, $returnSQL = false) { - if (! empty($limit) || ! empty($this->QBLimit)) + if ( ! empty($limit) || ! empty($this->QBLimit)) { throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.'); } @@ -224,7 +224,7 @@ public function delete($where = '', $limit = null, $reset_data = true, $returnSQ */ protected function _limit($sql) { - return $sql.' LIMIT '.$this->QBLimit.($this->QBOffset ? " OFFSET {$this->QBOffset}" : ''); + return $sql . ' LIMIT ' . $this->QBLimit . ($this->QBOffset ? " OFFSET {$this->QBOffset}" : ''); } //-------------------------------------------------------------------- @@ -234,8 +234,8 @@ protected function _limit($sql) * * Generates a platform-specific update string from the supplied data * - * @param $table - * @param $values + * @param string $table + * @param array $values * * @return string * @throws DatabaseException @@ -245,7 +245,7 @@ protected function _limit($sql) */ protected function _update($table, $values) { - if (! empty($this->QBLimit)) + if ( ! empty($this->QBLimit)) { throw new DatabaseException('Postgres does not support LIMITs with UPDATE queries.'); } @@ -287,13 +287,13 @@ protected function _updateBatch($table, $values, $index) foreach ($final as $k => $v) { $cases .= "{$k} = (CASE {$index}\n" - .implode("\n", $v) - ."\nELSE {$k} END), "; + . implode("\n", $v) + . "\nELSE {$k} END), "; } - $this->where("{$index} IN(".implode(',', $ids).')', null, false); + $this->where("{$index} IN(" . implode(',', $ids) . ')', null, false); - return "UPDATE {$table} SET ".substr($cases, 0, -2).$this->compileWhereHaving('QBWhere'); + return "UPDATE {$table} SET " . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); } //-------------------------------------------------------------------- @@ -303,7 +303,7 @@ protected function _updateBatch($table, $values, $index) * * Generates a platform-specific delete string from the supplied data * - * @param string the table name + * @param string $table The table name * * @return string */ @@ -323,13 +323,38 @@ protected function _delete($table) * If the database does not support the truncate() command, * then this method maps to 'DELETE FROM table' * - * @param string the table name + * @param string $table The table name * * @return string */ protected function _truncate($table) { - return 'TRUNCATE '.$table.' RESTART IDENTITY'; + return 'TRUNCATE ' . $table . ' RESTART IDENTITY'; + } + + //-------------------------------------------------------------------- + + /** + * Platform independent LIKE statement builder. + * + * In PostgreSQL, the ILIKE operator will perform case insensitive + * searches according to the current locale. + * + * @see https://www.postgresql.org/docs/9.2/static/functions-matching.html + * + * @param string|null $prefix + * @param string $column + * @param string|null $not + * @param string $bind + * @param bool $insensitiveSearch + * + * @return string $like_statement + */ + public function _like_statement(string $prefix = null, string $column, string $not = null, string $bind, bool $insensitiveSearch = false): string + { + $op = $insensitiveSearch === true ? 'ILIKE' : 'LIKE'; + + return "{$prefix} {$column} {$not} {$op} :{$bind}:"; } //-------------------------------------------------------------------- diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index fd1ac792fb2a..a60ccaa301ba 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,22 +29,21 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; -use CodeIgniter\DatabaseException; /** * Connection for Postgre */ class Connection extends BaseConnection implements ConnectionInterface { + /** * Database driver * @@ -83,20 +82,26 @@ public function connect($persistent = false) $this->buildDSN(); } - $this->connID = $persistent === true - ? pg_pconnect($this->DSN) : pg_connect($this->DSN); + // Strip pgsql if exists + if (mb_strpos($this->DSN, 'pgsql:') === 0) + { + $this->DSN = mb_substr($this->DSN, 6); + } + + // Convert semicolons to spaces. + $this->DSN = str_replace(';', ' ', $this->DSN); + + $this->connID = $persistent === true ? pg_pconnect($this->DSN) : pg_connect($this->DSN); if ($this->connID !== false) { - if ($persistent === true - && pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD - && pg_ping($this->connID) === false + if ($persistent === true && pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD && pg_ping($this->connID) === false ) { return false; } - empty($this->schema) or $this->simpleQuery("SET search_path TO {$this->schema},public"); + empty($this->schema) || $this->simpleQuery("SET search_path TO {$this->schema},public"); if ($this->setClientEncoding($this->charset) === false) { @@ -125,6 +130,16 @@ public function reconnect() //-------------------------------------------------------------------- + /** + * Close the database connection. + */ + protected function _close() + { + pg_close($this->connID); + } + + //-------------------------------------------------------------------- + /** * Select a specific database table to use. * @@ -151,13 +166,12 @@ public function getVersion() return $this->dataCache['version']; } - if ( ! $this->connID or ($pgVersion = pg_version($this->connID)) === false) + if ( ! $this->connID or ( $pgVersion = pg_version($this->connID)) === false) { - return false; + $this->initialize(); } - return isset($pgVersion['server']) - ? $this->dataCache['version'] = $pgVersion['server'] : false; + return isset($pgVersion['server']) ? $this->dataCache['version'] = $pgVersion['server'] : false; } //-------------------------------------------------------------------- @@ -165,9 +179,9 @@ public function getVersion() /** * Executes the query against the database. * - * @param $sql + * @param string $sql * - * @return mixed + * @return resource */ public function execute($sql) { @@ -198,7 +212,13 @@ public function affectedRows(): int */ public function escape($str) { - if (is_string($str) OR (is_object($str) && method_exists($str, '__toString'))) { + if (! $this->connID) + { + $this->initialize(); + } + + if (is_string($str) || ( is_object($str) && method_exists($str, '__toString'))) + { return pg_escape_literal($this->connID, $str); } elseif (is_bool($str)) @@ -219,6 +239,11 @@ public function escape($str) */ protected function _escapeString(string $str): string { + if (! $this->connID) + { + $this->initialize(); + } + return pg_escape_string($this->connID, $str); } @@ -233,13 +258,13 @@ protected function _escapeString(string $str): string */ protected function _listTables($prefixLimit = false): string { - $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'"; + $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'"; if ($prefixLimit !== false && $this->DBPrefix !== '') { - return $sql.' AND "table_name" LIKE \'' - .$this->escapeLikeString($this->DBPrefix)."%' " - .sprintf($this->likeEscapeStr, $this->likeEscapeChar); + return $sql . ' AND "table_name" LIKE \'' + . $this->escapeLikeString($this->DBPrefix) . "%' " + . sprintf($this->likeEscapeStr, $this->likeEscapeChar); } return $sql; @@ -259,7 +284,7 @@ protected function _listColumns(string $table = ''): string return 'SELECT "column_name" FROM "information_schema"."columns" WHERE LOWER("table_name") = ' - .$this->escape(strtolower($table)); + . $this->escape($this->DBPrefix.strtolower($table)); } //-------------------------------------------------------------------- @@ -270,12 +295,98 @@ protected function _listColumns(string $table = ''): string * @param string $table * @return array */ - public function fieldData(string $table) + public function _fieldData(string $table) { $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" FROM "information_schema"."columns" WHERE LOWER("table_name") = ' - .$this->escape(strtolower($table)); + . $this->escape(strtolower($table)); + + if (($query = $this->query($sql)) === false) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + for ($i = 0, $c = count($query); $i < $c; $i ++ ) + { + $retval[$i] = new \stdClass(); + $retval[$i]->name = $query[$i]->column_name; + $retval[$i]->type = $query[$i]->data_type; + $retval[$i]->default = $query[$i]->column_default; + $retval[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with index data + * + * @param string $table + * @return array + */ + public function _indexData(string $table) + { + $sql = 'SELECT "indexname", "indexdef" + FROM "pg_indexes" + WHERE LOWER("tablename") = ' . $this->escape(strtolower($table)) . ' + AND "schemaname" = ' . $this->escape('public'); + + if (($query = $this->query($sql)) === false) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->name = $row->indexname; + $_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef))); + $obj->fields = array_map(function($v) { + return trim($v); + }, $_fields); + + if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) + { + $obj->type = 'PRIMARY'; + } + else + { + $obj->type = (strpos($row->indexdef, 'CREATE UNIQUE') === 0) ? 'UNIQUE' :'INDEX'; + } + + $retval[] = $obj; + } + + return $retval; + } + + //-------------------------------------------------------------------- + +/** + * Returns an object with Foreign key data + * + * @param string $table + * @return array + */ + public function _foreignKeyData(string $table) + { + $sql = 'SELECT + tc.constraint_name, tc.table_name, kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = '.$this->escape('FOREIGN KEY').' AND tc.table_name = '.$this->escape($table); if (($query = $this->query($sql)) === false) { @@ -284,15 +395,14 @@ public function fieldData(string $table) $query = $query->getResultObject(); $retval = []; - for ($i = 0, $c = count($query); $i < $c; $i++) + foreach ($query as $row) { - $retval[$i] = new \stdClass(); - $retval[$i]->name = $query[$i]->column_name; - $retval[$i]->type = $query[$i]->data_type; - $retval[$i]->default = $query[$i]->column_default; - $retval[$i]->max_length = $query[$i]->character_maximum_length > 0 - ? $query[$i]->character_maximum_length - : $query[$i]->numeric_precision; + $obj = new \stdClass(); + $obj->constraint_name = $row->constraint_name; + $obj->table_name = $row->table_name; + $obj->foreign_table_name = $row->foreign_table_name; + + $retval[] = $obj; } return $retval; @@ -312,8 +422,8 @@ public function fieldData(string $table) public function error() { return [ - 'code' => '', - 'message' => pg_last_error($this->connID) + 'code' => '', + 'message' => pg_last_error($this->connID) ]; } @@ -328,9 +438,9 @@ public function insertID() { $v = pg_version($this->connID); // 'server' key is only available since PostgreSQL 7.4 - $v = isset($v['server']) ? $v['server'] : 0; + $v = $v['server'] ?? 0; - $table = func_num_args() > 0 ? func_get_arg(0) : null; + $table = func_num_args() > 0 ? func_get_arg(0) : null; $column = func_num_args() > 1 ? func_get_arg(1) : null; if ($table === null && $v >= '8.1') @@ -343,7 +453,7 @@ public function insertID() { $sql = "SELECT pg_get_serial_sequence('{$table}', '{$column}') AS seq"; $query = $this->query($sql); - $query = $query->row(); + $query = $query->getRow(); $seq = $query->seq; } else @@ -373,7 +483,7 @@ public function insertID() */ protected function buildDSN() { - $this->DSN === '' or $this->DSN = ''; + $this->DSN === '' || $this->DSN = ''; // If UNIX sockets are used, we shouldn't set a port if (strpos($this->hostname, '/') !== false) @@ -381,7 +491,7 @@ protected function buildDSN() $this->port = ''; } - $this->hostname === '' or $this->DSN = "host={$this->hostname} "; + $this->hostname === '' || $this->DSN = "host={$this->hostname} "; if ( ! empty($this->port) && ctype_digit($this->port)) { @@ -395,10 +505,10 @@ protected function buildDSN() // An empty password is valid! // password must be set to null to ignore it. - $this->password === null or $this->DSN .= "password='{$this->password}' "; + $this->password === null || $this->DSN .= "password='{$this->password}' "; } - $this->database === '' or $this->DSN .= "dbname={$this->database} "; + $this->database === '' || $this->DSN .= "dbname={$this->database} "; // We don't have these options as elements in our standard configuration // array, but they might be set by parse_url() if the configuration was @@ -430,4 +540,40 @@ protected function setClientEncoding($charset) } //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _transBegin(): bool + { + return (bool) pg_query($this->connID, 'BEGIN'); + } + + // -------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _transCommit(): bool + { + return (bool) pg_query($this->connID, 'COMMIT'); + } + + // -------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _transRollback(): bool + { + return (bool) pg_query($this->connID, 'ROLLBACK'); + } + + // -------------------------------------------------------------------- } diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index 24f5005912c5..446aa65f48a7 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -41,21 +41,30 @@ */ class Forge extends \CodeIgniter\Database\Forge { + + /** + * DROP CONSTRAINT statement + * + * @var string + */ + protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; + + /** * UNSIGNED support * * @var array */ protected $_unsigned = [ - 'INT2' => 'INTEGER', - 'SMALLINT' => 'INTEGER', - 'INT' => 'BIGINT', - 'INT4' => 'BIGINT', - 'INTEGER' => 'BIGINT', - 'INT8' => 'NUMERIC', - 'BIGINT' => 'NUMERIC', - 'REAL' => 'DOUBLE PRECISION', - 'FLOAT' => 'DOUBLE PRECISION' + 'INT2' => 'INTEGER', + 'SMALLINT' => 'INTEGER', + 'INT' => 'BIGINT', + 'INT4' => 'BIGINT', + 'INTEGER' => 'BIGINT', + 'INT8' => 'NUMERIC', + 'BIGINT' => 'NUMERIC', + 'REAL' => 'DOUBLE PRECISION', + 'FLOAT' => 'DOUBLE PRECISION' ]; /** @@ -67,6 +76,19 @@ class Forge extends \CodeIgniter\Database\Forge //-------------------------------------------------------------------- + /** + * CREATE TABLE attributes + * + * @param array $attributes Associative array of table attributes + * @return string + */ + protected function _createTableAttributes($attributes) + { + return ''; + } + + //-------------------------------------------------------------------- + /** * ALTER TABLE * @@ -74,7 +96,7 @@ class Forge extends \CodeIgniter\Database\Forge * @param string $table Table name * @param mixed $field Column definition * - * @return string|string[] + * @return string|array */ protected function _alterTable($alter_type, $table, $field) { @@ -83,9 +105,9 @@ protected function _alterTable($alter_type, $table, $field) return parent::_alterTable($alter_type, $table, $field); } - $sql = 'ALTER TABLE '.$this->db->escapeIdentifiers($table); + $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); $sqls = []; - for ($i = 0, $c = count($field); $i < $c; $i++) + for ($i = 0, $c = count($field); $i < $c; $i ++ ) { if ($field[$i]['_literal'] !== false) { @@ -94,39 +116,58 @@ protected function _alterTable($alter_type, $table, $field) if (version_compare($this->db->getVersion(), '8', '>=') && isset($field[$i]['type'])) { - $sqls[] = $sql.' ALTER COLUMN '.$this->db->escapeIdentifiers($field[$i]['name']) - ." TYPE {$field[$i]['type']}{$field[$i]['length']}"; + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) + . " TYPE {$field[$i]['type']}{$field[$i]['length']}"; } if ( ! empty($field[$i]['default'])) { - $sqls[] = $sql.' ALTER COLUMN '.$this->db->escapeIdentifiers($field[$i]['name']) - ." SET DEFAULT {$field[$i]['default']}"; + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) + . " SET DEFAULT {$field[$i]['default']}"; } if (isset($field[$i]['null'])) { - $sqls[] = $sql.' ALTER COLUMN '.$this->db->escapeIdentifiers($field[$i]['name']) - .($field[$i]['null'] === true ? ' DROP' : ' SET'). ' NOT NULL'; + $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) + . ($field[$i]['null'] === true ? ' DROP' : ' SET') . ' NOT NULL'; } if ( ! empty($field[$i]['new_name'])) { - $sqls[] = $sql.' RENAME COLUMN '.$this->db->escapeIdentifiers($field[$i]['name']) - .' TO '.$this->db->escapeIdentifiers($field[$i]['new_name']); + $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) + . ' TO ' . $this->db->escapeIdentifiers($field[$i]['new_name']); } if ( ! empty($field[$i]['comment'])) { - $sqls[] = 'COMMENT ON COLUMN'.$this->db->escapeIdentifiers($table) - .'.'.$this->db->escapeIdentifiers($field[$i]['name']) - ." IS {$field[$i]['comment']}"; + $sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table) + . '.' . $this->db->escapeIdentifiers($field[$i]['name']) + . " IS {$field[$i]['comment']}"; } } return $sqls; } + //-------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _processColumn($field) + { + return $this->db->escapeIdentifiers($field['name']) + . ' ' . $field['type'] . $field['length'] + . $field['default'] + . $field['null'] + . $field['auto_increment'] + . $field['unique']; + } + + //-------------------------------------------------------------------- /** @@ -157,7 +198,7 @@ protected function _attributeType(&$attributes) $attributes['UNSIGNED'] = false; return; case 'DATETIME': - $attributes['TYPE'] = 'TIMESTAMP'; + $attributes['TYPE'] = 'TIMESTAMP'; default: return; } @@ -182,4 +223,30 @@ protected function _attributeAutoIncrement(&$attributes, &$field) } //-------------------------------------------------------------------- + + /** + * Drop Table + * + * Generates a platform-specific DROP TABLE string + * + * @param string $table Table name + * @param bool $if_exists Whether to add an IF EXISTS condition + * @param bool $cascade + * + * @return string + */ + protected function _dropTable($table, $if_exists, $cascade) + { + $sql = parent::_dropTable($table, $if_exists, $cascade); + + if ($cascade === true) + { + $sql .= ' CASCADE'; + } + + return $sql; + } + + //-------------------------------------------------------------------- + } diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php new file mode 100644 index 000000000000..bcb95948fccc --- /dev/null +++ b/system/Database/Postgre/PreparedQuery.php @@ -0,0 +1,151 @@ +name = random_int(1, 10000000000000000); + + $sql = $this->parameterize($sql); + + // Update the query object since the parameters are slightly different + // than what was put in. + $this->query->setQuery($sql); + + if ( ! $this->statement = pg_prepare($this->db->connID, $this->name, $sql)) + { + $this->errorCode = 0; + $this->errorString = pg_last_error($this->db->connID); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @param array $data + * + * @return bool + */ + public function _execute($data) + { + if (is_null($this->statement)) + { + throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + $this->result = pg_execute($this->db->connID, $this->name, $data); + + return (bool) $this->result; + } + + //-------------------------------------------------------------------- + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->result; + } + + //-------------------------------------------------------------------- + + /** + * Replaces the ? placeholders with $1, $2, etc parameters for use + * within the prepared query. + * + * @param string $sql + * + * @return string + */ + public function parameterize(string $sql): string + { + // Track our current value + $count = 0; + + $sql = preg_replace_callback('/\?/', function($matches) use (&$count) { + $count ++; + return "\${$count}"; + }, $sql); + + return $sql; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/Postgre/Result.php b/system/Database/Postgre/Result.php index ad19aa828e83..2f9c8458a3f5 100644 --- a/system/Database/Postgre/Result.php +++ b/system/Database/Postgre/Result.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\ResultInterface; @@ -44,6 +43,7 @@ */ class Result extends BaseResult implements ResultInterface { + /** * Gets the number of fields in the result set. * @@ -64,7 +64,7 @@ public function getFieldCount(): int public function getFieldNames(): array { $fieldNames = []; - for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) + for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i ++ ) { $fieldNames[] = pg_field_name($this->resultID, $i); } @@ -83,12 +83,12 @@ public function getFieldData(): array { $retval = []; - for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) + for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i ++ ) { - $retval[$i] = new \stdClass(); - $retval[$i]->name = pg_field_name($this->resultID, $i); - $retval[$i]->type = pg_field_type($this->resultID, $i); - $retval[$i]->max_length = pg_field_size($this->resultID, $i); + $retval[$i] = new \stdClass(); + $retval[$i]->name = pg_field_name($this->resultID, $i); + $retval[$i]->type = pg_field_type($this->resultID, $i); + $retval[$i]->max_length = pg_field_size($this->resultID, $i); // $retval[$i]->primary_key = (int)($fieldData[$i]->flags & 2); // $retval[$i]->default = $fieldData[$i]->def; } diff --git a/system/Database/Postgre/Utils.php b/system/Database/Postgre/Utils.php index 4a02dd41b73c..61bf40f34b05 100644 --- a/system/Database/Postgre/Utils.php +++ b/system/Database/Postgre/Utils.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -41,6 +41,7 @@ */ class Utils extends \CodeIgniter\Database\BaseUtils { + /** * List databases statement * diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php new file mode 100644 index 000000000000..18f20eff6c54 --- /dev/null +++ b/system/Database/PreparedQueryInterface.php @@ -0,0 +1,99 @@ +db = $db; + $this->db = $db; } - + //-------------------------------------------------------------------- - - + /** * Sets the raw query string to use for this statement. * @@ -134,11 +134,11 @@ public function __construct(&$db) * * @return mixed */ - public function setQuery(string $sql, $binds=null) + public function setQuery(string $sql, $binds = null) { $this->originalQueryString = $sql; - if (! is_null($binds)) + if ( ! is_null($binds)) { $this->binds = $binds; } @@ -148,13 +148,29 @@ public function setQuery(string $sql, $binds=null) //-------------------------------------------------------------------- + /** + * Will store the variables to bind into the query later. + * + * @param array $binds + * + * @return $this + */ + public function setBinds(array $binds) + { + $this->binds = $binds; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Returns the final, processed query string after binding, etal * has been performed. * * @return mixed */ - public function getQuery() + public function getQuery(): string { if (empty($this->finalQueryString)) { @@ -173,8 +189,8 @@ public function getQuery() * for it's start and end values. If no end value is present, will * use the current time to determine total duration. * - * @param int $start - * @param int|null $end + * @param float $start + * @param float $end * * @return mixed */ @@ -230,13 +246,15 @@ public function getDuration(int $decimals = 6) /** * Stores the error description that happened for this query. - * - * @param int $code + * + * @param int $code * @param string $error + * + * @return Query */ public function setError(int $code, string $error) { - $this->errorCode = $code; + $this->errorCode = $code; $this->errorString = $error; return $this; @@ -259,7 +277,7 @@ public function hasError(): bool /** * Returns the error code created while executing this statement. * - * @return string + * @return int */ public function getErrorCode(): int { @@ -287,9 +305,8 @@ public function getErrorMessage(): string */ public function isWriteType(): bool { - return (bool)preg_match( - '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', - $this->originalQueryString); + return (bool) preg_match( + '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', $this->originalQueryString); } //-------------------------------------------------------------------- @@ -306,7 +323,7 @@ public function swapPrefix(string $orig, string $swap) { $sql = empty($this->finalQueryString) ? $this->originalQueryString : $this->finalQueryString; - $this->finalQueryString = preg_replace('/(\W)'.$orig.'(\S+?)/', '\\1'.$swap.'\\2', $sql); + $this->finalQueryString = preg_replace('/(\W)' . $orig . '(\S+?)/', '\\1' . $swap . '\\2', $sql); return $this; } @@ -320,7 +337,7 @@ public function swapPrefix(string $orig, string $swap) */ public function getOriginalQuery() { - return $this->originalQueryString; + return $this->originalQueryString; } //-------------------------------------------------------------------- @@ -332,11 +349,11 @@ protected function compileBinds() { $sql = $this->finalQueryString; - $hasNamedBinds = strpos($sql, ':') !== false; + $hasNamedBinds = strpos($sql, ':') !== false; if (empty($this->binds) || empty($this->bindMarker) || - (strpos($sql, $this->bindMarker) === false && - $hasNamedBinds === false) + (strpos($sql, $this->bindMarker) === false && + $hasNamedBinds === false) ) { return; @@ -344,18 +361,18 @@ protected function compileBinds() if ( ! is_array($this->binds)) { - $binds = [$this->binds]; + $binds = [$this->binds]; $bindCount = 1; } else { - $binds = $this->binds; + $binds = $this->binds; $bindCount = count($binds); } // Reverse the binds so that duplicate named binds // will be processed prior to the original binds. - if (! is_numeric(key(array_slice($binds, 0, 1)))) + if ( ! is_numeric(key(array_slice($binds, 0, 1)))) { $binds = array_reverse($binds); } @@ -378,23 +395,34 @@ protected function compileBinds() //-------------------------------------------------------------------- /** - * Match binfings + * Match bindings * @param string $sql * @param array $binds * @return string */ protected function matchNamedBinds(string $sql, array $binds) { + $replacers = []; + foreach ($binds as $placeholder => $value) { $escapedValue = $this->db->escape($value); - if (is_array($escapedValue)) + + // 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)) { - $escapedValue = '('.implode(',', $escapedValue).')'; + $escapedValue = '(' . implode(',', $escapedValue) . ')'; } - $sql = str_replace(':'.$placeholder, $escapedValue, $sql); + + $replacers[":{$placeholder}:"] = $escapedValue; + +// $sql = preg_replace('|:' . $placeholder . '(?!\w)|', $escapedValue, $sql); } + $sql = strtr($sql, $replacers); + return $sql; } @@ -413,11 +441,7 @@ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, i // Make sure not to replace a chunk inside a string that happens to match the bind marker if ($c = preg_match_all("/'[^']*'/i", $sql, $matches)) { - $c = preg_match_all('/'.preg_quote($this->bindMarker, '/').'/i', - str_replace($matches[0], - str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), - $sql, $c), - $matches, PREG_OFFSET_CAPTURE); + $c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE); // Bind values' count must match the count of markers in the query if ($bindCount !== $c) @@ -426,23 +450,21 @@ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, i } } // Number of binds must match bindMarkers in the string. - else if (($c = preg_match_all('/'.preg_quote($this->bindMarker, '/').'/i', $sql, $matches, - PREG_OFFSET_CAPTURE)) !== $bindCount) + else if (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) { return $sql; } do { - $c--; + $c --; $escapedValue = $this->db->escape($binds[$c]); if (is_array($escapedValue)) { - $escapedValue = '('.implode(',', $escapedValue).')'; + $escapedValue = '(' . implode(',', $escapedValue) . ')'; } $sql = substr_replace($sql, $escapedValue, $matches[0][$c][1], $ml); - } - while ($c !== 0); + } while ($c !== 0); return $sql; } @@ -451,15 +473,13 @@ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, i /** * Return text representation of the query - * - * @return type + * + * @return mixed|string */ public function __toString() { - return $this->getQuery(); + return $this->getQuery(); } //-------------------------------------------------------------------- - - } diff --git a/system/Database/QueryInterface.php b/system/Database/QueryInterface.php index 58d0bb589ff7..cae2a5d42556 100644 --- a/system/Database/QueryInterface.php +++ b/system/Database/QueryInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -46,6 +46,7 @@ */ interface QueryInterface { + /** * Sets the raw query string to use for this statement. * @@ -73,8 +74,8 @@ public function getQuery(); * for it's start and end values. If no end value is present, will * use the current time to determine total duration. * - * @param int $start - * @param int|null $end + * @param float $start + * @param float $end * * @return mixed */ @@ -96,7 +97,7 @@ public function getDuration(int $decimals = 6); /** * Stores the error description that happened for this query. - * + * * @param int $code * @param string $error */ @@ -116,7 +117,7 @@ public function hasError(): bool; /** * Returns the error code created while executing this statement. * - * @return string + * @return int */ public function getErrorCode(): int; @@ -151,5 +152,4 @@ public function isWriteType(): bool; public function swapPrefix(string $orig, string $swap); //-------------------------------------------------------------------- - } diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index 2032251fb20d..7f42e3f06ced 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -41,6 +41,7 @@ */ interface ResultInterface { + /** * Retrieve the results of the query. Typically an array of * individual data rows, which can be either an 'array', an @@ -257,5 +258,4 @@ public function freeResult(); public function dataSeek($n = 0); //-------------------------------------------------------------------- - } diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php new file mode 100644 index 000000000000..1c6f4c2eed07 --- /dev/null +++ b/system/Database/SQLite3/Builder.php @@ -0,0 +1,107 @@ +db->DBDebug) + { + throw new DatabaseException('SQLite3 doesn\'t support persistent connections.'); + } + try + { + return (! $this->password) + ? new \SQLite3($this->database) + : new \SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password); + } catch (\Exception $e) + { + throw new DatabaseException('SQLite3 error: '.$e->getMessage()); + } + } + + //-------------------------------------------------------------------- + + /** + * Keep or establish the connection if no queries have been sent for + * a length of time exceeding the server's idle timeout. + * + * @return mixed + */ + public function reconnect() + { + $this->close(); + $this->initialize(); + } + + //-------------------------------------------------------------------- + + /** + * Close the database connection. + * + * @return void + */ + protected function _close() + { + $this->connID->close(); + } + + //-------------------------------------------------------------------- + + /** + * Select a specific database table to use. + * + * @param string $databaseName + * + * @return mixed + */ + public function setDatabase(string $databaseName) + { + return false; + } + + //-------------------------------------------------------------------- + + /** + * Returns a string containing the version of the database being used. + * + * @return mixed + */ + public function getVersion() + { + if (isset($this->dataCache['version'])) + { + return $this->dataCache['version']; + } + + $version = \SQLite3::version(); + + return $this->dataCache['version'] = $version['versionString']; + } + + //-------------------------------------------------------------------- + + + /** + * Execute the query + * + * @param string $sql + * + * @return mixed \SQLite3Result object or bool + */ + public function execute($sql) + { + return $this->isWriteType($sql) + ? $this->connID->exec($sql) + : $this->connID->query($sql); + } + + //-------------------------------------------------------------------- + + /** + * Returns the total number of rows affected by this query. + * + * @return mixed + */ + public function affectedRows(): int + { + return $this->connID->changes(); + } + + //-------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string $str + * + * @return string + */ + protected function _escapeString(string $str): string + { + return $this->connID->escapeString($str); + } + + //-------------------------------------------------------------------- + + /** + * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param bool $prefixLimit + * + * @return string + */ + protected function _listTables($prefixLimit = false): string + { + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + .(($prefixLimit !== false && $this->DBPrefix != '') + ? ' AND "NAME" LIKE \''.$this->escapeLikeString($this->DBPrefix).'%\' '.sprintf($this->likeEscapeStr, + $this->likeEscapeChar) + : ''); + } + + //-------------------------------------------------------------------- + + /** + * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string $table + * + * @return string + */ + protected function _listColumns(string $table = ''): string + { + return 'PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, true, null, false).')'; + } + + + /** + * Fetch Field Names + * + * @param string $table Table name + * + * @return array|false + * @throws DatabaseException + */ + public function getFieldNames($table) + { + // Is there a cached result? + if (isset($this->dataCache['field_names'][$table])) + { + return $this->dataCache['field_names'][$table]; + } + + if (empty($this->connID)) + { + $this->initialize(); + } + + if (false === ($sql = $this->_listColumns($table))) + { + if ($this->DBDebug) + { + throw new DatabaseException('This feature is not available for the database you are using.'); + } + + return false; + } + + $query = $this->query($sql); + $this->dataCache['field_names'][$table] = []; + + foreach ($query->getResultArray() as $row) + { + // Do we know from where to get the column's name? + if (! isset($key)) + { + if (isset($row['column_name'])) + { + $key = 'column_name'; + } + elseif (isset($row['COLUMN_NAME'])) + { + $key = 'COLUMN_NAME'; + } + elseif (isset($row['name'])) + { + $key = 'name'; + } + else + { + // We have no other choice but to just get the first element's key. + $key = key($row); + } + } + + $this->dataCache['field_names'][$table][] = $row[$key]; + } + + return $this->dataCache['field_names'][$table]; + } + + //-------------------------------------------------------------------- + + + /** + * Returns an object with field data + * + * @param string $table + * + * @return array + */ + public function _fieldData(string $table) + { + + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, true, null, + false).')')) === false) + { + return false; + } + $query = $query->getResultObject(); + if (empty($query)) + { + return false; + } + $retval = []; + for ($i = 0, $c = count($query); $i < $c; $i++) + { + $retval[$i] = new \stdClass(); + $retval[$i]->name = $query[$i]->name; + $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; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with index data + * + * @param string $table + * + * @return array + */ + public function _indexData(string $table) + { + // Get indexes + // Don't use PRAGMA index_list, so we can preserve index order + $sql = "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=".$this->escape(strtolower($table)).""; + if (($query = $this->query($sql)) === false) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->name = $row->name; + + // Get fields for index + $obj->fields = []; + if (($fields = $this->query('PRAGMA index_info('.$this->escape(strtolower($row->name)).')')) === false) + { + return false; + } + $fields = $fields->getResultObject(); + + foreach ($fields as $field) + { + $obj->fields[] = $field->name; + } + + $retval[] = $obj; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns the last error code and message. + * + * Must return an array with keys 'code' and 'message': + * + * return ['code' => null, 'message' => null); + * + * @return array + */ + public function error(): array + { + return ['code' => $this->connID->lastErrorCode(), 'message' => $this->connID->lastErrorMsg()]; + } + + //-------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insertID(): int + { + return $this->connID->lastInsertRowID(); + } + + //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _transBegin(): bool + { + return $this->connID->exec('BEGIN TRANSACTION'); + } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _transCommit(): bool + { + return $this->connID->exec('END TRANSACTION'); + } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _transRollback(): bool + { + return $this->connID->exec('ROLLBACK'); + } + + //-------------------------------------------------------------------- + + /** + * Determines if the statement is a write-type query or not. + * + * @return bool + */ + public function isWriteType($sql): bool + { + return (bool)preg_match( + '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', + $sql); + } + + //-------------------------------------------------------------------- + + /** + * Checks to see if the current install supports Foreign Keys + * and has them enabled. + * + * @return bool + */ + protected function supportsForeignKeys(): bool + { + $result = $this->simpleQuery("PRAGMA foreign_keys"); + + return (bool)$result; + } + + /** + * Returns an object with Foreign key data + * + * @param string $table + * @return array + */ + public function _foreignKeyData(string $table) + { + if ($this->supportsForeignKeys() !== true) + { + return []; + } + + $tables = $this->listTables(); + + if (empty($tables)) + { + return []; + } + + $retval = []; + + foreach ($tables as $table) + { + $query = $this->query("PRAGMA foreign_key_list({$table})")->getResult(); + + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->constraint_name = $row->from.' to '. $row->table.'.'.$row->to; + $obj->table_name = $table; + $obj->foreign_table_name = $row->table; + + $retval[] = $obj; + } + } + + return $retval; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php new file mode 100644 index 000000000000..8de90fc74e3e --- /dev/null +++ b/system/Database/SQLite3/Forge.php @@ -0,0 +1,295 @@ +db->getVersion(), '3.3', '<')) + { + $this->createTableIfStr = false; + $this->dropTableIfStr = false; + } + } + + //-------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * + * @return bool + */ + public function createDatabase($db_name): bool + { + // In SQLite, a database is created when you connect to the database. + // We'll return TRUE so that an error isn't generated. + return true; + } + + //-------------------------------------------------------------------- + + /** + * Drop database + * + * @param string $db_name + * + * @return bool + * @throws \CodeIgniter\DatabaseException + */ + public function dropDatabase($db_name): bool + { + // In SQLite, a database is dropped when we delete a file + if (! file_exists($db_name)) + { + if ($this->db->DBDebug) + { + throw new DatabaseException('Unable to drop the specified database.'); + } + + return false; + } + + // We need to close the pseudo-connection first + $this->db->close(); + if (! @unlink($db_name)) + { + if ($this->db->DBDebug) + { + throw new DatabaseException('Unable to drop the specified database.'); + } + + return false; + } + + if (! empty($this->db->dataCache['db_names'])) + { + $key = array_search(strtolower($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); + if ($key !== false) + { + unset($this->db->dataCache['db_names'][$key]); + } + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * + * @return string|array + */ + protected function _alterTable($alter_type, $table, $field) + { + if (in_array($alter_type, ['DROP', 'CHANGE'], true)) + { + return false; + } + + return parent::_alterTable($alter_type, $table, $field); + } + + //-------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * + * @return string + */ + protected function _processColumn($field) + { + return $this->db->escapeIdentifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + //-------------------------------------------------------------------- + + /** + * Process indexes + * + * @param string $table + * + * @return array + */ + protected function _processIndexes($table) + { + $sqls = []; + + for ($i = 0, $c = count($this->keys); $i < $c; $i++) + { + $this->keys[$i] = (array)$this->keys[$i]; + + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) + { + if (! isset($this->fields[$this->keys[$i][$i2]])) + { + unset($this->keys[$i][$i2]); + } + } + if (count($this->keys[$i]) <= 0) + { + continue; + } + + if (in_array($i, $this->uniqueKeys)) + { + $sqls[] = 'CREATE UNIQUE INDEX '.$this->db->escapeIdentifiers($table.'_'.implode('_', $this->keys[$i])) + .' ON '.$this->db->escapeIdentifiers($table) + .' ('.implode(', ', $this->db->escapeIdentifiers($this->keys[$i])).');'; + continue; + } + + $sqls[] = 'CREATE INDEX '.$this->db->escapeIdentifiers($table.'_'.implode('_', $this->keys[$i])) + .' ON '.$this->db->escapeIdentifiers($table) + .' ('.implode(', ', $this->db->escapeIdentifiers($this->keys[$i])).');'; + } + + return $sqls; + } + + //-------------------------------------------------------------------- + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * + * @return void + */ + protected function _attributeType(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + + return; + default: + return; + } + } + + //-------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * + * @return void + */ + protected function _attributeAutoIncrement(&$attributes, &$field) + { + if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + && stripos($field['type'], 'int') !== false) + { + $field['type'] = 'INTEGER PRIMARY KEY'; + $field['default'] = ''; + $field['null'] = ''; + $field['unique'] = ''; + $field['auto_increment'] = ' AUTOINCREMENT'; + + $this->primaryKeys = []; + } + } + + //-------------------------------------------------------------------- + + /** + * Foreign Key Drop + * + * @param string $table Table name + * @param string $foreign_name Foreign name + * + * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException + */ + public function dropForeignKey($table, $foreign_name) + { + throw new DatabaseException(lang('Database.dropForeignKeyUnsupported')); + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php new file mode 100644 index 000000000000..af33a7d4d0f7 --- /dev/null +++ b/system/Database/SQLite3/PreparedQuery.php @@ -0,0 +1,134 @@ +statement = $this->db->connID->prepare($sql))) + { + $this->errorCode = $this->db->connID->lastErrorCode(); + $this->errorString = $this->db->connID->lastErrorMsg(); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + * + * @todo finalize() + * + * @param array $data + * + * @return bool + */ + public function _execute($data) + { + if (is_null($this->statement)) + { + throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + foreach ($data as $key=>$item) + { + // Determine the type string + if (is_integer($item)) + { + $bindType = SQLITE3_INTEGER; + } + elseif (is_float($item)) + { + $bindType = SQLITE3_FLOAT; + } + else + { + $bindType = SQLITE3_TEXT; + } + + // Bind it + $this->statement->bindValue($key+1, $item, $bindType); + } + + $this->result = $this->statement->execute(); + + return $this->result !== false; + } + + //-------------------------------------------------------------------- + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->result; + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php new file mode 100644 index 000000000000..6afa78b676d4 --- /dev/null +++ b/system/Database/SQLite3/Result.php @@ -0,0 +1,196 @@ +resultID->numColumns(); + } + + //-------------------------------------------------------------------- + + /** + * Generates an array of column names in the result set. + * + * @return array + */ + public function getFieldNames(): array + { + $fieldNames = []; + for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i ++ ) + { + $fieldNames[] = $this->resultID->columnName($i); + } + + return $fieldNames; + } + + //-------------------------------------------------------------------- + + /** + * Generates an array of objects representing field meta-data. + * + * @return array + */ + public function getFieldData(): array + { + static $data_types = [ + SQLITE3_INTEGER => 'integer', + SQLITE3_FLOAT => 'float', + SQLITE3_TEXT => 'text', + SQLITE3_BLOB => 'blob', + SQLITE3_NULL => 'null' + ]; + + $retval = []; + + for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i ++ ) + { + $retval[$i] = new \stdClass(); + $retval[$i]->name = $this->resultID->columnName($i); + $type = $this->resultID->columnType($i); + $retval[$i]->type = isset($data_types[$type]) ? $data_types[$type] : $type; + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Frees the current result. + * + * @return mixed + */ + public function freeResult() + { + if (is_object($this->resultID)) + { + $this->resultID->finalize(); + $this->resultID = false; + } + } + + //-------------------------------------------------------------------- + + /** + * Moves the internal pointer to the desired offset. This is called + * internally before fetching results to make sure the result set + * starts at zero. + * + * @param int $n + * + * @return mixed + * @throws \CodeIgniter\DatabaseException + */ + public function dataSeek($n = 0) + { + if ($n != 0) + { + if ($this->db->DBDebug) { + throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.'); + } + return false; + } + return $this->resultID->reset(); + } + + //-------------------------------------------------------------------- + + /** + * Returns the result set as an array. + * + * Overridden by driver classes. + * + * @return array + */ + protected function fetchAssoc() + { + return $this->resultID->fetchArray(SQLITE3_ASSOC); + } + + //-------------------------------------------------------------------- + + /** + * Returns the result set as an object. + * + * Overridden by child classes. + * + * @param string $className + * + * @return object + */ + protected function fetchObject($className = 'stdClass') + { + // No native support for fetching rows as objects + if (($row = $this->fetchAssoc()) === FALSE) + { + return FALSE; + } + elseif ($className === 'stdClass') + { + return (object) $row; + } + + $classObj = new $className(); + $classSet = \Closure::bind(function ($key, $value) { + $this->$key = $value; + }, $classObj, $className + ); + foreach (array_keys($row) as $key) + { + $classSet($key, $row[$key]); + } + return $classObj; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php new file mode 100644 index 000000000000..9df3b665ca47 --- /dev/null +++ b/system/Database/SQLite3/Utils.php @@ -0,0 +1,68 @@ +seedPath = $config->filesPath ?? APPPATH.'Database/'; + $this->seedPath = $config->filesPath ?? APPPATH . 'Database/'; if (empty($this->seedPath)) { throw new \InvalidArgumentException('Invalid filesPath set in the Config\Database.'); } - $this->seedPath = rtrim($this->seedPath, '/').'/Seeds/'; + $this->seedPath = rtrim($this->seedPath, '/') . '/Seeds/'; - if (! is_dir($this->seedPath)) + if ( ! is_dir($this->seedPath)) { throw new \InvalidArgumentException('Unable to locate the seeds directory. Please check Config\Database::filesPath'); } - $this->config =& $config; + $this->config = & $config; if (is_null($db)) { $db = \Config\Database::connect($this->DBGroup); } - $this->db =& $db; + $this->db = & $db; } //-------------------------------------------------------------------- @@ -121,28 +121,39 @@ public function __construct(BaseConfig $config, BaseConnection $db = null) * * @param string $class * - * @throws RuntimeException + * @throws \InvalidArgumentException */ public function call(string $class) { - if (empty($class)) - { - throw new \InvalidArgumentException('No Seeder was specified.'); - } - - $path = $this->seedPath.str_replace('.php', '', $class).'.php'; - - if (! is_file($path)) + if (empty($class)) { - throw new \InvalidArgumentException('The specified Seeder is not a valid file: '. $path); + throw new \InvalidArgumentException('No Seeder was specified.'); } - if (! class_exists($class, false)) + $path = str_replace('.php', '', $class) . '.php'; + + // If we have namespaced class, simply try to load it. + if (strpos($class, '\\') !== false) { - require $path; + $seeder = new $class($this->config); } + // Otherwise, try to load the class manually. + else + { + $path = $this->seedPath . $path; + + if ( ! is_file($path)) + { + throw new \InvalidArgumentException('The specified Seeder is not a valid file: ' . $path); + } - $seeder = new $class($this->config); + if ( ! class_exists($class, false)) + { + require_once $path; + } + + $seeder = new $class($this->config); + } $seeder->run(); @@ -159,13 +170,13 @@ public function call(string $class) /** * Sets the location of the directory that seed files can be located in. * - * @param sting $path + * @param string $path * - * @return $this + * @return Seeder */ public function setPath(string $path) { - $this->seedPath = rtrim($path, '/').'/'; + $this->seedPath = rtrim($path, '/') . '/'; return $this; } @@ -177,11 +188,11 @@ public function setPath(string $path) * * @param bool $silent * - * @return $this + * @return Seeder */ public function setSilent(bool $silent) { - $this->silent = $silent; + $this->silent = $silent; return $this; } @@ -202,5 +213,4 @@ public function run() } //-------------------------------------------------------------------- - } diff --git a/system/Debug/CustomExceptions.php b/system/Debug/CustomExceptions.php deleted file mode 100644 index acfac4bdb7b6..000000000000 --- a/system/Debug/CustomExceptions.php +++ /dev/null @@ -1,181 +0,0 @@ -ob_level = ob_get_level(); - $this->viewPath = rtrim($config->errorViewPath, '/ ').'/'; + $this->viewPath = rtrim($config->errorViewPath, '/ ') . '/'; + + $this->config = $config; + + $this->request = $request; + $this->response = $response; } //-------------------------------------------------------------------- @@ -102,57 +124,35 @@ public function initialize() */ public function exceptionHandler(\Throwable $exception) { - // Get Exception Info - these are available - // directly in the template that's displayed. - $type = get_class($exception); - $codes = $this->determineCodes($exception); - $code = $codes[0]; - $exit = $codes[1]; - $code = $exception->getCode(); - $message = $exception->getMessage(); - $file = $exception->getFile(); - $line = $exception->getLine(); - $trace = $exception->getTrace(); - $title = get_class($exception); - - if (empty($message)) - { - $message = '(null)'; - } + $codes = $this->determineCodes($exception); + $statusCode = $codes[0]; + $exitCode = $codes[1]; // Log it - - // Fire an Event - $templates_path = $this->viewPath; - if (empty($templates_path)) + if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes)) { - $templates_path = APPPATH.'Views/errors/'; + log_message('critical', $exception->getMessage()."\n{trace}", [ + 'trace' => $exception->getTraceAsString() + ]); } - if (is_cli()) - { - $templates_path .= 'cli/'; - } - else + if (! is_cli()) { - header('HTTP/1.1 500 Internal Server Error', true, 500); - $templates_path .= 'html/'; - } + $this->response->setStatusCode($statusCode); + $header = "HTTP/{$this->request->getProtocolVersion()} {$this->response->getStatusCode()} {$this->response->getReason()}"; + header($header, true, $statusCode); - $view = $this->determineView($exception, $templates_path); + if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) + { + $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send(); - if (ob_get_level() > $this->ob_level + 1) - { - ob_end_flush(); + exit($exitCode); + } } - ob_start(); - include($templates_path.$view); - $buffer = ob_get_contents(); - ob_end_clean(); - echo $buffer; + $this->render($exception, $statusCode); - exit($exit); + exit($exitCode); } //-------------------------------------------------------------------- @@ -177,7 +177,7 @@ public function errorHandler(int $severity, string $message, string $file = null // Convert it to an exception and pass it along. throw new \ErrorException($message, 0, $severity, $file, $line); } - + //-------------------------------------------------------------------- /** @@ -191,7 +191,7 @@ public function shutdownHandler() // If we've got an error that hasn't been displayed, then convert // it to an Exception and use the Exception handler to display it // to the user. - if (! is_null($error)) + if ( ! is_null($error)) { // Fatal Error? if (in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) @@ -216,7 +216,7 @@ protected function determineView(\Throwable $exception, string $template_path): { // Production environments should have a custom exception file. $view = 'production.php'; - $template_path = rtrim($template_path, '/ ').'/'; + $template_path = rtrim($template_path, '/ ') . '/'; if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) { @@ -224,15 +224,15 @@ protected function determineView(\Throwable $exception, string $template_path): } // 404 Errors - if ($exception instanceof \CodeIgniter\PageNotFoundException) + if ($exception instanceof \CodeIgniter\Exceptions\PageNotFoundException) { return 'error_404.php'; } // Allow for custom views based upon the status code - else if (is_file($template_path.'error_'.$exception->getCode().'.php')) + else if (is_file($template_path . 'error_' . $exception->getCode() . '.php')) { - return 'error_'.$exception->getCode().'.php'; + return 'error_' . $exception->getCode() . '.php'; } return $view; @@ -240,6 +240,69 @@ protected function determineView(\Throwable $exception, string $template_path): //-------------------------------------------------------------------- + /** + * Given an exception and status code will display the error to the client. + * + * @param \Throwable $exception + * @param int $statusCode + */ + protected function render(\Throwable $exception, int $statusCode) + { + // Determine directory with views + $path = $this->viewPath; + if (empty($path)) + { + $path = APPPATH . 'Views/errors/'; + } + + $path = is_cli() + ? $path.'cli/' + : $path.'html/'; + + // Determine the vew + $view = $this->determineView($exception, $path); + + // Prepare the vars + $vars = $this->collectVars($exception, $statusCode); + extract($vars); + + // Render it + if (ob_get_level() > $this->ob_level + 1) + { + ob_end_clean(); + } + + ob_start(); + include($path . $view); + $buffer = ob_get_contents(); + ob_end_clean(); + echo $buffer; + } + + //-------------------------------------------------------------------- + + /** + * Gathers the variables that will be made available to the view. + * + * @param \Throwable $exception + * @param int $statusCode + * + * @return array + */ + protected function collectVars(\Throwable $exception, int $statusCode) + { + return [ + 'title' => get_class($exception), + 'type' => get_class($exception), + 'code' => $statusCode, + 'message' => $exception->getMessage() ?? '(null)', + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace(), + ]; + } + + /** * Determines the HTTP status code and the exit status code for this request. * @@ -251,7 +314,7 @@ protected function determineCodes(\Throwable $exception): array { $statusCode = abs($exception->getCode()); - if ($statusCode < 100) + if ($statusCode < 100 || $statusCode > 599) { $exitStatus = $statusCode + EXIT__AUTO_MIN; // 9 is EXIT__AUTO_MIN if ($exitStatus > EXIT__AUTO_MAX) // 125 is EXIT__AUTO_MAX @@ -267,12 +330,11 @@ protected function determineCodes(\Throwable $exception): array return [ $statusCode ?? 500, - $exitStatus + $exitStatus ]; } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // Display Methods //-------------------------------------------------------------------- @@ -290,15 +352,15 @@ public static function cleanPath($file) { if (strpos($file, APPPATH) === 0) { - $file = 'APPPATH/'.substr($file, strlen(APPPATH)); + $file = 'APPPATH/' . substr($file, strlen(APPPATH)); } elseif (strpos($file, BASEPATH) === 0) { - $file = 'BASEPATH/'.substr($file, strlen(BASEPATH)); + $file = 'BASEPATH/' . substr($file, strlen(BASEPATH)); } elseif (strpos($file, FCPATH) === 0) { - $file = 'FCPATH/'.substr($file, strlen(FCPATH)); + $file = 'FCPATH/' . substr($file, strlen(FCPATH)); } return $file; @@ -318,19 +380,18 @@ public static function describeMemory(int $bytes): string { if ($bytes < 1024) { - return $bytes.'B'; + return $bytes . 'B'; } else if ($bytes < 1048576) { - return round($bytes/1024, 2).'KB'; + return round($bytes / 1024, 2) . 'KB'; } - return round($bytes/1048576, 2).'MB'; + return round($bytes / 1048576, 2) . 'MB'; } //-------------------------------------------------------------------- - /** * Creates a syntax-highlighted version of a PHP file. * @@ -342,7 +403,7 @@ public static function describeMemory(int $bytes): string */ public static function highlightFile($file, $lineNumber, $lines = 15) { - if (empty ($file) || ! is_readable($file)) + if (empty($file) || ! is_readable($file)) { return false; } @@ -360,8 +421,7 @@ public static function highlightFile($file, $lineNumber, $lines = 15) try { $source = file_get_contents($file); - } - catch (\Throwable $e) + } catch (\Throwable $e) { return false; } @@ -373,14 +433,14 @@ public static function highlightFile($file, $lineNumber, $lines = 15) $source = explode("\n", str_replace("\r\n", "\n", $source)); // Get just the part to show - $start = $lineNumber - (int)round($lines / 2); + $start = $lineNumber - (int) round($lines / 2); $start = $start < 0 ? 0 : $start; // Get just the lines we need to display, while keeping line numbers... $source = array_splice($source, $start, $lines, true); // Used to format the line number in the source - $format = '% '.strlen($start + $lines).'d'; + $format = '% ' . strlen($start + $lines) . 'd'; $out = ''; // Because the highlighting may have an uneven number @@ -394,26 +454,25 @@ public static function highlightFile($file, $lineNumber, $lines = 15) $spans += substr_count($row, ']+>#', $row, $tags); - $out .= sprintf("{$format} %s\n%s", - $n + $start + 1, - strip_tags($row), - implode('', $tags[0]) + $out .= sprintf("{$format} %s\n%s", $n + $start + 1, strip_tags($row), implode('', $tags[0]) ); } else { - $out .= sprintf(''.$format.' %s', $n + $start +1, $row) ."\n"; + $out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n"; } } - $out .= str_repeat('', $spans); + if ($spans > 0) + { + $out .= str_repeat('', $spans); + } - return '
'.$out.'
'; + return '
' . $out . '
'; } //-------------------------------------------------------------------- - } diff --git a/system/Debug/Iterator.php b/system/Debug/Iterator.php index a5df8c4021cf..d04d5061d6d5 100644 --- a/system/Debug/Iterator.php +++ b/system/Debug/Iterator.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -39,7 +39,6 @@ /** * Iterator for debugging. */ - class Iterator { @@ -91,19 +90,19 @@ public function add($name, \Closure $closure) * * @return string */ - public function run($iterations = 1000, $output=true) + public function run($iterations = 1000, $output = true) { foreach ($this->tests as $name => $test) { // clear memory before start gc_collect_cycles(); - $start = microtime(true); + $start = microtime(true); $start_mem = $max_memory = memory_get_usage(true); - for ($i = 0; $i < $iterations; $i++) + for ($i = 0; $i < $iterations; $i ++ ) { - $result = call_user_func($test); + $result = $test(); $max_memory = max($max_memory, memory_get_usage(true)); @@ -111,9 +110,9 @@ public function run($iterations = 1000, $output=true) } $this->results[$name] = [ - 'time' => microtime(true) - $start, + 'time' => microtime(true) - $start, 'memory' => $max_memory - $start_mem, - 'n' => $iterations, + 'n' => $iterations, ]; } @@ -127,7 +126,7 @@ public function run($iterations = 1000, $output=true) /** * Get results. - * + * * @return string */ public function getReport() @@ -137,6 +136,8 @@ public function getReport() return 'No results to display.'; } + helper('number'); + // Template $tpl = "
name : "#$key", ENT_SUBSTITUTE, 'UTF-8') ?>
- + '.print_r($value, true) ?> @@ -187,7 +190,7 @@
- + @@ -217,7 +220,7 @@ - + @@ -267,7 +270,6 @@ getHeaders(); ?> -

Headers

@@ -280,10 +282,14 @@ $value) : ?> - - - - + + + + + + + +
User AgentgetUserAgent() ?>getUserAgent()->getAgentString() ?>
getHeaderLine($name), 'html') ?>
getName(), 'html') ?>getValueLine(), 'html') ?>
@@ -293,7 +299,7 @@ setStatusCode(http_response_code()); ?>
@@ -321,7 +327,7 @@ $value) : ?>
getHeaderLine($name), 'html') ?>getHeaderLine($name), 'html') ?>
@@ -155,18 +156,20 @@ public function getReport() foreach ($this->results as $name => $result) { + $memory = number_to_size($result['memory'], 4); + $rows .= " - - + + "; } $tpl = str_replace('{rows}', $rows, $tpl); - return $tpl ."
"; + return $tpl . "
"; } //-------------------------------------------------------------------- - + } diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index ce1c71549151..b638ff63cf5c 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -67,12 +67,14 @@ class Timer * * @param string $name The name of this timer. * @param float $time Allows user to provide time. + * + * @return Timer */ public function start(string $name, float $time = null) { $this->timers[strtolower($name)] = [ - 'start' => ! empty($time) ? $time : microtime(true), - 'end' => null, + 'start' => ! empty($time) ? $time : microtime(true), + 'end' => null, ]; return $this; @@ -87,6 +89,8 @@ public function start(string $name, float $time = null) * it will be automatically stopped at that point. * * @param string $name The name of this timer. + * + * @return Timer */ public function stop(string $name) { @@ -107,8 +111,8 @@ public function stop(string $name) /** * Returns the duration of a recorded timer. * - * @param $name The name of the timer. - * @param int $decimals Number of decimal places. + * @param string $name The name of the timer. + * @param int $decimals Number of decimal places. * * @return null|float Returns null if timer exists by that name. * Returns a float representing the number of @@ -116,7 +120,7 @@ public function stop(string $name) */ public function getElapsedTime(string $name, int $decimals = 4) { - $name = strtolower($name); + $name = strtolower($name); if (empty($this->timers[$name])) { @@ -130,7 +134,7 @@ public function getElapsedTime(string $name, int $decimals = 4) $timer['end'] = microtime(true); } - return (float)number_format($timer['end'] - $timer['start'], $decimals); + return (float) number_format($timer['end'] - $timer['start'], $decimals); } //-------------------------------------------------------------------- @@ -153,7 +157,7 @@ public function getTimers(int $decimals = 4) $timer['end'] = microtime(true); } - $timer['duration'] = (float)number_format($timer['end'] - $timer['start'], $decimals); + $timer['duration'] = (float) number_format($timer['end'] - $timer['start'], $decimals); } return $timers; @@ -170,10 +174,8 @@ public function getTimers(int $decimals = 4) */ public function has(string $name) { - return array_key_exists(strtolower($name), $this->timers); + return array_key_exists(strtolower($name), $this->timers); } //-------------------------------------------------------------------- - - } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index f9875f4d1a98..7f2fb1d5ca77 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -1,6 +1,44 @@ toolbarCollectors as $collector) { - if ( ! class_exists($collector)) + if (! class_exists($collector)) { // @todo Log this! continue; @@ -49,70 +90,251 @@ public function __construct(BaseConfig $config) //-------------------------------------------------------------------- /** - * Run - * - * @param type $startTime - * @param type $totalTime - * @param type $startMemory - * @param type $request - * @param type $response - * @return type + * Returns all the data required by Debug Bar + * + * @param float $startTime App start time + * @param float $totalTime + * @param \CodeIgniter\HTTP\RequestInterface $request + * @param \CodeIgniter\HTTP\ResponseInterface $response + * + * @return string JSON encoded data */ - public function run($startTime, $totalTime, $startMemory, $request, $response): string + public function run($startTime, $totalTime, $request, $response): string { - $this->startTime = $startTime; - // Data items used within the view. - $collectors = $this->collectors; + $data['url'] = current_url(); + $data['method'] = $request->getMethod(true); + $data['isAJAX'] = $request->isAJAX(); + $data['startTime'] = $startTime; + $data['totalTime'] = $totalTime*1000; + $data['totalMemory'] = number_format((memory_get_peak_usage())/1024/1024, 3); + $data['segmentDuration'] = $this->roundTo($data['totalTime']/7, 5); + $data['segmentCount'] = (int)ceil($data['totalTime']/$data['segmentDuration']); + $data['CI_VERSION'] = \CodeIgniter\CodeIgniter::CI_VERSION; + $data['collectors'] = []; + + foreach($this->collectors as $collector) + { + $data['collectors'][] = [ + 'title' => $collector->getTitle(), + 'titleSafe' => $collector->getTitle(true), + 'titleDetails' => $collector->getTitleDetails(), + 'display' => $collector->display(), + 'badgeValue' => $collector->getBadgeValue(), + 'isEmpty' => $collector->isEmpty(), + 'hasTabContent' => $collector->hasTabContent(), + 'hasLabel' => $collector->hasLabel(), + 'icon' => $collector->icon(), + 'hasTimelineData' => $collector->hasTimelineData(), + 'timelineData' => $collector->timelineData(), + ]; + } - $totalTime = $totalTime * 1000; - $totalMemory = number_format((memory_get_peak_usage() - $startMemory) / 1048576, 3); - $segmentDuration = $this->roundTo($totalTime / 7, 5); - $segmentCount = (int)ceil($totalTime / $segmentDuration); - $varData = $this->collectVarData(); + foreach ($this->collectVarData() as $heading => $items) + { + $vardata = []; - ob_start(); - include(__DIR__.'/Toolbar/View/toolbar.tpl.php'); - $output = ob_get_contents(); - ob_end_clean(); + if (is_array($items)) + { + foreach ($items as $key => $value) + { + $vardata[esc($key)] = is_string($value) ? esc($value) : print_r($value, true); + } + } - return $output; + $data['vars']['varData'][esc($heading)] = $vardata; + } + + if (! empty($_SESSION)) + { + foreach ($_SESSION as $key => $value) + { + $data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : print_r($value, true); + } + } + + foreach ($request->getGet() as $name => $value) + { + $data['vars']['get'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value); + } + + foreach ($request->getPost() as $name => $value) + { + $data['vars']['post'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value); + } + + foreach ($request->getHeaders() as $header => $value) + { + if (empty($value)) + { + continue; + } + + if (! is_array($value)) + { + $value = [$value]; + } + + foreach ($value as $h) + { + $data['vars']['headers'][esc($h->getName())] = esc($h->getValueLine()); + } + } + + foreach ($request->getCookie() as $name => $value) + { + $data['vars']['cookies'][esc($name)] = esc($value); + } + + $data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP').'/'.$request->getProtocolVersion(); + + $data['vars']['response'] = [ + 'statusCode' => $response->getStatusCode(), + 'reason' => esc($response->getReason()), + 'contentType' => esc($response->getHeaderLine('content-type')), + ]; + + $data['config'] = \CodeIgniter\Debug\Toolbar\Collectors\Config::display(); + + if( $response->CSP !== null ) + { + $response->CSP->addImageSrc( 'data:' ); + } + + return json_encode($data); } //-------------------------------------------------------------------- /** - * Called within the view to display the timeline itself. + * Format output + * + * @param string $data JSON encoded Toolbar data + * @param string $format html, json, xml * - * @param int $segmentCount - * @param int $segmentDuration * @return string */ - protected function renderTimeline(int $segmentCount, int $segmentDuration): string + protected static function format(string $data, string $format = 'html') { - $displayTime = $segmentCount * $segmentDuration; + $data = json_decode($data, true); + + // History must be loaded on the fly + $filenames = glob(WRITEPATH.'debugbar/debugbar_*'); + $total = count($filenames); + rsort($filenames); + + $files = []; + + $current = self::$request->getGet('debugbar_time'); + $app = config(App::class); + + for ($i = 0; $i < $total; $i++) + { + // Oldest files will be deleted + if ($app->toolbarMaxHistory >= 0 && $i+1 > $app->toolbarMaxHistory) + { + unlink($filenames[$i]); + continue; + } + + // Get the contents of this specific history request + ob_start(); + include($filenames[$i]); + $contents = ob_get_contents(); + ob_end_clean(); + + $file = json_decode($contents, true); + + // Debugbar files shown in History Collector + $files[] = [ + 'time' => (int)$time = substr($filenames[$i], -10), + 'datetime' => date('Y-m-d H:i:s', $time), + 'active' => (int)($time == $current), + 'status' => $file['vars']['response']['statusCode'], + 'method' => $file['method'], + 'url' => $file['url'], + 'isAJAX' => $file['isAJAX'] ? 'Yes' : 'No', + 'contentType' => $file['vars']['response']['contentType'], + ]; + } - $rows = $this->collectTimelineData(); + // Set the History here. Class is not necessary + $data['collectors'][] = [ + 'title' => 'History', + 'titleSafe' => 'history', + 'titleDetails' => '', + 'display' => ['files' => $files], + 'badgeValue' => $count = count($files), + 'isEmpty' => ! (bool)$count, + 'hasTabContent' => true, + 'hasLabel' => true, + 'icon' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJySURBVEhL3ZU7aJNhGIVTpV6i4qCIgkIHxcXLErS4FBwUFNwiCKGhuTYJGaIgnRoo4qRu6iCiiIuIXXTTIkIpuqoFwaGgonUQlC5KafU5ycmNP0lTdPLA4fu+8573/a4/f6hXpFKpwUwmc9fDfweKbk+n07fgEv33TLSbtt/hvwNFT1PsG/zdTE0Gp+GFfD6/2fbVIxqNrqPIRbjg4t/hY8aztcngfDabHXbKyiiXy2vcrcPH8oDCry2FKDrA+Ar6L01E/ypyXzXaARjDGGcoeNxSDZXE0dHRA5VRE5LJ5CFy5jzJuOX2wHRHRnjbklZ6isQ3tIctBaAd4vlK3jLtkOVWqABBXd47jGHLmjTmSScttQV5J+SjfcUweFQEbsjAas5aqoCLXutJl7vtQsAzpRowYqkBinyCC8Vicb2lOih8zoldd0F8RD7qTFiqAnGrAy8stUAvi/hbqDM+YzkAFrLPdR5ZqoLXsd+Bh5YCIH7JniVdquUWxOPxDfboHhrI5XJ7HHhiqQXox+APe/Qk64+gGYVCYZs8cMpSFQj9JOoFzVqqo7k4HIvFYpscCoAjOmLffUsNUGRaQUwDlmofUa34ecsdgXdcXo4wbakBgiUFafXJV8A4DJ/2UrxUKm3E95H8RbjLcgOJRGILhnmCP+FBy5XvwN2uIPcy1AJvWgqC4xm2aU4Xb3lF4I+Tpyf8hRe5w3J7YLymSeA8Z3nSclv4WLRyFdfOjzrUFX0klJUEtZtntCNc+F69cz/FiDzEPtjzmcUMOr83kDQEX6pAJxJfpL3OX22n01YN7SZCoQnaSdoZ+Jz+PZihH3wt/xlCoT9M6nEtmRSPCQAAAABJRU5ErkJggg==', + 'hasTimelineData' => false, + 'timelineData' => [], + ]; $output = ''; + switch ($format) + { + case 'html': + $data['styles'] = []; + extract($data); + $parser = Services::parser(BASEPATH . 'Debug/Toolbar/Views/', null,false); + ob_start(); + include(__DIR__.'/Toolbar/Views/toolbar.tpl.php'); + $output = ob_get_contents(); + ob_end_clean(); + break; + case 'json': + $output = json_encode($data); + break; + case 'xml': + $formatter = new XMLFormatter; + $output = $formatter->format($data); + break; + } + + return $output; + } + + //-------------------------------------------------------------------- + + /** + * Called within the view to display the timeline itself. + * + * @param array $collectors + * @param float $startTime + * @param int $segmentCount + * @param int $segmentDuration + * + * @return string + */ + protected static function renderTimeline(array $collectors, $startTime, int $segmentCount, int $segmentDuration, array& $styles ): string + { + $displayTime = $segmentCount*$segmentDuration; + $rows = self::collectTimelineData($collectors); + $output = ''; + $styleCount = 0; + foreach ($rows as $row) { $output .= ""; $output .= ""; $output .= ""; - $output .= ""; - $output .= ""; + $output .= ""; - $output .= ""; + + $styleCount++; } return $output; @@ -125,24 +347,23 @@ protected function renderTimeline(int $segmentCount, int $segmentDuration): stri * * @return array */ - protected function collectTimelineData(): array + protected static function collectTimelineData($collectors): array { $data = []; // Collect it - foreach ($this->collectors as $collector) + foreach ($collectors as $collector) { - if (! $collector->hasTimelineData()) + if (! $collector['hasTimelineData']) { continue; } - $data = array_merge($data, $collector->timelineData()); + $data = array_merge($data, $collector['timelineData']); } // Sort it - return $data; } @@ -176,18 +397,74 @@ protected function collectVarData()// : array /** * Rounds a number to the nearest incremental value. * - * @param $number - * @param int $increments + * @param float $number + * @param int $increments * * @return float */ protected function roundTo($number, $increments = 5) { - $increments = 1 / $increments; + $increments = 1/$increments; - return (ceil($number * $increments) / $increments); + return (ceil($number*$increments)/$increments); } //-------------------------------------------------------------------- + /** + * + */ + public static function eventHandler() + { + self::$request = Services::request(); + + if(ENVIRONMENT == 'testing') + { + return; + } + + // If the request contains '?debugbar then we're + // simply returning the loading script + if (self::$request->getGet('debugbar') !== null) + { + // Let the browser know that we are sending javascript + header('Content-Type: application/javascript'); + + ob_start(); + include(BASEPATH.'Debug/Toolbar/toolbarloader.js.php'); + $output = ob_get_contents(); + @ob_end_clean(); + + exit($output); + } + + // Otherwise, if it includes ?debugbar_time, then + // we should return the entire debugbar. + if (self::$request->getGet('debugbar_time')) + { + helper('security'); + + // Negotiate the content-type to format the output + $format = self::$request->negotiate('media', [ + 'text/html', + 'application/json', + 'application/xml' + ]); + $format = explode('/', $format)[1]; + + $file = sanitize_filename('debugbar_'.self::$request->getGet('debugbar_time')); + $filename = WRITEPATH.'debugbar/'.$file; + + // Show the toolbar + if (file_exists($filename)) + { + $contents = self::format(file_get_contents($filename), $format); + exit($contents); + } + + // File was not written or do not exists + http_response_code(404); + exit(); // Exit here is needed to avoid load the index page + } + } } diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php index caf3b87c314c..c30051dda643 100644 --- a/system/Debug/Toolbar/Collectors/BaseCollector.php +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ @@ -41,6 +41,7 @@ */ class BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -57,6 +58,14 @@ class BaseCollector */ protected $hasTabContent = false; + /** + * Whether this collector needs to display + * a label or not. + * + * @var bool + */ + protected $hasLabel = false; + /** * Whether this collector has data that * should be shown in the Vars tab. @@ -81,14 +90,14 @@ class BaseCollector * @param bool $safe * @return string */ - public function getTitle($safe=false): string + public function getTitle($safe = false): string { if ($safe) { return str_replace(' ', '-', strtolower($this->title)); } - return $this->title; + return $this->title; } //-------------------------------------------------------------------- @@ -100,13 +109,11 @@ public function getTitle($safe=false): string */ public function getTitleDetails(): string { - return ''; + return ''; } //-------------------------------------------------------------------- - - /** * Does this collector need it's own tab? * @@ -114,7 +121,19 @@ public function getTitleDetails(): string */ public function hasTabContent(): bool { - return (bool)$this->hasTabContent; + return (bool) $this->hasTabContent; + } + + //-------------------------------------------------------------------- + + /** + * Does this collector have a label? + * + * @return bool + */ + public function hasLabel(): bool + { + return (bool) $this->hasLabel; } //-------------------------------------------------------------------- @@ -126,21 +145,20 @@ public function hasTabContent(): bool */ public function hasTimelineData(): bool { - return (bool)$this->hasTimeline; + return (bool) $this->hasTimeline; } //-------------------------------------------------------------------- - /** * Grabs the data for the timeline, properly formatted, * or returns an empty array. * - * @return bool + * @return array */ public function timelineData(): array { - if (! $this->hasTimeline) + if ( ! $this->hasTimeline) { return []; } @@ -158,7 +176,7 @@ public function timelineData(): array */ public function hasVarData() { - return (bool)$this->hasVarData; + return (bool) $this->hasVarData; } //-------------------------------------------------------------------- @@ -183,12 +201,11 @@ public function hasVarData() */ public function getVarData() { - return null; + return null; } //-------------------------------------------------------------------- - /** * Child classes should implement this to return the timeline data * formatted for correct usage. @@ -202,24 +219,23 @@ public function getVarData() * 'duration' => 15 // milliseconds * ] * - * @return mixed + * @return array */ - protected function formatTimelineData(): array - { - return []; - } + protected function formatTimelineData(): array + { + return []; + } //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - return ''; + return []; } //-------------------------------------------------------------------- @@ -237,18 +253,53 @@ public function cleanPath($file) { if (strpos($file, APPPATH) === 0) { - $file = 'APPPATH/'.substr($file, strlen(APPPATH)); + $file = 'APPPATH/' . substr($file, strlen(APPPATH)); } elseif (strpos($file, BASEPATH) === 0) { - $file = 'BASEPATH/'.substr($file, strlen(BASEPATH)); + $file = 'BASEPATH/' . substr($file, strlen(BASEPATH)); } elseif (strpos($file, FCPATH) === 0) { - $file = 'FCPATH/'.substr($file, strlen(FCPATH)); + $file = 'FCPATH/' . substr($file, strlen(FCPATH)); } return $file; } + /** + * Gets the "badge" value for the button. + * + * @return null + */ + public function getBadgeValue() + { + return null; + } + + /** + * Does this collector have any data collected? + * + * If not, then the toolbar button won't get shown. + * + * @return bool + */ + public function isEmpty() + { + return false; + } + + /** + * Returns the HTML to display the icon. Should either + * be SVG, or a base-64 encoded. + * + * Recommended dimensions are 24px x 24px + * + * @return string + */ + public function icon(): string + { + return ''; + } + } diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php new file mode 100644 index 000000000000..cd23cc747016 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -0,0 +1,25 @@ + CodeIgniter::CI_VERSION, + 'phpVersion' => phpversion(), + 'phpSAPI' => php_sapi_name(), + 'environment' => ENVIRONMENT, + 'baseURL' => $config->baseURL, + 'timezone' => app_timezone(), + 'locale' => Services::request()->getLocale(), + 'cspEnabled' => $config->CSPEnabled, + 'salt' => $config->salt, + ]; + } +} diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 4b49195c6161..84097fae519f 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,20 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ - -use CodeIgniter\Services; +use CodeIgniter\Database\Query; /** * Collector for the Database tab of the Debug Toolbar. */ class Database extends BaseCollector { + /** * Whether this collector has timeline data. * @@ -78,6 +78,13 @@ class Database extends BaseCollector */ protected $connections; + /** + * The query instances that have been collected + * through the DBQuery Event. + * + * @var array + */ + protected static $queries = []; //-------------------------------------------------------------------- @@ -91,6 +98,21 @@ public function __construct() //-------------------------------------------------------------------- + /** + * The static method used during Events to collect + * data. + * + * @param \CodeIgniter\Database\Query $query + * + * @internal param $ array \CodeIgniter\Database\Query + */ + public static function collect(Query $query) + { + static::$queries[] = $query; + } + + //-------------------------------------------------------------------- + /** * Returns timeline data formatted for the toolbar. * @@ -104,23 +126,21 @@ protected function formatTimelineData(): array { // Connection Time $data[] = [ - 'name' => 'Connecting to Database: "'.$alias.'"', - 'component' => 'Database', - 'start' => $connection->getConnectStart(), - 'duration' => $connection->getConnectDuration() + 'name' => 'Connecting to Database: "' . $alias . '"', + 'component' => 'Database', + 'start' => $connection->getConnectStart(), + 'duration' => $connection->getConnectDuration() ]; + } - $queries = $connection->getQueries(); - - foreach ($queries as $query) - { - $data[] = [ - 'name' => 'Query', - 'component' => 'Database', - 'start' => $query->getStartTime(true), - 'duration' => $query->getDuration() - ]; - } + foreach (static::$queries as $query) + { + $data[] = [ + 'name' => 'Query', + 'component' => 'Database', + 'start' => $query->getStartTime(true), + 'duration' => $query->getDuration() + ]; } return $data; @@ -129,63 +149,50 @@ protected function formatTimelineData(): array //-------------------------------------------------------------------- /** - * Returns the HTML to fill the Database tab in the toolbar. + * Returns the data of this collector to be formatted in the toolbar * - * @return string The data formatted for the toolbar. + * @return array */ - public function display(): string + public function display(): array { - $output = ''; - // Key words we want bolded $highlight = ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'AND', 'LEFT JOIN', 'ORDER BY', 'GROUP BY', - 'LIMIT', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'OR ', 'HAVING', 'OFFSET', 'NOT IN', - 'IN', 'LIKE', 'NOT LIKE', 'COUNT', 'MAX', 'MIN', 'ON', 'AS', 'AVG', 'SUM', '(', ')' + 'LIMIT', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'OR ', 'HAVING', 'OFFSET', 'NOT IN', + 'IN', 'LIKE', 'NOT LIKE', 'COUNT', 'MAX', 'MIN', 'ON', 'AS', 'AVG', 'SUM', '(', ')' ]; - $connectionCount = count($this->connections); + $data = [ + 'queries' => [] + ]; - foreach ($this->connections as $alias => $connection) + foreach (static::$queries as $query) { - if ($connectionCount > 1) - { - $output .= '

'.$alias.': '.$connection->getPlatform().' '.$connection->getVersion(). - '

'; - } - - $output .= '
{$name}".number_format($result['time'], 4)."{$result['memory']}" . number_format($result['time'], 4) . "{$memory}
{$row['name']}{$row['component']}".number_format($row['duration'] * 1000, 2)." ms"; + $output .= "".number_format($row['duration']*1000, 2)." ms"; - $offset = ((($row['start'] - $this->startTime) * 1000) / - $displayTime) * 100; - $length = (($row['duration'] * 1000) / $displayTime) * 100; - - $output .= ""; + $offset = ((($row['start']-$startTime)*1000)/$displayTime)*100; + $length = (($row['duration']*1000)/$displayTime)*100; + $styles['debug-bar-timeline-'.$styleCount] = "left: {$offset}%; width: {$length}%;"; + $output .= ""; $output .= "
'; - - $output .= ''; - $output .= ''; - $output .= ''; - $output .= ''; - - $output .= ''; - - $queries = $connection->getQueries(); + $sql = $query->getQuery(); - foreach ($queries as $query) + foreach ($highlight as $term) { - $output .= ''; - $output .=''; - - $sql = $query->getQuery(); - - foreach ($highlight as $term) - { - $sql = str_replace($term, "{$term}", $sql); - } - - $output .= ''; - $output .= ''; + $sql = str_replace($term, "{$term}", $sql); } - $output .= ''; - - $output .= '
TimeQuery String
'.($query->getDuration(5) * 1000).' ms'.$sql.'
'; + $data['queries'][] = [ + 'duration' => ($query->getDuration(5) * 1000) .' ms', + 'sql' => $sql + ]; } - return $output; + return $data; + } + + //-------------------------------------------------------------------- + + /** + * Gets the "badge" value for the button. + * + * @return int + */ + public function getBadgeValue() + { + return count(static::$queries); } //-------------------------------------------------------------------- @@ -197,17 +204,35 @@ public function display(): string */ public function getTitleDetails(): string { - $queryCount = 0; + return '(' . count(static::$queries) . ' Queries across ' . ($countConnection = count($this->connections)) . ' Connection' . + ($countConnection > 1 ? 's' : '') . ')'; + } - foreach ($this->connections as $connection) - { - $queryCount += $connection->getQueryCount(); - } + //-------------------------------------------------------------------- - return '('.$queryCount.' Queries across '.count($this->connections).' Connection'. - (count($this->connections) > 1 ? 's' : '').')'; + /** + * Does this collector have any data collected? + * + * @return bool + */ + public function isEmpty() + { + return empty(static::$queries); } //-------------------------------------------------------------------- + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADMSURBVEhLY6A3YExLSwsA4nIycQDIDIhRWEBqamo/UNF/SjDQjF6ocZgAKPkRiFeEhoYyQ4WIBiA9QAuWAPEHqBAmgLqgHcolGQD1V4DMgHIxwbCxYD+QBqcKINseKo6eWrBioPrtQBq/BcgY5ht0cUIYbBg2AJKkRxCNWkDQgtFUNJwtABr+F6igE8olGQD114HMgHIxAVDyAhA/AlpSA8RYUwoeXAPVex5qHCbIyMgwBCkAuQJIY00huDBUz/mUlBQDqHGjgBjAwAAACexpph6oHSQAAAAASUVORK5CYII='; + + } + } diff --git a/system/Debug/Toolbar/Collectors/Events.php b/system/Debug/Toolbar/Collectors/Events.php new file mode 100644 index 000000000000..e4125c39e8b9 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Events.php @@ -0,0 +1,181 @@ +viewer = Services::renderer(null, true); + } + + //-------------------------------------------------------------------- + + /** + * Child classes should implement this to return the timeline data + * formatted for correct usage. + * + * @return mixed + */ + protected function formatTimelineData(): array + { + $data = []; + + $rows = $this->viewer->getPerformanceData(); + + foreach ($rows as $name => $info) + { + $data[] = [ + 'name' => 'View: ' . $info['view'], + 'component' => 'Views', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'] + ]; + } + + return $data; + } + + //-------------------------------------------------------------------- + + /** + * Returns the data of this collector to be formatted in the toolbar + * + * @return array + */ + public function display(): array + { + $data = [ + 'events' => [] + ]; + + foreach (\CodeIgniter\Events\Events::getPerformanceLogs() as $row) + { + $key = $row['event']; + + if (! array_key_exists($key, $data['events'])) + { + $data['events'][$key] = [ + 'event' => $key, + 'duration' => number_format(($row['end']-$row['start']) * 1000, 2), + 'count' => 1, + ]; + + continue; + } + + $data['events'][$key]['duration'] += number_format(($row['end']-$row['start']) * 1000, 2); + $data['events'][$key]['count']++; + } + + return $data; + } + + //-------------------------------------------------------------------- + + /** + * Gets the "badge" value for the button. + */ + public function getBadgeValue() + { + return count(\CodeIgniter\Events\Events::getPerformanceLogs()); + } + + //-------------------------------------------------------------------- + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEASURBVEhL7ZXNDcIwDIVTsRBH1uDQDdquUA6IM1xgCA6MwJUN2hk6AQzAz0vl0ETUxC5VT3zSU5w81/mRMGZysixbFEVR0jSKNt8geQU9aRpFmp/keX6AbjZ5oB74vsaN5lSzA4tLSjpBFxsjeSuRy4d2mDdQTWU7YLbXTNN05mKyovj5KL6B7q3hoy3KwdZxBlT+Ipz+jPHrBqOIynZgcZonoukb/0ckiTHqNvDXtXEAaygRbaB9FvUTjRUHsIYS0QaSp+Dw6wT4hiTmYHOcYZsdLQ2CbXa4ftuuYR4x9vYZgdb4vsFYUdmABMYeukK9/SUme3KMFQ77+Yfzh8eYF8+orDuDWU5LAAAAAElFTkSuQmCC'; + + } +} diff --git a/system/Debug/Toolbar/Collectors/Files.php b/system/Debug/Toolbar/Collectors/Files.php index 59bc97dcca82..3e565a5c6d1d 100644 --- a/system/Debug/Toolbar/Collectors/Files.php +++ b/system/Debug/Toolbar/Collectors/Files.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ @@ -41,6 +41,7 @@ */ class Files extends BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -74,49 +75,75 @@ class Files extends BaseCollector */ public function getTitleDetails(): string { - return '( '.(int)count(get_included_files()).' )'; + return '( ' . (int) count(get_included_files()) . ' )'; } //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string - { - $output = ""; - - $files = get_included_files(); - - $count = 0; + public function display(): array + { + $rawFiles = get_included_files(); + $coreFiles = []; + $userFiles = []; - foreach ($files as $file) + foreach ($rawFiles as $file) { - ++$count; - $path = $this->cleanPath($file); if (strpos($path, 'BASEPATH') !== false) { - $output .= ""; + $coreFiles[] = [ + 'name' => basename($file), + 'path' => $path + ]; } else { - $output .= ""; + $userFiles[] = [ + 'name' => basename($file), + 'path' => $path + ]; } - - $output .= ""; - $output .= ""; - $output .= ""; } - $output .= "
". htmlspecialchars(str_replace('.php', '', basename($file)), ENT_SUBSTITUTE, 'UTF-8')."".htmlspecialchars($path, ENT_SUBSTITUTE, 'UTF-8')."
"; + sort($userFiles); + sort($coreFiles); - return $output; - } + return [ + 'coreFiles' => $coreFiles, + 'userFiles' => $userFiles, + ]; + } //-------------------------------------------------------------------- + + /** + * Displays the number of included files as a badge in the tab button. + * + * @return int + */ + public function getBadgeValue() + { + return count(get_included_files()); + } + + //-------------------------------------------------------------------- + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC'; + + } } diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php index b480edae22e0..bd15bfef4d3a 100644 --- a/system/Debug/Toolbar/Collectors/Logs.php +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,20 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ - -use CodeIgniter\Services; +use CodeIgniter\Config\Services; /** * Loags collector */ class Logs extends BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -67,38 +67,73 @@ class Logs extends BaseCollector */ protected $title = 'Logs'; + /** + * Our collected data. + * + * @var array + */ + protected $data; + //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar + * Returns the data of this collector to be formatted in the toolbar * - * @return string + * @return array */ - public function display(): string + public function display(): array { - $logger = Services::logger(true); - $logs = $logger->logCache; + $logs = $this->collectLogs(); if (empty($logs) || ! is_array($logs)) { - return '

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

'; + $logs = []; } - $output = ""; + return [ + 'logs' => $logs + ]; + } - foreach ($logs as $log) - { - $output .= ""; - $output .= ""; - $output .= ""; - $output .= ""; - } + //-------------------------------------------------------------------- + + /** + * Does this collector actually have any data to display? + */ + public function isEmpty() + { + $this->collectLogs(); + + return empty($this->data); + } + + //-------------------------------------------------------------------- + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACYSURBVEhLYxgFJIHU1FSjtLS0i0D8AYj7gEKMEBkqAaAFF4D4ERCvAFrwH4gDoFIMKSkpFkB+OTEYqgUTACXfA/GqjIwMQyD9H2hRHlQKJFcBEiMGQ7VgAqCBvUgK32dmZspCpagGGNPT0/1BLqeF4bQHQJePpiIwhmrBBEADR1MRfgB0+WgqAmOoFkwANHA0FY0CUgEDAwCQ0PUpNB3kqwAAAABJRU5ErkJggg=='; - return $output."
SeverityMessage
{$log['level']}".htmlspecialchars($log['msg'], ENT_SUBSTITUTE, 'UTF-8')."
"; } //-------------------------------------------------------------------- + /** + * Ensures the data has been collected. + */ + protected function collectLogs() + { + if (! is_null($this->data)) return; + + $logger = Services::logger(true); + $this->data = $logger->logCache; + } + //-------------------------------------------------------------------- } diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 47cbf7837f95..3bb25fd2857c 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,20 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ - -use CodeIgniter\Services; +use CodeIgniter\Config\Services; /** * Routes collector */ class Routes extends BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -70,52 +70,91 @@ class Routes extends BaseCollector //-------------------------------------------------------------------- /** - * Builds and returns the HTML needed to fill a tab to display - * within the Debug Bar - * - * @return string - */ - public function display(): string + * Returns the data of this collector to be formatted in the toolbar + * + * @return array + */ + public function display(): array { - $routes = Services::routes(true); + $rawRoutes = Services::routes(true); $router = Services::router(null, true); - $output = "

Matched Route

"; + /* + * Matched Route + */ + $route = $router->getMatchedRoute(); - $output .= ""; + // Get our parameters + $method = is_callable($router->controllerName()) ? new \ReflectionFunction($router->controllerName()) : new \ReflectionMethod($router->controllerName(), $router->methodName()); + $rawParams = $method->getParameters(); - if ($match = $router->getMatchedRoute()) + $params = []; + foreach ($rawParams as $key => $param) { - $output .= ""; - $output .= ""; + $params[] = [ + 'name' => $param->getName(), + 'value' => $router->params()[$key] ?? + "<empty> | default: " . var_export($param->getDefaultValue(), true) + ]; } + $matchedRoute = [ + [ + 'directory' => $router->directory(), + 'controller' => $router->controllerName(), + 'method' => $router->methodName(), + 'paramCount' => count($router->params()), + 'truePCount' => count($params), + 'params' => $params ?? [] + ] + ]; + + /* + * Defined Routes + */ + $rawRoutes = $rawRoutes->getRoutes(); + $routes = []; + + foreach ($rawRoutes as $from => $to) + { + $routes[] = [ + 'from' => $from, + 'to' => $to + ]; + } - $output .= ""; - $output .= ""; - $output .= ""; - $output .= ""; - - $output .= "
{$match[0]}{$match[1]}
Directory:".htmlspecialchars($router->directory())."
Controller:".htmlspecialchars($router->controllerName())."
Method:".htmlspecialchars($router->methodName())."
Params:".print_r($router->params(), true)."
"; + return [ + 'matchedRoute' => $matchedRoute, + 'routes' => $routes + ]; + } - $output .= "

Defined Routes

"; + //-------------------------------------------------------------------- - $output .= ""; + /** + * Returns a count of all the routes in the system. + * + * @return int + */ + public function getBadgeValue() + { + $rawRoutes = Services::routes(true); - $routes = $routes->getRoutes(); + return count($rawRoutes->getRoutes()); + } - foreach ($routes as $from => $to) - { - $output .= ""; - $output .= ""; - $output .= ""; - $output .= ""; - } + //-------------------------------------------------------------------- - $output .= "
".htmlspecialchars($from)."".htmlspecialchars($to)."
"; + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg=='; - return $output; } - - //-------------------------------------------------------------------- } diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php index 2c4e7fc55cf5..6e69ee74c677 100644 --- a/system/Debug/Toolbar/Collectors/Timers.php +++ b/system/Debug/Toolbar/Collectors/Timers.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,20 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ - -use CodeIgniter\Services; +use CodeIgniter\Config\Services; /** * Timers collector */ class Timers extends BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -84,19 +84,18 @@ protected function formatTimelineData(): array foreach ($rows as $name => $info) { - if ($name == 'total_execution') continue; + if ($name == 'total_execution') + continue; $data[] = [ - 'name' => ucwords(str_replace('_', ' ', $name)), - 'component' => 'Timer', - 'start' => $info['start'], - 'duration' => $info['end'] - $info['start'] + 'name' => ucwords(str_replace('_', ' ', $name)), + 'component' => 'Timer', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'] ]; } return $data; } - //-------------------------------------------------------------------- - } diff --git a/system/Debug/Toolbar/Collectors/Views.php b/system/Debug/Toolbar/Collectors/Views.php index 6f4734b8df8a..711da495c530 100644 --- a/system/Debug/Toolbar/Collectors/Views.php +++ b/system/Debug/Toolbar/Collectors/Views.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,21 +29,21 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com * @since Version 4.0.0 * @filesource */ - -use CodeIgniter\Services; -use CodeIgniter\View\RenderableInterface; +use CodeIgniter\Config\Services; +use CodeIgniter\View\RendererInterface; /** * Views collector */ class Views extends BaseCollector { + /** * Whether this collector has data that can * be displayed in the Timeline. @@ -60,6 +60,14 @@ class Views extends BaseCollector */ protected $hasTabContent = false; + /** + * Whether this collector needs to display + * a label or not. + * + * @var bool + */ + protected $hasLabel = true; + /** * Whether this collector has data that * should be shown in the Vars tab. @@ -78,10 +86,17 @@ class Views extends BaseCollector /** * Instance of the Renderer service - * @var RenderableInterface + * @var RendererInterface */ protected $viewer; + /** + * Views counter + * + * @var array + */ + protected $views = []; + //-------------------------------------------------------------------- /** @@ -89,12 +104,11 @@ class Views extends BaseCollector */ public function __construct() { - $this->viewer = Services::renderer(null, true); + $this->viewer = Services::renderer(null, true); } //-------------------------------------------------------------------- - /** * Child classes should implement this to return the timeline data * formatted for correct usage. @@ -110,10 +124,10 @@ protected function formatTimelineData(): array foreach ($rows as $name => $info) { $data[] = [ - 'name' => 'View: '.$info['view'], - 'component' => 'Views', - 'start' => $info['start'], - 'duration' => $info['end'] - $info['start'] + 'name' => 'View: ' . $info['view'], + 'component' => 'Views', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'] ]; } @@ -149,5 +163,26 @@ public function getVarData() //-------------------------------------------------------------------- + /** + * Returns a count of all views. + * + * @return int + */ + public function getBadgeValue() + { + return count($this->viewer->getPerformanceData()); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + * + * @return string + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII='; + } } diff --git a/system/Debug/Toolbar/View/toolbar.css b/system/Debug/Toolbar/View/toolbar.css deleted file mode 100644 index d6f018e09b60..000000000000 --- a/system/Debug/Toolbar/View/toolbar.css +++ /dev/null @@ -1,190 +0,0 @@ -body { - /* - Make room for the debug bar. - This height should be good. - Tried JS but wasn't having any luck - setting it to the true height. - Don't think this will interfere with - the vast majority of sites out there - but there is a possibility, so we'll - have to keep an eye out. - */ - padding-top: 30pt; -} -#debug-bar { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; - font-size: 12pt; - line-height: 1.5; - background: #fff; - border-bottom: 1px solid #ddd; - margin: 0; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 10000; - box-shadow: 0 3px 10px rgba(0,0,0,0.1); - overflow: hidden; - overflow-y: auto; - max-height: 62%; -} -#debug-bar h1, -#debug-bar h2, -#debug-bar h3{ - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; - color: #666; -} -#debug-bar p { - font-size: 10pt; - margin: 0 0 10pt 20pt; - padding: 0; -} -#debug-bar a { - text-decoration: none; -} -#debug-bar a:hover { - text-decoration: underline; - text-decoration-color: #4e4a4a !important; -} -#debug-bar .muted, -#debug-bar .muted td { - color: #bbb !important; -} -#debug-bar .toolbar { - display: block; - position: relative; - overflow: hidden; - overflow-y: auto; - white-space: nowrap; - border-bottom: 1px solid #eee; - padding: 3px 20px; - text-align: right; -} -#debug-bar h1 { - font-size: 13pt; - font-weight: 300; - margin: 0 2em 0 0; - padding: 0; - text-align: left; - display: inline-block; - float: left; -} -#debug-bar h2 { - font-size: 1.1rem; - font-weight: 200; - margin: 0; - padding: 0; -} -#debug-bar h2 span { - font-size: 80%; -} -#debug-bar h3 { - text-transform: uppercase; - font-size: 0.8rem; - font-weight: 200; - margin-left: 10pt; -} -#debug-bar span { - display: inline-block; - font-size: 10pt; - padding: .3em 1em .3em; - line-height: 1.0; - vertical-align: baseline; -} -#debug-bar table strong { - font-weight: 500; - color: rgba(0,0,0,0.3); -} -#debug-bar .ci-label { - border-radius: 0.25em; - text-shadow: none; - background-color: #eee; - border: 1px solid #ddd; - margin-left: 0.4em; -} -#debug-bar .ci-label a { - display: block; - width: 100%; - height: 100%; - color: inherit; - text-decoration: none; -} -#debug-bar .ci-label a:hover { - text-decoration: underline; -} -#debug-bar .ci-label.active { - font-weight: 400; - background-color: #ccc; - border-color: #bbb; - padding: .3em 0.9em .3em; -} -#debug-bar .tab { - display: none; - background: inherit; - padding: 1em 2em; -} - -#debug-bar table { - margin: 0 0 10pt 20pt; - font-size: 10pt; - border-collapse:collapse; - width: 100%; -} -#debug-bar td, -#debug-bar th { - display: table-cell; - text-align: left; -} -#debug-bar tr { - border: none; -} -#debug-bar td { - border: none; - padding: 2pt 10pt 2pt 5pt; - margin: 0; - -} -#debug-bar th { - padding-bottom: 0.7em; -} -#debug-bar tr td:first-child { - width: 20%; -} -#debug-bar tr td:first-child.narrow { - width: 7em; -} -#debug-bar tr:hover { - background-color: #f3f3f3; -} -#debug-bar table.timeline { - width: 100%; - margin-left: 0; -} -#debug-bar table.timeline th { - font-size: 0.7em; - font-weight: 200; - text-align: left; - padding-bottom: 1em; -} -#debug-bar table.timeline td, -#debug-bar table.timeline th { - border-left: 1px solid #ddd; - padding: 0 1em; - position: relative; -} -#debug-bar table.timeline tr td:first-child, -#debug-bar table.timeline tr th:first-child { - border-left: 0; - padding-left: 0; -} -#debug-bar table.timeline td { - padding: 5px !important; -} -#debug-bar table.timeline .timer { - position: absolute; - display: inline-block; - padding: 3px; - bottom: 9px; - border-radius: 3px; - background-color: #999; -} \ No newline at end of file diff --git a/system/Debug/Toolbar/View/toolbar.js b/system/Debug/Toolbar/View/toolbar.js deleted file mode 100644 index 32c373f46ed5..000000000000 --- a/system/Debug/Toolbar/View/toolbar.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Functionality for the CodeIgniter Debug Toolbar. - */ - -var ciDebugBar = { - - toolbar : null, - - //-------------------------------------------------------------------- - - init : function() - { - this.toolbar = document.getElementById('debug-bar'); - - // Pad the body to make room for the toolbar. - //document.getElementsByTagName("html")[0].style.paddingTop = this.toolbar.offsetHeight+"px !important"; - - ciDebugBar.createListeners(); - }, - - //-------------------------------------------------------------------- - - createListeners : function() - { - var buttons = [].slice.call(document.querySelectorAll('#debug-bar .ci-label a')); - - for (var i=0; i < buttons.length; i++) - { - buttons[i].addEventListener('click', ciDebugBar.showTab, true); - } - }, - - //-------------------------------------------------------------------- - - showTab: function() - { - // Get the target tab, if any - var tab = this.getAttribute('data-tab'); - - // Check our current state. - var state = document.getElementById(tab).style.display; - - if (tab == undefined) return true; - - // Hide all tabs - var tabs = document.querySelectorAll('#debug-bar .tab'); - - for (var i=0; i < tabs.length; i++) - { - tabs[i].style.display = 'none'; - } - - // Mark all labels as inactive - var labels = document.querySelectorAll('#debug-bar .ci-label'); - - for (var i=0; i < labels.length; i++) - { - ciDebugBar.removeClass(labels[i], 'active'); - } - - // Show/hide the selected tab - if (state != 'block') - { - document.getElementById(tab).style.display = 'block'; - ciDebugBar.addClass(this.parentNode, 'active'); - } - }, - - //-------------------------------------------------------------------- - - addClass : function(el, className) - { - if (el.classList) - { - el.classList.add(className); - } - else - { - el.className += ' ' + className; - } - }, - - //-------------------------------------------------------------------- - - removeClass : function(el, className) - { - if (el.classList) - { - el.classList.remove(className); - } - else - { - el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); - } - - }, - - //-------------------------------------------------------------------- - - /** - * Toggle display of a data table - * @param obj - */ - toggleDataTable : function(obj) - { - if (typeof obj == 'string') - { - obj = document.getElementById(obj + '_table'); - } - - if (obj) - { - obj.style.display = obj.style.display == 'none' ? 'block' : 'none'; - } - } - - //-------------------------------------------------------------------- - -}; diff --git a/system/Debug/Toolbar/View/toolbar.tpl.php b/system/Debug/Toolbar/View/toolbar.tpl.php deleted file mode 100644 index 46a4d786e2c7..000000000000 --- a/system/Debug/Toolbar/View/toolbar.tpl.php +++ /dev/null @@ -1,222 +0,0 @@ - - - - -
-
-

Debug Bar

- - ms - MB - Timeline - collectors as $c) : ?> - hasTabContent()) : ?> - getTitle()) ?> - - - Vars -
- - -
- - - - - - - - - - - - - renderTimeline($segmentCount, $segmentDuration, $totalTime) ?> - -
NAMECOMPONENTDURATION ms
-
- - - collectors as $c) : ?> - hasTabContent()) : ?> -
-

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

- - display() ?> -
- - - - -
- - - $items) : ?> - - -

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

No data to display.

- - - - - -

Session User Data

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

No data to display.

- - -

Session doesn't seem to be active.

- - -

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

- - getGet()) : ?> - -

$_GET

-
- - - - $value) : ?> - - - - - - -
- - - getPost()) : ?> - -

$_POST

-
- - - - $value) : ?> - - - - - - -
- - - getHeaders()) : ?> - -

Headers

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

Cookies

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

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

- - getHeaders()) : ?> - -

Headers

-
- - - - $value) : ?> - - - - - - -
getHeaderLine($header)) ?>
- -
- - - diff --git a/system/Debug/Toolbar/Views/_config.tpl.php b/system/Debug/Toolbar/Views/_config.tpl.php new file mode 100644 index 000000000000..ae4baed2ec06 --- /dev/null +++ b/system/Debug/Toolbar/Views/_config.tpl.php @@ -0,0 +1,60 @@ +

+ Read the CodeIgniter docs... +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeIgniter Version:{ ciVersion }
PHP Version:{ phpVersion }
PHP SAPI:{ phpSAPI }
Environment:{ environment }
Base URL: + { if $baseURL == '' } +
+ The $baseURL should always be set manually to prevent possible URL personification from external parties. +
+ { else } + { baseURL } + { endif } +
TimeZone:{ timezone }
Locale:{ locale }
Content Security Policy Enabled:{ if $cspEnabled } Yes { else } No { endif }
Salt Set?: + { if $salt == '' } +
+ You have not defined an application-wide $salt. This could lead to a less secure site. +
+ { else } + Set + { endif } +
diff --git a/system/Debug/Toolbar/Views/_database.tpl.php b/system/Debug/Toolbar/Views/_database.tpl.php new file mode 100644 index 000000000000..b5cf1a43a7d7 --- /dev/null +++ b/system/Debug/Toolbar/Views/_database.tpl.php @@ -0,0 +1,16 @@ + + + + + + + + + {queries} + + + + + {/queries} + +
TimeQuery String
{duration}{! sql !}
diff --git a/system/Debug/Toolbar/Views/_events.tpl.php b/system/Debug/Toolbar/Views/_events.tpl.php new file mode 100644 index 000000000000..88d732f41d5d --- /dev/null +++ b/system/Debug/Toolbar/Views/_events.tpl.php @@ -0,0 +1,18 @@ + + + + + + + + + + {events} + + + + + + {/events} + +
TimeEvent NameTimes Called
{ duration } ms{event}{count}
diff --git a/system/Debug/Toolbar/Views/_files.tpl.php b/system/Debug/Toolbar/Views/_files.tpl.php new file mode 100644 index 000000000000..9c992ab715c3 --- /dev/null +++ b/system/Debug/Toolbar/Views/_files.tpl.php @@ -0,0 +1,16 @@ + + + {userFiles} + + + + + {/userFiles} + {coreFiles} + + + + + {/coreFiles} + +
{name}{path}
{name}{path}
diff --git a/system/Debug/Toolbar/Views/_history.tpl.php b/system/Debug/Toolbar/Views/_history.tpl.php new file mode 100644 index 000000000000..9db00ecc4679 --- /dev/null +++ b/system/Debug/Toolbar/Views/_history.tpl.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + {files} + + + + + + + + + + {/files} + +
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
+ + {datetime}{status}{method}{url}{contentType}{isAJAX}
diff --git a/system/Debug/Toolbar/Views/_logs.tpl.php b/system/Debug/Toolbar/Views/_logs.tpl.php new file mode 100644 index 000000000000..7c80d849f321 --- /dev/null +++ b/system/Debug/Toolbar/Views/_logs.tpl.php @@ -0,0 +1,20 @@ +{ if $logs == [] } +

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

+{ else } + + + + + + + + + {logs} + + + + + {/logs} + +
SeverityMessage
{level}{msg}
+{ endif } diff --git a/system/Debug/Toolbar/Views/_routes.tpl.php b/system/Debug/Toolbar/Views/_routes.tpl.php new file mode 100644 index 000000000000..098804978ea0 --- /dev/null +++ b/system/Debug/Toolbar/Views/_routes.tpl.php @@ -0,0 +1,44 @@ +

Matched Route

+ + + + {matchedRoute} + + + + + + + + + + + + + + + + + {params} + + + + + {/params} + {/matchedRoute} + +
Directory:{directory}
Controller:{controller}
Method:{method}
Params:{paramCount} / {truePCount}
{name}{value}
+ + +

Defined Routes

+ + + + {routes} + + + + + {/routes} + +
{from}{to}
diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css new file mode 100644 index 000000000000..64caffaaeb8d --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -0,0 +1,401 @@ +#debug-icon { + position: fixed; + bottom: 0; + right: 0; + width: 36px; + height: 36px; + background: #fff; + border: 1px solid #ddd; + margin: 0px; + z-index: 10000; + box-shadow: 0 -3px 10px rgba(0, 0, 0, 0.1); + clear: both; + text-align: center; +} + +#debug-icon a svg { + margin: 4px; + max-width: 26px; + max-height: 26px; +} + +#debug-bar a:active, #debug-bar a:link, #debug-bar a:visited { + color: #dd4814; +} + +#debug-bar { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 36px; + background: #fff; + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 36px; + z-index: 10000; +} + +#debug-bar h1, +#debug-bar h2, +#debug-bar h3 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #666; + line-height: 1.5; +} + +#debug-bar p { + font-size: 12px; + margin: 0 0 10px 20px; + padding: 0; +} + +#debug-bar a { + text-decoration: none; +} + +#debug-bar a:hover { + text-decoration: underline; + text-decoration-color: #4e4a4a; +} + +#debug-bar .muted, +#debug-bar .muted td { + color: #bbb; +} + +#debug-bar .toolbar { + display: block; + background: inherit; + overflow: hidden; + overflow-y: auto; + white-space: nowrap; + box-shadow: 0 -3px 10px rgba(0, 0, 0, 0.1); + padding: 0 12px 0 12px; /* give room for OS X scrollbar */ + z-index: 10000; +} + +#debug-bar #toolbar-position > a { + padding: 0 6px; +} + +#debug-icon.fixed-top, +#debug-bar.fixed-top { + top: 0; + bottom: auto; +} + +#debug-icon.fixed-top, +#debug-bar.fixed-top .toolbar { + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); +} + +#debug-bar h1 { + font-size: 16px; + line-height: 36px; + font-weight: 300; + margin: 0 16px 0 0; + padding: 0; + text-align: left; + display: inline-block; + position: absolute; + right: 30px; + top: 0; + bottom: 0; +} + +#debug-bar-link { + padding: 6px; + position: absolute; + display: inline-block; + top: 0; + bottom: 0; + right: 10px; + font-size: 16px; + line-height: 36px; + width: 24px; +} + +#debug-bar h2 { + font-size: 16px; + font-weight: 300; + margin: 0; + padding: 0; +} + +#debug-bar h2 span { + font-size: 13px; +} + +#debug-bar h3 { + text-transform: uppercase; + font-size: 11px; + font-weight: 200; + margin-left: 10pt; +} + +#debug-bar span.ci-label { + display: inline-block; + font-size: 14px; + line-height: 36px; + vertical-align: baseline; +} + +#debug-bar span.ci-label img { + display: inline-block; + margin: 6px 3px 6px 0; + float: left; + clear: left; +} + +#debug-bar span.ci-label .badge { + display: inline-block; + padding: 3px 6px; + font-size: 75%; + font-weight: 500; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 10rem; + background-color: #5bc0de; + margin-left: 0.5em; +} + +#debug-bar span.ci-label .badge.active { + background-color: red; +} + +#debug-bar button { + border: 1px solid #ddd; + background-color: #fff; + cursor: pointer; + border-radius: 4px; + color: #333; +} + +#debug-bar button:hover { + background-color: #eaeaea; +} + +#debug-bar tr[data-active="1"] { + background-color: #dff0d8; +} + +#debug-bar tr[data-active="1"]:hover { + background-color: #a7d499; +} + +#debug-bar tr.current { + background-color: #FDC894; +} + +#debug-bar tr.current:hover { + background-color: #DD4814; +} + +#debug-bar table strong { + font-weight: 500; + color: rgba(0, 0, 0, 0.3); +} + +#debug-bar .ci-label { + text-shadow: none; +} + +#debug-bar .ci-label:hover { + background-color: #eaeaea; + cursor: pointer; +} + +#debug-bar .ci-label a { + display: block; + padding: 0 10px; + color: inherit; + text-decoration: none; +} + +#debug-bar .ci-label.active { + background-color: #eaeaea; + border-color: #bbb; +} + +#debug-bar .tab { + display: none; + background: inherit; + padding: 1em 2em; + border: solid #ddd; + border-width: 1px 0; + position: fixed; + bottom: 35px; + left: 0; + right: 0; + z-index: 9999; + box-shadow: 0 -3px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; + overflow-y: auto; + max-height: 62%; +} + +#debug-bar.fixed-top .tab { + top: 36px; + bottom: auto; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); +} + +#debug-bar table { + margin: 0 0 10px 20px; + font-size: 13px; + border-collapse: collapse; + width: 100%; +} + +#debug-bar td, +#debug-bar th { + display: table-cell; + text-align: left; +} + +#debug-bar tr { + border: none; +} + +#debug-bar td { + border: none; + padding: 2px 10px 2px 5px; + margin: 0; +} + +#debug-bar th { + padding-bottom: 0.7em; +} + +#debug-bar tr td:first-child { + width: 20%; +} + +#debug-bar tr td:first-child.narrow { + width: 7em; +} + +#debug-bar tr:hover { + background-color: #f3f3f3; +} + +#debug-bar table.timeline { + width: 100%; + margin-left: 0; +} + +#debug-bar table.timeline th { + font-size: 0.7em; + font-weight: 200; + text-align: left; + padding-bottom: 1em; +} + +#debug-bar table.timeline td, +#debug-bar table.timeline th { + border-left: 1px solid #ddd; + padding: 0 1em; + position: relative; +} + +#debug-bar table.timeline tr td:first-child, +#debug-bar table.timeline tr th:first-child { + border-left: 0; + padding-left: 0; +} + +#debug-bar table.timeline td { + padding: 5px; +} + +#debug-bar table.timeline .timer { + position: absolute; + display: inline-block; + padding: 3px; + bottom: 9px; + border-radius: 3px; + background-color: #999; +} + +#debug-bar .route-params, +#debug-bar .route-params-item { + vertical-align: top; +} + +#debug-bar .route-params-item td:first-child { + padding-left: 1em; + text-align: right; + font-style: italic; +} + +.debug-view.show-view { + border: 1px solid #dd4814; + margin: 4px; +} + +.debug-view-path { + background-color: #fdc894; + color: #000; + padding: 2px; + font-family: monospace; + font-size: 11px; + min-height: 16px; + text-align: left; +} + +.show-view .debug-view-path { + display: block !important; +} + +@media screen and (max-width: 748px) { + .hide-sm { + display: none !important; + } +} + +/** +simple styles to replace inline styles + */ +.debug-bar-width30 { + width: 30%; +} + +.debug-bar-width10 { + width: 10%; +} + +.debug-bar-width70p { + width: 70px; +} + +.debug-bar-width140p { + width: 140px; +} + +.debug-bar-width20e { + width: 20em; +} + +.debug-bar-width6r { + width: 6rem; +} + +.debug-bar-ndisplay { + display: none; +} + +.debug-bar-alignRight { + text-align: right; +} + +.debug-bar-alignLeft { + text-align: left; +} + +.debug-bar-noverflow { + overflow: hidden; +} diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js new file mode 100644 index 000000000000..944893e10c3b --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -0,0 +1,538 @@ +/* + * Functionality for the CodeIgniter Debug Toolbar. + */ + +var ciDebugBar = { + + toolbar : null, + icon : null, + + //-------------------------------------------------------------------- + + init : function() + { + this.toolbar = document.getElementById('debug-bar'); + this.icon = document.getElementById('debug-icon'); + + ciDebugBar.createListeners(); + ciDebugBar.setToolbarState(); + ciDebugBar.setToolbarPosition(); + ciDebugBar.toggleViewsHints(); + + document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + + // Allows to highlight the row of the current history request + var btn = document.querySelector('button[data-time="'+localStorage.getItem('debugbar-time')+'"]'); + ciDebugBar.addClass(btn.parentNode.parentNode, 'current'); + + historyLoad = document.getElementsByClassName('ci-history-load'); + + for (var i = 0; i < historyLoad.length; i++) + { + historyLoad[i].addEventListener('click', function() { + loadDoc(this.getAttribute('data-time')); + }, true); + } + + // Display the active Tab on page load + var tab = ciDebugBar.readCookie('debug-bar-tab'); + if (document.getElementById(tab)) { + var el = document.getElementById(tab); + el.style.display = 'block'; + ciDebugBar.addClass(el, 'active'); + tab = document.querySelector('[data-tab='+tab+']'); + if (tab) { + ciDebugBar.addClass(tab.parentNode, 'active'); + } + } + + }, + + //-------------------------------------------------------------------- + + createListeners : function() + { + var buttons = [].slice.call(document.querySelectorAll('#debug-bar .ci-label a')); + + for (var i=0; i < buttons.length; i++) + { + buttons[i].addEventListener('click', ciDebugBar.showTab, true); + } + }, + + //-------------------------------------------------------------------- + + showTab: function() + { + // Get the target tab, if any + var tab = document.getElementById(this.getAttribute('data-tab')); + + // If the label have not a tab stops here + if (! tab) { + return; + } + + // Remove debug-bar-tab cookie + ciDebugBar.createCookie('debug-bar-tab', '', -1); + + // Check our current state. + var state = tab.style.display; + + // Hide all tabs + var tabs = document.querySelectorAll('#debug-bar .tab'); + + for (var i=0; i < tabs.length; i++) + { + tabs[i].style.display = 'none'; + } + + // Mark all labels as inactive + var labels = document.querySelectorAll('#debug-bar .ci-label'); + + for (var i=0; i < labels.length; i++) + { + ciDebugBar.removeClass(labels[i], 'active'); + } + + // Show/hide the selected tab + if (state != 'block') + { + tab.style.display = 'block'; + ciDebugBar.addClass(this.parentNode, 'active'); + // Create debug-bar-tab cookie to persistent state + ciDebugBar.createCookie('debug-bar-tab', this.getAttribute('data-tab'), 365); + } + }, + + //-------------------------------------------------------------------- + + addClass : function(el, className) + { + if (el.classList) + { + el.classList.add(className); + } + else + { + el.className += ' ' + className; + } + }, + + //-------------------------------------------------------------------- + + removeClass : function(el, className) + { + if (el.classList) + { + el.classList.remove(className); + } + else + { + el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + + }, + + //-------------------------------------------------------------------- + + /** + * Toggle display of a data table + * @param obj + */ + toggleDataTable : function(obj) + { + if (typeof obj == 'string') + { + obj = document.getElementById(obj + '_table'); + } + + if (obj) + { + obj.style.display = obj.style.display == 'none' ? 'block' : 'none'; + } + }, + + //-------------------------------------------------------------------- + + /** + * Toggle tool bar from full to icon and icon to full + */ + toggleToolbar : function() + { + var open = ciDebugBar.toolbar.style.display != 'none'; + + ciDebugBar.icon.style.display = open == true ? 'inline-block' : 'none'; + ciDebugBar.toolbar.style.display = open == false ? 'inline-block' : 'none'; + + // Remember it for other page loads on this site + ciDebugBar.createCookie('debug-bar-state', '', -1); + ciDebugBar.createCookie('debug-bar-state', open == true ? 'minimized' : 'open' , 365); + }, + + //-------------------------------------------------------------------- + + /** + * Sets the initial state of the toolbar (open or minimized) when + * the page is first loaded to allow it to remember the state between refreshes. + */ + setToolbarState: function() + { + var open = ciDebugBar.readCookie('debug-bar-state'); + + ciDebugBar.icon.style.display = open != 'open' ? 'inline-block' : 'none'; + ciDebugBar.toolbar.style.display = open == 'open' ? 'inline-block' : 'none'; + }, + + //-------------------------------------------------------------------- + + toggleViewsHints: function() + { + // Avoid toggle hints on history requests that are not the initial + if (localStorage.getItem('debugbar-time') != localStorage.getItem('debugbar-time-new')) + { + var a = document.querySelector('a[data-tab="ci-views"]'); + a.href = '#'; + return; + } + + var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] + var sortedComments = []; + var comments = []; + + var getComments = function() + { + var nodes = []; + var result = []; + var xpathResults = document.evaluate( "//comment()[starts-with(., ' DEBUG-VIEW')]", document, null, XPathResult.ANY_TYPE, null); + var nextNode = xpathResults.iterateNext(); + while( nextNode ) + { + nodes.push( nextNode ); + nextNode = xpathResults.iterateNext(); + } + + // sort comment by opening and closing tags + for( var i = 0; i < nodes.length; ++i ) + { + // get file path + name to use as key + var path = nodes[i].nodeValue.substring( 18, nodes[i].nodeValue.length - 1 ); + + if( nodes[i].nodeValue[12] === 'S' ) // simple check for start comment + { + // create new entry + result[path] = [ nodes[i], null ]; + } + else if(result[path]) + { + // add to existing entry + result[path][1] = nodes[i]; + } + } + + return result; + }; + + // find node that has TargetNode as parentNode + var getParentNode = function( node, targetNode ) + { + if( node.parentNode === null ) + { + return null; + } + + if( node.parentNode !== targetNode ) + { + return getParentNode( node.parentNode, targetNode ); + } + + return node; + }; + + // define invalid & outer ( also invalid ) elements + const INVALID_ELEMENTS = [ 'NOSCRIPT', 'SCRIPT', 'STYLE' ]; + const OUTER_ELEMENTS = [ 'HTML', 'BODY', 'HEAD' ]; + + var getValidElementInner = function( node, reverse ) + { + // handle invalid tags + if( OUTER_ELEMENTS.indexOf( node.nodeName ) !== -1 ) + { + for( var i = 0; i < document.body.children.length; ++i ) + { + var index = reverse ? document.body.children.length - ( i + 1 ) : i; + var element = document.body.children[index]; + + // skip invalid tags + if( INVALID_ELEMENTS.indexOf( element.nodeName ) !== -1 ) continue; + + return [ element, reverse ]; + } + + return null; + } + + // get to next valid element + while( node !== null && INVALID_ELEMENTS.indexOf( node.nodeName ) !== -1 ) + { + node = reverse ? node.previousElementSibling : node.nextElementSibling; + } + + // return non array if we couldnt find something + if( node === null ) return null; + + return [ node, reverse ]; + }; + + // get next valid element ( to be safe to add divs ) + // @return [ element, skip element ] or null if we couldnt find a valid place + var getValidElement = function( nodeElement ) + { + if (nodeElement) { + if( nodeElement.nextElementSibling !== null ) + { + return getValidElementInner( nodeElement.nextElementSibling, false ) + || getValidElementInner( nodeElement.previousElementSibling, true ); + } + if( nodeElement.previousElementSibling !== null ) + { + return getValidElementInner( nodeElement.previousElementSibling, true ); + } + } + + // something went wrong! -> element is not in DOM + return null; + }; + + function showHints() { + // Had AJAX? Reset view blocks + sortedComments = getComments(); + + for( var key in sortedComments ) + { + var startElement = getValidElement( sortedComments[key][0] ); + var endElement = getValidElement( sortedComments[key][1] ); + + // skip if we couldnt get a valid element + if( startElement === null || endElement === null ) continue; + + // find element which has same parent as startelement + var jointParent = getParentNode( endElement[0], startElement[0].parentNode ); + if( jointParent === null ) + { + // find element which has same parent as endelement + jointParent = getParentNode( startElement[0], endElement[0].parentNode ); + if( jointParent === null ) + { + // both tries failed + continue; + } + else startElement[0] = jointParent; + } + else endElement[0] = jointParent; + + var debugDiv = document.createElement( 'div' ); // holder + var debugPath = document.createElement( 'div' ); // path + var childArray = startElement[0].parentNode.childNodes; // target child array + var parent = startElement[0].parentNode; + var start, end; + + // setup container + debugDiv.classList.add( 'debug-view' ); + debugDiv.classList.add( 'show-view' ); + debugPath.classList.add( 'debug-view-path' ); + debugPath.innerText = key; + debugDiv.appendChild( debugPath ); + + // calc distance between them + // start + for( var i = 0; i < childArray.length; ++i ) + { + // check for comment ( start & end ) -> if its before valid start element + if( childArray[i] === sortedComments[key][1] || + childArray[i] === sortedComments[key][0] || + childArray[i] === startElement[0] ) + { + start = i; + if( childArray[i] === sortedComments[key][0] ) start++; // increase to skip the start comment + break; + } + } + // adjust if we want to skip the start element + if( startElement[1] ) start++; + + // end + for( var i = start; i < childArray.length; ++i ) + { + if( childArray[i] === endElement[0] ) + { + end = i; + // dont break to check for end comment after end valid element + } + else if( childArray[i] === sortedComments[key][1] ) + { + // if we found the end comment, we can break + end = i; + break; + } + } + + // move elements + var number = end - start; + if( endElement[1] ) number++; + for( var i = 0; i < number; ++i ) + { + if( INVALID_ELEMENTS.indexOf( childArray[start] ) !== -1 ) + { + // skip invalid childs that can cause problems if moved + start++; + continue; + } + debugDiv.appendChild( childArray[start] ); + } + + // add container to DOM + nodeList.push( parent.insertBefore( debugDiv, childArray[start] ) ); + } + + ciDebugBar.createCookie('debug-view', 'show', 365); + ciDebugBar.addClass(btn, 'active'); + } + + function hideHints() { + for( var i = 0; i < nodeList.length; ++i ) + { + var index; + + // find index + for( var j = 0; j < nodeList[i].parentNode.childNodes.length; ++j ) + { + if( nodeList[i].parentNode.childNodes[j] === nodeList[i] ) + { + index = j; + break; + } + } + + // move child back + while( nodeList[i].childNodes.length !== 1 ) + { + nodeList[i].parentNode.insertBefore( nodeList[i].childNodes[1], nodeList[i].parentNode.childNodes[index].nextSibling ); + index++; + } + + nodeList[i].parentNode.removeChild( nodeList[i] ); + } + nodeList.length = 0; + + ciDebugBar.createCookie('debug-view', '', -1); + ciDebugBar.removeClass(btn, 'active'); + } + + var btn = document.querySelector('[data-tab=ci-views]'); + + // If the Views Collector is inactive stops here + if (! btn) + { + return; + } + + btn.parentNode.onclick = function() { + if (ciDebugBar.readCookie('debug-view')) + { + hideHints(); + } + else + { + showHints(); + } + }; + + // Determine Hints state on page load + if (ciDebugBar.readCookie('debug-view')) + { + showHints(); + } + }, + + //-------------------------------------------------------------------- + + setToolbarPosition: function () + { + var btnPosition = document.getElementById('toolbar-position'); + + if (ciDebugBar.readCookie('debug-bar-position') === 'top') + { + ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); + } + + btnPosition.addEventListener('click', function () { + var position = ciDebugBar.readCookie('debug-bar-position'); + + ciDebugBar.createCookie('debug-bar-position', '', -1); + + if (!position || position === 'bottom') + { + ciDebugBar.createCookie('debug-bar-position', 'top', 365); + ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); + } + else + { + ciDebugBar.createCookie('debug-bar-position', 'bottom', 365); + ciDebugBar.removeClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.removeClass(ciDebugBar.toolbar, 'fixed-top'); + } + }, true); + }, + + //-------------------------------------------------------------------- + + /** + * Helper to create a cookie. + * + * @param name + * @param value + * @param days + */ + createCookie : function(name,value,days) + { + if (days) + { + var date = new Date(); + + date.setTime(date.getTime()+(days*24*60*60*1000)); + + var expires = "; expires="+date.toGMTString(); + } + else + { + var expires = ""; + } + + document.cookie = name+"="+value+expires+"; path=/"; + }, + + //-------------------------------------------------------------------- + + readCookie : function(name) + { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + + for(var i=0;i < ca.length;i++) + { + var c = ca[i]; + while (c.charAt(0)==' ') + { + c = c.substring(1,c.length); + } + if (c.indexOf(nameEQ) == 0) + { + return c.substring(nameEQ.length,c.length); + } + } + return null; + } +}; diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php new file mode 100644 index 000000000000..9ec05e4ee5b6 --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -0,0 +1,280 @@ + + + + +
+
+ + + + + ms   MB + + + + + + + + + + + + + + + + + + + + + + + Vars + + + +

+ + + + + + + + + + +

+ + + + + +
+ + +
+ + + + + + + + + + + + + + +
NAMECOMPONENTDURATION ms
+
+ + + + + +
+

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

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

No data to display.

+ + + + + + +

Session User Data

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

No data to display.

+ + +

Session doesn't seem to be active.

+ + +

Request ( )

+ + + +

$_GET

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

$_POST

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

Headers

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

Cookies

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

Response ( )

+ + + +

Headers

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

System Configuration

+ + setData($config)->render('_config.tpl') ?> +
+
+ diff --git a/system/Debug/Toolbar/toolbarloader.js.php b/system/Debug/Toolbar/toolbarloader.js.php new file mode 100644 index 000000000000..d1e09df0eb11 --- /dev/null +++ b/system/Debug/Toolbar/toolbarloader.js.php @@ -0,0 +1,84 @@ + +document.addEventListener('DOMContentLoaded', loadDoc, false); + +function loadDoc(time) { + if (isNaN(time)) { + time = document.getElementById("debugbar_loader").getAttribute("data-time"); + localStorage.setItem('debugbar-time', time); + } + + localStorage.setItem('debugbar-time-new', time); + + var url = ""; + + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState === 4 && this.status === 200) { + var toolbar = document.getElementById("toolbarContainer"); + if (!toolbar) { + toolbar = document.createElement('div'); + toolbar.setAttribute('id', 'toolbarContainer'); + document.body.appendChild(toolbar); + } + + // copy for easier manipulation + let responseText = this.responseText; + + // get csp blocked parts + // the style block is the first and starts at 0 + { + let PosBeg = responseText.indexOf( '>', responseText.indexOf( '', PosBeg ); + document.getElementById( 'debugbar_dynamic_style' ).innerHTML = responseText.substr( PosBeg, PosEnd ) + responseText = responseText.substr( PosEnd + 8 ); + } + // the script block starts right after style blocks ended + { + let PosBeg = responseText.indexOf( '>', responseText.indexOf( '' ); + document.getElementById( 'debugbar_dynamic_script' ).innerHTML = responseText.substr( PosBeg, PosEnd - PosBeg ); + responseText = responseText.substr( PosEnd + 9 ); + } + // check for last style block + { + let PosBeg = responseText.indexOf( '>', responseText.lastIndexOf( '', PosBeg ); + document.getElementById( 'debugbar_dynamic_style' ).innerHTML += responseText.substr( PosBeg, PosEnd - PosBeg ); + responseText = responseText.substr( 0, PosBeg ); + } + + toolbar.innerHTML = responseText; + if (typeof ciDebugBar === 'object') { + ciDebugBar.init(); + } + } else if (this.readyState === 4 && this.status === 404) { + console.log('CodeIgniter DebugBar: File "WRITEPATH/debugbar/debugbar_' + time + '" not found.'); + } + }; + + xhttp.open("GET", url + "?debugbar_time=" + time, true); + xhttp.send(); +} + +// Track all AJAX requests +var oldXHR = window.XMLHttpRequest; + +function newXHR() { + var realXHR = new oldXHR(); + realXHR.addEventListener("readystatechange", function() { + // Only success responses and URLs that do not contains "debugbar_time" are tracked + if (realXHR.readyState === 4 && realXHR.status.toString()[0] === '2' && realXHR.responseURL.indexOf('debugbar_time') === -1) { + var debugbarTime = realXHR.getResponseHeader('Debugbar-Time'); + if (debugbarTime) { + var h2 = document.querySelector('#ci-history > h2'); + h2.innerHTML = 'History You have new debug data. '; + var badge = document.querySelector('a[data-tab="ci-history"] > span > .badge'); + badge.className += ' active'; + } + } + }, false); + return realXHR; +} + +window.XMLHttpRequest = newXHR; + diff --git a/system/Email/Email.php b/system/Email/Email.php new file mode 100644 index 000000000000..8472f36387cb --- /dev/null +++ b/system/Email/Email.php @@ -0,0 +1,2544 @@ + '1 (Highest)', + 2 => '2 (High)', + 3 => '3 (Normal)', + 4 => '4 (Low)', + 5 => '5 (Lowest)', + ]; + + /** + * mbstring.func_overload flag + * + * @var bool + */ + protected static $func_overload; + + /** + * Logger instance to record error messages and awarnings. + * @var \PSR\Log\LoggerInterface + */ + protected $logger; + + //-------------------------------------------------------------------- + + /** + * Constructor - Sets Email Preferences + * + * The constructor can be passed an array of config values + * + * @param array|null $config + */ + public function __construct($config = null) + { + $this->initialize($config); + + isset(self::$func_overload) || self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + + log_message('info', 'Email Class Initialized'); + } + + //-------------------------------------------------------------------- + + /** + * Initialize preferences + * + * @param array|\Config\Email $config + * + * @return Email + */ + public function initialize($config) + { + $this->clear(); + + if ($config instanceof \Config\Email) + { + $config = get_object_vars($config); + } + + foreach (get_class_vars(get_class($this)) as $key => $value) + { + if (property_exists($this, $key) && isset($config[$key])) + { + $method = 'set'.ucfirst($key); + + if (method_exists($this, $method)) + { + $this->$method($config[$key]); + } + else + { + $this->$key = $config[$key]; + } + } + } + + $this->charset = strtoupper($this->charset); + $this->SMTPAuth = isset($this->SMTPUser[0], $this->SMTPPass[0]); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Initialize the Email Data + * + * @param bool $clearAttachments + * + * @return Email + */ + public function clear($clearAttachments = false) + { + $this->subject = ''; + $this->body = ''; + $this->finalBody = ''; + $this->headerStr = ''; + $this->replyToFlag = false; + $this->recipients = []; + $this->CCArray = []; + $this->BCCArray = []; + $this->headers = []; + $this->debugMessage = []; + + $this->setHeader('Date', $this->setDate()); + + if ($clearAttachments !== false) + { + $this->attachments = []; + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set FROM + * + * @param string $from + * @param string $name + * @param string|null $returnPath Return-Path + * + * @return Email + */ + public function setFrom($from, $name = '', $returnPath = null) + { + if (preg_match('/\<(.*)\>/', $from, $match)) + { + $from = $match[1]; + } + + if ($this->validate) + { + $this->validateEmail($this->stringToArray($from)); + if ($returnPath) + { + $this->validateEmail($this->stringToArray($returnPath)); + } + } + + // prepare the display name + if ($name !== '') + { + // only use Q encoding if there are characters that would require it + if (! preg_match('/[\200-\377]/', $name)) + { + // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes + $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"'; + } + else + { + $name = $this->prepQEncoding($name); + } + } + + $this->setHeader('From', $name.' <'.$from.'>'); + + isset($returnPath) || $returnPath = $from; + $this->setHeader('Return-Path', '<'.$returnPath.'>'); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Reply-to + * + * @param string $replyto + * @param string $name + * + * @return Email + */ + public function setReplyTo($replyto, $name = '') + { + if (preg_match('/\<(.*)\>/', $replyto, $match)) + { + $replyto = $match[1]; + } + + if ($this->validate) + { + $this->validateEmail($this->stringToArray($replyto)); + } + + if ($name !== '') + { + // only use Q encoding if there are characters that would require it + if (! preg_match('/[\200-\377]/', $name)) + { + // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes + $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"'; + } + else + { + $name = $this->prepQEncoding($name); + } + } + + $this->setHeader('Reply-To', $name.' <'.$replyto.'>'); + $this->replyToFlag = true; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Recipients + * + * @param string $to + * + * @return Email + */ + public function setTo($to) + { + $to = $this->stringToArray($to); + $to = $this->cleanEmail($to); + + if ($this->validate) + { + $this->validateEmail($to); + } + + if ($this->getProtocol() !== 'mail') + { + $this->setHeader('To', implode(', ', $to)); + } + + $this->recipients = $to; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set CC + * + * @param string $cc + * + * @return Email + */ + public function setCC($cc) + { + $cc = $this->cleanEmail($this->stringToArray($cc)); + + if ($this->validate) + { + $this->validateEmail($cc); + } + + $this->setHeader('Cc', implode(', ', $cc)); + + if ($this->getProtocol() === 'smtp') + { + $this->CCArray = $cc; + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set BCC + * + * @param string $bcc + * @param string $limit + * + * @return Email + */ + public function setBCC($bcc, $limit = '') + { + if ($limit !== '' && is_numeric($limit)) + { + $this->BCCBatchMode = true; + $this->BCCBatchSize = $limit; + } + + $bcc = $this->cleanEmail($this->stringToArray($bcc)); + + if ($this->validate) + { + $this->validateEmail($bcc); + } + + if ($this->getProtocol() === 'smtp' || ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) + { + $this->BCCArray = $bcc; + } + else + { + $this->setHeader('Bcc', implode(', ', $bcc)); + } + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Email Subject + * + * @param string $subject + * + * @return Email + */ + public function setSubject($subject) + { + $subject = $this->prepQEncoding($subject); + $this->setHeader('Subject', $subject); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Body + * + * @param string $body + * + * @return Email + */ + public function setMessage($body) + { + $this->body = rtrim(str_replace("\r", '', $body)); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Assign file attachments + * + * @param string $file Can be local path, URL or buffered content + * @param string $disposition 'attachment' + * @param string|null $newname + * @param string $mime + * + * @return Email + */ + public function attach($file, $disposition = '', $newname = null, $mime = '') + { + if ($mime === '') + { + if (strpos($file, '://') === false && ! file_exists($file)) + { + $this->setErrorMessage(lang('email.attachmentMissing', [$file])); + + return false; + } + + if (! $fp = @fopen($file, 'rb')) + { + $this->setErrorMessage(lang('email.attachmentUnreadable', [$file])); + + return false; + } + + $fileContent = stream_get_contents($fp); + $mime = $this->mimeTypes(pathinfo($file, PATHINFO_EXTENSION)); + fclose($fp); + } + else + { + $fileContent =& $file; // buffered file + } + + $this->attachments[] = [ + 'name' => [$file, $newname], + 'disposition' => empty($disposition) ? 'attachment' : $disposition, + // Can also be 'inline' Not sure if it matters + 'type' => $mime, + 'content' => chunk_split(base64_encode($fileContent)), + 'multipart' => 'mixed', + ]; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set and return attachment Content-ID + * + * Useful for attached inline pictures + * + * @param string $filename + * + * @return string + */ + public function setAttachmentCID($filename) + { + for ($i = 0, $c = count($this->attachments); $i < $c; $i++) + { + if ($this->attachments[$i]['name'][0] === $filename) + { + $this->attachments[$i]['multipart'] = 'related'; + $this->attachments[$i]['cid'] = uniqid(basename($this->attachments[$i]['name'][0]).'@'); + + return $this->attachments[$i]['cid']; + } + } + + return false; + } + + //-------------------------------------------------------------------- + + /** + * Add a Header Item + * + * @param string $header + * @param string $value + * + * @return Email + */ + public function setHeader($header, $value) + { + $this->headers[$header] = str_replace(["\n", "\r"], '', $value); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Convert a String to an Array + * + * @param string $email + * + * @return array + */ + protected function stringToArray($email) + { + if (! is_array($email)) + { + return (strpos($email, ',') !== false) + ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) + : (array)trim($email); + } + + return $email; + } + + //-------------------------------------------------------------------- + + /** + * Set Multipart Value + * + * @param string $str + * + * @return Email + */ + public function setAltMessage($str) + { + $this->altMessage = (string)$str; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Mailtype + * + * @param string $type + * + * @return Email + */ + public function setMailType($type = 'text') + { + $this->mailType = ($type === 'html') ? 'html' : 'text'; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Wordwrap + * + * @param bool $wordWrap + * + * @return Email + */ + public function setWordWrap($wordWrap = true) + { + $this->wordWrap = (bool)$wordWrap; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Protocol + * + * @param string $protocol + * + * @return Email + */ + public function setProtocol($protocol = 'mail') + { + $this->protocol = in_array($protocol, $this->protocols, true) ? strtolower($protocol) : 'mail'; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Priority + * + * @param int $n + * + * @return Email + */ + public function setPriority($n = 3) + { + $this->priority = preg_match('/^[1-5]$/', $n) ? (int)$n : 3; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set Newline Character + * + * @param string $newline + * + * @return Email + */ + public function setNewline($newline = "\n") + { + $this->newline = in_array($newline, ["\n", "\r\n", "\r"]) ? $newline : "\n"; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Set CRLF + * + * @param string $CRLF + * + * @return Email + */ + public function setCRLF($CRLF = "\n") + { + $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Get the Message ID + * + * @return string + */ + protected function getMessageID() + { + $from = str_replace(['>', '<'], '', $this->headers['Return-Path']); + + return '<'.uniqid('').strstr($from, '@').'>'; + } + + //-------------------------------------------------------------------- + + /** + * Get Mail Protocol + * + * @return string + */ + protected function getProtocol() + { + $this->protocol = strtolower($this->protocol); + in_array($this->protocol, $this->protocols, true) || $this->protocol = 'mail'; + + return $this->protocol; + } + + //-------------------------------------------------------------------- + + /** + * Get Mail Encoding + * + * @return string + */ + protected function getEncoding() + { + in_array($this->encoding, $this->bitDepths) || $this->encoding = '8bit'; + + foreach ($this->baseCharsets as $charset) + { + if (strpos($this->charset, $charset) === 0) + { + $this->encoding = '7bit'; + } + } + + return $this->encoding; + } + + //-------------------------------------------------------------------- + + /** + * Get content type (text/html/attachment) + * + * @return string + */ + protected function getContentType() + { + if ($this->mailType === 'html') + { + return empty($this->attachments) ? 'html' : 'html-attach'; + } + elseif ($this->mailType === 'text' && ! empty($this->attachments)) + { + return 'plain-attach'; + } + else + { + return 'plain'; + } + } + + //-------------------------------------------------------------------- + + /** + * Set RFC 822 Date + * + * @return string + */ + protected function setDate() + { + $timezone = date('Z'); + $operator = ($timezone[0] === '-') ? '-' : '+'; + $timezone = abs($timezone); + $timezone = floor($timezone/3600)*100+($timezone%3600)/60; + + return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone); + } + + //-------------------------------------------------------------------- + + /** + * Mime message + * + * @return string + */ + protected function getMimeMessage() + { + return 'This is a multi-part message in MIME format.'.$this->newline.'Your email application may not support this format.'; + } + + //-------------------------------------------------------------------- + + /** + * Validate Email Address + * + * @param string $email + * + * @return bool + */ + public function validateEmail($email) + { + if (! is_array($email)) + { + $this->setErrorMessage(lang('email.mustBeArray')); + + return false; + } + + foreach ($email as $val) + { + if (! $this->isValidEmail($val)) + { + $this->setErrorMessage(lang('email.invalidAddress', $val)); + + return false; + } + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Email Validation + * + * @param string $email + * + * @return bool + */ + public function isValidEmail($email) + { + if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && $atpos = strpos($email, '@')) + { + $email = self::substr($email, 0, ++$atpos).idn_to_ascii(self::substr($email, $atpos), 0, + INTL_IDNA_VARIANT_UTS46); + } + + return (bool)filter_var($email, FILTER_VALIDATE_EMAIL); + } + + //-------------------------------------------------------------------- + + /** + * Clean Extended Email Address: Joe Smith + * + * @param string $email + * + * @return string + */ + public function cleanEmail($email) + { + if (! is_array($email)) + { + return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email; + } + + $cleanEmail = []; + + foreach ($email as $addy) + { + $cleanEmail[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy; + } + + return $cleanEmail; + } + + //-------------------------------------------------------------------- + + /** + * Build alternative plain text message + * + * Provides the raw message for use in plain-text headers of + * HTML-formatted emails. + * If the user hasn't specified his own alternative message + * it creates one by stripping the HTML + * + * @return string + */ + protected function getAltMessage() + { + if (! empty($this->altMessage)) + { + return ($this->wordWrap) + ? $this->wordWrap($this->altMessage, 76) + : $this->altMessage; + } + + $body = preg_match('/\(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body; + $body = str_replace("\t", '', preg_replace('# '.$message."\n"; + $msg .= strtoupper($level) . ' - ' . $date . ' --> ' . $message . "\n"; flock($fp, LOCK_EX); @@ -151,8 +149,6 @@ public function handle($level, $message): bool return is_int($result); } - + //-------------------------------------------------------------------- - - } diff --git a/system/Log/Handlers/HandlerInterface.php b/system/Log/Handlers/HandlerInterface.php index 1032888632f7..1a2aaba08029 100644 --- a/system/Log/Handlers/HandlerInterface.php +++ b/system/Log/Handlers/HandlerInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,15 +29,15 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - interface HandlerInterface { + /** * Handles logging the message. * If the handler returns false, then execution of handlers @@ -57,7 +57,7 @@ public function handle($level, $message): bool; * Checks whether the Handler will handle logging items of this * log Level. * - * @param int $level + * @param string $level * * @return bool */ @@ -75,5 +75,4 @@ public function canHandle(string $level): bool; public function setDateFormat(string $format); //-------------------------------------------------------------------- - } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index f09b87abbee7..3726c42c34e6 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,14 +29,14 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use Psr\Log\LoggerInterface; +use CodeIgniter\Log\Exceptions\LogException; /** * The CodeIgntier Logger @@ -69,14 +69,14 @@ class Logger implements LoggerInterface * @var array */ protected $logLevels = [ - 'emergency' => 1, - 'alert' => 2, - 'critical' => 3, - 'error' => 4, - 'debug' => 5, - 'warning' => 6, - 'notice' => 7, - 'info' => 8, + 'emergency' => 1, + 'alert' => 2, + 'critical' => 3, + 'error' => 4, + 'warning' => 5, + 'notice' => 6, + 'info' => 7, + 'debug' => 8, ]; /** @@ -144,23 +144,23 @@ class Logger implements LoggerInterface /** * Constructor. - * + * * @param type $config * @param bool $debug * @throws \RuntimeException */ public function __construct($config, bool $debug = CI_DEBUG) { - $this->loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int)$config->threshold); + $this->loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int) $config->threshold); // Now convert loggable levels to strings. // We only use numbers to make the threshold setting convenient for users. - if (count($this->loggableLevels)) + if ($this->loggableLevels) { $temp = []; foreach ($this->loggableLevels as $level) { - $temp[] = array_search((int)$level, $this->logLevels); + $temp[] = array_search((int) $level, $this->logLevels); } $this->loggableLevels = $temp; @@ -169,16 +169,16 @@ public function __construct($config, bool $debug = CI_DEBUG) $this->dateFormat = $config->dateFormat ?? $this->dateFormat; - if (! is_array($config->handlers) || empty($config->handlers)) + if ( ! is_array($config->handlers) || empty($config->handlers)) { - throw new \RuntimeException('LoggerConfig must provide at least one Handler.'); + throw LogException::forNoHandlers('LoggerConfig'); } // Save the handler configuration for later. // Instances will be created on demand. $this->handlerConfig = $config->handlers; - $this->cacheLogs = (bool)$debug; + $this->cacheLogs = $debug; if ($this->cacheLogs) { $this->logCache = []; @@ -331,13 +331,13 @@ public function log($level, $message, array $context = []): bool { if (is_numeric($level)) { - $level = array_search((int)$level, $this->logLevels); + $level = array_search((int) $level, $this->logLevels); } // Is the level a valid level? - if (! array_key_exists($level, $this->logLevels)) + if ( ! array_key_exists($level, $this->logLevels)) { - throw new \InvalidArgumentException($level.' is an invalid log level.'); + throw LogException::forInvalidLogLevel($level); } // Does the app want to log this right now? @@ -349,7 +349,7 @@ public function log($level, $message, array $context = []): bool // Parse our placeholders $message = $this->interpolate($message, $context); - if (! is_string($message)) + if ( ! is_string($message)) { $message = print_r($message, true); } @@ -357,26 +357,30 @@ public function log($level, $message, array $context = []): bool if ($this->cacheLogs) { $this->logCache[] = [ - 'level' => $level, - 'msg' => $message + 'level' => $level, + 'msg' => $message ]; } foreach ($this->handlerConfig as $className => $config) { + if ( ! array_key_exists($className, $this->handlers)) { + $this->handlers[$className] = new $className($config); + } + /** * @var \CodeIgniter\Log\Handlers\HandlerInterface */ - $handler = new $className($config); + $handler = $this->handlers[$className]; - if (! $handler->canHandle($level)) + if ( ! $handler->canHandle($level)) { continue; } // If the handler returns false, then we // don't execute any other handlers. - if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) + if ( ! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) { break; } @@ -406,7 +410,8 @@ public function log($level, $message, array $context = []): bool */ protected function interpolate($message, array $context = []) { - if (! is_string($message)) return $message; + if ( ! is_string($message)) + return $message; // build a replacement array with braces around the context keys $replace = []; @@ -417,17 +422,17 @@ protected function interpolate($message, array $context = []) // or error, both of which implement the 'Throwable' interface. if ($key == 'exception' && $val instanceof \Throwable) { - $val = $val->getMessage().' '.$this->cleanFileNames($val->getFile()).':'. $val->getLine(); + $val = $val->getMessage() . ' ' . $this->cleanFileNames($val->getFile()) . ':' . $val->getLine(); } // todo - sanitize input before writing to file? - $replace['{'.$key.'}'] = $val; + $replace['{' . $key . '}'] = $val; } // Add special placeholders - $replace['{post_vars}'] = '$_POST: '.print_r($_POST, true); - $replace['{get_vars}'] = '$_GET: '.print_r($_GET, true); - $replace['{env}'] = ENVIRONMENT; + $replace['{post_vars}'] = '$_POST: ' . print_r($_POST, true); + $replace['{get_vars}'] = '$_GET: ' . print_r($_GET, true); + $replace['{env}'] = ENVIRONMENT; // Allow us to log the file/line that we are logging from if (strpos($message, '{file}') !== false) @@ -443,7 +448,7 @@ protected function interpolate($message, array $context = []) { preg_match('/env:[^}]+/', $message, $matches); - if (count($matches)) + if ($matches) { foreach ($matches as $str) { @@ -455,7 +460,7 @@ protected function interpolate($message, array $context = []) if (isset($_SESSION)) { - $replace['{session_vars}'] = '$_SESSION: '.print_r($_SESSION, true); + $replace['{session_vars}'] = '$_SESSION: ' . print_r($_SESSION, true); } // interpolate replacement values into the message and return @@ -492,13 +497,12 @@ public function determineFile() return [ $file, - $line + $line ]; } //-------------------------------------------------------------------- - /** * Cleans the paths of filenames by replacing APPPATH, BASEPATH, FCPATH * with the actual var. i.e. @@ -517,10 +521,8 @@ protected function cleanFileNames($file) $file = str_replace(BASEPATH, 'BASEPATH/', $file); $file = str_replace(FCPATH, 'FCPATH/', $file); - return $file; + return $file; } //-------------------------------------------------------------------- - - } diff --git a/system/Model.php b/system/Model.php index 462892fdc4d7..aa9d0aeda22a 100644 --- a/system/Model.php +++ b/system/Model.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -27,22 +27,23 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\Config\BaseConfig; -use Config\App; use Config\Database; +use CodeIgniter\I18n\Time; +use CodeIgniter\Pager\Pager; +use CodeIgniter\Config\BaseConfig; use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; -use phpDocumentor\Reflection\DocBlock\Tag\VarTag; +use CodeIgniter\Validation\ValidationInterface; +use CodeIgniter\Database\Exceptions\DataException; /** * Class Model @@ -60,9 +61,19 @@ * - ensure validation is run against objects when saving items * * @package CodeIgniter + * @mixin BaseBuilder */ class Model { + + /** + * Pager instance. + * Populated after calling $this->paginate() + * + * @var Pager + */ + public $pager; + /** * Name of database table * @@ -152,6 +163,13 @@ class Model */ protected $tempUseSoftDeletes; + /** + * The column used to save soft delete state + * + * @var string + */ + protected $deletedField = 'deleted'; + /** * Used by asArray and asObject to provide * temporary overrides of model default. @@ -182,39 +200,96 @@ class Model */ protected $builder; + /** + * Rules used to validate data in insert, update, and save methods. + * The array must match the format of data passed to the Validation + * library. + * + * @var array + */ + protected $validationRules = []; + + /** + * Contains any custom error messages to be + * used during data validation. + * + * @var array + */ + protected $validationMessages = []; + + /** + * Skip the model's validation. Used in conjunction with skipValidation() + * to skip data validation for any future calls. + * + * @var bool + */ + protected $skipValidation = false; + + /** + * Our validator instance. + * + * @var \CodeIgniter\Validation\Validation + */ + protected $validation; + + /** + * Callbacks. Each array should contain the method + * names (within the model) that should be called + * when those events are triggered. With the exception + * of 'afterFind', all methods are passed the same + * items that are given to the update/insert method. + * 'afterFind' will also include the results that were found. + * + * @var array + */ + protected $beforeInsert = []; + protected $afterInsert = []; + protected $beforeUpdate = []; + protected $afterUpdate = []; + protected $afterFind = []; + protected $beforeDelete = []; + protected $afterDelete = []; + + /** + * Holds information passed in via 'set' + * so that we can capture it (not the builder) + * and ensure it gets validated first. + * + * @var array + */ + protected $tempData = []; + //-------------------------------------------------------------------- /** * Model constructor. * * @param ConnectionInterface $db - * @param BaseConfig $config Config/App() + * @param ValidationInterface $validation */ - public function __construct(ConnectionInterface &$db = null, BaseConfig $config = null) + public function __construct(ConnectionInterface &$db = null, ValidationInterface $validation = null) { if ($db instanceof ConnectionInterface) { - $this->db =& $db; + $this->db = & $db; } else { $this->db = Database::connect($this->DBGroup); } - if (is_null($config) || ! isset($config->salt)) + $this->tempReturnType = $this->returnType; + $this->tempUseSoftDeletes = $this->useSoftDeletes; + + if (is_null($validation)) { - $config = new App(); + $validation = \Config\Services::validation(null, false); } - $this->salt = $config->salt ?: ''; - unset($config); - - $this->tempReturnType = $this->returnType; - $this->tempUseSoftDeletes = $this->useSoftDeletes; + $this->validation = $validation; } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // CRUD & FINDERS //-------------------------------------------------------------------- @@ -223,67 +298,45 @@ public function __construct(ConnectionInterface &$db = null, BaseConfig $config * Fetches the row of database from $this->table with a primary key * matching $id. * - * @param mixed|array $id One primary key or an array of primary keys + * @param mixed|array|null $id One primary key or an array of primary keys * * @return array|object|null The resulting row of data, or null. */ - public function find($id) + public function find($id = null) { $builder = $this->builder(); if ($this->tempUseSoftDeletes === true) { - $builder->where('deleted', 0); + $builder->where($this->deletedField, 0); } if (is_array($id)) { - $row = $builder->whereIn($this->primaryKey, $id) - ->get(); - $row = $row->getResult(); + $row = $builder->whereIn($this->table.'.'.$this->primaryKey, $id) + ->get(); + $row = $row->getResult($this->tempReturnType); } - else + elseif (is_numeric($id) || is_string($id)) { - $row = $builder->where($this->primaryKey, $id) - ->get(); + $row = $builder->where($this->table.'.'.$this->primaryKey, $id) + ->get(); $row = $row->getFirstRow($this->tempReturnType); } - - $this->tempReturnType = $this->returnType; - $this->tempUseSoftDeletes = $this->useSoftDeletes; - - return $row; - } - - //-------------------------------------------------------------------- - - /** - * Extract a subset of data - * - * @param $key - * @param null $value - * - * @return array|null The rows of data. - */ - public function findWhere($key, $value = null) - { - $builder = $this->builder(); - - if ($this->tempUseSoftDeletes === true) + else { - $builder->where('deleted', 0); - } + $row = $builder->get(); - $rows = $builder->where($key, $value) - ->get(); + $row = $row->getResult($this->tempReturnType); + } - $rows = $rows->getResult($this->tempReturnType); + $row = $this->trigger('afterFind', ['id' => $id, 'data' => $row]); - $this->tempReturnType = $this->returnType; + $this->tempReturnType = $this->returnType; $this->tempUseSoftDeletes = $this->useSoftDeletes; - return $rows; + return $row['data']; } //-------------------------------------------------------------------- @@ -303,18 +356,20 @@ public function findAll(int $limit = 0, int $offset = 0) if ($this->tempUseSoftDeletes === true) { - $builder->where('deleted', 0); + $builder->where($this->deletedField, 0); } $row = $builder->limit($limit, $offset) - ->get(); + ->get(); $row = $row->getResult($this->tempReturnType); - $this->tempReturnType = $this->returnType; + $row = $this->trigger('afterFind', ['data' => $row, 'limit' => $limit, 'offset' => $offset]); + + $this->tempReturnType = $this->returnType; $this->tempUseSoftDeletes = $this->useSoftDeletes; - return $row; + return $row['data']; } //-------------------------------------------------------------------- @@ -331,220 +386,258 @@ public function first() if ($this->tempUseSoftDeletes === true) { - $builder->where('deleted', 0); + $builder->where($this->deletedField, 0); } // Some databases, like PostgreSQL, need order // information to consistently return correct results. if (empty($builder->QBOrderBy)) { - $builder->orderBy($this->primaryKey, 'asc'); + $builder->orderBy($this->table.'.'.$this->primaryKey, 'asc'); } - + $row = $builder->limit(1, 0) - ->get(); + ->get(); $row = $row->getFirstRow($this->tempReturnType); + $row = $this->trigger('afterFind', ['data' => $row]); + $this->tempReturnType = $this->returnType; - return $row; + return $row['data']; } //-------------------------------------------------------------------- /** - * Finds a single record by a "hashed" primary key. Used in conjunction - * with $this->getIDHash(). + * Captures the builder's set() method so that we can validate the + * data here. This allows it to be used with any of the other + * builder methods and still get validated data, like replace. * - * THIS IS NOT VALID TO USE FOR SECURITY! + * @param $key + * @param string $value + * @param bool|null $escape * - * @param string $hashedID - * - * @return array|null|object + * @return $this */ - public function findByHashedID(string $hashedID) + public function set($key, $value = '', bool $escape = null) { - return $this->find($this->decodeID($hashedID)); + $data = is_array($key) + ? $key + : [$key => $value]; + + $this->tempData['escape'] = $escape; + $this->tempData['data'] = array_merge($this->tempData['data'] ?? [], $data); + + return $this; } //-------------------------------------------------------------------- /** - * Returns a "hashed id", which isn't really hashed, but that's - * become a fairly common term for this. Essentially creates - * an obfuscated id, intended to be used to disguise the - * ID from incrementing IDs to get access to things they shouldn't. - * - * THIS IS NOT VALID TO USE FOR SECURITY! - * - * Note, at some point we might want to move to something more - * complex. The hashid library is good, but only works on integers. - * - * @see http://hashids.org/php/ - * @see http://raymorgan.net/web-development/how-to-obfuscate-integer-ids/ + * A convenience method that will attempt to determine whether the + * data should be inserted or updated. Will work with either + * an array or object. When using with custom class objects, + * you must ensure that the class will provide access to the class + * variables, even if through a magic method. * - * @param $id + * @param array|object $data * - * @return mixed + * @return bool */ - public function encodeID($id) + public function save($data) { - // Strings don't currently have a secure - // method, so simple base64 encoding will work for now. - if (! is_numeric($id)) + // If $data is using a custom class with public or protected + // properties representing the table elements, we need to grab + // them as an array. + if (is_object($data) && ! $data instanceof \stdClass) { - return '=_'.base64_encode($id); + $data = static::classToArray($data, $this->dateFormat); } - $id = (int)$id; - if ($id < 1) + if (is_object($data) && isset($data->{$this->primaryKey})) { - return false; + $response = $this->update($data->{$this->primaryKey}, $data); } - if ($id > pow(2,31)) + elseif (is_array($data) && ! empty($data[$this->primaryKey])) { - return false; + $response = $this->update($data[$this->primaryKey], $data); + } + else + { + $response = $this->insert($data); } - $segment1 = $this->getHash($id,16); - $segment2 = $this->getHash($segment1,8); - $dec = (int)base_convert($segment2,16,10); - $dec = ($dec>$id)?$dec-$id:$dec+$id; - $segment2 = base_convert($dec,10,16); - $segment2 = str_pad($segment2,8,'0',STR_PAD_LEFT); - $segment3 = $this->getHash($segment1.$segment2,8); - $hex = $segment1.$segment2.$segment3; - $bin = pack('H*',$hex); - $oid = base64_encode($bin); - $oid = str_replace(array('+','/','='),array('$',':',''),$oid); - - return $oid; + return $response; } //-------------------------------------------------------------------- /** - * Decodes our hashed id. + * Takes a class an returns an array of it's public and protected + * properties as an array suitable for use in creates and updates. * - * @see http://raymorgan.net/web-development/how-to-obfuscate-integer-ids/ + * @param string|object $data + * @param string $dateFormat * - * @param $hash - * - * @return mixed + * @return array */ - public function decodeID($hash) + public static function classToArray($data, string $dateFormat = 'datetime'): array { - // Was it a simple string we encoded? - if (substr($hash, 0, 2) == '=_') - { - $hash = substr($hash, 2); - return base64_decode($hash); - } + $mirror = new \ReflectionClass($data); + $props = $mirror->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED); - if (! preg_match('/^[A-Z0-9\:\$]{21,23}$/i',$hash)) { - return 0; - } - $hash = str_replace(array('$',':'),array('+','/'),$hash); - $bin = base64_decode($hash); - $hex = unpack('H*',$bin); $hex = $hex[1]; - if (! preg_match('/^[0-9a-f]{32}$/',$hex)) - { - return 0; - } - $segment1 = substr($hex,0,16); - $segment2 = substr($hex,16,8); - $segment3 = substr($hex,24,8); - $exp2 = $this->getHash($segment1,8); - $exp3 = $this->getHash($segment1.$segment2,8); - if ($segment3 != $exp3) + $properties = []; + + // Loop over each property, + // saving the name/value in a new array we can return. + foreach ($props as $prop) { - return 0; + // Must make protected values accessible. + $prop->setAccessible(true); + $propName = $prop->getName(); + $properties[$propName] = $prop->getValue($data); + + // Convert any Time instances to appropriate $dateFormat + if ($properties[$propName] instanceof Time) + { + $converted = (string)$properties[$propName]; + + switch($dateFormat) + { + case 'datetime': + $converted = $properties[$propName]->format('Y-m-d H:i:s'); + break; + case 'date': + $converted = $properties[$propName]->format('Y-m-d'); + break; + case 'int': + $converted = $properties[$propName]->getTimestamp(); + break; + } + + $properties[$prop->getName()] = $converted; + } } - $v1 = (int)base_convert($segment2,16,10); - $v2 = (int)base_convert($exp2,16,10); - $id = abs($v1-$v2); - return $id; + return $properties; } //-------------------------------------------------------------------- /** - * Used for our hashed IDs. Requires $salt to be defined - * within the Config\App file. + * Inserts data into the current table. If an object is provided, + * it will attempt to convert it to an array. * - * @param $str - * @param $len + * @param array|object $data + * @param bool $returnID Whether insert ID should be returned or not. * - * @return string + * @return int|string|bool */ - protected function getHash($str, $len) + public function insert($data = null, bool $returnID = true) { - return substr(sha1($str.$this->salt),0,$len); - } + $escape = null; - //-------------------------------------------------------------------- + if (empty($data)) + { + $data = $this->tempData['data'] ?? null; + $escape = $this->tempData['escape'] ?? null; + $this->tempData = []; + } - /** - * A convenience method that will attempt to determine whether the - * data should be inserted or updated. Will work with either - * an array or object. When using with custom class objects, - * you must ensure that the class will provide access to the class - * variables, even if through a magic method. - * - * @param $data - * - * @return bool - */ - public function save($data) - { - if (is_object($data) && isset($data->{$this->primaryKey})) + // If $data is using a custom class with public or protected + // properties representing the table elements, we need to grab + // them as an array. + if (is_object($data) && ! $data instanceof \stdClass) { - return $this->update($data->{$this->primaryKey}, $data); + $data = static::classToArray($data, $this->dateFormat); } - elseif (is_array($data) && array_key_exists($this->primaryKey, $data)) + + // If it's still a stdClass, go ahead and convert to + // an array so doProtectFields and other model methods + // don't have to do special checks. + if (is_object($data)) { - return $this->update($data[$this->primaryKey], $data); + $data = (array) $data; } - return $this->insert($data); - } + // Validate data before saving. + if ($this->skipValidation === false) + { + if ($this->validate($data) === false) + { + return false; + } + } - //-------------------------------------------------------------------- + // Save the original data so it can be passed to + // any Model Event callbacks and not stripped + // by doProtectFields + $originalData = $data; - /** - * Inserts data into the current table. If an object is provided, - * it will attempt to convert it to an array. - * - * @param $data - * - * @return bool - */ - public function insert($data) - { // Must be called first so we don't // strip out created_at values. $data = $this->doProtectFields($data); if ($this->useTimestamps && ! array_key_exists($this->createdField, $data)) { - $data[$this->createdField] = $this->setDate(); + $date = $this->setDate(); + $data[$this->createdField] = $date; + $data[$this->updatedField] = $date; } + $data = $this->trigger('beforeInsert', ['data' => $data]); + if (empty($data)) { - throw new \InvalidArgumentException('No data to insert.'); + throw DataException::forEmptyDataset('insert'); } // Must use the set() method to ensure objects get converted to arrays - $return = $this->builder() - ->set($data) - ->insert(); + $result = $this->builder() + ->set($data['data'], '', $escape) + ->insert(); + + $this->trigger('afterInsert', ['data' => $originalData, 'result' => $result]); + + // If insertion failed, get our of here + if ( ! $result) + { + return $result; + } + + // otherwise return the insertID, if requested. + return $returnID ? $this->db->insertID() : $result; + } + + //-------------------------------------------------------------------- - if (! $return) return $return; + /** + * Compiles batch insert strings and runs the queries, validating each row prior. + * + * @param array $set An associative array of insert values + * @param bool $escape Whether to escape values and identifiers + * + * @param int $batchSize + * @param bool $testing + * + * @return int Number of rows inserted or FALSE on failure + */ + public function insertBatch($set = null, $escape = null, $batchSize = 100, $testing = false) + { + if (is_array($set) && $this->skipValidation === false) + { + foreach ($set as $row) + { + if ($this->validate($row) === false) + { + return false; + } + } + } - return $this->db->insertID(); + return $this->builder()->insertBatch($set, $escape, $batchSize, $testing); } //-------------------------------------------------------------------- @@ -553,13 +646,57 @@ public function insert($data) * Updates a single record in $this->table. If an object is provided, * it will attempt to convert it into an array. * - * @param $id - * @param $data + * @param int|array|string $id + * @param array|object $data * * @return bool */ - public function update($id, $data) + public function update($id = null, $data = null) { + $escape = null; + + if (is_numeric($id)) + { + $id = [$id]; + } + + if (empty($data)) + { + $data = $this->tempData['data'] ?? null; + $escape = $this->tempData['escape'] ?? null; + $this->tempData = []; + } + + // If $data is using a custom class with public or protected + // properties representing the table elements, we need to grab + // them as an array. + if (is_object($data) && ! $data instanceof \stdClass) + { + $data = static::classToArray($data, $this->dateFormat); + } + + // If it's still a stdClass, go ahead and convert to + // an array so doProtectFields and other model methods + // don't have to do special checks. + if (is_object($data)) + { + $data = (array) $data; + } + + // Validate data before saving. + if ($this->skipValidation === false) + { + if ($this->validate($data) === false) + { + return false; + } + } + + // Save the original data so it can be passed to + // any Model Event callbacks and not stripped + // by doProtectFields + $originalData = $data; + // Must be called first so we don't // strip out updated_at values. $data = $this->doProtectFields($data); @@ -569,75 +706,107 @@ public function update($id, $data) $data[$this->updatedField] = $this->setDate(); } + $data = $this->trigger('beforeUpdate', ['id' => $id, 'data' => $data]); + if (empty($data)) { - throw new \InvalidArgumentException('No data to update.'); + throw DataException::forEmptyDataset('update'); + } + + $builder = $this->builder(); + + if ($id) + { + $builder = $builder->whereIn($this->table.'.'.$this->primaryKey, $id); } // Must use the set() method to ensure objects get converted to arrays - return $this->builder() - ->where($this->primaryKey, $id) - ->set($data) - ->update(); + $result = $builder + ->set($data['data'], '', $escape) + ->update(); + + $this->trigger('afterUpdate', ['id' => $id, 'data' => $originalData, 'result' => $result]); + + return $result; } //-------------------------------------------------------------------- /** - * Deletes a single record from $this->table where $id matches - * the table's primaryKey + * Update_Batch * - * @param mixed $id The rows primary key - * @param bool $purge Allows overriding the soft deletes setting. + * Compiles an update string and runs the query * - * @return mixed - * @throws DatabaseException + * @param array $set An associative array of update values + * @param string $index The where key + * @param int $batchSize The size of the batch to run + * @param bool $returnSQL True means SQL is returned, false will execute the query + * + * @return mixed Number of rows affected or FALSE on failure + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function delete($id, $purge = false) + public function updateBatch($set = null, $index = null, $batchSize = 100, $returnSQL = false) { - if ($this->useSoftDeletes && ! $purge) + if (is_array($set) && $this->skipValidation === false) { - return $this->builder() - ->where($this->primaryKey, $id) - ->update(['deleted' => 1]); + foreach ($set as $row) + { + if ($this->validate($row) === false) + { + return false; + } + } } - return $this->builder() - ->where($this->primaryKey, $id) - ->delete(); + return $this->builder()->updateBatch($set, $index, $batchSize, $returnSQL); } //-------------------------------------------------------------------- /** - * Deletes multiple records from $this->table where the specified - * key/value matches. + * Deletes a single record from $this->table where $id matches + * the table's primaryKey * - * @param $key - * @param null $value - * @param bool $purge Allows overriding the soft deletes setting. + * @param int|array|null $id The rows primary key(s) + * @param bool $purge Allows overriding the soft deletes setting. * * @return mixed - * @throws DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function deleteWhere($key, $value = null, $purge = false) + public function delete($id = null, $purge = false) { - // Don't let them shoot themselves in the foot... - if (empty($key)) + if (! empty($id) && is_numeric($id)) + { + $id = [$id]; + } + + $builder = $this->builder(); + if (! empty($id)) { - throw new DatabaseException('You must provided a valid key to deleteWhere.'); + $builder = $builder->whereIn($this->primaryKey, $id); } + $this->trigger('beforeDelete', ['id' => $id, 'purge' => $purge]); + if ($this->useSoftDeletes && ! $purge) { - return $this->builder() - ->where($key, $value) - ->update(['deleted' => 1]); + $set[$this->deletedField] = 1; + + if ($this->useTimestamps) + { + $set[$this->updatedField] = $this->setDate(); + } + + $result = $builder->update($set); + } + else + { + $result = $builder->delete(); } - return $this->builder() - ->where($key, $value) - ->delete(); + $this->trigger('afterDelete', ['id' => $id, 'purge' => $purge, 'result' => $result, 'data' => null]); + + return $result; } //-------------------------------------------------------------------- @@ -647,7 +816,6 @@ public function deleteWhere($key, $value = null, $purge = false) * through soft deletes (deleted = 1) * * @return bool|mixed - * @throws DatabaseException */ public function purgeDeleted() { @@ -657,8 +825,8 @@ public function purgeDeleted() } return $this->builder() - ->where('deleted', 1) - ->delete(); + ->where($this->deletedField, 1) + ->delete(); } //-------------------------------------------------------------------- @@ -669,7 +837,7 @@ public function purgeDeleted() * * @param bool $val * - * @return $this + * @return Model */ public function withDeleted($val = true) { @@ -684,26 +852,52 @@ public function withDeleted($val = true) * Works with the find* methods to return only the rows that * have been deleted. * - * @return $this + * @return Model */ public function onlyDeleted() { $this->tempUseSoftDeletes = false; $this->builder() - ->where('deleted', 1); + ->where($this->deletedField, 1); return $this; } //-------------------------------------------------------------------- + /** + * Replace + * + * Compiles an replace into string and runs the query + * + * @param null $data + * @param bool $returnSQL + * + * @return bool TRUE on success, FALSE on failure + */ + public function replace($data = null, $returnSQL = false) + { + // Validate data before saving. + if (! empty($data) && $this->skipValidation === false) + { + if ($this->validate($data) === false) + { + return false; + } + } + + return $this->builder()->replace($data, $returnSQL); + } + //-------------------------------------------------------------------- // Utility //-------------------------------------------------------------------- /** * Sets the return type of the results to be as an associative array. + * + * @return Model */ public function asArray() { @@ -722,7 +916,7 @@ public function asArray() * * @param string $class * - * @return $this + * @return Model */ public function asObject(string $class = 'object') { @@ -741,12 +935,12 @@ public function asObject(string $class = 'object') * @param int $size * @param \Closure $userFunc * - * @throws DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DataException */ public function chunk($size = 100, \Closure $userFunc) { $total = $this->builder() - ->countAllResults(false); + ->countAllResults(false); $offset = 0; @@ -758,7 +952,7 @@ public function chunk($size = 100, \Closure $userFunc) if ($rows === false) { - throw new DatabaseException('Unable to get results from the query.'); + throw DataException::forEmptyDataset('chunk'); } $rows = $rows->getResult(); @@ -787,13 +981,24 @@ public function chunk($size = 100, \Closure $userFunc) * Expects a GET variable (?page=2) that specifies the page of results * to display. * - * @param int $perPage + * @param int $perPage + * @param string $group Will be used by the pagination library + * to identify a unique pagination set. + * @param int $page Optional page number (useful when the page number is provided in different way) * * @return array|null */ - public function paginate($perPage = 20) + public function paginate(int $perPage = 20, string $group = 'default', int $page = 0) { - $page = $_GET['page'] ?? 1; + // Get the necessary parts. + $page = $page >= 1 ? $page : (ctype_digit($_GET['page'] ?? '') && $_GET['page'] > 1 ? $_GET['page'] : 1); + + $total = $this->countAllResults(false); + + // Store it in the Pager library so it can be + // paginated in the views. + $pager = \Config\Services::pager(); + $this->pager = $pager->store($group, $page, $perPage, $total); $offset = ($page - 1) * $perPage; @@ -808,24 +1013,23 @@ public function paginate($perPage = 20) * * @param bool $protect * - * @return $this + * @return Model */ public function protect(bool $protect = true) { - $this->protectFields = $protect; + $this->protectFields = $protect; return $this; } //-------------------------------------------------------------------- - /** * Provides a shared instance of the Query Builder. * * @param string $table * - * @return BaseBuilder|Database\QueryBuilder + * @return BaseBuilder */ protected function builder(string $table = null) { @@ -856,18 +1060,21 @@ protected function builder(string $table = null) * Used by insert() and update() to protect against mass assignment * vulnerabilities. * - * @param $data + * @param array $data * - * @return mixed - * @throws DatabaseException + * @return array + * @throws \CodeIgniter\Database\Exceptions\DataException */ protected function doProtectFields($data) { - if (empty($this->allowedFields)) + if ($this->protectFields === false) { - if ($this->protectFields === false) return $data; + return $data; + } - throw new DatabaseException('No Allowed fields specified for model: '. get_class($this)); + if (empty($this->allowedFields)) + { + throw DataException::forInvalidAllowedFields(get_class($this)); } foreach ($data as $key => $val) @@ -900,7 +1107,7 @@ protected function doProtectFields($data) */ protected function setDate($userData = null) { - $currentDate = is_numeric($userData) ? (int)$userData : time(); + $currentDate = is_numeric($userData) ? (int) $userData : time(); switch ($this->dateFormat) { @@ -920,27 +1127,248 @@ protected function setDate($userData = null) /** * Specify the table associated with a model - * + * * @param string $table * - * @return $this + * @return Model */ public function setTable(string $table) { - $this->table = $table; + $this->table = $table; return $this; } //-------------------------------------------------------------------- + /** + * Grabs the last error(s) that occurred. If data was validated, + * it will first check for errors there, otherwise will try to + * grab the last error from the Database connection. + * + * @param bool $forceDB Always grab the db error, not validation + * + * @return array|null + */ + public function errors(bool $forceDB = false) + { + // Do we have validation errors? + if ($forceDB === false && $this->skipValidation === false) + { + $errors = $this->validation->getErrors(); + + if ( ! empty($errors)) + { + return $errors; + } + } + + // Still here? Grab the database-specific error, if any. + $error = $this->db->getError(); + + return $error['message'] ?? null; + } + + //-------------------------------------------------------------------- + //-------------------------------------------------------------------- + // Validation + //-------------------------------------------------------------------- + + /** + * Set the value of the skipValidation flag. + * + * @param bool $skip + * + * @return Model + */ + public function skipValidation(bool $skip = true) + { + $this->skipValidation = $skip; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Validate the data against the validation rules (or the validation group) + * specified in the class property, $validationRules. + * + * @param array $data + * + * @return bool + */ + public function validate($data): bool + { + if ($this->skipValidation === true || empty($this->validationRules) || empty($data)) + { + return true; + } + + // Query Builder works with objects as well as arrays, + // but validation requires array, so cast away. + if (is_object($data)) + { + $data = (array) $data; + } + + // ValidationRules can be either a string, which is the group name, + // or an array of rules. + if (is_string($this->validationRules)) + { + $valid = $this->validation->run($data, $this->validationRules); + } + 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); + } + + return (bool) $valid; + } + + //-------------------------------------------------------------------- + + /** + * Replace any placeholders within the rules with the values that + * match the 'key' of any properties being set. For example, if + * we had the following $data array: + * + * [ 'id' => 13 ] + * + * and the following rule: + * + * 'required|is_unique[users,email,id,{id}]' + * + * The value of {id} would be replaced with the actual id in the form data: + * + * 'required|is_unique[users,email,id,13]' + * + * @param array $rules + * @param array $data + * + * @return array + */ + protected function fillPlaceholders(array $rules, array $data) + { + $replacements = []; + + foreach ($data as $key => $value) + { + $replacements["{{$key}}"] = $value; + } + + if (! empty($replacements)) + { + foreach ($rules as &$rule) + { + if (is_array($rule)) + { + foreach ($rule as &$row) + { + $row = strtr($row, $replacements); + } + continue; + } + + $rule = strtr($rule, $replacements); + } + } + + return $rules; + } + + //-------------------------------------------------------------------- + + /** + * Returns the model's defined validation rules so that they + * can be used elsewhere, if needed. + * + * @return array + */ + public function getValidationRules(array $options=[]) + { + $rules = $this->validationRules; + + if (isset($options['except'])) + { + $rules = array_diff_key($rules, array_flip($options['except'])); + } + elseif (isset($options['only'])) + { + $rules = array_intersect_key($rules, array_flip($options['only'])); + } + + return $rules; + } + + //-------------------------------------------------------------------- + + /** + * Returns the model's define validation messages so they + * can be used elsewhere, if needed. + * + * @return array + */ + public function getValidationMessages() + { + return $this->validationMessages; + } + + //-------------------------------------------------------------------- + + /** + * A simple event trigger for Model Events that allows additional + * data manipulation within the model. Specifically intended for + * usage by child models this can be used to format data, + * save/load related classes, etc. + * + * It is the responsibility of the callback methods to return + * the data itself. + * + * Each $data array MUST have a 'data' key with the relevant + * data for callback methods (like an array of key/value pairs to insert + * or update, an array of results, etc) + * + * @param string $event + * @param array $data + * + * @return mixed + * @throws \CodeIgniter\Database\Exceptions\DataException + */ + protected function trigger(string $event, array $data) + { + // Ensure it's a valid event + if ( ! isset($this->{$event}) || empty($this->{$event})) + { + return $data; + } + + foreach ($this->{$event} as $callback) + { + if ( ! method_exists($this, $callback)) + { + throw DataException::forInvalidMethodTriggered($callback); + } + + $data = $this->{$callback}($data); + } + + return $data; + } + + //-------------------------------------------------------------------- //-------------------------------------------------------------------- // Magic //-------------------------------------------------------------------- /** - * Provides/instantiates the builder/db connection. + * Provides/instantiates the builder/db connection and model's table/primary key names and return type. * * @param string $name * @@ -948,7 +1376,11 @@ public function setTable(string $table) */ public function __get(string $name) { - if (isset($this->db->$name)) + if(in_array($name, ['primaryKey', 'table', 'returnType'])) + { + return $this->{$name}; + } + elseif (isset($this->db->$name)) { return $this->db->$name; } @@ -969,29 +1401,29 @@ public function __get(string $name) * @param string $name * @param array $params * - * @return $this|null + * @return Model|null */ - public function __call(string $name, array $params) + public function __call($name, array $params) { $result = null; if (method_exists($this->db, $name)) { - $result = call_user_func_array([$this->db, $name], $params); + $result = $this->db->$name(...$params); } - elseif (method_exists($this->builder(), $name)) + elseif (method_exists($builder = $this->builder(), $name)) { - $result = call_user_func_array([$this->builder(), $name], $params); + $result = $builder->$name(...$params); } - // Don't return the builder object, since - // that will interrupt the usability flow + // Don't return the builder object unless specifically requested + //, since that will interrupt the usability flow // and break intermingling of model and builder methods. - if (empty($result)) + if ($name !== 'builder' && empty($result)) { return $result; } - if ( ! $result instanceof BaseBuilder) + if ($name !== 'builder' && ! $result instanceof BaseBuilder) { return $result; } @@ -1000,5 +1432,4 @@ public function __call(string $name, array $params) } //-------------------------------------------------------------------- - } diff --git a/system/Pager/Exceptions/PagerException.php b/system/Pager/Exceptions/PagerException.php new file mode 100644 index 000000000000..239d7939e94e --- /dev/null +++ b/system/Pager/Exceptions/PagerException.php @@ -0,0 +1,17 @@ +config = $config; + $this->view = $view; + } + + //-------------------------------------------------------------------- + + /** + * Handles creating and displaying the + * + * @param string $group + * @param string $template The output template alias to render. + * + * @return string + */ + public function links(string $group = 'default', string $template = 'default_full'): string + { + $this->ensureGroup($group); + + return $this->displayLinks($group, $template); + } + + //-------------------------------------------------------------------- + + /** + * Creates simple Next/Previous links, instead of full pagination. + * + * @param string $group + * @param string $template + * + * @return string + */ + public function simpleLinks(string $group = 'default', string $template = 'default_simple'): string + { + $this->ensureGroup($group); + + return $this->displayLinks($group, $template); + } + + //-------------------------------------------------------------------- + + /** + * Allows for a simple, manual, form of pagination where all of the data + * is provided by the user. The URL is the current URI. + * + * @param int $page + * @param int $perPage + * @param int $total + * @param string $template The output template alias to render. + * + * @return string + */ + public function makeLinks(int $page, int $perPage, int $total, string $template = 'default_full'): string + { + $name = time(); + + $this->store($name, $page, $perPage, $total); + + return $this->displayLinks($name, $template); + } + + //-------------------------------------------------------------------- + + /** + * Does the actual work of displaying the view file. Used internally + * by links(), simpleLinks(), and makeLinks(). + * + * @param string $group + * @param string $template + * + * @return string + */ + protected function displayLinks(string $group, string $template) + { + $pager = new PagerRenderer($this->getDetails($group)); + + if ( ! array_key_exists($template, $this->config->templates)) + { + throw PagerException::forInvalidTemplate($template); + } + + return $this->view->setVar('pager', $pager) + ->render($this->config->templates[$template]); + } + + //-------------------------------------------------------------------- + + /** + * Stores a set of pagination data for later display. Most commonly used + * by the model to automate the process. + * + * @param string $group + * @param int $page + * @param int $perPage + * @param int $total + * + * @return mixed + */ + public function store(string $group, int $page, int $perPage, int $total) + { + $this->ensureGroup($group); + + $this->groups[$group]['currentPage'] = $page; + $this->groups[$group]['perPage'] = $perPage; + $this->groups[$group]['total'] = $total; + $this->groups[$group]['pageCount'] = ceil($total / $perPage); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Sets the path that an aliased group of links will use. + * + * @param string $group + * @param string $path + * + * @return mixed + */ + public function setPath(string $path, string $group = 'default') + { + $this->ensureGroup($group); + + $this->groups[$group]['uri']->setPath($path); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Returns the total number of pages. + * + * @param string|null $group + * + * @return int + */ + public function getPageCount(string $group = 'default'): int + { + $this->ensureGroup($group); + + return $this->groups[$group]['pageCount']; + } + + //-------------------------------------------------------------------- + + /** + * Returns the number of the current page of results. + * + * @param string|null $group + * + * @return int + */ + public function getCurrentPage(string $group = 'default'): int + { + $this->ensureGroup($group); + + return $this->groups[$group]['currentPage']; + } + + //-------------------------------------------------------------------- + + /** + * Tells whether this group of results has any more pages of results. + * + * @param string|null $group + * + * @return bool + */ + public function hasMore(string $group = 'default'): bool + { + $this->ensureGroup($group); + + return ($this->groups[$group]['currentPage'] * $this->groups[$group]['perPage']) < $this->groups[$group]['total']; + } + + //-------------------------------------------------------------------- + + /** + * Returns the last page, if we have a total that we can calculate with. + * + * @param string $group + * + * @return int|null + */ + public function getLastPage(string $group = 'default') + { + $this->ensureGroup($group); + + if ( ! is_numeric($this->groups[$group]['total']) || ! is_numeric($this->groups[$group]['perPage'])) + { + return null; + } + + return ceil($this->groups[$group]['total'] / $this->groups[$group]['perPage']); + } + + //-------------------------------------------------------------------- + + /** + * Determines the first page # that should be shown. + * + * @param string $group + * + * @return int + */ + public function getFirstPage(string $group = 'default') + { + $this->ensureGroup($group); + + // @todo determine based on a 'surroundCount' value + return 1; + } + + //-------------------------------------------------------------------- + + /** + * Returns the URI for a specific page for the specified group. + * + * @param int|null $page + * @param string $group + * @param bool $returnObject + * + * @return string|\CodeIgniter\HTTP\URI + */ + public function getPageURI(int $page = null, string $group = 'default', $returnObject = false) + { + $this->ensureGroup($group); + + $uri = $this->groups[$group]['uri']; + + if ($this->only) + { + $query = array_intersect_key($_GET, array_flip($this->only)); + + $query['page'] = $page; + + $uri->setQueryArray($query); + } + else + { + $uri->addQuery('page', $page); + } + + return $returnObject === true ? $uri : (string) $uri; + } + + //-------------------------------------------------------------------- + + /** + * Returns the full URI to the next page of results, or null. + * + * @param string $group + * @param bool $returnObject + * + * @return string|null + */ + public function getNextPageURI(string $group = 'default', $returnObject = false) + { + $this->ensureGroup($group); + + $last = $this->getLastPage($group); + $curr = $this->getCurrentPage($group); + $page = null; + + if ( ! empty($last) && ! empty($curr) && $last == $curr) + { + return null; + } + + if ($last > $curr) + { + $page = $curr + 1; + } + + return $this->getPageURI($page, $group, $returnObject); + } + + //-------------------------------------------------------------------- + + /** + * Returns the full URL to the previous page of results, or null. + * + * @param string $group + * @param bool $returnObject + * + * @return string|null + */ + public function getPreviousPageURI(string $group = 'default', $returnObject = false) + { + $this->ensureGroup($group); + + $first = $this->getFirstPage($group); + $curr = $this->getCurrentPage($group); + $page = null; + + if ( ! empty($first) && ! empty($curr) && $first == $curr) + { + return null; + } + + if ($first < $curr) + { + $page = $curr - 1; + } + + return $this->getPageURI($page, $group, $returnObject); + } + + //-------------------------------------------------------------------- + + /** + * Returns the number of results per page that should be shown. + * + * @param string $group + * + * @return int + */ + public function getPerPage(string $group = 'default'): int + { + $this->ensureGroup($group); + + return (int) $this->groups[$group]['perPage']; + } + + //-------------------------------------------------------------------- + + /** + * Returns an array with details about the results, including + * total, per_page, current_page, last_page, next_url, prev_url, from, to. + * Does not include the actual data. This data is suitable for adding + * a 'data' object to with the result set and converting to JSON. + * + * @param string $group + * + * @return array + */ + public function getDetails(string $group = 'default'): array + { + if ( ! array_key_exists($group, $this->groups)) + { + throw PagerException::forInvalidPaginationGroup($group); + } + + $newGroup = $this->groups[$group]; + + $newGroup['next'] = $this->getNextPageURI($group); + $newGroup['previous'] = $this->getPreviousPageURI($group); + + return $newGroup; + } + + //-------------------------------------------------------------------- + + /** + * Sets only allowed queries on pagination links. + * + * @param array $queries + * + * @return Pager + */ + public function only(array $queries):Pager + { + $this->only = $queries; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Ensures that an array exists for the group specified. + * + * @param string $group + */ + protected function ensureGroup(string $group) + { + if (array_key_exists($group, $this->groups)) + { + return; + } + + $this->groups[$group] = [ + 'uri' => clone Services::request()->uri, + 'hasMore' => false, + 'total' => null, + 'currentPage' => $_GET['page_' . $group] ?? $_GET['page'] ?? 1, + 'perPage' => $this->config->perPage, + 'pageCount' => 1, + ]; + + if ($_GET) + { + $this->groups[$group]['uri'] = $this->groups[$group]['uri']->setQueryArray($_GET); + } + } + + //-------------------------------------------------------------------- +} diff --git a/system/Pager/PagerInterface.php b/system/Pager/PagerInterface.php new file mode 100644 index 000000000000..ab0ec8064062 --- /dev/null +++ b/system/Pager/PagerInterface.php @@ -0,0 +1,197 @@ +first = 1; + $this->last = $details['pageCount']; + $this->current = $details['currentPage']; + $this->total = $details['total']; + $this->uri = $details['uri']; + $this->pageCount = $details['pageCount']; + } + + //-------------------------------------------------------------------- + + /** + * Sets the total number of links that should appear on either + * side of the current page. Adjusts the first and last counts + * to reflect it. + * + * @param int|null $count + * + * @return PagerRenderer + */ + public function setSurroundCount(int $count = null) + { + $this->updatePages($count); + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Checks to see if there is a "previous" page before our "first" page. + * + * @return bool + */ + public function hasPrevious(): bool + { + return $this->first > 1; + } + + //-------------------------------------------------------------------- + + /** + * Returns a URL to the "previous" page. The previous page is NOT the + * page before the current page, but is the page just before the + * "first" page. + * + * You MUST call hasPrevious() first, or this value may be invalid. + * + * @return string|null + */ + public function getPrevious() + { + if ( ! $this->hasPrevious()) + { + return null; + } + + $uri = clone $this->uri; + + $uri->addQuery('page', $this->first - 1); + + return (string) $uri; + } + + //-------------------------------------------------------------------- + + /** + * Checks to see if there is a "next" page after our "last" page. + * + * @return bool + */ + public function hasNext(): bool + { + return $this->pageCount > $this->last; + } + + //-------------------------------------------------------------------- + + /** + * Returns a URL to the "next" page. The next page is NOT, the + * page after the current page, but is the page that follows the + * "last" page. + * + * You MUST call hasNext() first, or this value may be invalid. + * + * @return string|null + */ + public function getNext() + { + if ( ! $this->hasNext()) + { + return null; + } + + $uri = clone $this->uri; + + $uri->addQuery('page', $this->last + 1); + + return (string) $uri; + } + + //-------------------------------------------------------------------- + + /** + * Returns the URI of the first page. + * + * @return string + */ + public function getFirst(): string + { + $uri = clone $this->uri; + + $uri->addQuery('page', 1); + + return (string) $uri; + } + + //-------------------------------------------------------------------- + + /** + * Returns the URI of the last page. + * + * @return string + */ + public function getLast(): string + { + $uri = clone $this->uri; + + $uri->addQuery('page', $this->pageCount); + + return (string) $uri; + } + + //-------------------------------------------------------------------- + + /** + * Returns an array of links that should be displayed. Each link + * is represented by another array containing of the URI the link + * should go to, the title (number) of the link, and a boolean + * value representing whether this link is active or not. + * + * @return array + */ + public function links(): array + { + $links = []; + + $uri = clone $this->uri; + + for ($i = $this->first; $i <= $this->last; $i ++ ) + { + $links[] = [ + 'uri' => (string) $uri->addQuery('page', $i), + 'title' => (int) $i, + 'active' => ($i == $this->current) + ]; + } + + return $links; + } + + //-------------------------------------------------------------------- + + /** + * Updates the first and last pages based on $surroundCount, + * which is the number of links surrounding the active page + * to show. + * + * @param int|null $count The new "surroundCount" + */ + protected function updatePages(int $count = null) + { + if (is_null($count)) + { + return; + } + + $this->first = $this->current - $count > 0 ? (int) ($this->current - $count) : 1; + $this->last = $this->current + $count <= $this->pageCount ? (int) ($this->current + $count) : (int) $this->pageCount; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Pager/Views/default_full.php b/system/Pager/Views/default_full.php new file mode 100644 index 000000000000..4eb10aa79235 --- /dev/null +++ b/system/Pager/Views/default_full.php @@ -0,0 +1,39 @@ +setSurroundCount(2) ?> + + diff --git a/system/Pager/Views/default_simple.php b/system/Pager/Views/default_simple.php new file mode 100644 index 000000000000..bc550b4f80b5 --- /dev/null +++ b/system/Pager/Views/default_simple.php @@ -0,0 +1,15 @@ +setSurroundCount(0) ?> + diff --git a/system/Router/Exceptions/RouterException.php b/system/Router/Exceptions/RouterException.php new file mode 100644 index 000000000000..7117c55f8fbd --- /dev/null +++ b/system/Router/Exceptions/RouterException.php @@ -0,0 +1,17 @@ + '.*', - 'segment' => '[^/]+', - 'num' => '[0-9]+', - 'alpha' => '[a-zA-Z]+', - 'alphanum' => '[a-zA-Z0-9]+', + 'any' => '.*', + 'segment' => '[^/]+', + 'alphanum' => '[a-zA-Z0-9]+', + 'num' => '[0-9]+', + 'alpha' => '[a-zA-Z]+', + 'hash' => '[^/]+', ]; /** @@ -127,7 +131,25 @@ class RouteCollection implements RouteCollectionInterface * * @var array */ - protected $routes = []; + protected $routes = [ + '*' => [], + 'options' => [], + 'get' => [], + 'head' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'trace' => [], + 'connect' => [], + 'cli' => [], + ]; + + /** + * Array of routes options + * + * @var array + */ + protected $routesOptions = []; /** * The current method that the script is being called by. @@ -165,15 +187,38 @@ class RouteCollection implements RouteCollectionInterface */ protected $currentOptions = null; + /** + * A little performance booster. + * @var bool + */ + protected $didDiscover = false; + + /** + * @var \CodeIgniter\Autoloader\FileLocator + */ + protected $fileLocator; + + /** + * @var \Config\Modules + */ + protected $moduleConfig; + //-------------------------------------------------------------------- /** * Constructor + * + * @param FileLocator $locator + * @param Config/Modules $moduleConfig */ - public function __construct() + public function __construct(FileLocator $locator, $moduleConfig) { // Get HTTP verb - $this->HTTPVerb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli'; + $this->HTTPVerb = strtolower($_SERVER['REQUEST_METHOD'] ?? 'cli'); + + $this->fileLocator = $locator; + + $this->moduleConfig = $moduleConfig; } //-------------------------------------------------------------------- @@ -191,7 +236,7 @@ public function __construct() * * @return mixed */ - public function addPlaceholder(string $placeholder, string $pattern = null): RouteCollectionInterface + public function addPlaceholder($placeholder, string $pattern = null): RouteCollectionInterface { if ( ! is_array($placeholder)) { @@ -287,7 +332,7 @@ public function setTranslateURIDashes(bool $value): RouteCollectionInterface * * @param bool $value * - * @return RouteCollection + * @return RouteCollectionInterface */ public function setAutoRoute(bool $value): RouteCollectionInterface { @@ -307,11 +352,11 @@ public function setAutoRoute(bool $value): RouteCollectionInterface * * @param callable|null $callable * - * @return $this + * @return RouteCollectionInterface */ public function set404Override($callable = null): RouteCollectionInterface { - $this->override404 = $callable; + $this->override404 = $callable; return $this; } @@ -331,15 +376,57 @@ public function get404Override() //-------------------------------------------------------------------- + /** + * Will attempt to discover any additional routes, either through + * the local PSR4 namespaces, or through selected Composer packages. + * (Composer coming soon...) + */ + protected function discoverRoutes() + { + if ($this->didDiscover) return; + + // We need this var in local scope + // so route files can access it. + $routes = $this; + + /* + * Discover Local Files + */ + if ($this->moduleConfig->shouldDiscover('routes')) + { + $files = $this->fileLocator->search('Config/Routes.php'); + + foreach ($files as $file) + { + // Don't include our main file again... + if ($file == APPPATH . 'Config/Routes.php') + continue; + + include $file; + } + } + + /* + * Discover Composer files (coming soon) + */ + + $this->didDiscover = true; + } + + //-------------------------------------------------------------------- + /** * Sets the default constraint to be used in the system. Typically * for use with the 'resources' method. * - * @param $placeholder + * @param string $placeholder + * + * @return RouteCollectionInterface */ public function setDefaultConstraint(string $placeholder): RouteCollectionInterface { - if (array_key_exists($placeholder, $this->placeholders)) { + if (array_key_exists($placeholder, $this->placeholders)) + { $this->defaultPlaceholder = $placeholder; } @@ -373,9 +460,19 @@ public function getDefaultMethod(): string //-------------------------------------------------------------------- /** - * Returns the current value of the translateURIDashses setting. + * Returns the default namespace as set in the Routes config file. * - * @param bool|false $val + * @return string + */ + public function getDefaultNamespace(): string + { + return $this->defaultNamespace; + } + + //-------------------------------------------------------------------- + + /** + * Returns the current value of the translateURIDashses setting. * * @return mixed */ @@ -401,16 +498,33 @@ public function shouldAutoRoute(): bool /** * Returns the raw array of available routes. * + * @param null $verb + * * @return array */ - public function getRoutes(): array + public function getRoutes($verb = null): array { + if (empty($verb)) + { + $verb = $this->getHTTPVerb(); + } + + // Since this is the entry point for the Router, + // take a moment to do any route discovery + // we might need to do. + $this->discoverRoutes(); + $routes = []; - foreach ($this->routes as $r) + if (isset($this->routes[$verb])) { - $key = key($r['route']); - $routes[$key] = $r['route'][$key]; + $collection = array_merge($this->routes['*'], $this->routes[$verb]); + + foreach ($collection as $r) + { + $key = key($r['route']); + $routes[$key] = $r['route'][$key]; + } } return $routes; @@ -418,6 +532,20 @@ public function getRoutes(): array //-------------------------------------------------------------------- + /** + * Returns one or all routes options + * + * @param string $from + * + * @return array + */ + public function getRoutesOptions(string $from = null) + { + return $from ? $this->routesOptions[$from] ?? [] : $this->routesOptions; + } + + //-------------------------------------------------------------------- + /** * Returns the current HTTP Verb being used. * @@ -442,17 +570,16 @@ public function getHTTPVerb(): string */ public function map(array $routes = [], array $options = null): RouteCollectionInterface { - foreach ($routes as $from => $to) - { - $this->add($from, $to, $options); - } + foreach ($routes as $from => $to) + { + $this->add($from, $to, $options); + } return $this; } //-------------------------------------------------------------------- - /** * Adds a single route to the collection. * @@ -463,11 +590,11 @@ public function map(array $routes = [], array $options = null): RouteCollectionI * @param array|string $to * @param $options * - * @return self RouteCollectionInterface + * @return RouteCollectionInterface */ public function add(string $from, $to, array $options = null): RouteCollectionInterface { - $this->create($from, $to, $options); + $this->create('*', $from, $to, $options); return $this; } @@ -482,16 +609,18 @@ public function add(string $from, $to, array $options = null): RouteCollectionIn * @param string $from The pattern to match against * @param string $to Either a route name or a URI to redirect to * @param int $status The HTTP status code that should be returned with this redirect + * + * @return RouteCollection */ public function addRedirect(string $from, string $to, int $status = 302) { // Use the named route's pattern if this is a named route. - if (array_key_exists($to, $this->routes)) + if (array_key_exists($to, $this->routes['*'])) { - $to = $this->routes[$to]['route']; + $to = $this->routes['*'][$to]['route']; } - $this->create($from, $to, ['redirect' => $status]); + $this->create('*', $from, $to, ['redirect' => $status]); return $this; } @@ -507,7 +636,7 @@ public function addRedirect(string $from, string $to, int $status = 302) */ public function isRedirect(string $from): bool { - foreach ($this->routes as $name => $route) + foreach ($this->routes['*'] as $name => $route) { // Named route? if ($name == $from || key($route['route']) == $from) @@ -530,7 +659,7 @@ public function isRedirect(string $from): bool */ public function getRedirectCode(string $from): int { - foreach ($this->routes as $name => $route) + foreach ($this->routes['*'] as $name => $route) { // Named route? if ($name == $from || key($route['route']) == $from) @@ -543,9 +672,6 @@ public function getRedirectCode(string $from): int } //-------------------------------------------------------------------- - - - //-------------------------------------------------------------------- // Grouping Routes //-------------------------------------------------------------------- @@ -567,16 +693,16 @@ public function getRedirectCode(string $from): int */ public function group($name, ...$params) { - $oldGroup = $this->group; + $oldGroup = $this->group; $oldOptions = $this->currentOptions; // To register a route, we'll set a flag so that our router // so it will see the group name. - $this->group = ltrim($oldGroup.'/'.$name, '/'); + $this->group = ltrim($oldGroup . '/' . $name, '/'); $callback = array_pop($params); - if (count($params) && is_array($params[0])) + if ($params && is_array($params[0])) { $this->currentOptions = array_shift($params); } @@ -591,7 +717,6 @@ public function group($name, ...$params) } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // HTTP Verb-based routing //-------------------------------------------------------------------- @@ -602,6 +727,7 @@ public function group($name, ...$params) // The options array is typically used to pass in an 'as' or var, but may // be expanded in the future. See the docblock for 'add' method above for // current list of globally available options. + // /** @@ -617,10 +743,12 @@ public function group($name, ...$params) * // Generates the following routes: * HTTP Verb | Path | Action | Used for... * ----------+-------------+---------------+----------------- - * GET /photos listAll display a list of photos + * GET /photos index display a list of photos + * GET /photos/new new new a specific photo + * GET /photos/{id}/edit edit edit a specific photo * GET /photos/{id} show display a specific photo * POST /photos create create a new photo - * PUT /photos/{id} update update an existing photo + * PUT/PATCH /photos/{id} update update an existing photo * DELETE /photos/{id} delete delete an existing photo * * If 'websafe' option is present, the following paths are also available: @@ -642,37 +770,61 @@ public function resource(string $name, array $options = null): RouteCollectionIn // If a new controller is specified, then we replace the // $name value with the name of the new controller. - if (isset($options['controller'])) { + if (isset($options['controller'])) + { $new_name = ucfirst(filter_var($options['controller'], FILTER_SANITIZE_STRING)); } // In order to allow customization of allowed id values // we need someplace to store them. - $id = isset($this->placeholders[$this->defaultPlaceholder]) ? $this->placeholders[$this->defaultPlaceholder] : - '(:segment)'; + $id = $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)'; - if (isset($options['placeholder'])) { + if (isset($options['placeholder'])) + { $id = $options['placeholder']; } // Make sure we capture back-references - $id = '('.trim($id, '()').')'; + $id = '(' . trim($id, '()') . ')'; - $methods = isset($options['only']) - ? is_string($options['only']) ? explode(',', $options['only']) : $options['only'] - : ['listAll', 'show', 'create', 'update', 'delete']; + $methods = isset($options['only']) ? is_string($options['only']) ? explode(',', $options['only']) : $options['only'] : ['index', 'show', 'create', 'update', 'delete', 'new', 'edit']; - if (in_array('listAll', $methods)) $this->get($name, $new_name . '::listAll', $options); - if (in_array('show', $methods)) $this->get($name . '/' . $id, $new_name . '::show/$1', $options); - if (in_array('create', $methods)) $this->post($name, $new_name . '::create', $options); - if (in_array('update', $methods)) $this->put($name . '/' . $id, $new_name . '::update/$1', $options); - if (in_array('delete', $methods)) $this->delete($name . '/' . $id, $new_name . '::delete/$1', $options); + if(isset($options['except'])) + { + $options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']); + $c = count($methods); + for($i = 0; $i < $c; $i++) + { + if(in_array($methods[$i], $options['except'])) + { + unset($methods[$i]); + } + } + } + + if (in_array('index', $methods)) + $this->get($name, $new_name . '::index', $options); + if (in_array('new', $methods)) + $this->get($name. '/new', $new_name . '::new', $options); + if (in_array('edit', $methods)) + $this->get($name . '/' . $id. '/edit', $new_name . '::edit/$1', $options); + if (in_array('show', $methods)) + $this->get($name . '/' . $id, $new_name . '::show/$1', $options); + if (in_array('create', $methods)) + $this->post($name, $new_name . '::create', $options); + if (in_array('update', $methods)) + $this->put($name . '/' . $id, $new_name . '::update/$1', $options); + $this->patch($name . '/' . $id, $new_name . '::update/$1', $options); + if (in_array('delete', $methods)) + $this->delete($name . '/' . $id, $new_name . '::delete/$1', $options); // Web Safe? 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); + if (in_array('delete', $methods)) + $this->post($name . '/' . $id . '/delete', $new_name . '::delete/$1', $options); } return $this; @@ -690,8 +842,10 @@ public function resource(string $name, array $options = null): RouteCollectionIn * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function match(array $verbs = [], string $from, string $to, array $options = null): RouteCollectionInterface + public function match(array $verbs = [], string $from, $to, array $options = null): RouteCollectionInterface { foreach ($verbs as $verb) { @@ -711,13 +865,12 @@ public function match(array $verbs = [], string $from, string $to, array $option * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function get(string $from, string $to, array $options = null): RouteCollectionInterface + public function get(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'get') - { - $this->create($from, $to, $options); - } + $this->create('get', $from, $to, $options); return $this; } @@ -730,13 +883,12 @@ public function get(string $from, string $to, array $options = null): RouteColle * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function post(string $from, string $to, array $options = null): RouteCollectionInterface + public function post(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'post') - { - $this->create($from, $to, $options); - } + $this->create('post', $from, $to, $options); return $this; } @@ -749,13 +901,12 @@ public function post(string $from, string $to, array $options = null): RouteColl * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function put(string $from, string $to, array $options = null): RouteCollectionInterface + public function put(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'put') - { - $this->create($from, $to, $options); - } + $this->create('put', $from, $to, $options); return $this; } @@ -768,13 +919,12 @@ public function put(string $from, string $to, array $options = null): RouteColle * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function delete(string $from, string $to, array $options = null): RouteCollectionInterface + public function delete(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'delete') - { - $this->create($from, $to, $options); - } + $this->create('delete', $from, $to, $options); return $this; } @@ -787,13 +937,12 @@ public function delete(string $from, string $to, array $options = null): RouteCo * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function head(string $from, string $to, array $options = null): RouteCollectionInterface + public function head(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'head') - { - $this->create($from, $to, $options); - } + $this->create('head', $from, $to, $options); return $this; } @@ -806,13 +955,12 @@ public function head(string $from, string $to, array $options = null): RouteColl * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function patch(string $from, string $to, array $options = null): RouteCollectionInterface + public function patch(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'patch') - { - $this->create($from, $to, $options); - } + $this->create('patch', $from, $to, $options); return $this; } @@ -825,13 +973,12 @@ public function patch(string $from, string $to, array $options = null): RouteCol * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function options(string $from, string $to, array $options = null): RouteCollectionInterface + public function options(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'options') - { - $this->create($from, $to, $options); - } + $this->create('options', $from, $to, $options); return $this; } @@ -844,13 +991,12 @@ public function options(string $from, string $to, array $options = null): RouteC * @param $from * @param $to * @param array $options + * + * @return \CodeIgniter\Router\RouteCollectionInterface */ - public function cli(string $from, string $to, array $options = null): RouteCollectionInterface + public function cli(string $from, $to, array $options = null): RouteCollectionInterface { - if ($this->HTTPVerb == 'cli') - { - $this->create($from, $to, $options); - } + $this->create('cli', $from, $to, $options); return $this; } @@ -860,16 +1006,16 @@ public function cli(string $from, string $to, array $options = null): RouteColle /** * Limits the routes to a specified ENVIRONMENT or they won't run. * - * @param $env - * @param callable $callback + * @param string $env + * @param \Closure $callback * - * @return $this + * @return RouteCollectionInterface */ public function environment(string $env, \Closure $callback): RouteCollectionInterface { if (ENVIRONMENT == $env) { - call_user_func($callback, $this); + $callback($this); } return $this; @@ -891,47 +1037,95 @@ public function environment(string $env, \Closure $callback): RouteCollectionInt * reverseRoute('Controller::method', $param1, $param2); * * @param string $search - * @param ...$params + * @param array ...$params + * + * @return string|false */ - public function reverseRoute(string $search, ...$params): string + public function reverseRoute(string $search, ...$params) { // Named routes get higher priority. - if (array_key_exists($search, $this->routes)) + foreach ($this->routes as $verb => $collection) { - return $this->fillRouteParams(key($this->routes[$search]['route']), $params); + if (array_key_exists($search, $collection)) + { + return $this->fillRouteParams(key($collection[$search]['route']), $params); + } } // If it's not a named route, then loop over // all routes to find a match. - foreach ($this->routes as $route) + foreach ($this->routes as $verb => $collection) { - $from = key($route['route']); - $to = $route['route'][$from]; - - // Lose any namespace slash at beginning of strings - // to ensure more consistent match. - $to = ltrim($to, '\\'); - $search = ltrim($search, '\\'); - - // If there's any chance of a match, then it will - // be with $search at the beginning of the $to string. - if (strpos($to, $search) !== 0) + foreach ($collection as $route) { - continue; + $from = key($route['route']); + $to = $route['route'][$from]; + + // Lose any namespace slash at beginning of strings + // to ensure more consistent match. + $to = ltrim($to, '\\'); + $search = ltrim($search, '\\'); + + // If there's any chance of a match, then it will + // be with $search at the beginning of the $to string. + if (strpos($to, $search) !== 0) + { + continue; + } + + // Ensure that the number of $params given here + // matches the number of back-references in the route + if (substr_count($to, '$') != count($params)) + { + continue; + } + + return $this->fillRouteParams($from, $params); } + } - // Ensure that the number of $params given here - // matches the number of back-references in the route - if (substr_count($to, '$') != count($params)) - { - continue; - } + // If we're still here, then we did not find a match. + return false; + } - return $this->fillRouteParams($from, $params); + //-------------------------------------------------------------------- + + /** + * Checks a route (using the "from") to see if it's filtered or not. + * + * @param string $search + * + * @return bool + */ + public function isFiltered(string $search): bool + { + return isset($this->routesOptions[$search]['filter']); + } + + //-------------------------------------------------------------------- + + /** + * Returns the filter that should be applied for a single route, along + * with any parameters it might have. Parameters are found by splitting + * the parameter name on a colon to separate the filter name from the parameter list, + * and the splitting the result on commas. So: + * + * 'role:admin,manager' + * + * has a filter of "role", with parameters of ['admin', 'manager']. + * + * @param string $search + * + * @return string + */ + public function getFilterForRoute(string $search): string + { + if (! $this->isFiltered($search)) + { + return ''; } - // If we're still here, then we did not find a match. - throw new \InvalidArgumentException('Unable to locate a valid route.'); + return $this->routesOptions[$search]['filter']; } //-------------------------------------------------------------------- @@ -939,7 +1133,7 @@ public function reverseRoute(string $search, ...$params): string /** * Given a * - * @param array $from + * @param string $from * @param array|null $params * * @return string @@ -951,7 +1145,7 @@ protected function fillRouteParams(string $from, array $params = null): string if (empty($matches[0])) { - return $from; + return '/' . ltrim($from, '/'); } // Build our resulting string, inserting the $params in @@ -960,17 +1154,19 @@ protected function fillRouteParams(string $from, array $params = null): string { // Ensure that the param we're inserting matches // the expected param type. - if (preg_match("/{$pattern}/", $params[$index])) + $pos = strpos($from, $pattern); + + if (preg_match("|{$pattern}|", $params[$index])) { - $from = str_replace($pattern, $params[$index], $from); + $from = substr_replace($from, $params[$index], $pos, strlen($pattern)); } else { - throw new \LogicException('A parameter does not match the expected type.'); + throw RouterException::forInvalidParameterType(); } } - return $from; + return '/' . ltrim($from, '/'); } //-------------------------------------------------------------------- @@ -980,23 +1176,28 @@ protected function fillRouteParams(string $from, array $params = null): string * the request method(s) that this route will work for. They can be separated * by a pipe character "|" if there is more than one. * - * @param string $from - * @param array $to - * @param array $options + * @param string $verb + * @param string $from + * @param $to + * @param array|null $options */ - protected function create(string $from, $to, array $options = null) + protected function create(string $verb, string $from, $to, array $options = null) { - $prefix = is_null($this->group) ? '' : $this->group.'/'; + $prefix = is_null($this->group) ? '' : $this->group . '/'; - $from = filter_var($prefix.$from, FILTER_SANITIZE_STRING); + $from = filter_var($prefix . $from, FILTER_SANITIZE_STRING); - if (is_null($options)) + // While we want to add a route within a group of '/', + // it doens't work with matching, so remove them... + if ($from != '/') { - $options = $this->currentOptions; + $from = trim($from, '/'); } + $options = array_merge((array)$this->currentOptions, (array)$options); + // Hostname limiting? - if (isset($options['hostname']) && ! empty($options['hostname'])) + if (! empty($options['hostname'])) { // @todo determine if there's a way to whitelist hosts? if (strtolower($_SERVER['HTTP_HOST']) != strtolower($options['hostname'])) @@ -1024,16 +1225,12 @@ protected function create(string $from, $to, array $options = null) // Get a constant string to work with. $to = preg_replace('/(\$\d+)/', '$X', $to); - for ($i = (int)$options['offset'] + 1; $i < (int)$options['offset'] + 7; $i++) + for ($i = (int) $options['offset'] + 1; $i < (int) $options['offset'] + 7; $i ++ ) { $to = preg_replace_callback( - '/\$X/', - function ($m) use ($i) - { - return '$'.$i; - }, - $to, - 1 + '/\$X/', function ($m) use ($i) { + return '$' . $i; + }, $to, 1 ); } } @@ -1042,33 +1239,45 @@ function ($m) use ($i) // so that the Router doesn't need to know about any of this. foreach ($this->placeholders as $tag => $pattern) { - $from = str_ireplace(':'.$tag, $pattern, $from); + $from = str_ireplace(':' . $tag, $pattern, $from); } // If no namespace found, add the default namespace if (is_string($to) && strpos($to, '\\') === false) { $namespace = $options['namespace'] ?? $this->defaultNamespace; - $to = trim($namespace, '\\') .'\\'.$to; + $to = trim($namespace, '\\') . '\\' . $to; } // Always ensure that we escape our namespace so we're not pointing to // \CodeIgniter\Routes\Controller::method. if (is_string($to)) { - $to = '\\'.ltrim($to, '\\'); + $to = '\\' . ltrim($to, '\\'); } $name = $options['as'] ?? $from; - $this->routes[$name] = [ + // Don't overwrite any existing 'froms' so that auto-discovered routes + // do not overwrite any application/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])) + { + return; + } + + $this->routes[$verb][$name] = [ 'route' => [$from => $to] ]; + $this->routesOptions[$from] = $options; + // Is this a redirect? if (isset($options['redirect']) && is_numeric($options['redirect'])) { - $this->routes[$name]['redirect'] = $options['redirect']; + $this->routes['*'][$name]['redirect'] = $options['redirect']; } } @@ -1096,7 +1305,7 @@ private function checkSubdomains($subdomains) // Routes can be limited to any sub-domain. In that case, though, // it does require a sub-domain to be present. - if (! empty($this->currentSubdomain) && in_array('*', $subdomains)) + if ( ! empty($this->currentSubdomain) && in_array('*', $subdomains)) { return true; } @@ -1123,17 +1332,28 @@ private function checkSubdomains($subdomains) */ private function determineCurrentSubdomain() { - $parsedUrl = parse_url($_SERVER['HTTP_HOST']); + // We have to ensure that a scheme exists + // on the URL else parse_url will mis-interpret + // 'host' as the 'path'. + $url = $_SERVER['HTTP_HOST']; + if (strpos($url, 'http') !== 0) + { + $url = 'http://' . $url; + } + + $parsedUrl = parse_url($url); $host = explode('.', $parsedUrl['host']); - if ($host[0] == 'www') unset($host[0]); + if ($host[0] == 'www') + unset($host[0]); // Get rid of any domains, which will be the last unset($host[count($host)]); // Account for .co.uk, .co.nz, etc. domains - if (end($host) == 'co') $host = array_slice($host, 0, -1); + if (end($host) == 'co') + $host = array_slice($host, 0, -1); // If we only have 1 part left, then we don't have a sub-domain. if (count($host) == 1) @@ -1144,6 +1364,6 @@ private function determineCurrentSubdomain() return array_shift($host); } - //-------------------------------------------------------------------- + //-------------------------------------------------------------------- } diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index 51b3ea13ade9..346934394607 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -76,7 +76,7 @@ public function add(string $from, $to); * * @return mixed */ - public function addPlaceholder(string $placeholder, string $pattern=null); + public function addPlaceholder($placeholder, string $pattern = null); //-------------------------------------------------------------------- @@ -141,7 +141,7 @@ public function setTranslateURIDashes(bool $value); * * @param bool $value * - * @return RouteCollection + * @return RouteCollectionInterface */ public function setAutoRoute(bool $value): self; @@ -156,7 +156,7 @@ public function setAutoRoute(bool $value): self; * * @param callable|null $callable * - * @return $this + * @return RouteCollectionInterface */ public function set404Override($callable = null): self; @@ -172,8 +172,6 @@ public function get404Override(); //-------------------------------------------------------------------- - - /** * Returns the name of the default controller. With Namespace. * @@ -195,8 +193,6 @@ public function getDefaultMethod(); /** * Returns the current value of the translateURIDashses setting. * - * @param bool|false $val - * * @return mixed */ public function shouldTranslateURIDashes(); @@ -244,10 +240,11 @@ public function getHTTPVerb(); * reverseRoute('Controller::method', $param1, $param2); * * @param string $search - * @param ...$params - * @return string + * @param array ...$params + * + * @return string|false */ - public function reverseRoute(string $search, ...$params): string; + public function reverseRoute(string $search, ...$params); //-------------------------------------------------------------------- @@ -272,5 +269,4 @@ public function isRedirect(string $from): bool; public function getRedirectCode(string $from): int; //-------------------------------------------------------------------- - } diff --git a/system/Router/Router.php b/system/Router/Router.php index 32698c5e6686..5704fc5b00aa 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,29 +29,33 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\PageNotFoundException; +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Router\Exceptions\RouterException; /** * Routing exception */ -class RedirectException extends \Exception {} +class RedirectException extends \Exception +{ + +} /** * Request router. */ class Router implements RouterInterface { + /** * A RouteCollection instance. * - * @var RouteCollectionInterface + * @var RouteCollection */ protected $collection; @@ -106,6 +110,26 @@ class Router implements RouterInterface */ protected $matchedRoute = null; + /** + * The options set for the matched route. + * + * @var array|null + */ + protected $matchedRouteOptions = null; + + /** + * The locale that was detected in a route. + * @var string + */ + protected $detectedLocale = null; + + /** + * The filter info from Route Collection + * if the matched route should be filtered. + * @var string + */ + protected $filterInfo; + //-------------------------------------------------------------------- /** @@ -118,7 +142,7 @@ public function __construct(RouteCollectionInterface $routes) $this->collection = $routes; $this->controller = $this->collection->getDefaultController(); - $this->method = $this->collection->getDefaultMethod(); + $this->method = $this->collection->getDefaultMethod(); } //-------------------------------------------------------------------- @@ -129,7 +153,7 @@ public function __construct(RouteCollectionInterface $routes) * * This is the main entry point when using the Router. * - * @param null $uri + * @param string $uri * * @return mixed */ @@ -139,11 +163,18 @@ public function handle(string $uri = null) // everything runs off of it's default settings. if (empty($uri)) { - return $this->controller; + return strpos($this->controller, '\\') === false + ? $this->collection->getDefaultNamespace() . $this->controller + : $this->controller; } if ($this->checkRoutes($uri)) { + if ($this->collection->isFiltered($uri)) + { + $this->filterInfo = $this->collection->getFilterForRoute($uri); + } + return $this->controller; } @@ -162,6 +193,18 @@ public function handle(string $uri = null) //-------------------------------------------------------------------- + /** + * Returns the filter info for the matched route, if any. + * + * @return string + */ + public function getFilter() + { + return $this->filterInfo; + } + + //-------------------------------------------------------------------- + /** * Returns the name of the matched controller. * @@ -200,7 +243,7 @@ public function get404Override() $routeArray = explode('::', $route); return [ - $routeArray[0], // Controller + $routeArray[0], // Controller $routeArray[1] ?? 'index' // Method ]; } @@ -215,11 +258,10 @@ public function get404Override() //-------------------------------------------------------------------- - /** * Returns the binds that have been matched and collected * during the parsing process as an array, ready to send to - * call_user_func_array(). + * instance->method(...$params). * * @return mixed */ @@ -240,7 +282,7 @@ public function params(): array */ public function directory(): string { - return ! empty($this->directory) ? $this->directory : ''; + return ! empty($this->directory) ? $this->directory : ''; } //-------------------------------------------------------------------- @@ -253,12 +295,22 @@ public function directory(): string */ public function getMatchedRoute() { - return $this->matchedRoute; + return $this->matchedRoute; } //-------------------------------------------------------------------- + /** + * Returns all options set for the matched route + * + * @return array|null + */ + public function getMatchedRouteOptions() + { + return $this->matchedRouteOptions; + } + //-------------------------------------------------------------------- /** * Sets the value that should be used to match the index.php file. Defaults @@ -287,15 +339,40 @@ public function setIndexPage($page): self * * @return $this */ - public function setTranslateURIDashes($val = false): self + public function setTranslateURIDashes(bool $val = false): self { - $this->translateURIDashes = (bool)$val; + $this->translateURIDashes = $val; return $this; } //-------------------------------------------------------------------- + /** + * Returns true/false based on whether the current route contained + * a {locale} placeholder. + * + * @return bool + */ + public function hasLocale() + { + return (bool) $this->detectedLocale; + } + + //-------------------------------------------------------------------- + + /** + * Returns the detected locale, if any, or null. + * + * @return string + */ + public function getLocale() + { + return $this->detectedLocale; + } + + //-------------------------------------------------------------------- + /** * Compares the uri string against the routes that the * RouteCollection class defined for us, attempting to find a match. @@ -304,10 +381,15 @@ public function setTranslateURIDashes($val = false): self * @param string $uri The URI path to compare against the routes * * @return bool Whether the route was matched or not. + * @throws \CodeIgniter\Router\RedirectException */ protected function checkRoutes(string $uri): bool { - $routes = $this->collection->getRoutes(); + $routes = $this->collection->getRoutes($this->collection->getHTTPVerb()); + + $uri = $uri == '/' + ? $uri + : ltrim($uri, '/ '); // Don't waste any time if (empty($routes)) @@ -318,9 +400,29 @@ protected function checkRoutes(string $uri): bool // Loop through the route array looking for wildcards foreach ($routes as $key => $val) { + // Are we dealing with a locale? + if (strpos($key, '{locale}') !== false) + { + $localeSegment = array_search('{locale}', explode('/', $key)); + + // Replace it with a regex so it + // will actually match. + $key = str_replace('{locale}', '[^/]+', $key); + } + // Does the RegEx match? - if (preg_match('#^'.$key.'$#', $uri, $matches)) + if (preg_match('#^' . $key . '$#', $uri, $matches)) { + // Store our locale so CodeIgniter object can + // assign it to the Request. + if (isset($localeSegment)) + { + // The following may be inefficient, but doesn't upset NetBeans :-/ + $temp = (explode('/', $uri)); + $this->detectedLocale = $temp[$localeSegment]; + unset($localeSegment); + } + // Are we using Closures? If so, then we need // to collect the params into an array // so it can be passed to the controller method later. @@ -335,12 +437,27 @@ protected function checkRoutes(string $uri): bool $this->matchedRoute = [$key, $val]; + $this->matchedRouteOptions = $this->collection->getRoutesOptions($key); + return true; } // Are we using the default method for back-references? + + // Support resource route when function with subdirectory + // ex: $routes->resource('Admin/Admins'); + if (strpos($val, '$') !== false && strpos($key, '(') !== false && strpos($key, '/') !== false) + { + $replacekey = str_replace('/(.*)', '', $key); + $val = preg_replace('#^' . $key . '$#', $val, $uri); + $val = str_replace($replacekey, str_replace("/", "\\", $replacekey), $val); + } elseif (strpos($val, '$') !== false && strpos($key, '(') !== false) { - $val = preg_replace('#^'.$key.'$#', $val, $uri); + $val = preg_replace('#^' . $key . '$#', $val, $uri); + } + elseif (strpos($key, '/') !== false) + { + $val = str_replace('/', '\\', $val); } // Is this route supposed to redirect to another? @@ -353,6 +470,8 @@ protected function checkRoutes(string $uri): bool $this->matchedRoute = [$key, $val]; + $this->matchedRouteOptions = $this->collection->getRoutesOptions($key); + return true; } } @@ -375,7 +494,7 @@ public function autoRoute(string $uri) $segments = $this->validateRequest($segments); // If we don't have any segments left - try the default controller; - // WARNING: Directories get shifted out of tge segments array. + // WARNING: Directories get shifted out of the segments array. if (empty($segments)) { $this->setDefaultController(); @@ -389,15 +508,29 @@ public function autoRoute(string $uri) // Use the method name if it exists. // If it doesn't, no biggie - the default method name // has already been set. - if (! empty($segments)) + if ( ! empty($segments)) { $this->method = array_shift($segments); } - if (! empty($segments)) + if ( ! empty($segments)) { $this->params = $segments; } + + // Load the file so that it's available for CodeIgniter. + $file = APPPATH . 'Controllers/' . $this->directory . $this->controller . '.php'; + if (file_exists($file)) + { + include_once $file; + } + + // Ensure the controller stores the fully-qualified class name + // We have to check for a length over 1, since by default it will be '\' + if (strpos($this->controller, '\\') === false && strlen($this->collection->getDefaultNamespace()) > 1) + { + $this->controller = str_replace('/', '\\', $this->collection->getDefaultNamespace() . $this->directory . $this->controller); + } } //-------------------------------------------------------------------- @@ -411,21 +544,17 @@ public function autoRoute(string $uri) */ protected function validateRequest(array $segments) { - $c = count($segments); + $c = count($segments); $directory_override = isset($this->directory); // Loop through our segments and return as soon as a controller // is found or when such a directory doesn't exist - while ($c-- > 0) + while ($c -- > 0) { - $test = $this->directory.ucfirst($this->translateURIDashes === true - ? str_replace('-', '_', $segments[0]) - : $segments[0] - ); - - if ( ! file_exists(APPPATH.'Controllers/'.$test.'.php') - && $directory_override === false - && is_dir(APPPATH.'Controllers/'.$this->directory.$segments[0]) + $test = $this->directory . ucfirst($this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[0] + ); + + if ( ! file_exists(APPPATH . 'Controllers/' . $test . '.php') && $directory_override === false && is_dir(APPPATH . 'Controllers/' . $this->directory . ucfirst($segments[0])) ) { $this->setDirectory(array_shift($segments), true); @@ -449,13 +578,15 @@ protected function validateRequest(array $segments) */ protected function setDirectory(string $dir = null, $append = false) { + $dir = ucfirst($dir); + if ($append !== TRUE || empty($this->directory)) { - $this->directory = str_replace('.', '', trim($dir, '/')).'/'; + $this->directory = str_replace('.', '', trim($dir, '/')) . '/'; } else { - $this->directory .= str_replace('.', '', trim($dir, '/')).'/'; + $this->directory .= str_replace('.', '', trim($dir, '/')) . '/'; } } @@ -513,7 +644,7 @@ protected function setDefaultController() { if (empty($this->controller)) { - throw new \RuntimeException('Unable to determine what should be displayed. A default route has not been specified in the routing file.'); + throw RouterException::forMissingDefaultRoute(); } // Is the method being specified? @@ -522,7 +653,7 @@ protected function setDefaultController() $this->method = 'index'; } - if (! file_exists(APPPATH.'Controllers/'.$this->directory.ucfirst($class).'.php')) + if ( ! file_exists(APPPATH . 'Controllers/' . $this->directory . ucfirst($class) . '.php')) { return; } diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index c5566eabb208..cb145a526dba 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,9 +29,9 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ @@ -55,7 +55,7 @@ public function __construct(RouteCollectionInterface $routes); * Scans the URI and attempts to match the current URI to the * one of the defined routes in the RouteCollection. * - * @param null $uri + * @param string $uri * * @return mixed */ @@ -85,7 +85,7 @@ public function methodName(); /** * Returns the binds that have been matched and collected * during the parsing process as an array, ready to send to - * call_user_func_array(). + * instance->method(...$params). * * @return mixed */ @@ -106,5 +106,4 @@ public function params(); public function setIndexPage($page); //-------------------------------------------------------------------- - } diff --git a/system/Security/Exceptions/SecurityException.php b/system/Security/Exceptions/SecurityException.php new file mode 100644 index 000000000000..dda6a9d5da44 --- /dev/null +++ b/system/Security/Exceptions/SecurityException.php @@ -0,0 +1,12 @@ +', '<', '>', "'", '"', '&', '$', '#', '{', '}', '[', ']', '=', ';', '?', '%20', '%22', - '%3c', // < - '%253c', // < - '%3e', // > - '%0e', // > - '%28', // ( - '%29', // ) - '%2528', // ( - '%26', // & - '%24', // $ - '%3f', // ? - '%3b', // ; - '%3d' // = - ); + '%3c', // < + '%253c', // < + '%3e', // > + '%0e', // > + '%28', // ( + '%29', // ) + '%2528', // ( + '%26', // & + '%24', // $ + '%3f', // ? + '%3b', // ; + '%3d' // = + ]; //-------------------------------------------------------------------- @@ -166,20 +149,18 @@ class Security public function __construct($config) { // Store our CSRF-related settings - $this->CSRFEnabled = $config->CSRFProtection; - $this->CSRFExpire = $config->CSRFExpire; - $this->CSRFTokenName = $config->CSRFTokenName; - $this->CSRFCookieName = $config->CSRFCookieName; - $this->CSRFRegenerate = $config->CSRFRegenerate; - $this->CSRFExcludeURIs = $config->CSRFExcludeURIs; + $this->CSRFExpire = $config->CSRFExpire; + $this->CSRFTokenName = $config->CSRFTokenName; + $this->CSRFCookieName = $config->CSRFCookieName; + $this->CSRFRegenerate = $config->CSRFRegenerate; if (isset($config->cookiePrefix)) { - $this->CSRFCookieName = $config->cookiePrefix.$this->CSRFCookieName; + $this->CSRFCookieName = $config->cookiePrefix . $this->CSRFCookieName; } // Store cookie-related settings - $this->cookiePath = $config->cookiePath; + $this->cookiePath = $config->cookiePath; $this->cookieDomain = $config->cookieDomain; $this->cookieSecure = $config->cookieSecure; @@ -192,9 +173,9 @@ public function __construct($config) /** * CSRF Verify - * + * * @param RequestInterface $request - * @return $this + * @return $this|false * @throws \LogicException */ public function CSRFVerify(RequestInterface $request) @@ -205,29 +186,11 @@ public function CSRFVerify(RequestInterface $request) return $this->CSRFSetCookie($request); } - // Check if URI has been whitelisted from CSRF checks - if (is_array($this->CSRFExcludeURIs) && count($this->CSRFExcludeURIs)) - { - $uri = (string)$request->uri; - - foreach ($this->CSRFExcludeURIs as $excluded) - { - // @TODO modify to support UTF8 once our method of determining this - // has been determined. -// if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri)) - if (preg_match('#^'.$excluded.'$#i', $uri)) - { - return $this; - } - } - } - // Do the tokens exist in both the _POST and _COOKIE arrays? - if ( ! isset($_POST[$this->CSRFTokenName], $_COOKIE[$this->CSRFCookieName]) - || $_POST[$this->CSRFTokenName] !== $_COOKIE[$this->CSRFCookieName] + if ( ! isset($_POST[$this->CSRFTokenName], $_COOKIE[$this->CSRFCookieName]) || $_POST[$this->CSRFTokenName] !== $_COOKIE[$this->CSRFCookieName] ) // Do the tokens match? { - throw new \LogicException('The action you requested is not allowed', 403); + throw SecurityException::forDisallowedAction(); } // We kill this since we're done and we don't want to pollute the _POST array @@ -238,7 +201,6 @@ public function CSRFVerify(RequestInterface $request) { // Nothing should last forever unset($_COOKIE[$this->CSRFCookieName]); - $this->_csrf_hash = null; } $this->CSRFSetHash(); @@ -255,13 +217,15 @@ public function CSRFVerify(RequestInterface $request) * CSRF Set Cookie * * @codeCoverageIgnore - * @param RequestInterface $request - * @return $this + * + * @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request + * + * @return Security|false */ public function CSRFSetCookie(RequestInterface $request) { - $expire = time() + $this->CSRFExpire; - $secure_cookie = (bool)$this->cookieSecure; + $expire = time() + $this->CSRFExpire; + $secure_cookie = (bool) $this->cookieSecure; if ($secure_cookie && ! $request->isSecure()) { @@ -269,13 +233,7 @@ public function CSRFSetCookie(RequestInterface $request) } setcookie( - $this->CSRFCookieName, - $this->CSRFHash, - $expire, - $this->cookiePath, - $this->cookieDomain, - $secure_cookie, - true // Enforce HTTP only cookie for security + $this->CSRFCookieName, $this->CSRFHash, $expire, $this->cookiePath, $this->cookieDomain, $secure_cookie, true // Enforce HTTP only cookie for security ); log_message('info', 'CSRF cookie sent'); @@ -322,14 +280,13 @@ protected function CSRFSetHash() // We don't necessarily want to regenerate it with // each page load since a page could contain embedded // sub-pages causing this feature to fail - if (isset($_COOKIE[$this->CSRFCookieName]) && is_string($_COOKIE[$this->CSRFCookieName]) - && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->CSRFCookieName]) === 1 + if (isset($_COOKIE[$this->CSRFCookieName]) && is_string($_COOKIE[$this->CSRFCookieName]) && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->CSRFCookieName]) === 1 ) { return $this->CSRFHash = $_COOKIE[$this->CSRFCookieName]; } - $rand = random_bytes(16); + $rand = random_bytes(16); $this->CSRFHash = bin2hex($rand); } @@ -370,12 +327,10 @@ public function sanitizeFilename($str, $relative_path = false) { $old = $str; $str = str_replace($bad, '', $str); - } - while ($old !== $str); + } while ($old !== $str); return stripslashes($str); } //-------------------------------------------------------------------- - } diff --git a/system/Session/Exceptions/SessionException.php b/system/Session/Exceptions/SessionException.php new file mode 100644 index 000000000000..f4733c0ef785 --- /dev/null +++ b/system/Session/Exceptions/SessionException.php @@ -0,0 +1,32 @@ +cookiePrefix = $config->cookiePrefix; - $this->cookieDomain = $config->cookoieDomain; - $this->cookiePath = $config->cookiePath; + $this->cookieDomain = $config->cookieDomain; + $this->cookiePath = $config->cookiePath; $this->cookieSecure = $config->cookieSecure; - $this->cookieName = $config->sessionCookieName; - $this->matchIP = $config->sessionMatchIP; + $this->cookieName = $config->sessionCookieName; + $this->matchIP = $config->sessionMatchIP; + $this->savePath = $config->sessionSavePath; } //-------------------------------------------------------------------- @@ -132,15 +140,9 @@ public function __construct(BaseConfig $config) */ protected function destroyCookie(): bool { - return setcookie( - $this->cookieName, - null, - 1, - $this->cookiePath, - $this->cookieDomain, - $this->cookieSecure, - true - ); + return setcookie( + $this->cookieName, null, 1, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true + ); } //-------------------------------------------------------------------- @@ -150,11 +152,11 @@ protected function destroyCookie(): bool * (databases other than PostgreSQL and MySQL) to act as if they * do acquire a lock. * - * @param string $session_id + * @param string $sessionID * * @return bool */ - protected function lockSession(string $session_id): bool + protected function lockSession(string $sessionID): bool { $this->lock = true; return true; @@ -176,4 +178,24 @@ protected function releaseLock(): bool //-------------------------------------------------------------------- + /** + * Fail + * + * Drivers other than the 'files' one don't (need to) use the + * session.save_path INI setting, but that leads to confusing + * error messages emitted by PHP when open() or write() fail, + * as the message contains session.save_path ... + * To work around the problem, the drivers will call this method + * so that the INI is set just in time for the error message to + * be properly generated. + * + * @return mixed + */ + protected function fail() + { + ini_set('session.save_path', $this->savePath); + + return false; + } + } diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php new file mode 100644 index 000000000000..527431661889 --- /dev/null +++ b/system/Session/Handlers/DatabaseHandler.php @@ -0,0 +1,420 @@ +table = $config->sessionSavePath; + + if (empty($this->table)) + { + throw SessionException::forMissingDatabaseTable(); + } + + // Get DB Connection + $this->DBGroup = ! empty($config->sessionDBGroup) ? $config->sessionDBGroup : 'default'; + + $this->db = Database::connect($this->DBGroup); + + // Determine Database type + $driver = strtolower(get_class($this->db)); + if (strpos($driver, 'mysql') !== false) + { + $this->platform = 'mysql'; + } + elseif (strpos($driver, 'postgre') !== false) + { + $this->platform = 'postgre'; + } + } + + //-------------------------------------------------------------------- + + /** + * Open + * + * Ensures we have an initialized database connection. + * + * @param string $savePath Path to session files' directory + * @param string $name Session cookie name + * + * @return bool + * @throws \Exception + */ + public function open($savePath, $name): bool + { + if (empty($this->db->connID)) + { + $this->db->initialize(); + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Read + * + * Reads session data and acquires a lock + * + * @param string $sessionID Session ID + * + * @return string Serialized session data + */ + public function read($sessionID) + { + if ($this->lockSession($sessionID) == false) + { + $this->fingerprint = md5(''); + return ''; + } + + // Needed by write() to detect session_regenerate_id() calls + $this->sessionID = $sessionID; + + $builder = $this->db->table($this->table) + ->select('data') + ->where('id', $sessionID); + + if ($this->matchIP) + { + $builder = $builder->where('ip_address', $_SERVER['REMOTE_ADDR']); + } + + $result = $builder->get()->getRow(); + + if ($result === null) + { + // PHP7 will reuse the same SessionHandler object after + // ID regeneration, so we need to explicitly set this to + // FALSE instead of relying on the default ... + $this->rowExists = FALSE; + $this->fingerprint = md5(''); + + return ''; + } + + // PostgreSQL's variant of a BLOB datatype is Bytea, which is a + // PITA to work with, so we use base64-encoded data in a TEXT + // field instead. + if (is_bool($result)) + { + $result = ''; + } + else + { + $result = ($this->platform === 'postgre') ? base64_decode(rtrim($result->data)) : $result->data; + } + + $this->fingerprint = md5($result); + $this->rowExists = true; + + return $result; + } + + //-------------------------------------------------------------------- + + /** + * Write + * + * Writes (create / update) session data + * + * @param string $sessionID Session ID + * @param string $sessionData Serialized session data + * + * @return bool + */ + public function write($sessionID, $sessionData): bool + { + if ($this->lock === false) + { + return $this->fail(); + } + + // Was the ID regenerated? + elseif ($sessionID !== $this->sessionID) + { + if ( ! $this->releaseLock() || ! $this->lockSession($sessionID)) + { + return $this->fail(); + } + + $this->rowExists = false; + $this->sessionID = $sessionID; + } + + if ($this->rowExists === false) + { + $insertData = [ + 'id' => $sessionID, + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'timestamp' => time(), + 'data' => $this->platform === 'postgre' ? base64_encode($sessionData) : $sessionData + ]; + + if ( ! $this->db->table($this->table)->insert($insertData)) + { + return $this->fail(); + } + + $this->fingerprint = md5($sessionData); + $this->rowExists = true; + + return true; + } + + $builder = $this->db->table($this->table)->where('id', $sessionID); + + if ($this->matchIP) + { + $builder = $builder->where('ip_address', $_SERVER['REMOTE_ADDR']); + } + + $updateData = [ + 'timestamp' => time() + ]; + + if ($this->fingerprint !== md5($sessionData)) + { + $updateData['data'] = ($this->platform === 'postgre') ? base64_encode($sessionData) : $sessionData; + } + + if ( ! $builder->update($updateData)) + { + return $this->fail(); + } + + $this->fingerprint = md5($sessionData); + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Close + * + * Releases locks and closes file descriptor. + * + * @return bool + */ + public function close(): bool + { + return ($this->lock && ! $this->releaseLock()) ? $this->fail() : true; + } + + //-------------------------------------------------------------------- + + /** + * Destroy + * + * Destroys the current session. + * + * @param string $sessionID + * + * @return bool + */ + public function destroy($sessionID): bool + { + if ($this->lock) + { + $builder = $this->db->table($this->table)->where('id', $sessionID); + + if ($this->matchIP) + { + $builder = $builder->where('ip_address', $_SERVER['REMOTE_ADDR']); + } + + if ( ! $builder->delete()) + { + return $this->fail(); + } + } + + if ($this->close()) + { + $this->destroyCookie(); + + return true; + } + + return $this->fail(); + } + + //-------------------------------------------------------------------- + + /** + * Garbage Collector + * + * Deletes expired sessions + * + * @param int $maxlifetime Maximum lifetime of sessions + * + * @return bool + */ + public function gc($maxlifetime): bool + { + return ($this->db->table($this->table)->delete('timestamp < ' . (time() - $maxlifetime))) ? true : $this->fail(); + } + + //-------------------------------------------------------------------- + + protected function lockSession(string $sessionID): bool + { + if ($this->platform === 'mysql') + { + $arg = md5($sessionID . ($this->matchIP ? '_' . $_SERVER['REMOTE_ADDR'] : '')); + if ($this->db->query("SELECT GET_LOCK('{$arg}', 300) AS ci_session_lock")->getRow()->ci_session_lock) + { + $this->lock = $arg; + return true; + } + + return $this->fail(); + } + elseif ($this->platform === 'postgre') + { + $arg = "hashtext('{$sessionID}')" . ($this->matchIP ? ", hashtext('{$_SERVER['REMOTE_ADDR']}')" : ''); + if ($this->db->simpleQuery("SELECT pg_advisory_lock({$arg})")) + { + $this->lock = $arg; + return true; + } + + return $this->fail(); + } + + // Unsupported DB? Let the parent handle the simplified version. + return parent::lockSession($sessionID); + } + + //-------------------------------------------------------------------- + + /** + * Releases the lock, if any. + * + * @return bool + */ + protected function releaseLock(): bool + { + if ( ! $this->lock) + { + return true; + } + + if ($this->platform === 'mysql') + { + if ($this->db->query("SELECT RELEASE_LOCK('{$this->lock}') AS ci_session_lock")->getRow()->ci_session_lock) + { + $this->lock = false; + return true; + } + + return $this->fail(); + } + elseif ($this->platform === 'postgre') + { + if ($this->db->simpleQuery("SELECT pg_advisory_unlock({$this->lock})")) + { + $this->lock = false; + return true; + } + + return $this->fail(); + } + + // Unsupported DB? Let the parent handle the simple version. + return parent::releaseLock(); + } + + //-------------------------------------------------------------------- +} diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index c9b54be35f5b..ebe3e16a2960 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,21 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Session\Exceptions\SessionException; /** * Session handler using file system for storage */ class FileHandler extends BaseHandler implements \SessionHandlerInterface { + /** * Where to save the session files to. * @@ -77,8 +78,10 @@ class FileHandler extends BaseHandler implements \SessionHandlerInterface * Constructor * @param BaseConfig $config */ - public function __construct(BaseConfig $config) + public function __construct($config) { + parent::__construct($config); + if ( ! empty($config->sessionSavePath)) { $this->savePath = rtrim($config->sessionSavePath, '/\\'); @@ -86,7 +89,13 @@ public function __construct(BaseConfig $config) } else { - $this->savePath = rtrim(ini_get('session.save_path'), '/\\'); + $sessionPath = rtrim(ini_get('session.save_path'), '/\\'); + if (! $sessionPath) + { + $sessionPath = WRITEPATH . 'session'; + } + + $this->savePath = $sessionPath; } } @@ -100,7 +109,8 @@ public function __construct(BaseConfig $config) * @param string $savePath Path to session files' directory * @param string $name Session cookie name * - * @return bool + * @return bool + * @throws \Exception */ public function open($savePath, $name): bool { @@ -108,20 +118,18 @@ public function open($savePath, $name): bool { if ( ! mkdir($savePath, 0700, true)) { - throw new \Exception("Session: Configured save path '".$this->savePath. - "' is not a directory, doesn't exist or cannot be created."); + throw SessionException::forInvalidSavePath($this->savePath); } } elseif ( ! is_writable($savePath)) { - throw new \Exception("Session: Configured save path '".$this->savePath. - "' is not writable by the PHP process."); + throw SessionException::forWriteProtectedSavePath($this->savePath); } $this->savePath = $savePath; - $this->filePath = $this->savePath.'/' - .$name // we'll use the session cookie name as a prefix to avoid collisions - .($this->matchIP ? md5($_SERVER['REMOTE_ADDR']) : ''); + $this->filePath = $this->savePath . '/' + . $name // we'll use the session cookie name as a prefix to avoid collisions + . ($this->matchIP ? md5($_SERVER['REMOTE_ADDR']) : ''); return true; } @@ -133,27 +141,28 @@ public function open($savePath, $name): bool * * Reads session data and acquires a lock * - * @param string $session_id Session ID + * @param string $sessionID Session ID * * @return string Serialized session data */ - public function read($session_id) + public function read($sessionID) { // This might seem weird, but PHP 5.6 introduced session_reset(), // which re-reads session data if ($this->fileHandle === null) { + $this->fileNew = ! file_exists($this->filePath . $sessionID); - if (($this->fileHandle = fopen($this->filePath.$session_id, 'c+b')) === false) + if (($this->fileHandle = fopen($this->filePath . $sessionID, 'c+b')) === false) { - $this->logger->error("Session: Unable to open file '".$this->filePath.$session_id."'."); + $this->logger->error("Session: Unable to open file '" . $this->filePath . $sessionID . "'."); return false; } if (flock($this->fileHandle, LOCK_EX) === false) { - $this->logger->error("Session: Unable to obtain lock for file '".$this->filePath.$session_id."'."); + $this->logger->error("Session: Unable to obtain lock for file '" . $this->filePath . $sessionID . "'."); fclose($this->fileHandle); $this->fileHandle = null; @@ -161,11 +170,11 @@ public function read($session_id) } // Needed by write() to detect session_regenerate_id() calls - $this->sessionID = $session_id; + $this->sessionID = $sessionID; if ($this->fileNew) { - chmod($this->filePath.$session_id, 0600); + chmod($this->filePath . $sessionID, 0600); $this->fingerprint = md5(''); return ''; @@ -177,7 +186,7 @@ public function read($session_id) } $session_data = ''; - for ($read = 0, $length = filesize($this->filePath.$session_id); $read < $length; $read += strlen($buffer)) + for ($read = 0, $length = filesize($this->filePath . $sessionID); $read < $length; $read += strlen($buffer)) { if (($buffer = fread($this->fileHandle, $length - $read)) === false) { @@ -199,16 +208,16 @@ public function read($session_id) * * Writes (create / update) session data * - * @param string $session_id Session ID - * @param string $session_data Serialized session data + * @param string $sessionID Session ID + * @param string $sessionData Serialized session data * * @return bool */ - public function write($session_id, $session_data): bool + public function write($sessionID, $sessionData): bool { // 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 ($session_id !== $this->sessionID && ( ! $this->close() || $this->read($session_id) === false)) + if ($sessionID !== $this->sessionID && ( ! $this->close() || $this->read($sessionID) === false)) { return false; } @@ -217,11 +226,9 @@ public function write($session_id, $session_data): bool { return false; } - elseif ($this->fingerprint === md5($session_data)) + elseif ($this->fingerprint === md5($sessionData)) { - return ($this->fileNew) - ? true - : touch($this->filePath.$session_id); + return ($this->fileNew) ? true : touch($this->filePath . $sessionID); } if ( ! $this->fileNew) @@ -230,11 +237,11 @@ public function write($session_id, $session_data): bool rewind($this->fileHandle); } - if (($length = strlen($session_data)) > 0) + if (($length = strlen($sessionData)) > 0) { for ($written = 0; $written < $length; $written += $result) { - if (($result = fwrite($this->fileHandle, substr($session_data, $written))) === false) + if (($result = fwrite($this->fileHandle, substr($sessionData, $written))) === false) { break; } @@ -242,14 +249,14 @@ public function write($session_id, $session_data): bool if ( ! is_int($result)) { - $this->_fingerprint = md5(substr($session_data, 0, $written)); + $this->fingerprint = md5(substr($sessionData, 0, $written)); $this->logger->error('Session: Unable to write data.'); return false; } } - $this->fingerprint = md5($session_data); + $this->fingerprint = md5($sessionData); return true; } @@ -293,17 +300,13 @@ public function destroy($session_id): bool { if ($this->close()) { - return file_exists($this->filePath.$session_id) - ? (unlink($this->filePath.$session_id) && $this->destroyCookie()) - : true; + return file_exists($this->filePath . $session_id) ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true; } elseif ($this->filePath !== null) { clearstatcache(); - return file_exists($this->filePath.$session_id) - ? (unlink($this->filePath.$session_id) && $this->destroyCookie()) - : true; + return file_exists($this->filePath . $session_id) ? (unlink($this->filePath . $session_id) && $this->destroyCookie()) : true; } return false; @@ -324,7 +327,7 @@ public function gc($maxlifetime): bool { if ( ! is_dir($this->savePath) || ($directory = opendir($this->savePath)) === false) { - $this->logger->debug("Session: Garbage collector couldn't list files under directory '".$this->savePath."'."); + $this->logger->debug("Session: Garbage collector couldn't list files under directory '" . $this->savePath . "'."); return false; } @@ -332,24 +335,19 @@ public function gc($maxlifetime): bool $ts = time() - $maxlifetime; $pattern = sprintf( - '/^%s[0-9a-f]{%d}$/', - preg_quote($this->cookieName, '/'), - ($this->matchIP === true ? 72 : 40) + '/^%s[0-9a-f]{%d}$/', preg_quote($this->cookieName, '/'), ($this->matchIP === true ? 72 : 40) ); 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 . '/' . $file) || ($mtime = filemtime($this->savePath . '/' . $file)) === false || $mtime > $ts ) { continue; } - unlink($this->savePath.'/'.$file); + unlink($this->savePath . '/' . $file); } closedir($directory); @@ -358,5 +356,4 @@ public function gc($maxlifetime): bool } //-------------------------------------------------------------------- - } diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index e4b7175767c5..47f4e36dee8b 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,20 +29,21 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Session\Exceptions\SessionException; /** * Session handler using Memcache for persistence */ class MemcachedHandler extends BaseHandler implements \SessionHandlerInterface { + /** * Memcached instance * @@ -75,9 +76,9 @@ class MemcachedHandler extends BaseHandler implements \SessionHandlerInterface /** * Constructor - * + * * @param BaseConfig $config - * @throws \Exception + * @throws \CodeIgniter\Session\Exceptions\SessionException */ public function __construct(BaseConfig $config) { @@ -85,12 +86,12 @@ public function __construct(BaseConfig $config) if (empty($this->savePath)) { - throw new \Exception('Session: No Memcached save path configured.'); + throw SessionException::forEmptySavepath(); } if ($this->matchIP === true) { - $this->keyPrefix .= $_SERVER['REMOTE_ADDR'].':'; + $this->keyPrefix .= $_SERVER['REMOTE_ADDR'] . ':'; } $this->sessionExpiration = $config->sessionExpiration; @@ -117,15 +118,14 @@ public function open($save_path, $name) foreach ($this->memcached->getServerList() as $server) { - $server_list[] = $server['host'].':'.$server['port']; + $server_list[] = $server['host'] . ':' . $server['port']; } - if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, - PREG_SET_ORDER) + if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, PREG_SET_ORDER) ) { $this->memcached = null; - $this->logger->error('Session: Invalid Memcached save path format: '.$this->savePath); + $this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath); return false; } @@ -133,19 +133,19 @@ public function open($save_path, $name) foreach ($matches as $match) { // If Memcached already has this server (or if the port is invalid), skip it - if (in_array($match[1].':'.$match[2], $server_list, true)) + if (in_array($match[1] . ':' . $match[2], $server_list, true)) { - $this->logger->debug('Session: Memcached server pool already has '.$match[1].':'.$match[2]); + $this->logger->debug('Session: Memcached server pool already has ' . $match[1] . ':' . $match[2]); continue; } - if ( ! $this->memcached->addServer($match[1], $match[2], isset($match[3]) ? $match[3] : 0)) + if ( ! $this->memcached->addServer($match[1], $match[2], $match[3] ?? 0)) { - $this->logger->error('Could not add '.$match[1].':'.$match[2].' to Memcached server pool.'); + $this->logger->error('Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.'); } else { - $server_list[] = $match[1].':'.$match[2]; + $server_list[] = $match[1] . ':' . $match[2]; } } @@ -166,18 +166,18 @@ public function open($save_path, $name) * * Reads session data and acquires a lock * - * @param string $session_id Session ID + * @param string $sessionID Session ID * * @return string Serialized session data */ - public function read($session_id) + public function read($sessionID) { - if (isset($this->memcached) && $this->lockSession($session_id)) + if (isset($this->memcached) && $this->lockSession($sessionID)) { // Needed by write() to detect session_regenerate_id() calls - $this->sessionID = $session_id; + $this->sessionID = $sessionID; - $session_data = (string)$this->memcached->get($this->keyPrefix.$session_id); + $session_data = (string) $this->memcached->get($this->keyPrefix . $sessionID); $this->fingerprint = md5($session_data); return $session_data; @@ -193,38 +193,38 @@ public function read($session_id) * * Writes (create / update) session data * - * @param string $session_id Session ID - * @param string $session_data Serialized session data + * @param string $sessionID Session ID + * @param string $sessionData Serialized session data * * @return bool */ - public function write($session_id, $session_data) + public function write($sessionID, $sessionData) { if ( ! isset($this->memcached)) { return false; } // Was the ID regenerated? - elseif ($session_id !== $this->sessionID) + elseif ($sessionID !== $this->sessionID) { - if ( ! $this->releaseLock() || ! $this->lockSession($session_id)) + if ( ! $this->releaseLock() || ! $this->lockSession($sessionID)) { return false; } $this->fingerprint = md5(''); - $this->sessionID = $session_id; + $this->sessionID = $sessionID; } if (isset($this->lockKey)) { $this->memcached->replace($this->lockKey, time(), 300); - if ($this->fingerprint !== ($fingerprint = md5($session_data))) + if ($this->fingerprint !== ($fingerprint = md5($sessionData))) { - if ($this->memcached->set($this->keyPrefix.$session_id, $session_data, $this->sessionExpiration)) + if ($this->memcached->set($this->keyPrefix . $sessionID, $sessionData, $this->sessionExpiration)) { - $this->_fingerprint = $fingerprint; + $this->fingerprint = $fingerprint; return true; } @@ -232,7 +232,7 @@ public function write($session_id, $session_data) return false; } - return $this->memcached->touch($this->keyPrefix.$session_id, $this->sessionExpiration); + return $this->memcached->touch($this->keyPrefix . $sessionID, $this->sessionExpiration); } return false; @@ -281,7 +281,7 @@ public function destroy($session_id) { if (isset($this->memcached, $this->lockKey)) { - $this->memcached->delete($this->keyPrefix.$session_id); + $this->memcached->delete($this->keyPrefix . $session_id); return $this->destroyCookie(); } @@ -313,11 +313,11 @@ public function gc($maxlifetime) * * Acquires an (emulated) lock. * - * @param string $session_id Session ID + * @param string $sessionID Session ID * * @return bool */ - protected function lockSession(string $session_id): bool + protected function lockSession(string $sessionID): bool { if (isset($this->lockKey)) { @@ -325,8 +325,8 @@ protected function lockSession(string $session_id): bool } // 30 attempts to obtain a lock, in case another request already has it - $lock_key = $this->keyPrefix.$session_id.':lock'; - $attempt = 0; + $lock_key = $this->keyPrefix . $sessionID . ':lock'; + $attempt = 0; do { @@ -338,19 +338,18 @@ protected function lockSession(string $session_id): bool if ( ! $this->memcached->set($lock_key, time(), 300)) { - $this->logger->error('Session: Error while trying to obtain lock for '.$this->keyPrefix.$session_id); + $this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID); return false; } $this->lockKey = $lock_key; break; - } - while (++$attempt < 30); + } while (++ $attempt < 30); if ($attempt === 30) { - $this->logger->error('Session: Unable to obtain lock for '.$this->keyPrefix.$session_id.' after 30 attempts, aborting.'); + $this->logger->error('Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.'); return false; } @@ -374,21 +373,20 @@ protected function releaseLock(): bool if (isset($this->memcached, $this->lockKey) && $this->lock) { if ( ! $this->memcached->delete($this->lockKey) && - $this->memcached->getResultCode() !== \Memcached::RES_NOTFOUND + $this->memcached->getResultCode() !== \Memcached::RES_NOTFOUND ) { - $this->logger->error('Session: Error while trying to free lock for '.$this->lockKey); + $this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey); return false; } $this->lockKey = null; - $this->lock = false; + $this->lock = false; } return true; } //-------------------------------------------------------------------- - } diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 8c33abbe481a..33752fd4636c 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,14 +29,14 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Session\Exceptions\SessionException; /** * Session handler using Redis for persistence @@ -65,6 +65,13 @@ class RedisHandler extends BaseHandler implements \SessionHandlerInterface */ protected $lockKey; + /** + * Key exists flag + * + * @var bool + */ + protected $keyExists = FALSE; + /** * Number of seconds until the session ends. * @@ -76,8 +83,9 @@ class RedisHandler extends BaseHandler implements \SessionHandlerInterface /** * Constructor - * + * * @param BaseConfig $config + * * @throws \Exception */ public function __construct(BaseConfig $config) @@ -86,30 +94,30 @@ public function __construct(BaseConfig $config) if (empty($this->savePath)) { - throw new \Exception('Session: No Redis save path configured.'); + throw SessionException::forEmptySavepath(); } elseif (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->savePath, $matches)) { - isset($matches[3]) OR $matches[3] = ''; // Just to avoid undefined index notices below + isset($matches[3]) || $matches[3] = ''; // Just to avoid undefined index notices below $this->savePath = [ - 'host' => $matches[1], - 'port' => empty($matches[2]) ? null : $matches[2], - 'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : null, - 'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int)$match[1] : null, - 'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float)$match[1] : null, + 'host' => $matches[1], + 'port' => empty($matches[2]) ? null : $matches[2], + 'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : null, + 'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : null, + 'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float) $match[1] : null, ]; preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->keyPrefix = $match[1]; } else { - throw new \Exception('Session: Invalid Redis save path format: '.$this->savePath); + throw SessionException::forInvalidSavePathFormat($this->savePath); } if ($this->matchIP === true) { - $this->keyPrefix .= $_SERVER['REMOTE_ADDR'].':'; + $this->keyPrefix .= $_SERVER['REMOTE_ADDR'] . ':'; } $this->sessionExpiration = $config->sessionExpiration; @@ -139,13 +147,13 @@ public function open($save_path, $name) { $this->logger->error('Session: Unable to connect to Redis with the configured settings.'); } - elseif (isset($this->_config['save_path']['password']) && ! $redis->auth($this->_config['save_path']['password'])) + elseif (isset($this->savePath['password']) && ! $redis->auth($this->savePath['password'])) { $this->logger->error('Session: Unable to authenticate to Redis instance.'); } - elseif (isset($this->_config['save_path']['database']) && ! $redis->select($this->_config['save_path']['database'])) + elseif (isset($this->savePath['database']) && ! $redis->select($this->savePath['database'])) { - $this->logger->error('Session: Unable to select Redis database with index '.$this->_config['save_path']['database']); + $this->logger->error('Session: Unable to select Redis database with index ' . $this->savePath['database']); } else { @@ -163,19 +171,21 @@ public function open($save_path, $name) * * Reads session data and acquires a lock * - * @param string $session_id Session ID + * @param string $sessionID Session ID + * * @return string Serialized session data */ - public function read($session_id) + public function read($sessionID) { - if (isset($this->redis) && $this->lockSession($session_id)) + if (isset($this->redis) && $this->lockSession($sessionID)) { // Needed by write() to detect session_regenerate_id() calls - $this->_session_id = $session_id; + $this->sessionID = $sessionID; - $session_data = (string) $this->redis->get($this->keyPrefix.$session_id); - $this->_fingerprint = md5($session_data); + $session_data = $this->redis->get($this->keyPrefix . $sessionID); + is_string($session_data) ? $this->keyExists = TRUE : $session_data = ''; + $this->fingerprint = md5($session_data); return $session_data; } @@ -189,44 +199,46 @@ public function read($session_id) * * Writes (create / update) session data * - * @param string $session_id Session ID - * @param string $session_data Serialized session data + * @param string $sessionID Session ID + * @param string $sessionData Serialized session data + * * @return bool */ - public function write($session_id, $session_data) + public function write($sessionID, $sessionData) { if ( ! isset($this->redis)) { return FALSE; } // Was the ID regenerated? - elseif ($session_id !== $this->sessionID) + elseif ($sessionID !== $this->sessionID) { - if ( ! $this->releaseLock() || ! $this->lockSession($session_id)) + if ( ! $this->releaseLock() || ! $this->lockSession($sessionID)) { return FALSE; } - $this->_fingerprint = md5(''); - $this->_session_id = $session_id; + $this->keyExists = FALSE; + $this->sessionID = $sessionID; } if (isset($this->lockKey)) { $this->redis->setTimeout($this->lockKey, 300); - if ($this->fingerprint !== ($fingerprint = md5($session_data))) + if ($this->fingerprint !== ($fingerprint = md5($sessionData)) || $this->keyExists === FALSE) { - if ($this->redis->set($this->keyPrefix.$session_id, $session_data, $this->sessionExpiration)) + if ($this->redis->set($this->keyPrefix . $sessionID, $sessionData, $this->sessionExpiration)) { - $this->_fingerprint = $fingerprint; + $this->fingerprint = $fingerprint; + $this->keyExists = TRUE; return TRUE; } return FALSE; } - return $this->redis->setTimeout($this->keyPrefix.$session_id, $this->sessionExpiration); + return $this->redis->setTimeout($this->keyPrefix . $sessionID, $this->sessionExpiration); } return FALSE; @@ -245,7 +257,8 @@ public function close() { if (isset($this->redis)) { - try { + try + { if ($this->redis->ping() === '+PONG') { isset($this->lockKey) && $this->redis->delete($this->lockKey); @@ -255,10 +268,9 @@ public function close() return FALSE; } } - } - catch (\RedisException $e) + } catch (\RedisException $e) { - $this->logger->error('Session: Got RedisException on close(): '.$e->getMessage()); + $this->logger->error('Session: Got RedisException on close(): ' . $e->getMessage()); } $this->redis = NULL; @@ -276,16 +288,17 @@ public function close() * * Destroys the current session. * - * @param string $session_id Session ID - * @return bool + * @param string $sessionID + * + * @return bool */ - public function destroy($session_id) + public function destroy($sessionID) { if (isset($this->redis, $this->lockKey)) { - if (($result = $this->redis->delete($this->keyPrefix.$session_id)) !== 1) + if (($result = $this->redis->delete($this->keyPrefix . $sessionID)) !== 1) { - $this->logger->debug('Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.'); + $this->logger->debug('Session: Redis::delete() expected to return 1, got ' . var_export($result, TRUE) . ' instead.'); } return $this->destroyCookie(); @@ -317,18 +330,22 @@ public function gc($maxlifetime) * * Acquires an (emulated) lock. * - * @param string $session_id Session ID + * @param string $sessionID Session ID + * * @return bool */ - protected function lockSession(string $session_id): bool + protected function lockSession(string $sessionID): bool { - if (isset($this->lockKey)) + // PHP 7 reuses the SessionHandler object on regeneration, + // so we need to check here if the lock key is for the + // correct session ID. + if ($this->lockKey === $this->keyPrefix . $sessionID . ':lock') { return $this->redis->setTimeout($this->lockKey, 300); } // 30 attempts to obtain a lock, in case another request already has it - $lock_key = $this->keyPrefix.$session_id.':lock'; + $lock_key = $this->keyPrefix . $sessionID . ':lock'; $attempt = 0; do @@ -341,23 +358,22 @@ protected function lockSession(string $session_id): bool if ( ! $this->redis->setex($lock_key, 300, time())) { - $this->logger->error('Session: Error while trying to obtain lock for '.$this->keyPrefix.$session_id); + $this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID); return FALSE; } $this->lockKey = $lock_key; break; - } - while (++$attempt < 30); + } while (++ $attempt < 30); if ($attempt === 30) { - log_message('error', 'Session: Unable to obtain lock for '.$this->keyPrefix.$session_id.' after 30 attempts, aborting.'); + log_message('error', 'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.'); return FALSE; } elseif ($ttl === -1) { - log_message('debug', 'Session: Lock for '.$this->keyPrefix.$session_id.' had no TTL, overriding.'); + log_message('debug', 'Session: Lock for ' . $this->keyPrefix . $sessionID . ' had no TTL, overriding.'); } $this->lock = TRUE; @@ -379,17 +395,16 @@ protected function releaseLock(): bool { if ( ! $this->redis->delete($this->lockKey)) { - $this->logger->error('Session: Error while trying to free lock for '.$this->lockKey); + $this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey); return FALSE; } $this->lockKey = NULL; - $this->lock = FALSE; + $this->lock = FALSE; } return TRUE; } - + //-------------------------------------------------------------------- - } diff --git a/system/Session/Session.php b/system/Session/Session.php index 3f32c5f5c43b..7086baaad580 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -1,6 +1,4 @@ -driver = $driver; @@ -174,6 +166,8 @@ public function __construct(\SessionHandlerInterface $driver, \Config\App $confi $this->cookieDomain = $config->cookieDomain; $this->cookiePath = $config->cookiePath; $this->cookieSecure = $config->cookieSecure; + + helper('array'); } //-------------------------------------------------------------------- @@ -183,49 +177,55 @@ public function __construct(\SessionHandlerInterface $driver, \Config\App $confi */ public function start() { - if (is_cli()) + if (is_cli() && ENVIRONMENT !== 'testing') { $this->logger->debug('Session: Initialization under CLI aborted.'); return; - } + } elseif ((bool) ini_get('session.auto_start')) { $this->logger->error('Session: session.auto_start is enabled in php.ini. Aborting.'); return; } + elseif (session_status() === PHP_SESSION_ACTIVE) + { + $this->logger->warning('Session: Sessions is enabled, and one exists.Please don\'t $session->start();'); + + return; + } - if (! $this->driver instanceof \SessionHandlerInterface) + if ( ! $this->driver instanceof \SessionHandlerInterface) { - $this->logger->error("Session: Handler '".$this->driver. + $this->logger->error("Session: Handler '" . $this->driver . "' doesn't implement SessionHandlerInterface. Aborting."); } $this->configure(); - session_set_save_handler($this->driver, true); + $this->setSaveHandler(); // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers if (isset($_COOKIE[$this->sessionCookieName]) && ( - ! is_string($_COOKIE[$this->sessionCookieName]) || ! preg_match('/^[0-9a-f]{40}$/', $_COOKIE[$this->sessionCookieName]) + ! is_string($_COOKIE[$this->sessionCookieName]) || ! preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->sessionCookieName]) ) ) { unset($_COOKIE[$this->sessionCookieName]); } - session_start(); + $this->startSession(); // Is session ID auto-regeneration configured? (ignoring ajax requests) if ((empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') && ($regenerate_time = $this->sessionTimeToUpdate) > 0 ) { - if (! isset($_SESSION['__ci_last_regenerate'])) + if ( ! isset($_SESSION['__ci_last_regenerate'])) { $_SESSION['__ci_last_regenerate'] = time(); - } + } elseif ($_SESSION['__ci_last_regenerate'] < (time() - $regenerate_time)) { $this->regenerate((bool) $this->sessionRegenerateDestroy); @@ -235,20 +235,14 @@ public function start() // unless it is being currently created or regenerated elseif (isset($_COOKIE[$this->sessionCookieName]) && $_COOKIE[$this->sessionCookieName] === session_id()) { - setcookie( - $this->sessionCookieName, - session_id(), - (empty($this->sessionExpiration) ? 0 : time() + $this->sessionExpiration), - $this->cookiePath, - $this->cookieDomain, - $this->cookieSecure, - true - ); + $this->setCookie(); } $this->initVars(); - $this->logger->info("Session: Class initialized using '".$this->sessionDriverName."' driver."); + $this->logger->info("Session: Class initialized using '" . $this->sessionDriverName . "' driver."); + + return $this; } //-------------------------------------------------------------------- @@ -263,21 +257,14 @@ public function start() public function stop() { setcookie( - $this->sessionCookieName, - session_id(), - 1, - $this->cookiePath, - $this->cookieDomain, - $this->cookieSecure, - true + $this->sessionCookieName, session_id(), 1, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true ); - + session_regenerate_id(true); } //-------------------------------------------------------------------- - /** * Configuration. * @@ -288,24 +275,20 @@ protected function configure() if (empty($this->sessionCookieName)) { $this->sessionCookieName = ini_get('session.name'); - } + } else { ini_set('session.name', $this->sessionCookieName); } session_set_cookie_params( - $this->sessionExpiration, - $this->cookiePath, - $this->cookieDomain, - $this->cookieSecure, - true // HTTP only; Yes, this is intentional and not configurable for security reasons. + $this->sessionExpiration, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true // HTTP only; Yes, this is intentional and not configurable for security reasons. ); if (empty($this->sessionExpiration)) { $this->sessionExpiration = (int) ini_get('session.gc_maxlifetime'); - } + } else { ini_set('session.gc_maxlifetime', (int) $this->sessionExpiration); @@ -316,8 +299,58 @@ protected function configure() ini_set('session.use_strict_mode', 1); ini_set('session.use_cookies', 1); ini_set('session.use_only_cookies', 1); - ini_set('session.hash_function', 1); - ini_set('session.hash_bits_per_character', 4); + + $this->configureSidLength(); + } + + // ------------------------------------------------------------------------ + + /** + * Configure session ID length + * + * To make life easier, we used to force SHA-1 and 4 bits per + * character on everyone. And of course, someone was unhappy. + * + * Then PHP 7.1 broke backwards-compatibility because ext/session + * is such a mess that nobody wants to touch it with a pole stick, + * and the one guy who does, nobody has the energy to argue with. + * + * So we were forced to make changes, and OF COURSE something was + * going to break and now we have this pile of shit. -- Narf + * + * @return void + */ + protected function configureSidLength() + { + $bits_per_character = (int) (ini_get('session.sid_bits_per_character') !== false + ? ini_get('session.sid_bits_per_character') + : 4); + $sid_length = (int) (ini_get('session.sid_length') !== false + ? ini_get('session.sid_length') + : 40); + if (($sid_length * $bits_per_character) < 160) + { + $bits = ($sid_length * $bits_per_character); + // Add as many more characters as necessary to reach at least 160 bits + $sid_length += (int) ceil((160 % $bits) / $bits_per_character); + ini_set('session.sid_length', $sid_length); + } + + // Yes, 4,5,6 are the only known possible values as of 2016-10-27 + switch ($bits_per_character) + { + case 4: + $this->sidRegexp = '[0-9a-f]'; + break; + case 5: + $this->sidRegexp = '[0-9a-v]'; + break; + case 6: + $this->sidRegexp = '[0-9a-zA-Z,-]'; + break; + } + + $this->sidRegexp .= '{' . $sid_length . '}'; } //-------------------------------------------------------------------- @@ -330,31 +363,31 @@ protected function configure() */ protected function initVars() { - if (! empty($_SESSION['__ci_vars'])) + if (empty($_SESSION['__ci_vars'])) { - $current_time = time(); + return; + } - foreach ($_SESSION['__ci_vars'] as $key => &$value) + $current_time = time(); + + foreach ($_SESSION['__ci_vars'] as $key => &$value) + { + if ($value === 'new') { - if ($value === 'new') - { - $_SESSION['__ci_vars'][$key] = 'old'; - } - // Hacky, but 'old' will (implicitly) always be less than time() ;) - // DO NOT move this above the 'new' check! - elseif ($value < $current_time) - { - unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]); - } + $_SESSION['__ci_vars'][$key] = 'old'; } - - if (empty($_SESSION['__ci_vars'])) + // Hacky, but 'old' will (implicitly) always be less than time() ;) + // DO NOT move this above the 'new' check! + elseif ($value < $current_time) { - unset($_SESSION['__ci_vars']); + unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]); } } - $this->userdata = & $_SESSION; + if (empty($_SESSION['__ci_vars'])) + { + unset($_SESSION['__ci_vars']); + } } //-------------------------------------------------------------------- @@ -390,15 +423,15 @@ public function destroy() /** * Sets user data into the session. - * + * * If $data is a string, then it is interpreted as a session property * key, and $value is expected to be non-null. - * + * * If $data is an array, it is expected to be an array of key/value pairs * to be set as session properties. * - * @param $data Property name or associative array of properties - * @param null $value Property value if single key provided + * @param string|array $data Property name or associative array of properties + * @param string|array $value Property value if single key provided */ public function set($data, $value = null) { @@ -406,7 +439,14 @@ public function set($data, $value = null) { foreach ($data as $key => &$value) { - $_SESSION[$key] = $value; + if (is_int($key)) + { + $_SESSION[$value] = null; + } + else + { + $_SESSION[$key] = $value; + } } return; @@ -423,31 +463,37 @@ public function set($data, $value = null) * If the property exists as "normal", returns it. * Otherwise, returns an array of any temp or flash data values with the * property key. - * + * * Replaces the legacy method $session->userdata(); * - * @param $key Identifier of the session property to retrieve + * @param string $key Identifier of the session property to retrieve * @return array|null The property value(s) */ - public function get($key = null) + public function get(string $key = null) { - if (isset($key)) + if (! empty($key) && $value = dot_array_search($key, $_SESSION)) { - return isset($_SESSION[$key]) ? $_SESSION[$key] : null; - } + return $value; + } elseif (empty($_SESSION)) { return []; } + if (! empty($key)) + { + return null; + } + $userdata = []; $_exclude = array_merge( - ['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys() + ['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys() ); - foreach (array_keys($_SESSION) as $key) + $keys = array_keys($_SESSION); + foreach ($keys as $key) { - if (! in_array($key, $_exclude, true)) + if ( ! in_array($key, $_exclude, true)) { $userdata[$key] = $_SESSION[$key]; } @@ -465,11 +511,29 @@ public function get($key = null) * * @return bool */ - public function has($key) + public function has(string $key) { return isset($_SESSION[$key]); } + //-------------------------------------------------------------------- + + /** + * Push new value onto session value that is array. + * + * @param string $key Identifier of the session property we are interested in. + * @param array $data value to be pushed to existing session key. + * + * @return void + */ + public function push(string $key, array $data) + { + if ($this->has($key) && is_array($value = $this->get($key))) + { + $this->set($key, array_merge($value, $data)); + } + } + //-------------------------------------------------------------------- /** @@ -478,8 +542,8 @@ public function has($key) * If $key is an array, it is interpreted as an array of string property * identifiers to remove. Otherwise, it is interpreted as the identifier * of a specific session property to remove. - * - * @param $key Identifier of the session property or properties to remove. + * + * @param string|array $key Identifier of the session property or properties to remove. */ public function remove($key) { @@ -502,8 +566,8 @@ public function remove($key) * Magic method to set variables in the session by simply calling * $session->foo = bar; * - * @param $key Identifier of the session property to set. - * @param $value + * @param string $key Identifier of the session property to set. + * @param $value */ public function __set($key, $value) { @@ -516,7 +580,7 @@ public function __set($key, $value) * Magic method to get session variables by simply calling * $foo = $session->foo; * - * @param $key Identifier of the session property to remove. + * @param string $key Identifier of the session property to remove. * * @return null|string */ @@ -527,7 +591,7 @@ public function __get($key) if (isset($_SESSION[$key])) { return $_SESSION[$key]; - } + } elseif ($key === 'session_id') { return session_id(); @@ -545,13 +609,13 @@ public function __get($key) * Sets data into the session that will only last for a single request. * Perfect for use with single-use status update messages. * - * If $data is an array, it is interpreted as an associative array of + * If $data is an array, it is interpreted as an associative array of * key/value pairs for flashdata properties. - * Otherwise, it is interpreted as the identifier of a specific + * Otherwise, it is interpreted as the identifier of a specific * flashdata property, with $value containing the property value. - * - * @param $data Property identifier or associative array of properties - * @param null $value Property value if $data is a scalar + * + * @param array|string $data Property identifier or associative array of properties + * @param string|array $value Property value if $data is a scalar */ public function setFlashdata($data, $value = null) { @@ -563,13 +627,13 @@ public function setFlashdata($data, $value = null) /** * Retrieve one or more items of flash data from the session. - * + * * If the item key is null, return all flashdata. * * @param string $key Property identifier * @return array|null The requested property value, or an associative array of them */ - public function getFlashdata($key = null) + public function getFlashdata(string $key = null) { if (isset($key)) { @@ -579,11 +643,11 @@ public function getFlashdata($key = null) $flashdata = []; - if (! empty($_SESSION['__ci_vars'])) + if ( ! empty($_SESSION['__ci_vars'])) { foreach ($_SESSION['__ci_vars'] as $key => &$value) { - is_int($value) OR $flashdata[$key] = $_SESSION[$key]; + is_int($value) || $flashdata[$key] = $_SESSION[$key]; } } @@ -597,7 +661,7 @@ public function getFlashdata($key = null) * * @param string $key Property identifier or array of them */ - public function keepFlashdata($key) + public function keepFlashdata(string $key) { $this->markAsFlashdata($key); } @@ -606,17 +670,18 @@ public function keepFlashdata($key) /** * Mark a session property or properties as flashdata. - * - * @param $key Property identifier or array of them - * @return False if any of the properties are not already set + * + * @param array|string $key Property identifier or array of them + * + * @return bool False if any of the properties are not already set */ public function markAsFlashdata($key) { if (is_array($key)) { - for ($i = 0, $c = count($key); $i < $c; $i++) + for ($i = 0, $c = count($key); $i < $c; $i ++ ) { - if (! isset($_SESSION[$key[$i]])) + if ( ! isset($_SESSION[$key[$i]])) { return false; } @@ -629,7 +694,7 @@ public function markAsFlashdata($key) return true; } - if (! isset($_SESSION[$key])) + if ( ! isset($_SESSION[$key])) { return false; } @@ -653,7 +718,7 @@ public function unmarkFlashdata($key) return; } - is_array($key) OR $key = [$key]; + is_array($key) || $key = [$key]; foreach ($key as $k) { @@ -678,7 +743,7 @@ public function unmarkFlashdata($key) */ public function getFlashKeys() { - if (! isset($_SESSION['__ci_vars'])) + if ( ! isset($_SESSION['__ci_vars'])) { return []; } @@ -686,7 +751,7 @@ public function getFlashKeys() $keys = []; foreach (array_keys($_SESSION['__ci_vars']) as $key) { - is_int($_SESSION['__ci_vars'][$key]) OR $keys[] = $key; + is_int($_SESSION['__ci_vars'][$key]) || $keys[] = $key; } return $keys; @@ -701,20 +766,20 @@ public function getFlashKeys() * Sets new data into the session, and marks it as temporary data * with a set lifespan. * - * @param $data Session data key or associative array of items - * @param null $value Value to store - * @param int $ttl Time-to-live in seconds + * @param string|array $data Session data key or associative array of items + * @param null $value Value to store + * @param int $ttl Time-to-live in seconds */ public function setTempdata($data, $value = null, $ttl = 300) { $this->set($data, $value); - $this->markAsTempdata(is_array($data) ? array_keys($data) : $data, $ttl); + $this->markAsTempdata($data, $ttl); } //-------------------------------------------------------------------- /** - * Returns either a single piece of tempdata, or all temp data currently + * Returns either a single piece of tempdata, or all temp data currently * in the session. * * @param $key Session data key @@ -730,7 +795,7 @@ public function getTempdata($key = null) $tempdata = []; - if (! empty($_SESSION['__ci_vars'])) + if ( ! empty($_SESSION['__ci_vars'])) { foreach ($_SESSION['__ci_vars'] as $key => &$value) { @@ -760,9 +825,10 @@ public function removeTempdata($key) * Mark one of more pieces of data as being temporary, meaning that * it has a set lifespan within the session. * - * @param $key Property identifier or array of them - * @param int $ttl Time to live, in seconds - * @return bool False if any of the properties were not set + * @param string|array $key Property identifier or array of them + * @param int $ttl Time to live, in seconds + * + * @return bool False if any of the properties were not set */ public function markAsTempdata($key, $ttl = 300) { @@ -779,13 +845,17 @@ public function markAsTempdata($key, $ttl = 300) { $k = $v; $v = $ttl; - } + } + elseif (is_string($v)) + { + $v = time() + $ttl; + } else { $v += time(); } - if (! isset($_SESSION[$k])) + if ( ! array_key_exists($k, $_SESSION)) { return false; } @@ -798,7 +868,7 @@ public function markAsTempdata($key, $ttl = 300) return true; } - if (! isset($_SESSION[$key])) + if ( ! isset($_SESSION[$key])) { return false; } @@ -814,7 +884,7 @@ public function markAsTempdata($key, $ttl = 300) * Unmarks temporary data in the session, effectively removing its * lifespan and allowing it to live as long as the session does. * - * @param $key Property identifier or array of them + * @param string|array $key Property identifier or array of them */ public function unmarkTempdata($key) { @@ -823,7 +893,7 @@ public function unmarkTempdata($key) return; } - is_array($key) OR $key = [$key]; + is_array($key) || $key = [$key]; foreach ($key as $k) { @@ -848,7 +918,7 @@ public function unmarkTempdata($key) */ public function getTempKeys() { - if (! isset($_SESSION['__ci_vars'])) + if ( ! isset($_SESSION['__ci_vars'])) { return []; } @@ -863,4 +933,45 @@ public function getTempKeys() } //-------------------------------------------------------------------- + + /** + * Sets the driver as the session handler in PHP. + * Extracted for easier testing. + */ + protected function setSaveHandler() + { + session_set_save_handler($this->driver, true); + } + + //-------------------------------------------------------------------- + + /** + * Starts the session. + * Extracted for testing reasons. + */ + protected function startSession() + { + if (ENVIRONMENT === 'testing') + { + $_SESSION = []; + return; + } + + session_start(); + } + + //-------------------------------------------------------------------- + + /** + * Takes care of setting the cookie on the client side. + * Extracted for testing reasons. + */ + protected function setCookie() + { + setcookie( + $this->sessionCookieName, session_id(), (empty($this->sessionExpiration) ? 0 : time() + $this->sessionExpiration), $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true + ); + } + + //-------------------------------------------------------------------- } diff --git a/system/Session/SessionInterface.php b/system/Session/SessionInterface.php index 45c2c7a63648..f435ee289490 100644 --- a/system/Session/SessionInterface.php +++ b/system/Session/SessionInterface.php @@ -1,6 +1,4 @@ -userdata(); * - * @param $key Identifier of the session property to retrieve - * @return array|null The property value(s) + * @param string $key Identifier of the session property to retrieve + * + * @return array|null The property value(s) */ - public function get($key = null); + public function get(string $key = null); //-------------------------------------------------------------------- @@ -116,8 +108,8 @@ public function has(string $key); * If $key is an array, it is interpreted as an array of string property * identifiers to remove. Otherwise, it is interpreted as the identifier * of a specific session property to remove. - * - * @param $key Identifier of the session property or properties to remove. + * + * @param string|array $key Identifier of the session property or properties to remove. */ public function remove($key); @@ -127,13 +119,13 @@ public function remove($key); * Sets data into the session that will only last for a single request. * Perfect for use with single-use status update messages. * - * If $data is an array, it is interpreted as an associative array of + * If $data is an array, it is interpreted as an associative array of * key/value pairs for flashdata properties. - * Otherwise, it is interpreted as the identifier of a specific + * Otherwise, it is interpreted as the identifier of a specific * flashdata property, with $value containing the property value. - * - * @param $data Property identifier or associative array of properties - * @param null $value Property value if $data is a scalar + * + * @param string|array $data Property identifier or associative array of properties + * @param null $value Property value if $data is a scalar */ public function setFlashdata($data, $value = null); @@ -141,11 +133,11 @@ public function setFlashdata($data, $value = null); /** * Retrieve one or more items of flash data from the session. - * + * * If the item key is null, return all flashdata. * * @param string $key Property identifier - * @return array|null The requested property value, or an associative + * @return array|null The requested property value, or an associative * array of them */ public function getFlashdata(string $key = null); @@ -163,8 +155,9 @@ public function keepFlashdata(string $key); /** * Mark a session property or properties as flashdata. - * - * @param $key Property identifier or array of them + * + * @param string|array $key Property identifier or array of them + * * @return False if any of the properties are not already set */ public function markAsFlashdata($key); @@ -174,7 +167,7 @@ public function markAsFlashdata($key); /** * Unmark data in the session as flashdata. * - * @param mixed $key Property identifier or array of them + * @param string|array $key Property identifier or array of them */ public function unmarkFlashdata($key); @@ -193,19 +186,19 @@ public function getFlashKeys(); * Sets new data into the session, and marks it as temporary data * with a set lifespan. * - * @param $data Session data key or associative array of items - * @param null $value Value to store - * @param int $ttl Time-to-live in seconds + * @param string|array $data Session data key or associative array of items + * @param mixed $value Value to store + * @param int $ttl Time-to-live in seconds */ public function setTempdata($data, $value = null, $ttl = 300); //-------------------------------------------------------------------- /** - * Returns either a single piece of tempdata, or all temp data currently + * Returns either a single piece of tempdata, or all temp data currently * in the session. * - * @param $key Session data key + * @param string $key Session data key * @return mixed Session data value or null if not found. */ public function getTempdata($key = null); @@ -225,9 +218,10 @@ public function removeTempdata($key); * Mark one of more pieces of data as being temporary, meaning that * it has a set lifespan within the session. * - * @param $key Property identifier or array of them - * @param int $ttl Time to live, in seconds - * @return bool False if any of the properties were not set + * @param string|array $key Property identifier or array of them + * @param int $ttl Time to live, in seconds + * + * @return bool False if any of the properties were not set */ public function markAsTempdata($key, $ttl = 300); @@ -237,7 +231,7 @@ public function markAsTempdata($key, $ttl = 300); * Unmarks temporary data in the session, effectively removing its * lifespan and allowing it to live as long as the session does. * - * @param $key Property identifier or array of them + * @param string|array $key Property identifier or array of them */ public function unmarkTempdata($key); diff --git a/system/Test/CIDatabaseTestCase.php b/system/Test/CIDatabaseTestCase.php index 46fc24fb265a..8514a735a30d 100644 --- a/system/Test/CIDatabaseTestCase.php +++ b/system/Test/CIDatabaseTestCase.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,23 +29,23 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use CodeIgniter\ConfigException; +use Config\Services; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\MigrationRunner; -use Config\Services; +use CodeIgniter\Exceptions\ConfigException; /** * CIDatabaseTestCase */ class CIDatabaseTestCase extends CIUnitTestCase { + /** * Should the db be refreshed before * each test? @@ -69,7 +69,14 @@ class CIDatabaseTestCase extends CIUnitTestCase * * @var string */ - protected $basePath = APPPATH.'../tests/_support/_database'; + protected $basePath = TESTPATH . '_support/Database'; + + /** + * The namespace to help us fird the migration classes. + * + * @var string + */ + protected $namespace = 'Tests\Support'; /** * The name of the database group to connect to. @@ -81,20 +88,20 @@ class CIDatabaseTestCase extends CIUnitTestCase /** * Our database connection. - * + * * @var BaseConnection */ protected $db; /** * Migration Runner instance. - * + * * @var MigrationRunner|mixed */ protected $migrations; /** - * Seeder instance + * Seeder instance * * @var \CodeIgniter\Database\Seeder */ @@ -125,7 +132,7 @@ public function loadDependencies() $config->enabled = true; $this->migrations = Services::migrations($config, $this->db); - $this->migrations->setSilent(true); + $this->migrations->setSilent(false); } if ($this->seeder === null) @@ -145,27 +152,43 @@ public function loadDependencies() */ public function setUp() { + parent::setUp(); + $this->loadDependencies(); if ($this->refresh === true) { - if (! empty($this->basePath)) + if ( ! empty($this->namespace)) { - $this->migrations->setPath(rtrim($this->basePath, '/').'/migrations'); + $this->migrations->setNamespace($this->namespace); } - $this->db->table('migrations')->truncate(); - $this->migrations->version(0, 'tests'); - $this->migrations->latest('tests'); + // Delete all of the tables to ensure we're at a clean start. + $tables = $this->db->listTables(); + + if (is_array($tables)) + { + $forge = \Config\Database::forge('tests'); + + foreach ($tables as $table) + { + if ($table == $this->db->DBPrefix.'migrations') continue; + + $forge->dropTable($table, true); + } + } + + $this->migrations->version(0, null, 'tests'); + $this->migrations->latest(null, 'tests'); } - if (! empty($this->seed)) + if ( ! empty($this->seed)) { - if (! empty($this->basePath)) + if ( ! empty($this->basePath)) { - $this->seeder->setPath(rtrim($this->basePath, '/').'/seeds'); + $this->seeder->setPath(rtrim($this->basePath, '/') . '/seeds'); } - + $this->seed($this->seed); } } @@ -178,20 +201,19 @@ public function setUp() */ public function tearDown() { - if (! empty($this->insertCache)) - { - foreach ($this->insertCache as $row) - { - $this->db->table($row[0]) - ->where($row[1]) - ->delete(); - } - } + if ( ! empty($this->insertCache)) + { + foreach ($this->insertCache as $row) + { + $this->db->table($row[0]) + ->where($row[1]) + ->delete(); + } + } } //-------------------------------------------------------------------- - /** * Seeds that database with a specific seeder. * @@ -199,11 +221,10 @@ public function tearDown() */ public function seed(string $name) { - return $this->seeder->call($name); + return $this->seeder->call($name); } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // Database Test Helpers //-------------------------------------------------------------------- @@ -220,31 +241,31 @@ public function seed(string $name) public function dontSeeInDatabase(string $table, array $where) { $count = $this->db->table($table) - ->where($where) - ->countAllResults(); + ->where($where) + ->countAllResults(); - $this->assertTrue($count == 0, 'Row was found in database'); + $this->assertTrue($count == 0, 'Row was found in database'); } - + //-------------------------------------------------------------------- /** * Asserts that records that match the conditions in $where DO * exist in the database. - * + * * @param string $table * @param array $where * * @return bool - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function seeInDatabase(string $table, array $where) { $count = $this->db->table($table) - ->where($where) - ->countAllResults(); + ->where($where) + ->countAllResults(); - $this->assertTrue($count > 0, 'Row not found in database'); + $this->assertTrue($count > 0, 'Row not found in database: ' . $this->db->showLastQuery()); } //-------------------------------------------------------------------- @@ -258,20 +279,20 @@ public function seeInDatabase(string $table, array $where) * @param array $where * * @return bool - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function grabFromDatabase(string $table, string $column, array $where) { - $query = $this->db->table($table) - ->select($column) - ->where($where) - ->get(); + $query = $this->db->table($table) + ->select($column) + ->where($where) + ->get(); $query = $query->getRow(); return $query->$column ?? false; } - + //-------------------------------------------------------------------- /** @@ -281,7 +302,7 @@ public function grabFromDatabase(string $table, string $column, array $where) * @param string $table * @param array $data * - * @throws \CodeIgniter\DatabaseException + * @return bool */ public function hasInDatabase(string $table, array $data) { @@ -289,8 +310,8 @@ public function hasInDatabase(string $table, array $data) $table, $data ]; - $this->db->table($table) - ->insert($data); + return $this->db->table($table) + ->insert($data); } //-------------------------------------------------------------------- @@ -304,17 +325,16 @@ public function hasInDatabase(string $table, array $data) * @param array $where * * @return bool - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function seeNumRecords(int $expected, string $table, array $where) { - $count = $this->db->table($table) - ->where($where) - ->countAllResults(); + $count = $this->db->table($table) + ->where($where) + ->countAllResults(); $this->assertEquals($expected, $count, 'Wrong number of matching rows in database.'); } - + //-------------------------------------------------------------------- - } diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index 5f32b8f45692..b9aa95862023 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,37 +29,137 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - -use PHPUnit_Framework_TestCase; -use CodeIgniter\Log\TestLogger; +use Config\Paths; +use CodeIgniter\Events\Events; +use PHPUnit\Framework\TestCase; +use Tests\Support\Log\TestLogger; /** * PHPunit test case. */ -class CIUnitTestCase extends PHPUnit_Framework_TestCase +class CIUnitTestCase extends TestCase { use ReflectionHelper; + /** + * @var \CodeIgniter\CodeIgniter + */ + protected $app; + + /** + * Path to Config folder, relative + * to the system folder. + * @var string + */ + protected $configPath = '../application/Config'; + + public function setUp() + { + parent::setUp(); + + if (! $this->app) + { + $this->app = $this->createApplication(); + } + + helper('url'); + } + /** * Custom function to hook into CodeIgniter's Logging mechanism * to check if certain messages were logged during code execution. * * @param string $level * @param null $expectedMessage + * + * @throws \Exception */ public function assertLogged(string $level, $expectedMessage = null) { $result = TestLogger::didLog($level, $expectedMessage); $this->assertTrue($result); + return $result; } - //-------------------------------------------------------------------- + /** + * Hooks into CodeIgniter's Events system to check if a specific + * event was triggered or not. + * + * @param string $eventName + * + * @return bool + * @throws \Exception + */ + public function assertEventTriggered(string $eventName): bool + { + $found = false; + $eventName = strtolower($eventName); + + foreach (Events::getPerformanceLogs() as $log) + { + if ($log['event'] !== $eventName) continue; + + $found = true; + break; + } + $this->assertTrue($found); + return $found; + } + + /** + * Loads up an instance of CodeIgniter + * and gets the environment setup. + * + * @return mixed + */ + protected function createApplication() + { + $systemPath = realpath(__DIR__.'/../'); + + require_once $systemPath.'/'.$this->configPath.'/Paths.php'; + $paths = $this->adjustPaths(new \Config\Paths()); + + $app = require $systemPath.'/bootstrap.php'; + return $app; + } + + /** + * Attempts to adjust our system paths to account + * for relative location of our tests folder. + * Not foolproof, but works well for default locations. + * + * @param \Config\Paths $paths + * + * @return \Config\Paths + */ + protected function adjustPaths(Paths $paths) + { + $tests = [ + 'systemDirectory', 'applicationDirectory', 'writableDirectory', 'testsDirectory' + ]; + + foreach ($tests as $test) + { + if (is_dir($paths->$test) || strpos($paths->$test, '../') !== 0) + { + continue; + } + + $check = substr($paths->$test, 3); + if (is_dir($check)) + { + $paths->$test = $check; + } + } + + return $paths; + } } diff --git a/system/Test/FeatureResponse.php b/system/Test/FeatureResponse.php new file mode 100644 index 000000000000..f05a5cdc70e4 --- /dev/null +++ b/system/Test/FeatureResponse.php @@ -0,0 +1,365 @@ +response = $response; + + if (is_string($this->response->getBody())) + { + $this->domParser = (new DOMParser())->withString($this->response->getBody()); + } + } + + //-------------------------------------------------------------------- + // Simple Response Checks + //-------------------------------------------------------------------- + + /** + * Boils down the possible responses into a bolean valid/not-valid + * response type. + * + * @return bool + */ + public function isOK(): bool + { + // Only 200 and 300 range status codes + // are considered valid. + if ($this->response->getStatusCode() >= 400 || $this->response->getStatusCode() < 200) + { + return false; + } + + // Empty bodies are not considered valid. + if (empty($this->response->getBody())) + { + return false; + } + + return true; + } + + /** + * Returns whether or not the Response was a redirect response + * + * @return bool + */ + public function isRedirect(): bool + { + return $this->response instanceof RedirectResponse; + } + + /** + * Assert that the given response was a redirect. + * + * @throws \Exception + */ + public function assertRedirect() + { + $this->assertTrue($this->isRedirect(), "Response is not a RedirectResponse."); + } + + /** + * Asserts that the status is a specific value. + * + * @param int $code + * + * @throws \Exception + */ + public function assertStatus(int $code) + { + $this->assertEquals($code, (int)$this->response->getStatusCode()); + } + + /** + * Asserts that the Response is considered OK. + * + * @throws \Exception + */ + public function assertOK() + { + $this->assertTrue($this->isOK(), "{$this->response->getStatusCode()} is not a successful status code, or the Response has an empty body."); + } + + //-------------------------------------------------------------------- + // Session Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that an SESSION key has been set and, optionally, test it's value. + * + * @param string $key + * @param null $value + * + * @throws \Exception + */ + public function assertSessionHas(string $key, $value = null) + { + $this->assertTrue(array_key_exists($key, $_SESSION), "'{$key}' is not in the current \$_SESSION"); + + if ($value !== null) + { + $this->assertEquals($value, $_SESSION[$key], "The value of '{$key}' ({$value}) does not match expected value."); + } + } + + /** + * Asserts the session is missing $key. + * + * @param string $key + * + * @throws \Exception + */ + public function assertSessionMissing(string $key) + { + $this->assertFalse(array_key_exists($key, $_SESSION), "'{$key}' should not be present in \$_SESSION."); + } + + //-------------------------------------------------------------------- + // Header Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the Response contains a specific header. + * + * @param string $key + * @param null $value + * + * @throws \Exception + */ + public function assertHeader(string $key, $value = null) + { + $this->assertTrue($this->response->hasHeader($key), "'{$key}' is not a valid Response header."); + + if ($value !== null) + { + $this->assertEquals($value, $this->response->getHeaderLine($key), "The value of '{$key}' header ({$this->response->getHeaderLine($key)}) does not match expected value."); + } + } + + /** + * Asserts the Response headers does not contain the specified header. + * + * @param string $key + * + * @throws \Exception + */ + public function assertHeaderMissing(string $key) + { + $this->assertFalse($this->response->hasHeader($key), "'{$key}' should not be in the Response headers."); + } + + //-------------------------------------------------------------------- + // Cookie Assertions + //-------------------------------------------------------------------- + + /** + * Asserts that the response has the specified cookie. + * + * @param string $key + * @param null $value + * @param string|null $prefix + * + * @throws \Exception + */ + public function assertCookie(string $key, $value = null, string $prefix = '') + { + $this->assertTrue($this->response->hasCookie($key, $value, $prefix), "No cookie found named '{$key}'."); + } + + /** + * Assert the Response does not have the specified cookie set. + * + * @param string $key + * @param null $value + * @param string $prefix + * + * @throws \Exception + */ + public function assertCookieMissing(string $key) + { + $this->assertFalse($this->response->hasCookie($key), "Cookie named '{$key}' should not be set."); + } + + /** + * Asserts that a cookie exists and has an expired time. + * + * @param string $key + * @param string $prefix + * + * @throws \Exception + */ + public function assertCookieExpired(string $key, string $prefix = '') + { + $this->assertTrue($this->response->hasCookie($key, null, $prefix)); + $this->assertGreaterThan(time(), $this->response->getCookie($key, $prefix)['expires']); + } + + //-------------------------------------------------------------------- + // DomParser Assertions + //-------------------------------------------------------------------- + + /** + * Assert that the desired text can be found in the result body. + * + * @param string|null $search + * @param string|null $element + * + * @throws \Exception + */ + public function assertSee(string $search = null, string $element = null) + { + $this->assertTrue($this->domParser->see($search, $element), "Do not see '{$search}' in response."); + } + + /** + * Asserts that we do not see the specified text. + * + * @param string|null $search + * @param string|null $element + * + * @throws \Exception + */ + public function assertDontSee(string $search = null, string $element = null) + { + $this->assertTrue($this->domParser->dontSee($search, $element), "I should not see '{$search}' in response."); + } + + /** + * Assert that we see an element selected via a CSS selector. + * + * @param string $search + * + * @throws \Exception + */ + public function assertSeeElement(string $search) + { + $this->assertTrue($this->domParser->seeElement($search), "Do not see element with selector '{$search} in response.'"); + } + + /** + * Assert that we do not see an element selected via a CSS selector. + * + * @param string $search + * + * @throws \Exception + */ + public function assertDontSeeElement(string $search) + { + $this->assertTrue($this->domParser->dontSeeElement($search), "I should not see an element with selector '{$search}' in response.'"); + } + + /** + * Assert that we see a link with the matching text and/or class. + * + * @param string $text + * @param string|null $details + * + * @throws \Exception + */ + public function assertSeeLink(string $text, string $details=null) + { + $this->assertTrue($this->domParser->seeLink($text, $details), "Do no see anchor tag with the text {$text} in response."); + } + + /** + * Assert that we see an input with name/value. + * + * @param string $field + * @param string|null $value + * + * @throws \Exception + */ + public function assertSeeInField(string $field, string $value=null) + { + $this->assertTrue($this->domParser->seeInField($field, $value), "Do no see input named {$field} with value {$value} in response."); + } + + //-------------------------------------------------------------------- + // JSON Methods + //-------------------------------------------------------------------- + + /** + * Returns the response's body as JSON + * + * @return mixed|string + */ + public function getJSON() + { + $response = $this->response->getJSON(); + + if (is_null($response)) + { + $this->fail('The Response contained invalid JSON.'); + } + + return $response; + } + + /** + * + * + * @param array $fragment + * + * @throws \Exception + */ + public function assertJSONFragment(array $fragment) + { + $json = json_decode($this->getJSON(), true); + + $this->assertArraySubset($fragment, $json, false, "Response does not contain a matching JSON fragment."); + } + + /** + * Asserts that the JSON exactly matches the passed in data. + * If the value being passed in is a string, it must be a json_encoded string. + * + * @param string|array $test + * + * @throws \Exception + */ + public function assertJSONExact($test) + { + $json = $this->getJSON(); + + if (is_array($test)) + { + $config = new \Config\Format(); + $formatter = $config->getFormatter('application/json'); + $test = $formatter->format($test); + } + + $this->assertJsonStringEqualsJsonString($test, $json, "Response does not contain matching JSON."); + } + + + //-------------------------------------------------------------------- + // XML Methods + //-------------------------------------------------------------------- + + /** + * Returns the response' body as XML + * + * @return mixed|string + */ + public function getXML() + { + return $this->response->getXML(); + } +} diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php new file mode 100644 index 000000000000..1a596a59873b --- /dev/null +++ b/system/Test/FeatureTestCase.php @@ -0,0 +1,252 @@ +{$route[0]}($route[1], $route[2]); + } + } + + $this->routes = $collection; + + return $this; + } + + /** + * Sets any values that should exist during this session. + * + * @param array $values + * + * @return $this + */ + public function withSession(array $values) + { + $this->session = $values; + + return $this; + } + + /** + * Don't run any events while running this test. + * + * @return $this + */ + public function skipEvents() + { + Events::simulate(true); + + return $this; + } + + /** + * Calls a single URI, executes it, and returns a FeatureResponse + * instance that can be used to run many assertions against. + * + * @param string $method + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function call(string $method, string $path, array $params = null) + { + // Simulate having a blank session + $_SESSION = []; + + $request = $this->setupRequest($method, $path, $params); + + $request = $this->populateGlobals($method, $request, $params); + + $response = $this->app + ->setRequest($request) + ->run($this->routes, true); + + $featureResponse = new FeatureResponse($response); + + return $featureResponse; + } + + /** + * Performs a GET request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function get(string $path, array $params = null) + { + return $this->call('get', $path, $params); + } + + /** + * Performs a POST request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function post(string $path, array $params = null) + { + return $this->call('post', $path, $params); + } + + /** + * Performs a PUT request + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function put(string $path, array $params = null) + { + return $this->call('put', $path, $params); + } + + /** + * Performss a PATCH request + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function patch(string $path, array $params = null) + { + return $this->call('patch', $path, $params); + } + + /** + * Performs a DELETE request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function delete(string $path, array $params = null) + { + return $this->call('delete', $path, $params); + } + + /** + * Performs an OPTIONS request. + * + * @param string $path + * @param array|null $params + * + * @return \CodeIgniter\Test\FeatureResponse + * @throws \CodeIgniter\HTTP\RedirectException + * @throws \Exception + */ + public function options(string $path, array $params = null) + { + return $this->call('delete', $path, $params); + } + + /** + * Setup a Request object to use so that CodeIgniter + * won't try to auto-populate some of the items. + * + * @param string $method + * @param string|null $path + * + * @return \CodeIgniter\HTTP\IncomingRequest + */ + protected function setupRequest(string $method, string $path=null, $params = null) + { + $config = config(App::class); + $uri = new URI($config->baseURL .'/'. trim($path, '/ ')); + + $request = new IncomingRequest($config, clone($uri), $params, new UserAgent()); + $request->uri = $uri; + + $request->setMethod($method); + $request->setProtocolVersion('1.1'); + + return $request; + } + + /** + * Populates the data of our Request with "global" data + * relevant to the request, like $_POST data. + * + * Always populate the GET vars based on the URI. + * + * @param string $method + * @param \CodeIgniter\HTTP\Request $request + * @param array|null $params + * + * @return \CodeIgniter\HTTP\Request + */ + protected function populateGlobals(string $method, Request $request, array $params = null) + { + $request->setGlobal('get', $this->getPrivateProperty($request->uri, 'query')); + if ($method !== 'get') + { + $request->setGlobal($method, $params); + } + + $_SESSION = $this->session; + + return $request; + } +} diff --git a/system/Test/Filters/CITestStreamFilter.php b/system/Test/Filters/CITestStreamFilter.php new file mode 100644 index 000000000000..8adbb0407902 --- /dev/null +++ b/system/Test/Filters/CITestStreamFilter.php @@ -0,0 +1,64 @@ +data; + $consumed += $bucket->datalen; + } + return PSFS_PASS_ON; + } + +} + +// @codeCoverageIgnoreStart +stream_filter_register('CITestStreamFilter', 'CodeIgniter\Test\Filters\CITestStreamFilter'); +// @codeCoverageIgnoreEnd diff --git a/system/Test/ReflectionHelper.php b/system/Test/ReflectionHelper.php index c81d924ce182..06dae5edcbc1 100644 --- a/system/Test/ReflectionHelper.php +++ b/system/Test/ReflectionHelper.php @@ -7,7 +7,7 @@ * * This content is released under the MIT License (MIT) * - * Copyright (c) 2014 - 2016, British Columbia Institute of Technology + * Copyright (c) 2014-2018 British Columbia Institute of Technology * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,13 +29,12 @@ * * @package CodeIgniter * @author CodeIgniter Dev Team - * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) - * @license http://opensource.org/licenses/MIT MIT License - * @link http://codeigniter.com + * @copyright 2014-2018 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 * @filesource */ - use ReflectionMethod; use ReflectionObject; use ReflectionClass; @@ -45,9 +44,10 @@ */ trait ReflectionHelper { + /** * Find a private method invoker. - * + * * @param object|string $obj object or class name * @param string $method method name * @return \Closure @@ -66,10 +66,11 @@ public static function getPrivateMethodInvoker($obj, $method) /** * Find an accessible property. - * - * @param type $obj - * @param type $property - * @return type + * + * @param object $obj + * @param string $property + * + * @return \ReflectionProperty */ private static function getAccessibleRefProperty($obj, $property) { @@ -90,7 +91,7 @@ private static function getAccessibleRefProperty($obj, $property) /** * Set a private property. - * + * * @param object|string $obj object or class name * @param string $property property name * @param mixed $value value @@ -103,7 +104,7 @@ public static function setPrivateProperty($obj, $property, $value) /** * Retrieve a private property. - * + * * @param object|string $obj object or class name * @param string $property property name * @return mixed value @@ -113,4 +114,5 @@ public static function getPrivateProperty($obj, $property) $ref_property = self::getAccessibleRefProperty($obj, $property); return $ref_property->getValue($obj); } + } diff --git a/system/ThirdParty/Kint/.gitignore b/system/ThirdParty/Kint/.gitignore deleted file mode 100755 index 21a62842326d..000000000000 --- a/system/ThirdParty/Kint/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -/config.php -/.idea \ No newline at end of file diff --git a/system/ThirdParty/Kint/Kint.class.php b/system/ThirdParty/Kint/Kint.class.php deleted file mode 100755 index ee440f11710f..000000000000 --- a/system/ThirdParty/Kint/Kint.class.php +++ /dev/null @@ -1,842 +0,0 @@ -= 0 ); - -require KINT_DIR . 'config.default.php'; -require KINT_DIR . 'inc/kintVariableData.class.php'; -require KINT_DIR . 'inc/kintParser.class.php'; -require KINT_DIR . 'inc/kintObject.class.php'; -require KINT_DIR . 'decorators/rich.php'; -require KINT_DIR . 'decorators/plain.php'; - -if ( is_readable( KINT_DIR . 'config.php' ) ) { - require KINT_DIR . 'config.php'; -} - -# init settings -if ( !empty( $GLOBALS['_kint_settings'] ) ) { - Kint::enabled( $GLOBALS['_kint_settings']['enabled'] ); - - foreach ( $GLOBALS['_kint_settings'] as $key => $val ) { - property_exists( 'Kint', $key ) and Kint::$$key = $val; - } - - unset( $GLOBALS['_kint_settings'], $key, $val ); -} - -class Kint -{ - // these are all public and 1:1 config array keys so you can switch them easily - private static $_enabledMode; # stores mode and active statuses - - public static $returnOutput; - public static $fileLinkFormat; - public static $displayCalledFrom; - public static $charEncodings; - public static $maxStrLength; - public static $appRootDirs; - public static $maxLevels; - public static $theme; - public static $expandedByDefault; - - public static $cliDetection; - public static $cliColors; - - const MODE_RICH = 'r'; - const MODE_WHITESPACE = 'w'; - const MODE_CLI = 'c'; - const MODE_PLAIN = 'p'; - - - public static $aliases = array( - 'methods' => array( - array( 'kint', 'dump' ), - array( 'kint', 'trace' ), - ), - 'functions' => array( - 'd', - 'dd', - 'ddd', - 's', - 'sd', - ) - ); - - private static $_firstRun = true; - - /** - * Enables or disables Kint, can globally enforce the rendering mode. If called without parameters, returns the - * current mode. - * - * @param mixed $forceMode - * null or void - return current mode - * false - disable (no output) - * true - enable and detect cli automatically - * Kint::MODE_* - enable and force selected mode disregarding detection and function - * shorthand (s()/d()), note that you can still override this - * with the "~" modifier - * - * @return mixed previously set value if a new one is passed - */ - public static function enabled( $forceMode = null ) - { - # act both as a setter... - if ( isset( $forceMode ) ) { - $before = self::$_enabledMode; - self::$_enabledMode = $forceMode; - - return $before; - } - - # ...and a getter - return self::$_enabledMode; - } - - /** - * Prints a debug backtrace, same as Kint::dump(1) - * - * @param array $trace [OPTIONAL] you can pass your own trace, otherwise, `debug_backtrace` will be called - * - * @return mixed - */ - public static function trace( $trace = null ) - { - if ( !self::enabled() ) return ''; - - return self::dump( isset( $trace ) ? $trace : debug_backtrace( true ) ); - } - - - /** - * Dump information about variables, accepts any number of parameters, supports modifiers: - * - * clean up any output before kint and place the dump at the top of page: - * - Kint::dump() - * ***** - * expand all nodes on display: - * ! Kint::dump() - * ***** - * dump variables disregarding their depth: - * + Kint::dump() - * ***** - * return output instead of displaying it: - * @ Kint::dump() - * ***** - * force output as plain text - * ~ Kint::dump() - * - * Modifiers are supported by all dump wrapper functions, including Kint::trace(). Space is optional. - * - * - * You can also use the following shorthand to display debug_backtrace(): - * Kint::dump( 1 ); - * - * Passing the result from debug_backtrace() to kint::dump() as a single parameter will display it as trace too: - * $trace = debug_backtrace( true ); - * Kint::dump( $trace ); - * Or simply: - * Kint::dump( debug_backtrace() ); - * - * - * @param mixed $data - * - * @return void|string - */ - public static function dump( $data = null ) - { - if ( !self::enabled() ) return ''; - - list( $names, $modifiers, $callee, $previousCaller, $miniTrace ) = self::_getCalleeInfo( - defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) - ? debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ) - : debug_backtrace() - ); - $modeOldValue = self::enabled(); - $firstRunOldValue = self::$_firstRun; - - # process modifiers: @, +, !, ~ and - - if ( strpos( $modifiers, '-' ) !== false ) { - self::$_firstRun = true; - while ( ob_get_level() ) { - ob_end_clean(); - } - } - if ( strpos( $modifiers, '!' ) !== false ) { - $expandedByDefaultOldValue = self::$expandedByDefault; - self::$expandedByDefault = true; - } - if ( strpos( $modifiers, '+' ) !== false ) { - $maxLevelsOldValue = self::$maxLevels; - self::$maxLevels = false; - } - if ( strpos( $modifiers, '@' ) !== false ) { - $returnOldValue = self::$returnOutput; - self::$returnOutput = true; - self::$_firstRun = true; - } - if ( strpos( $modifiers, '~' ) !== false ) { - self::enabled( self::MODE_WHITESPACE ); - } - - # set mode for current run - $mode = self::enabled(); - if ( $mode === true ) { - $mode = PHP_SAPI === 'cli' - ? self::MODE_CLI - : self::MODE_RICH; - } - self::enabled( $mode ); - - $decorator = self::enabled() === self::MODE_RICH - ? 'Kint_Decorators_Rich' - : 'Kint_Decorators_Plain'; - - $output = ''; - if ( self::$_firstRun ) { - $output .= call_user_func( array( $decorator, 'init' ) ); - } - - - $trace = false; - if ( $names === array( null ) && func_num_args() === 1 && $data === 1 ) { # Kint::dump(1) shorthand - $trace = KINT_PHP53 ? debug_backtrace( true ) : debug_backtrace(); - } elseif ( func_num_args() === 1 && is_array( $data ) ) { - $trace = $data; # test if the single parameter is result of debug_backtrace() - } - $trace and $trace = self::_parseTrace( $trace ); - - - $output .= call_user_func( array( $decorator, 'wrapStart' ) ); - if ( $trace ) { - $output .= call_user_func( array( $decorator, 'decorateTrace' ), $trace ); - } else { - $data = func_num_args() === 0 - ? array( "[[no arguments passed]]" ) - : func_get_args(); - - foreach ( $data as $k => $argument ) { - kintParser::reset(); - # when the dump arguments take long to generate output, user might have changed the file and - # Kint might not parse the arguments correctly, so check if names are set and while the - # displayed names might be wrong, at least don't throw an error - $output .= call_user_func( - array( $decorator, 'decorate' ), - kintParser::factory( $argument, isset( $names[ $k ] ) ? $names[ $k ] : '' ) - ); - } - } - - $output .= call_user_func( array( $decorator, 'wrapEnd' ), $callee, $miniTrace, $previousCaller ); - - self::enabled( $modeOldValue ); - - self::$_firstRun = false; - if ( strpos( $modifiers, '~' ) !== false ) { - self::$_firstRun = $firstRunOldValue; - } else { - self::enabled( $modeOldValue ); - } - if ( strpos( $modifiers, '!' ) !== false ) { - self::$expandedByDefault = $expandedByDefaultOldValue; - } - if ( strpos( $modifiers, '+' ) !== false ) { - self::$maxLevels = $maxLevelsOldValue; - } - if ( strpos( $modifiers, '@' ) !== false ) { - self::$returnOutput = $returnOldValue; - self::$_firstRun = $firstRunOldValue; - return $output; - } - - if ( self::$returnOutput ) return $output; - - echo $output; - return ''; - } - - - /** - * generic path display callback, can be configured in the settings; purpose is to show relevant path info and hide - * as much of the path as possible. - * - * @param string $file - * - * @return string - */ - public static function shortenPath( $file ) - { - $file = str_replace( '\\', '/', $file ); - $shortenedName = $file; - $replaced = false; - if ( is_array( self::$appRootDirs ) ) foreach ( self::$appRootDirs as $path => $replaceString ) { - if ( empty( $path ) ) continue; - - $path = str_replace( '\\', '/', $path ); - - if ( strpos( $file, $path ) === 0 ) { - $shortenedName = $replaceString . substr( $file, strlen( $path ) ); - $replaced = true; - break; - } - } - - # fallback to find common path with Kint dir - if ( !$replaced ) { - $pathParts = explode( '/', str_replace( '\\', '/', KINT_DIR ) ); - $fileParts = explode( '/', $file ); - $i = 0; - foreach ( $fileParts as $i => $filePart ) { - if ( !isset( $pathParts[ $i ] ) || $pathParts[ $i ] !== $filePart ) break; - } - - $shortenedName = ( $i ? '.../' : '' ) . implode( '/', array_slice( $fileParts, $i ) ); - } - - return $shortenedName; - } - - public static function getIdeLink( $file, $line ) - { - return str_replace( array( '%f', '%l' ), array( $file, $line ), self::$fileLinkFormat ); - } - - /** - * trace helper, shows the place in code inline - * - * @param string $file full path to file - * @param int $lineNumber the line to display - * @param int $padding surrounding lines to show besides the main one - * - * @return bool|string - */ - private static function _showSource( $file, $lineNumber, $padding = 7 ) - { - if ( !$file OR !is_readable( $file ) ) { - # continuing will cause errors - return false; - } - - # open the file and set the line position - $file = fopen( $file, 'r' ); - $line = 0; - - # Set the reading range - $range = array( - 'start' => $lineNumber - $padding, - 'end' => $lineNumber + $padding - ); - - # set the zero-padding amount for line numbers - $format = '% ' . strlen( $range['end'] ) . 'd'; - - $source = ''; - while ( ( $row = fgets( $file ) ) !== false ) { - # increment the line number - if ( ++$line > $range['end'] ) { - break; - } - - if ( $line >= $range['start'] ) { - # make the row safe for output - $row = htmlspecialchars( $row, ENT_NOQUOTES, 'UTF-8' ); - - # trim whitespace and sanitize the row - $row = '' . sprintf( $format, $line ) . ' ' . $row; - - if ( $line === $lineNumber ) { - # apply highlighting to this row - $row = '
' . $row . '
'; - } else { - $row = '
' . $row . '
'; - } - - # add to the captured source - $source .= $row; - } - } - - # close the file - fclose( $file ); - - return $source; - } - - - /** - * returns parameter names that the function was passed, as well as any predefined symbols before function - * call (modifiers) - * - * @param array $trace - * - * @return array( $parameters, $modifier, $callee, $previousCaller ) - */ - private static function _getCalleeInfo( $trace ) - { - $previousCaller = array(); - $miniTrace = array(); - $prevStep = array(); - - # go from back of trace to find first occurrence of call to Kint or its wrappers - while ( $step = array_pop( $trace ) ) { - - if ( self::_stepIsInternal( $step ) ) { - $previousCaller = $prevStep; - break; - } elseif ( isset( $step['file'], $step['line'] ) ) { - unset( $step['object'], $step['args'] ); - array_unshift( $miniTrace, $step ); - } - - $prevStep = $step; - } - $callee = $step; - - if ( !isset( $callee['file'] ) || !is_readable( $callee['file'] ) ) return false; - - - # open the file and read it up to the position where the function call expression ended - $file = fopen( $callee['file'], 'r' ); - $line = 0; - $source = ''; - while ( ( $row = fgets( $file ) ) !== false ) { - if ( ++$line > $callee['line'] ) break; - $source .= $row; - } - fclose( $file ); - $source = self::_removeAllButCode( $source ); - - - if ( empty( $callee['class'] ) ) { - $codePattern = $callee['function']; - } else { - if ( $callee['type'] === '::' ) { - $codePattern = $callee['class'] . "\x07*" . $callee['type'] . "\x07*" . $callee['function'];; - } else /*if ( $callee['type'] === '->' )*/ { - $codePattern = ".*\x07*" . $callee['type'] . "\x07*" . $callee['function'];; - } - } - - // todo if more than one call in one line - not possible to determine variable names - // todo does not recognize string concat - # get the position of the last call to the function - preg_match_all( " - [ - # beginning of statement - [\x07{(] - - # search for modifiers (group 1) - ([-+!@~]*)? - - # spaces - \x07* - - # check if output is assigned to a variable (group 2) todo: does not detect concat - ( - \\$[a-z0-9_]+ # variable - \x07*\\.?=\x07* # assignment - )? - - # possibly a namespace symbol - \\\\? - - # spaces again - \x07* - - # main call to Kint - ({$codePattern}) - - # spaces everywhere - \x07* - - # find the character where kint's opening bracket resides (group 3) - (\\() - - ]ix", - $source, - $matches, - PREG_OFFSET_CAPTURE - ); - - $modifiers = end( $matches[1] ); - $assignment = end( $matches[2] ); - $callToKint = end( $matches[3] ); - $bracket = end( $matches[4] ); - - if ( empty( $callToKint ) ) { - # if a wrapper is misconfigured, don't display the whole file as variable name - return array( array(), $modifiers, $callee, $previousCaller, $miniTrace ); - } - - $modifiers = $modifiers[0]; - if ( $assignment[1] !== -1 ) { - $modifiers .= '@'; - } - - $paramsString = preg_replace( "[\x07+]", ' ', substr( $source, $bracket[1] + 1 ) ); - # we now have a string like this: - # ); - - # remove everything in brackets and quotes, we don't need nested statements nor literal strings which would - # only complicate separating individual arguments - $c = strlen( $paramsString ); - $inString = $escaped = $openedBracket = $closingBracket = false; - $i = 0; - $inBrackets = 0; - $openedBrackets = array(); - - while ( $i < $c ) { - $letter = $paramsString[ $i ]; - - if ( !$inString ) { - if ( $letter === '\'' || $letter === '"' ) { - $inString = $letter; - } elseif ( $letter === '(' || $letter === '[' ) { - $inBrackets++; - $openedBrackets[] = $openedBracket = $letter; - $closingBracket = $openedBracket === '(' ? ')' : ']'; - } elseif ( $inBrackets && $letter === $closingBracket ) { - $inBrackets--; - array_pop( $openedBrackets ); - $openedBracket = end( $openedBrackets ); - $closingBracket = $openedBracket === '(' ? ')' : ']'; - } elseif ( !$inBrackets && $letter === ')' ) { - $paramsString = substr( $paramsString, 0, $i ); - break; - } - } elseif ( $letter === $inString && !$escaped ) { - $inString = false; - } - - # replace whatever was inside quotes or brackets with untypeable characters, we don't - # need that info. We'll later replace the whole string with '...' - if ( $inBrackets > 0 ) { - if ( $inBrackets > 1 || $letter !== $openedBracket ) { - $paramsString[ $i ] = "\x07"; - } - } - if ( $inString ) { - if ( $letter !== $inString || $escaped ) { - $paramsString[ $i ] = "\x07"; - } - } - - $escaped = !$escaped && ( $letter === '\\' ); - $i++; - } - - # by now we have an un-nested arguments list, lets make it to an array for processing further - $arguments = explode( ',', preg_replace( "[\x07+]", '...', $paramsString ) ); - - # test each argument whether it was passed literary or was it an expression or a variable name - $parameters = array(); - $blacklist = array( 'null', 'true', 'false', 'array(...)', 'array()', '"..."', '[...]', 'b"..."', ); - foreach ( $arguments as $argument ) { - $argument = trim( $argument ); - - if ( is_numeric( $argument ) - || in_array( str_replace( "'", '"', strtolower( $argument ) ), $blacklist, true ) - ) { - $parameters[] = null; - } else { - $parameters[] = $argument; - } - } - - return array( $parameters, $modifiers, $callee, $previousCaller, $miniTrace ); - } - - /** - * removes comments and zaps whitespace & true, T_INLINE_HTML => true, T_DOC_COMMENT => true - ); - $whiteSpaceTokens = array( - T_WHITESPACE => true, T_CLOSE_TAG => true, - T_OPEN_TAG => true, T_OPEN_TAG_WITH_ECHO => true, - ); - - $cleanedSource = ''; - foreach ( token_get_all( $source ) as $token ) { - if ( is_array( $token ) ) { - if ( isset( $commentTokens[ $token[0] ] ) ) continue; - - if ( isset( $whiteSpaceTokens[ $token[0] ] ) ) { - $token = "\x07"; - } else { - $token = $token[1]; - } - } elseif ( $token === ';' ) { - $token = "\x07"; - } - - $cleanedSource .= $token; - } - return $cleanedSource; - } - - /** - * returns whether current trace step belongs to Kint or its wrappers - * - * @param $step - * - * @return array - */ - private static function _stepIsInternal( $step ) - { - if ( isset( $step['class'] ) ) { - foreach ( self::$aliases['methods'] as $alias ) { - if ( $alias[0] === strtolower( $step['class'] ) && $alias[1] === strtolower( $step['function'] ) ) { - return true; - } - } - return false; - } else { - return in_array( strtolower( $step['function'] ), self::$aliases['functions'], true ); - } - } - - private static function _parseTrace( array $data ) - { - $trace = array(); - $traceFields = array( 'file', 'line', 'args', 'class' ); - $fileFound = false; # file element must exist in one of the steps - - # validate whether a trace was indeed passed - while ( $step = array_pop( $data ) ) { - if ( !is_array( $step ) || !isset( $step['function'] ) ) return false; - if ( !$fileFound && isset( $step['file'] ) && file_exists( $step['file'] ) ) { - $fileFound = true; - } - - $valid = false; - foreach ( $traceFields as $element ) { - if ( isset( $step[ $element ] ) ) { - $valid = true; - break; - } - } - if ( !$valid ) return false; - - if ( self::_stepIsInternal( $step ) ) { - $step = array( - 'file' => $step['file'], - 'line' => $step['line'], - 'function' => '', - ); - array_unshift( $trace, $step ); - break; - } - if ( $step['function'] !== 'spl_autoload_call' ) { # meaningless - array_unshift( $trace, $step ); - } - } - if ( !$fileFound ) return false; - - $output = array(); - foreach ( $trace as $step ) { - if ( isset( $step['file'] ) ) { - $file = $step['file']; - - if ( isset( $step['line'] ) ) { - $line = $step['line']; - # include the source of this step - if ( self::enabled() === self::MODE_RICH ) { - $source = self::_showSource( $file, $line ); - } - } - } - - $function = $step['function']; - - if ( in_array( $function, array( 'include', 'include_once', 'require', 'require_once' ) ) ) { - if ( empty( $step['args'] ) ) { - # no arguments - $args = array(); - } else { - # sanitize the included file path - $args = array( 'file' => self::shortenPath( $step['args'][0] ) ); - } - } elseif ( isset( $step['args'] ) ) { - if ( empty( $step['class'] ) && !function_exists( $function ) ) { - # introspection on closures or language constructs in a stack trace is impossible before PHP 5.3 - $params = null; - } else { - try { - if ( isset( $step['class'] ) ) { - if ( method_exists( $step['class'], $function ) ) { - $reflection = new ReflectionMethod( $step['class'], $function ); - } else if ( isset( $step['type'] ) && $step['type'] == '::' ) { - $reflection = new ReflectionMethod( $step['class'], '__callStatic' ); - } else { - $reflection = new ReflectionMethod( $step['class'], '__call' ); - } - } else { - $reflection = new ReflectionFunction( $function ); - } - - # get the function parameters - $params = $reflection->getParameters(); - } catch ( Exception $e ) { # avoid various PHP version incompatibilities - $params = null; - } - } - - $args = array(); - foreach ( $step['args'] as $i => $arg ) { - if ( isset( $params[ $i ] ) ) { - # assign the argument by the parameter name - $args[ $params[ $i ]->name ] = $arg; - } else { - # assign the argument by number - $args[ '#' . ( $i + 1 ) ] = $arg; - } - } - } - - if ( isset( $step['class'] ) ) { - # Class->method() or Class::method() - $function = $step['class'] . $step['type'] . $function; - } - - // todo it's possible to parse the object name out from the source! - $output[] = array( - 'function' => $function, - 'args' => isset( $args ) ? $args : null, - 'file' => isset( $file ) ? $file : null, - 'line' => isset( $line ) ? $line : null, - 'source' => isset( $source ) ? $source : null, - 'object' => isset( $step['object'] ) ? $step['object'] : null, - ); - - unset( $function, $args, $file, $line, $source ); - } - - return $output; - } -} - - -if ( !function_exists( 'd' ) ) { - /** - * Alias of Kint::dump() - * - * @return string - */ - function d() - { - if ( !Kint::enabled() ) return ''; - $_ = func_get_args(); - return call_user_func_array( array( 'Kint', 'dump' ), $_ ); - } -} - -if ( !function_exists( 'dd' ) ) { - /** - * Alias of Kint::dump() - * [!!!] IMPORTANT: execution will halt after call to this function - * - * @return string - * @deprecated - */ - function dd() - { - if ( !Kint::enabled() ) return ''; - - echo "
Kint: dd() is being deprecated, please use ddd() instead
\n"; - $_ = func_get_args(); - call_user_func_array( array( 'Kint', 'dump' ), $_ ); - die; - } -} - -if ( !function_exists( 'ddd' ) ) { - /** - * Alias of Kint::dump() - * [!!!] IMPORTANT: execution will halt after call to this function - * - * @return string - */ - function ddd() - { - if ( !Kint::enabled() ) return ''; - $_ = func_get_args(); - call_user_func_array( array( 'Kint', 'dump' ), $_ ); - die; - } -} - -if ( !function_exists( 's' ) ) { - /** - * Alias of Kint::dump(), however the output is in plain htmlescaped text and some minor visibility enhancements - * added. If run in CLI mode, output is pure whitespace. - * - * To force rendering mode without autodetecting anything: - * - * Kint::enabled( Kint::MODE_PLAIN ); - * Kint::dump( $variable ); - * - * [!!!] IMPORTANT: execution will halt after call to this function - * - * @return string - */ - function s() - { - $enabled = Kint::enabled(); - if ( !$enabled ) return ''; - - if ( $enabled === Kint::MODE_WHITESPACE ) { # if already in whitespace, don't elevate to plain - $restoreMode = Kint::MODE_WHITESPACE; - } else { - $restoreMode = Kint::enabled( # remove cli colors in cli mode; remove rich interface in HTML mode - PHP_SAPI === 'cli' ? Kint::MODE_WHITESPACE : Kint::MODE_PLAIN - ); - } - - $params = func_get_args(); - $dump = call_user_func_array( array( 'Kint', 'dump' ), $params ); - Kint::enabled( $restoreMode ); - return $dump; - } -} - -if ( !function_exists( 'sd' ) ) { - /** - * @see s() - * - * [!!!] IMPORTANT: execution will halt after call to this function - * - * @return string - */ - function sd() - { - $enabled = Kint::enabled(); - if ( !$enabled ) return ''; - - if ( $enabled !== Kint::MODE_WHITESPACE ) { - Kint::enabled( - PHP_SAPI === 'cli' ? Kint::MODE_WHITESPACE : Kint::MODE_PLAIN - ); - } - - $params = func_get_args(); - call_user_func_array( array( 'Kint', 'dump' ), $params ); - die; - } -} diff --git a/system/ThirdParty/Kint/LICENCE b/system/ThirdParty/Kint/LICENCE deleted file mode 100755 index 936fe1f168f3..000000000000 --- a/system/ThirdParty/Kint/LICENCE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Rokas Šleinius (raveren@gmail.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/system/ThirdParty/Kint/README.md b/system/ThirdParty/Kint/README.md deleted file mode 100755 index 2bae3ae3fbd2..000000000000 --- a/system/ThirdParty/Kint/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Kint - debugging helper for PHP developers - -[![Total Downloads](https://poser.pugx.org/raveren/kint/downloads.png)](https://packagist.org/packages/raveren/kint) - -> **New version** v1.0.0 is released with more than two years of active development - changes are too numerous to list, but there's CLI output and literally hundreds of improvements and additions. - -![Screenshot](http://raveren.github.com/kint/img/preview.png) - -## What am I looking at? - -At first glance Kint is just a pretty replacement for **[var_dump()](http://php.net/manual/en/function.var-dump.php)**, **[print_r()](http://php.net/manual/en/function.print-r.php)** and **[debug_backtrace()](http://php.net/manual/en/function.debug-backtrace.php)**. - -However, it's much, *much* more than that. Even the excellent `xdebug` var_dump improvements don't come close - you will eventually wonder how you developed without it. - -Just to list some of the most useful features: - - * The **variable name and place in code** where Kint was called from is displayed; - * You can **disable all Kint output easily and on the fly** - so you can even debug live systems without anyone knowing (even though you know you shouldn't be doing that!:). - * **CLI is detected** and formatted for automatically (but everything can be overridden on the fly) - if your setup supports it, the output is colored too: - ![Kint CLI output](http://i.imgur.com/6B9MCLw.png) - * **Debug backtraces** are finally fully readable, actually informative and a pleasure to the eye. - * Kint has been **in active development for more than six years** and is shipped with [Drupal 8](https://www.drupal.org/) by default as part of its devel suite. You can trust it not being abandoned or getting left behind in features. - * Variable content is **displayed in the most informative way** - and you *never, ever* miss anything! Kint guarantees you see every piece of physically available information about everything you are dumping*; - * in some cases, the content is truncated where it would otherwise be too large to view anyway - but the user is always made aware of that; - * Some variable content types have an alternative display - for example you will be able see `JSON` in its raw form - but also as an associative array: - ![Kint displays data intelligently](http://i.imgur.com/9P57Ror.png) - There are more than ten custom variable type displays inbuilt and more are added periodically. - - -## Installation and Usage - -One of the main goals of Kint is to be **zero setup**. - -[Download the archive](https://github.com/raveren/kint/archive/master.zip) and simply -```php -');`) - so even if you accidentally leave a dump in production, no one will know. - * `sd()` and `ddd()` are shorthands for `s();die;` and `d();die;` respectively. - * **Important:** The older shorthand `dd()` is deprecated due to compatibility issues and will eventually be removed. Use the analogous `ddd()` instead. - * When looking at Kint output, press D on the keyboard and you will be able to traverse the tree with arrows and tab keys - and expand/collapse nodes with space or enter. - * Double clicking the `[+]` sign in the output will expand/collapse ALL nodes; triple clicking big blocks of text will select it all. - * Clicking the tiny arrows on the right of the output open it in a separate window where you can keep it for comparison. - * To catch output from Kint just assign it to a variablebeta -```php -$o = Kint::dump($GLOBALS); -// yes, the assignment is automatically detected, and $o -// now holds whatever was going to be printed otherwise. - -// it also supports modifiers (read on) for the variable: -~$o = Kint::dump($GLOBALS); // this output will be in whitespace -``` - * There are a couple of real-time modifiers you can use: - * `~d($var)` this call will output in plain text format. - * `+d($var)` will disregard depth level limits and output everything (careful, this can hang your browser on huge objects) - * `!d($var)` will show expanded rich output. - * `-d($var)` will attempt to `ob_clean` the previous output so if you're dumping something inside a HTML page, you will still see Kint output. - You can combine modifiers too: `~+d($var)` - * To force a specific dump output type just pass it to the `Kint::enabled()` method. Available options are: `Kint::MODE_RICH` (default), `Kint::MODE_PLAIN`, `Kint::MODE_WHITESPACE` and `Kint::MODE_CLI`: -```php -Kint::enabled(Kint::MODE_WHITESPACE); -$kintOutput = Kint::dump($GLOBALS); -// now $kintOutput can be written to a text log file and -// be perfectly readable from there -``` - * To change display theme, use `Kint::$theme = '';` where available options are: `'original'` (default), `'solarized'`, `'solarized-dark'` and `'aante-light'`. Here's an (outdated) preview: - ![Kint themes](http://raveren.github.io/kint/img/theme-preview.png) - * Kint also includes a naïve profiler you may find handy. It's for determining relatively which code blocks take longer than others: -```php -Kint::dump( microtime() ); // just pass microtime() -sleep( 1 ); -Kint::dump( microtime(), 'after sleep(1)' ); -sleep( 2 ); -ddd( microtime(), 'final call, after sleep(2)' ); -``` - ![Kint profiling feature](http://i.imgur.com/tmHUMW4.png) ----- - -[Visit the project page](http://raveren.github.com/kint/) for documentation, configuration, and more advanced usage examples. - -### Author - -**Rokas Šleinius** (Raveren) - -### License - -Licensed under the MIT License diff --git a/system/ThirdParty/Kint/composer.json b/system/ThirdParty/Kint/composer.json deleted file mode 100755 index 78eb1b654f45..000000000000 --- a/system/ThirdParty/Kint/composer.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "raveren/kint", - "description": "Kint - debugging helper for PHP developers", - "keywords": ["kint", "php", "debug"], - "type": "library", - "homepage": "https://github.com/raveren/kint", - "license": "MIT", - "authors": [ - { - "name": "Rokas Šleinius", - "homepage": "https://github.com/raveren" - }, - { - "name": "Contributors", - "homepage": "https://github.com/raveren/kint/contributors" - } - ], - "require": { - "php": ">=5.1.0" - }, - "autoload": { - "files": ["Kint.class.php"] - }, - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/config.default.php b/system/ThirdParty/Kint/config.default.php deleted file mode 100755 index 72f0a6933596..000000000000 --- a/system/ThirdParty/Kint/config.default.php +++ /dev/null @@ -1,94 +0,0 @@ - '<ROOT>' ) - * - * [!] EXAMPLE (for Kohana framework): - * - * $_kintSettings['appRootDirs'] = array( - * APPPATH => 'APPPATH', // make sure the constants are already defined at the time of including this config file - * SYSPATH => 'SYSPATH', - * MODPATH => 'MODPATH', - * DOCROOT => 'DOCROOT', - * ); - * - * [!] EXAMPLE #2 (for a semi-universal approach) - * - * $_kintSettings['appRootDirs'] = array( - * realpath( __DIR__ . '/../../..' ) => 'ROOT', // go up as many levels as needed in the realpath() param - * ); - * - * $_kintSettings['fileLinkFormat'] = 'http://localhost:8091/?message=%f:%l'; - * - */ -$_kintSettings['appRootDirs'] = isset( $_SERVER['DOCUMENT_ROOT'] ) - ? array( $_SERVER['DOCUMENT_ROOT'] => '<ROOT>' ) - : array(); - - -/** @var int max length of string before it is truncated and displayed separately in full. Zero or false to disable */ -$_kintSettings['maxStrLength'] = 80; - -/** @var array possible alternative char encodings in order of probability, eg. array('windows-1251') */ -$_kintSettings['charEncodings'] = array( - 'UTF-8', - 'Windows-1252', # Western; includes iso-8859-1, replace this with windows-1251 if you have Russian code - 'euc-jp', # Japanese - - # all other charsets cannot be differentiated by PHP and/or are not supported by mb_* functions, - # I need a better means of detecting the codeset, no idea how though :( - - // 'iso-8859-13', # Baltic - // 'windows-1251', # Cyrillic - // 'windows-1250', # Central European - // 'shift_jis', # Japanese - // 'iso-2022-jp', # Japanese -); - - -/** @var int max array/object levels to go deep, if zero no limits are applied */ -$_kintSettings['maxLevels'] = 7; - - -/** @var string name of theme for rich view */ -$_kintSettings['theme'] = 'original'; - - -/** @var bool enable detection when Kint is command line. Formats output with whitespace only; does not HTML-escape it */ -$_kintSettings['cliDetection'] = true; - -/** @var bool in addition to above setting, enable detection when Kint is run in *UNIX* command line. - * Attempts to add coloring, but if seen as plain text, the color information is visible as gibberish - */ -$_kintSettings['cliColors'] = true; - - -unset( $_kintSettings ); \ No newline at end of file diff --git a/system/ThirdParty/Kint/decorators/plain.php b/system/ThirdParty/Kint/decorators/plain.php deleted file mode 100755 index ca292a1d66fd..000000000000 --- a/system/ThirdParty/Kint/decorators/plain.php +++ /dev/null @@ -1,335 +0,0 @@ - '1', 'dark' => '2', - 'italic' => '3', 'underline' => '4', - 'blink' => '5', 'reverse' => '7', - 'concealed' => '8', 'default' => '39', - - # colors - 'black' => '30', 'red' => '31', - 'green' => '32', 'yellow' => '33', - 'blue' => '34', 'magenta' => '35', - 'cyan' => '36', 'light_gray' => '37', - 'dark_gray' => '90', 'light_red' => '91', - 'light_green' => '92', 'light_yellow' => '93', - 'light_blue' => '94', 'light_magenta' => '95', - 'light_cyan' => '96', 'white' => '97', - - # backgrounds - 'bg_default' => '49', 'bg_black' => '40', - 'bg_red' => '41', 'bg_green' => '42', - 'bg_yellow' => '43', 'bg_blue' => '44', - 'bg_magenta' => '45', 'bg_cyan' => '46', - 'bg_light_gray' => '47', 'bg_dark_gray' => '100', - 'bg_light_red' => '101', 'bg_light_green' => '102', - 'bg_light_yellow' => '103', 'bg_light_blue' => '104', - 'bg_light_magenta' => '105', 'bg_light_cyan' => '106', - 'bg_white' => '107', - ); - private static $_utfSymbols = array( - '┌', '═', '┐', - '│', - '└', '─', '┘', - ); - private static $_winShellSymbols = array( - "\xda", "\xdc", "\xbf", - "\xb3", - "\xc0", "\xc4", "\xd9", - ); - private static $_htmlSymbols = array( - "┌", "▄", "┐", - "│", - "└", "─", "┘", - ); - - public static function decorate( kintVariableData $kintVar, $level = 0 ) - { - $output = ''; - if ( $level === 0 ) { - $name = $kintVar->name ? $kintVar->name : 'literal'; - $kintVar->name = null; - - $output .= self::_title( $name ); - } - - - $space = str_repeat( $s = ' ', $level ); - $output .= $space . self::_drawHeader( $kintVar ); - - - if ( $kintVar->extendedValue !== null ) { - $output .= ' ' . ( $kintVar->type === 'array' ? '[' : '(' ) . PHP_EOL; - - - if ( is_array( $kintVar->extendedValue ) ) { - foreach ( $kintVar->extendedValue as $v ) { - $output .= self::decorate( $v, $level + 1 ); - } - } elseif ( is_string( $kintVar->extendedValue ) ) { - $output .= $space . $s . $kintVar->extendedValue . PHP_EOL; # "depth too great" or similar - } else { - $output .= self::decorate( $kintVar->extendedValue, $level + 1 ); //it's kintVariableData - } - $output .= $space . ( $kintVar->type === 'array' ? ']' : ')' ) . PHP_EOL; - } else { - $output .= PHP_EOL; - } - - return $output; - } - - public static function decorateTrace( $traceData ) - { - $output = self::_title( 'TRACE' ); - $lastStep = count( $traceData ); - foreach ( $traceData as $stepNo => $step ) { - $title = str_pad( ++$stepNo . ': ', 4, ' ' ); - - $title .= self::_colorize( - ( isset( $step['file'] ) ? self::_buildCalleeString( $step ) : 'PHP internal call' ), - 'title' - ); - - if ( !empty( $step['function'] ) ) { - $title .= ' ' . $step['function']; - if ( isset( $step['args'] ) ) { - $title .= '('; - if ( empty( $step['args'] ) ) { - $title .= ')'; - } else { - } - $title .= PHP_EOL; - } - } - - $output .= $title; - - if ( !empty( $step['args'] ) ) { - $appendDollar = $step['function'] === '{closure}' ? '' : '$'; - - $i = 0; - foreach ( $step['args'] as $name => $argument ) { - $argument = kintParser::factory( - $argument, - $name ? $appendDollar . $name : '#' . ++$i - ); - $argument->operator = $name ? ' =' : ':'; - $maxLevels = Kint::$maxLevels; - if ( $maxLevels ) { - Kint::$maxLevels = $maxLevels + 2; - } - $output .= self::decorate( $argument, 2 ); - if ( $maxLevels ) { - Kint::$maxLevels = $maxLevels; - } - } - $output .= ' )' . PHP_EOL; - } - - if ( !empty( $step['object'] ) ) { - $output .= self::_colorize( - ' ' . self::_char( '─', 27 ) . ' Callee object ' . self::_char( '─', 34 ), - 'title' - ); - - $maxLevels = Kint::$maxLevels; - if ( $maxLevels ) { - # in cli the terminal window is filled too quickly to display huge objects - Kint::$maxLevels = Kint::enabled() === Kint::MODE_CLI - ? 1 - : $maxLevels + 1; - } - $output .= self::decorate( kintParser::factory( $step['object'] ), 1 ); - if ( $maxLevels ) { - Kint::$maxLevels = $maxLevels; - } - } - - if ( $stepNo !== $lastStep ) { - $output .= self::_colorize( self::_char( '─', 80 ), 'title' ); - } - } - - return $output; - } - - - private static function _colorize( $text, $type, $nlAfter = true ) - { - $nlAfter = $nlAfter ? PHP_EOL : ''; - - switch ( Kint::enabled() ) { - case Kint::MODE_PLAIN: - if ( !self::$_enableColors ) return $text . $nlAfter; - - switch ( $type ) { - case 'value': - $text = "{$text}"; - break; - case 'type': - $text = "{$text}"; - break; - case 'title': - $text = "{$text}"; - break; - } - - return $text . $nlAfter; - break; - case Kint::MODE_CLI: - if ( !self::$_enableColors ) return $text . $nlAfter; - - $optionsMap = array( - 'title' => "\x1b[36m", # cyan - 'type' => "\x1b[35;1m", # magenta bold - 'value' => "\x1b[32m", # green - ); - - return $optionsMap[ $type ] . $text . "\x1b[0m" . $nlAfter; - break; - case Kint::MODE_WHITESPACE: - default: - return $text . $nlAfter; - break; - } - } - - - private static function _char( $char, $repeat = null ) - { - switch ( Kint::enabled() ) { - case Kint::MODE_PLAIN: - $char = self::$_htmlSymbols[ array_search( $char, self::$_utfSymbols, true ) ]; - break; - case Kint::MODE_CLI: - $inWindowsShell = PHP_SAPI === 'cli' && DIRECTORY_SEPARATOR !== '/'; - if ( $inWindowsShell ) { - $char = self::$_winShellSymbols[ array_search( $char, self::$_utfSymbols, true ) ]; - } - break; - case Kint::MODE_WHITESPACE: - default: - break; - } - - return $repeat ? str_repeat( $char, $repeat ) : $char; - } - - private static function _title( $text ) - { - $escaped = kintParser::escape( $text ); - $lengthDifference = strlen( $escaped ) - strlen( $text ); - return - self::_colorize( - self::_char( '┌' ) . self::_char( '─', 78 ) . self::_char( '┐' ) . PHP_EOL - . self::_char( '│' ), - 'title', - false - ) - - . self::_colorize( str_pad( $escaped, 78 + $lengthDifference, ' ', STR_PAD_BOTH ), 'title', false ) - - . self::_colorize( self::_char( '│' ) . PHP_EOL - . self::_char( '└' ) . self::_char( '─', 78 ) . self::_char( '┘' ), - 'title' - ); - } - - public static function wrapStart() - { - if ( Kint::enabled() === Kint::MODE_PLAIN ) { - return '
';
-		}
-		return '';
-	}
-
-	public static function wrapEnd( $callee, $miniTrace, $prevCaller )
-	{
-		$lastLine = self::_colorize( self::_char( "═", 80 ), 'title' );
-		$lastChar = Kint::enabled() === Kint::MODE_PLAIN ? '
' : ''; - - - if ( !Kint::$displayCalledFrom ) return $lastLine . $lastChar; - - - return $lastLine . self::_colorize( 'Called from ' . self::_buildCalleeString( $callee ), 'title' ) . $lastChar; - } - - - private static function _drawHeader( kintVariableData $kintVar ) - { - $output = ''; - - if ( $kintVar->access ) { - $output .= ' ' . $kintVar->access; - } - - if ( $kintVar->name !== null && $kintVar->name !== '' ) { - $output .= ' ' . kintParser::escape( $kintVar->name ); - } - - if ( $kintVar->operator ) { - $output .= ' ' . $kintVar->operator; - } - - $output .= ' ' . self::_colorize( $kintVar->type, 'type', false ); - - if ( $kintVar->size !== null ) { - $output .= ' (' . $kintVar->size . ')'; - } - - - if ( $kintVar->value !== null && $kintVar->value !== '' ) { - $output .= ' ' . self::_colorize( - $kintVar->value, # escape shell - 'value', - false - ); - } - - return ltrim( $output ); - } - - private static function _buildCalleeString( $callee ) - { - if ( Kint::enabled() === Kint::MODE_CLI ) { // todo win/nix - return "{$callee['file']}:{$callee['line']}"; - } - - $url = Kint::getIdeLink( $callee['file'], $callee['line'] ); - $shortenedName = Kint::shortenPath( $callee['file'] ) . ':' . $callee['line']; - - if ( Kint::enabled() === Kint::MODE_PLAIN ) { - if ( strpos( $url, 'http://' ) === 0 ) { - $calleeInfo = "{$shortenedName}"; - } else { - $calleeInfo = "{$shortenedName}"; - } - } else { - $calleeInfo = $shortenedName; - } - - return $calleeInfo; - } - - public static function init() - { - self::$_enableColors = - Kint::$cliColors - && ( DIRECTORY_SEPARATOR === '/' || getenv( 'ANSICON' ) !== false || getenv( 'ConEmuANSI' ) === 'ON' ); - - return Kint::enabled() === Kint::MODE_PLAIN - ? '' - : ''; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/decorators/rich.php b/system/ThirdParty/Kint/decorators/rich.php deleted file mode 100755 index bb6c97825709..000000000000 --- a/system/ThirdParty/Kint/decorators/rich.php +++ /dev/null @@ -1,319 +0,0 @@ -'; - - $extendedPresent = $kintVar->extendedValue !== null || $kintVar->_alternatives !== null; - - if ( $extendedPresent ) { - $class = 'kint-parent'; - if ( Kint::$expandedByDefault ) { - $class .= ' kint-show'; - } - $output .= '
'; - } else { - $output .= '
'; - } - - if ( $extendedPresent ) { - $output .= ''; - } - - $output .= self::_drawHeader( $kintVar ) . $kintVar->value . '
'; - - - if ( $extendedPresent ) { - $output .= '
'; - } - - if ( isset( $kintVar->extendedValue ) ) { - - if ( is_array( $kintVar->extendedValue ) ) { - foreach ( $kintVar->extendedValue as $v ) { - $output .= self::decorate( $v ); - } - } elseif ( is_string( $kintVar->extendedValue ) ) { - $output .= '
' . $kintVar->extendedValue . '
'; - } else { - $output .= self::decorate( $kintVar->extendedValue ); //it's kint's container - } - - } elseif ( isset( $kintVar->_alternatives ) ) { - $output .= "
    "; - - foreach ( $kintVar->_alternatives as $k => $var ) { - $active = $k === 0 ? ' class="kint-active-tab"' : ''; - $output .= "" . self::_drawHeader( $var, false ) . ''; - } - - $output .= "
    "; - - foreach ( $kintVar->_alternatives as $var ) { - $output .= "
  • "; - - $var = $var->value; - - if ( is_array( $var ) ) { - foreach ( $var as $v ) { - $output .= is_string( $v ) - ? '
    ' . $v . '
    ' - : self::decorate( $v ); - } - } elseif ( is_string( $var ) ) { - $output .= '
    ' . $var . '
    '; - } elseif ( isset( $var ) ) { - throw new Exception( - 'Kint has encountered an error, ' - . 'please paste this report to https://github.com/raveren/kint/issues
    ' - . 'Error encountered at ' . basename( __FILE__ ) . ':' . __LINE__ . '
    ' - . ' variables: ' - . htmlspecialchars( var_export( $kintVar->_alternatives, true ), ENT_QUOTES ) - ); - } - - $output .= "
  • "; - } - - $output .= "
"; - } - if ( $extendedPresent ) { - $output .= '
'; - } - - $output .= ''; - - return $output; - } - - public static function decorateTrace( $traceData ) - { - $output = '
'; - - foreach ( $traceData as $i => $step ) { - $class = 'kint-parent'; - if ( Kint::$expandedByDefault ) { - $class .= ' kint-show'; - } - - $output .= '
' - . '' . ( $i + 1 ) . ' ' - . '' - . ''; - - if ( isset( $step['file'] ) ) { - $output .= self::_ideLink( $step['file'], $step['line'] ); - } else { - $output .= 'PHP internal call'; - } - - $output .= ''; - - $output .= $step['function']; - - if ( isset( $step['args'] ) ) { - $output .= '(' . implode( ', ', array_keys( $step['args'] ) ) . ')'; - } - $output .= '
'; - $firstTab = ' class="kint-active-tab"'; - $output .= '
    '; - - if ( !empty( $step['source'] ) ) { - $output .= "Source"; - $firstTab = ''; - } - - if ( !empty( $step['args'] ) ) { - $output .= "Arguments"; - $firstTab = ''; - } - - if ( !empty( $step['object'] ) ) { - kintParser::reset(); - $calleeDump = kintParser::factory( $step['object'] ); - - $output .= "Callee object [{$calleeDump->type}]"; - } - - - $output .= '
    '; - - - if ( !empty( $step['source'] ) ) { - $output .= "
  • {$step['source']}
  • "; - } - - if ( !empty( $step['args'] ) ) { - $output .= "
  • "; - foreach ( $step['args'] as $k => $arg ) { - kintParser::reset(); - $output .= self::decorate( kintParser::factory( $arg, $k ) ); - } - $output .= "
  • "; - } - if ( !empty( $step['object'] ) ) { - $output .= "
  • " . self::decorate( $calleeDump ) . "
  • "; - } - - $output .= '
'; - } - $output .= '
'; - - return $output; - } - - - /** - * called for each dump, opens the html tag - * - * @param array $callee caller information taken from debug backtrace - * - * @return string - */ - public static function wrapStart() - { - return "
"; - } - - - /** - * closes Kint::_wrapStart() started html tags and displays callee information - * - * @param array $callee caller information taken from debug backtrace - * @param array $miniTrace full path to kint call - * @param array $prevCaller previous caller information taken from debug backtrace - * - * @return string - */ - public static function wrapEnd( $callee, $miniTrace, $prevCaller ) - { - if ( !Kint::$displayCalledFrom ) { - return '
'; - } - - $callingFunction = ''; - $calleeInfo = ''; - $traceDisplay = ''; - if ( isset( $prevCaller['class'] ) ) { - $callingFunction = $prevCaller['class']; - } - if ( isset( $prevCaller['type'] ) ) { - $callingFunction .= $prevCaller['type']; - } - if ( isset( $prevCaller['function'] ) - && !in_array( $prevCaller['function'], array( 'include', 'include_once', 'require', 'require_once' ) ) - ) { - $callingFunction .= $prevCaller['function'] . '()'; - } - $callingFunction and $callingFunction = " [{$callingFunction}]"; - - - if ( isset( $callee['file'] ) ) { - $calleeInfo .= 'Called from ' . self::_ideLink( $callee['file'], $callee['line'] ); - } - - if ( !empty( $miniTrace ) ) { - $traceDisplay = '
    '; - foreach ( $miniTrace as $step ) { - $traceDisplay .= '
  1. ' . self::_ideLink( $step['file'], $step['line'] ); // closing tag not required - if ( isset( $step['function'] ) - && !in_array( $step['function'], array( 'include', 'include_once', 'require', 'require_once' ) ) - ) { - $classString = ' ['; - if ( isset( $step['class'] ) ) { - $classString .= $step['class']; - } - if ( isset( $step['type'] ) ) { - $classString .= $step['type']; - } - $classString .= $step['function'] . '()]'; - $traceDisplay .= $classString; - } - } - $traceDisplay .= '
'; - - $calleeInfo = '' . $calleeInfo; - } - - - return "
" - . ' ' - . "{$calleeInfo}{$callingFunction}{$traceDisplay}" - . "
"; - } - - - private static function _drawHeader( kintVariableData $kintVar, $verbose = true ) - { - $output = ''; - if ( $verbose ) { - if ( $kintVar->access !== null ) { - $output .= "" . $kintVar->access . " "; - } - - if ( $kintVar->name !== null && $kintVar->name !== '' ) { - $output .= "" . kintParser::escape( $kintVar->name ) . " "; - } - - if ( $kintVar->operator !== null ) { - $output .= $kintVar->operator . " "; - } - } - - if ( $kintVar->type !== null ) { - if ( $verbose ) { - $output .= ""; - } - - $output .= $kintVar->type; - - if ( $verbose ) { - $output .= ""; - } else { - $output .= " "; - } - } - - - if ( $kintVar->size !== null ) { - $output .= "(" . $kintVar->size . ") "; - } - - return $output; - } - - private static function _ideLink( $file, $line ) - { - $shortenedPath = Kint::shortenPath( $file ); - if ( !Kint::$fileLinkFormat ) return $shortenedPath . ':' . $line; - - $ideLink = Kint::getIdeLink( $file, $line ); - $class = ( strpos( $ideLink, 'http://' ) === 0 ) ? 'class="kint-ide-link" ' : ''; - return "{$shortenedPath}:{$line}"; - } - - - /** - * produces css and js required for display. May be called multiple times, will only produce output once per - * pageload or until `-` or `@` modifier is used - * - * @return string - */ - public static function init() - { - $baseDir = KINT_DIR . 'view/compiled/'; - - if ( !is_readable( $cssFile = $baseDir . Kint::$theme . '.css' ) ) { - $cssFile = $baseDir . 'original.css'; - } - - return - '' - . '\n"; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/examples/overview.php b/system/ThirdParty/Kint/examples/overview.php deleted file mode 100755 index 70f5be91910b..000000000000 --- a/system/ThirdParty/Kint/examples/overview.php +++ /dev/null @@ -1,176 +0,0 @@ -additionalData = $data; } - - /** - * @return \DateTime date object - */ - public function getCreatedDate() { } - - /** - * @param \DateTime $date - */ - public function setCreatedDate( DateTime $date ) { $this->createdDate = $date; } - - /** - * Dummy method that triggers trace - */ - public function ensure() { Kint::trace(); } -} - -class UserManager -{ - private $user; - - /** - * Get user from manager - */ - public function getUser() { return $this->user; } - - /** - * Debug specific user - * - * @param \User $user - */ - public function debugUser( $user ) - { - $this->user = $user; - d( $this->getUser() ); - } - - /** - * Ensure user (triggers ensure() method on \User object that trace) - * - * @void - */ - public function ensureUser() { $this->user->ensure(); } -} - -$user = new User; -$user->setAdditionalData( array( - 'last_login' => new DateTime(), - 'current_unix_timestamp' => time(), - 'random_rgb_color_code' => '#FF9900', - 'impressions' => 60, - 'nickname' => 'Someuser', - ) -); -$user->setCreatedDate( new DateTime( '2013-10-10' ) ); -$userManager = new UserManager(); - -for ( $i = 1; $i < 6; $i++ ) { - $tabularData[] = array( - 'date' => "2013-01-0{$i}", - 'allowed' => $i % 3 == 0, - 'action' => "action {$i}", - 'clicks' => rand( 100, 50000 ), - 'impressions' => rand( 10000, 500000 ), - ); - - if ( $i % 2 == 0 ) { - unset( $tabularData[ $i - 1 ]['clicks'] ); - } -} - -$nestedArray = array(); - -for ( $i = 1; $i < 6; $i++ ) { - $nestedArray["user group {$i}"] = array( - "user {$i}" => array( - 'name' => "Name {$i}", - 'surname' => "Surname {$i}" - ), - - 'data' => array( - 'conversions' => rand( 100, 5000 ), - 'spent' => array( 'currency' => 'EUR', 'amount' => rand( 10000, 500000 ) ) - ), - ); -} -?> - - - Kint PHP debugging tool - overview - - -
- - -

Kint PHP debugging tool - overview

-
-

Debug variables

-debugUser( $user ); -d( $userManager, $tabularData ); -d( $nestedArray ); -?> -

Trace

-ensureUser(); ?> - - \ No newline at end of file diff --git a/system/ThirdParty/Kint/inc/kintObject.class.php b/system/ThirdParty/Kint/inc/kintObject.class.php deleted file mode 100755 index 63651237f2d0..000000000000 --- a/system/ThirdParty/Kint/inc/kintObject.class.php +++ /dev/null @@ -1,19 +0,0 @@ - self::$_level, - 'objects' => self::$_objects, - ); - - self::$_level++; - - $varData = new kintVariableData; - $varData->name = $name; - - # first parse the variable based on its type - $varType = gettype( $variable ); - $varType === 'unknown type' and $varType = 'unknown'; # PHP 5.4 inconsistency - $methodName = '_parse_' . $varType; - - # objects can be presented in a different way altogether, INSTEAD, not ALONGSIDE the generic parser - if ( $varType === 'object' ) { - foreach ( self::$_objectParsers as $parserClass ) { - $className = 'Kint_Objects_' . $parserClass; - - /** @var $object KintObject */ - $object = new $className; - if ( ( $alternativeTabs = $object->parse( $variable ) ) !== false ) { - self::$_skipAlternatives = true; - $alternativeDisplay = new kintVariableData; - $alternativeDisplay->type = $object->name; - $alternativeDisplay->value = $object->value; - $alternativeDisplay->name = $name; - - foreach ( $alternativeTabs as $name => $values ) { - $alternative = kintParser::factory( $values ); - $alternative->type = $name; - if ( Kint::enabled() === Kint::MODE_RICH ) { - empty( $alternative->value ) and $alternative->value = $alternative->extendedValue; - $alternativeDisplay->_alternatives[] = $alternative; - } else { - $alternativeDisplay->extendedValue[] = $alternative; - } - } - - self::$_skipAlternatives = false; - self::$_level = $revert['level']; - self::$_objects = $revert['objects']; - return $alternativeDisplay; - } - } - } - - # base type parser returning false means "stop processing further": e.g. recursion - if ( self::$methodName( $variable, $varData ) === false ) { - self::$_level--; - return $varData; - } - - if ( Kint::enabled() === Kint::MODE_RICH && !self::$_skipAlternatives ) { - # if an alternative returns something that can be represented in an alternative way, don't :) - self::$_skipAlternatives = true; - - # now check whether the variable can be represented in a different way - foreach ( self::$_customDataTypes as $parserClass ) { - $className = 'Kint_Parsers_' . $parserClass; - - /** @var $parser kintParser */ - $parser = new $className; - $parser->name = $name; # the parser may overwrite the name value, so set it first - - if ( $parser->_parse( $variable ) !== false ) { - $varData->_alternatives[] = $parser; - } - } - - - # if alternatives exist, push extendedValue to their front and display it as one of alternatives - if ( !empty( $varData->_alternatives ) && isset( $varData->extendedValue ) ) { - $_ = new kintVariableData; - - $_->value = $varData->extendedValue; - $_->type = 'contents'; - $_->size = null; - - array_unshift( $varData->_alternatives, $_ ); - $varData->extendedValue = null; - } - - self::$_skipAlternatives = false; - } - - self::$_level = $revert['level']; - self::$_objects = $revert['objects']; - - if ( strlen( $varData->name ) > 80 ) { - $varData->name = - self::_substr( $varData->name, 0, 37 ) - . '...' - . self::_substr( $varData->name, -38, null ); - } - return $varData; - } - - private static function _checkDepth() - { - return Kint::$maxLevels != 0 && self::$_level >= Kint::$maxLevels; - } - - private static function _isArrayTabular( array $variable ) - { - if ( Kint::enabled() !== Kint::MODE_RICH ) return false; - - $arrayKeys = array(); - $keys = null; - $closeEnough = false; - foreach ( $variable as $row ) { - if ( !is_array( $row ) || empty( $row ) ) return false; - - foreach ( $row as $col ) { - if ( !empty( $col ) && !is_scalar( $col ) ) return false; // todo add tabular "tolerance" - } - - if ( isset( $keys ) && !$closeEnough ) { - # let's just see if the first two rows have same keys, that's faster and has the - # positive side effect of easily spotting missing keys in later rows - if ( $keys !== array_keys( $row ) ) return false; - - $closeEnough = true; - } else { - $keys = array_keys( $row ); - } - - $arrayKeys = array_unique( array_merge( $arrayKeys, $keys ) ); - } - - return $arrayKeys; - } - - private static function _decorateCell( kintVariableData $kintVar ) - { - if ( $kintVar->extendedValue !== null || !empty( $kintVar->_alternatives ) ) { - return '' . Kint_Decorators_Rich::decorate( $kintVar ) . ''; - } - - $output = 'value !== null ) { - $output .= ' title="' . $kintVar->type; - - if ( $kintVar->size !== null ) { - $output .= " (" . $kintVar->size . ")"; - } - - $output .= '">' . $kintVar->value; - } else { - $output .= '>'; - - if ( $kintVar->type !== 'NULL' ) { - $output .= '' . $kintVar->type; - - if ( $kintVar->size !== null ) { - $output .= "(" . $kintVar->size . ")"; - } - - $output .= ''; - } else { - $output .= 'NULL'; - } - } - - - return $output . ''; - } - - - public static function escape( $value, $encoding = null ) - { - if ( empty( $value ) ) return $value; - - if ( Kint::enabled() === Kint::MODE_CLI ) { - $value = str_replace( "\x1b", "\\x1b", $value ); - } - - if ( Kint::enabled() === Kint::MODE_CLI || Kint::enabled() === Kint::MODE_WHITESPACE ) return $value; - - $encoding or $encoding = self::_detectEncoding( $value ); - $value = htmlspecialchars( $value, ENT_NOQUOTES, $encoding === 'ASCII' ? 'UTF-8' : $encoding ); - - - if ( $encoding === 'UTF-8' ) { - // todo we could make the symbols hover-title show the code for the invisible symbol - # when possible force invisible characters to have some sort of display (experimental) - $value = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', '?', $value ); - } - - # this call converts all non-ASCII characters into html chars of format - if ( function_exists( 'mb_encode_numericentity' ) ) { - $value = mb_encode_numericentity( - $value, - array( 0x80, 0xffff, 0, 0xffff, ), - $encoding - ); - } - - return $value; - } - - - private static $_dealingWithGlobals = false; - - private static function _parse_array( &$variable, kintVariableData $variableData ) - { - isset( self::$_marker ) or self::$_marker = "\x00" . uniqid(); - - # naturally, $GLOBALS variable is an intertwined recursion nightmare, use black magic - $globalsDetector = false; - if ( array_key_exists( 'GLOBALS', $variable ) && is_array( $variable['GLOBALS'] ) ) { - $globalsDetector = "\x01" . uniqid(); - - $variable['GLOBALS'][ $globalsDetector ] = true; - if ( isset( $variable[ $globalsDetector ] ) ) { - unset( $variable[ $globalsDetector ] ); - self::$_dealingWithGlobals = true; - } else { - unset( $variable['GLOBALS'][ $globalsDetector ] ); - $globalsDetector = false; - } - } - - $variableData->type = 'array'; - $variableData->size = count( $variable ); - - if ( $variableData->size === 0 ) { - return; - } - if ( isset( $variable[ self::$_marker ] ) ) { # recursion; todo mayhaps show from where - if ( self::$_dealingWithGlobals ) { - $variableData->value = '*RECURSION*'; - } else { - unset( $variable[ self::$_marker ] ); - $variableData->value = self::$_marker; - } - return false; - } - if ( self::_checkDepth() ) { - $variableData->extendedValue = "*DEPTH TOO GREAT*"; - return false; - } - - $isSequential = self::_isSequential( $variable ); - - if ( $variableData->size > 1 && ( $arrayKeys = self::_isArrayTabular( $variable ) ) !== false ) { - $variable[ self::$_marker ] = true; # this must be AFTER _isArrayTabular - $firstRow = true; - $extendedValue = ''; - - foreach ( $variable as $rowIndex => & $row ) { - # display strings in their full length - self::$_placeFullStringInValue = true; - - if ( $rowIndex === self::$_marker ) continue; - - if ( isset( $row[ self::$_marker ] ) ) { - $variableData->value = "*RECURSION*"; - return false; - } - - - $extendedValue .= ''; - if ( $isSequential ) { - $output = ''; - } else { - $output = self::_decorateCell( kintParser::factory( $rowIndex ) ); - } - if ( $firstRow ) { - $extendedValue .= ''; - } - - # we iterate the known full set of keys from all rows in case some appeared at later rows, - # as we only check the first two to assume - foreach ( $arrayKeys as $key ) { - if ( $firstRow ) { - $extendedValue .= ''; - } - - if ( !array_key_exists( $key, $row ) ) { - $output .= ''; - continue; - } - - $var = kintParser::factory( $row[ $key ] ); - - if ( $var->value === self::$_marker ) { - $variableData->value = '*RECURSION*'; - return false; - } elseif ( $var->value === '*RECURSION*' ) { - $output .= ''; - } else { - $output .= self::_decorateCell( $var ); - } - unset( $var ); - } - - if ( $firstRow ) { - $extendedValue .= ''; - $firstRow = false; - } - - $extendedValue .= $output . ''; - } - self::$_placeFullStringInValue = false; - - $variableData->extendedValue = $extendedValue . '
' . '#' . ( $rowIndex + 1 ) . ' ' . self::escape( $key ) . '*RECURSION*
'; - - } else { - $variable[ self::$_marker ] = true; - $extendedValue = array(); - - foreach ( $variable as $key => & $val ) { - if ( $key === self::$_marker ) continue; - - $output = kintParser::factory( $val ); - if ( $output->value === self::$_marker ) { - $variableData->value = "*RECURSION*"; // recursion occurred on a higher level, thus $this is recursion - return false; - } - if ( !$isSequential ) { - $output->operator = '=>'; - } - $output->name = $isSequential ? null : "'" . $key . "'"; - $extendedValue[] = $output; - } - $variableData->extendedValue = $extendedValue; - } - - if ( $globalsDetector ) { - self::$_dealingWithGlobals = false; - } - - unset( $variable[ self::$_marker ] ); - } - - - private static function _parse_object( &$variable, kintVariableData $variableData ) - { - if ( function_exists( 'spl_object_hash' ) ) { - $hash = spl_object_hash( $variable ); - } else { - ob_start(); - var_dump( $variable ); - preg_match( '[#(\d+)]', ob_get_clean(), $match ); - $hash = $match[1]; - } - - $castedArray = (array) $variable; - $variableData->type = get_class( $variable ); - $variableData->size = count( $castedArray ); - - if ( isset( self::$_objects[ $hash ] ) ) { - $variableData->value = '*RECURSION*'; - return false; - } - if ( self::_checkDepth() ) { - $variableData->extendedValue = "*DEPTH TOO GREAT*"; - return false; - } - - - # ArrayObject (and maybe ArrayIterator, did not try yet) unsurprisingly consist of mainly dark magic. - # What bothers me most, var_dump sees no problem with it, and ArrayObject also uses a custom, - # undocumented serialize function, so you can see the properties in internal functions, but - # can never iterate some of them if the flags are not STD_PROP_LIST. Fun stuff. - if ( $variableData->type === 'ArrayObject' || is_subclass_of( $variable, 'ArrayObject' ) ) { - $arrayObjectFlags = $variable->getFlags(); - $variable->setFlags( ArrayObject::STD_PROP_LIST ); - } - - self::$_objects[ $hash ] = true; // todo store reflectorObject here for alternatives cache - $reflector = new ReflectionObject( $variable ); - - # add link to definition of userland objects - if ( Kint::enabled() === Kint::MODE_RICH && Kint::$fileLinkFormat && $reflector->isUserDefined() ) { - $url = Kint::getIdeLink( $reflector->getFileName(), $reflector->getStartLine() ); - - $class = ( strpos( $url, 'http://' ) === 0 ) ? 'class="kint-ide-link" ' : ''; - $variableData->type = "{$variableData->type}"; - } - $variableData->size = 0; - - $extendedValue = array(); - $encountered = array(); - - # copy the object as an array as it provides more info than Reflection (depends) - foreach ( $castedArray as $key => $value ) { - /* casting object to array: - * integer properties are inaccessible; - * private variables have the class name prepended to the variable name; - * protected variables have a '*' prepended to the variable name. - * These prepended values have null bytes on either side. - * http://www.php.net/manual/en/language.types.array.php#language.types.array.casting - */ - if ( $key{0} === "\x00" ) { - - $access = $key{1} === "*" ? "protected" : "private"; - - // Remove the access level from the variable name - $key = substr( $key, strrpos( $key, "\x00" ) + 1 ); - } else { - $access = "public"; - } - - $encountered[ $key ] = true; - - $output = kintParser::factory( $value, self::escape( $key ) ); - $output->access = $access; - $output->operator = '->'; - $extendedValue[] = $output; - $variableData->size++; - } - - foreach ( $reflector->getProperties() as $property ) { - $name = $property->name; - if ( $property->isStatic() || isset( $encountered[ $name ] ) ) continue; - - if ( $property->isProtected() ) { - $property->setAccessible( true ); - $access = "protected"; - } elseif ( $property->isPrivate() ) { - $property->setAccessible( true ); - $access = "private"; - } else { - $access = "public"; - } - - $value = $property->getValue( $variable ); - - $output = kintParser::factory( $value, self::escape( $name ) ); - $output->access = $access; - $output->operator = '->'; - $extendedValue[] = $output; - $variableData->size++; - } - - if ( isset( $arrayObjectFlags ) ) { - $variable->setFlags( $arrayObjectFlags ); - } - - if ( $variableData->size ) { - $variableData->extendedValue = $extendedValue; - } - } - - - private static function _parse_boolean( &$variable, kintVariableData $variableData ) - { - $variableData->type = 'bool'; - $variableData->value = $variable ? 'TRUE' : 'FALSE'; - } - - private static function _parse_double( &$variable, kintVariableData $variableData ) - { - $variableData->type = 'float'; - $variableData->value = $variable; - } - - private static function _parse_integer( &$variable, kintVariableData $variableData ) - { - $variableData->type = 'integer'; - $variableData->value = $variable; - } - - private static function _parse_null( &$variable, kintVariableData $variableData ) - { - $variableData->type = 'NULL'; - } - - private static function _parse_resource( &$variable, kintVariableData $variableData ) - { - $resourceType = get_resource_type( $variable ); - $variableData->type = "resource ({$resourceType})"; - - if ( $resourceType === 'stream' && $meta = stream_get_meta_data( $variable ) ) { - - if ( isset( $meta['uri'] ) ) { - $file = $meta['uri']; - - if ( function_exists( 'stream_is_local' ) ) { - // Only exists on PHP >= 5.2.4 - if ( stream_is_local( $file ) ) { - $file = Kint::shortenPath( $file ); - } - } - - $variableData->value = $file; - } - } - } - - private static function _parse_string( &$variable, kintVariableData $variableData ) - { - $variableData->type = 'string'; - - $encoding = self::_detectEncoding( $variable ); - if ( $encoding !== 'ASCII' ) { - $variableData->type .= ' ' . $encoding; - } - - - $variableData->size = self::_strlen( $variable, $encoding ); - if ( Kint::enabled() !== Kint::MODE_RICH ) { - $variableData->value = '"' . self::escape( $variable, $encoding ) . '"'; - return; - } - - - if ( !self::$_placeFullStringInValue ) { - - $strippedString = preg_replace( '[\s+]', ' ', $variable ); - if ( Kint::$maxStrLength && $variableData->size > Kint::$maxStrLength ) { - - // encode and truncate - $variableData->value = '"' - . self::escape( self::_substr( $strippedString, 0, Kint::$maxStrLength, $encoding ), $encoding ) - . '…"'; - $variableData->extendedValue = self::escape( $variable, $encoding ); - - return; - } elseif ( $variable !== $strippedString ) { // omit no data from display - - $variableData->value = '"' . self::escape( $variable, $encoding ) . '"'; - $variableData->extendedValue = self::escape( $variable, $encoding ); - - return; - } - } - - $variableData->value = '"' . self::escape( $variable, $encoding ) . '"'; - } - - private static function _parse_unknown( &$variable, kintVariableData $variableData ) - { - $type = gettype( $variable ); - $variableData->type = "UNKNOWN" . ( !empty( $type ) ? " ({$type})" : '' ); - $variableData->value = var_export( $variable, true ); - } - -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/inc/kintVariableData.class.php b/system/ThirdParty/Kint/inc/kintVariableData.class.php deleted file mode 100755 index b5fedcc396d1..000000000000 --- a/system/ThirdParty/Kint/inc/kintVariableData.class.php +++ /dev/null @@ -1,87 +0,0 @@ -= 0)); +define('KINT_PHP522', (version_compare(PHP_VERSION, '5.2.2') >= 0)); +define('KINT_PHP523', (version_compare(PHP_VERSION, '5.2.3') >= 0)); +define('KINT_PHP524', (version_compare(PHP_VERSION, '5.2.4') >= 0)); +define('KINT_PHP525', (version_compare(PHP_VERSION, '5.2.5') >= 0)); +define('KINT_PHP53', (version_compare(PHP_VERSION, '5.3') >= 0)); +define('KINT_PHP56', (version_compare(PHP_VERSION, '5.6') >= 0)); +define('KINT_PHP70', (version_compare(PHP_VERSION, '7.0') >= 0)); +define('KINT_PHP72', (version_compare(PHP_VERSION, '7.2') >= 0)); +eval(gzuncompress('xmw mE&MZ,9ly%94ɦĘd3ݤeǣ={p?<}K. +/&)3dm6P(' . "\0" . 'BP(:(?Gs4NTLg9%Am nF|0ꦽd:"\'/[Gf:]+i>{AG[,&Pw{P~1$Zd' . "\0" . 'ٰ#xʳlB' . "\0" . '%y|T$EƓɨvEf/~5Ϣ$L٨-~R)Eqw:՚<ɓNH}H~hY\'Yq&\\\'y sO^M3Dpfd}t|ry!q ,dla.(3dPo:"QhwQ!|[M +g^)asKD($߮x4-9-7, }:' . "\0" . 'TQXΦN)Q]tn +8)@ +,Nt{ރ]CHo .tibE!Y3 TT.!H(le 06]&8zsz&vEu-5 +5gVL6zl-ہPPAuE5IY*V QsS51؊\\-%FӡhH!8 ZbƋ(R#O+N\'+\\E̞^$8/~yݏ8??8lY\\{l|5jƺ%d +P Nu' . "\0" . '/S,&tB!X C_R H|-PXE?b>y8ʧdBҏbtaL Ii.\\v~KjT-fA!T0C삂i2RfB-(ɩ}ia9[Mf|WƢ8<U}:܍uΦrLSմĜ%DMqD},%S03Oqw|eQ og$r\'ɨf=k}ZC\'P*PliۏVSYKU-vT/ͥWJ#Tl{2' . "\0" . 'q=#柕1^nRuơZPYvF; jQ:OĚOs#+| eq&<ۥ_jXqqu + O@lD+1|)r[)eTr@8R|oY Dr"K?Fg' . "\0" . '/bWZiI$_Ԭ“Oc5@`/W7eLڢC@5aZ.9vV0L_Sʞ9hRZ@C&4WfUsXnE1j}*EosM6Z3ӛ|bڢH3b ,{SbBv̯U?Dh_u) %3+`Gub5R;R +q-IXG22f2l yN9HĪSՖ% \'$d\\ ^롩n3v<Qi lrI5\\<F<@C/9! +6!dq%z ~YfC2$`xUjs[V8+&fYfIYj%ԮӖ 2do(o7Y>IG`OR~hчd0M}%r&bBď4KƗ0/!Б&Rи +3Fi1i IG4nY|e>@vO1Y 5e;UB7%"֢FM4`:`f]gE{Qp\\2bDa +`RTGw0kjˣSFc3vhǠ,/jx٧@<Ś8JhCn3DZ%X/!NKQ7=@Qt-SHN !UVH,o}"Z4fhӗCay-P5yaĒBk%SXf.#+q4;97f +wf~EدUA4D@UuUP#;ѣGլ`A tx?ӶhhPvZqH# `8AH˵+Gp[Dv3 +Zbmq8`^}Ē=Gݶ$e`@٫("$uN`Ke_- /lWy46l(&WN?5#T{b_i]VS{ c ]\\锢X;,WY)ח\'ᢡ 5Uc_iQlV%힝5(V7nܚ[VUv>a3T*L A#!EC@5PR P` )kQYaL;4R ϣ9Qڧf IN6eh4lBA-FɲDJ-h0AZ>?_ t0Ns Kl醊\\ ]A+fuDgg7\'oj}' . "\0" . 'ڰ{zr~xp~A qӮuCm8y{x~rj?=3 O] 䞝SM㗷L6mn%G\\&^21v$:8nA`11i| \\<4BMI[,b&.e[<\\7B[-U>L<=--; [ +0N(LzLnX\\@=hkoa6ˢ\\2 ÇQ?àBec;K$ZdLZЋ/T)I ෥0636yϴ϶pf]nKNX/Png5Ci>B 3ih!y|VQT,fMjr8DW% +UR}p!#V7f^}Ge$Eކ)ҧZ/Kإd$<1LE|5dy0fV$)}oXh!%\'h35AiU֪U:΂&TAҔRPJQc:".y,DDaxR2/GNRgҁڔWFGbẉf r %uAtvb:d#>U2^;IkAdN|ƓB;c^4*ЅlGAFwLYbeOhbe]fnJerjH}w5FKU%CFeV>FCXM~0 H8UVVxy 5 yڙx :@XХ^Pha/_᥽鮳4gJ/3F05/[CԲoɃ\'y.ҧ/W`s"(܇j. +wwms]dq\'yfUV xۢG AV#(?Y +| / H( +W&ȐG]Z]@SA_(.F~q$42Q^(]FCFg%PTCUNR,#+\',`I_iQ&QV鴝jo~Y-mw j0- r +xJWDȶ8=' . "\0" . ':)YP "MofZe%5Hd\\Y^eS[WXҪ,}e}PR )KGܖYYhd &8:Ad+uIocw%S+OYxdFw΁hT䳥HcxfyYſdM!2kH),ޖ(P cEVnPBE4_ \\=rXY:idU2' . "\0" . 'p' . "\0" . 'jIC0~VVˉk +ʂ:ujQ3x&c!)屾{>(ǨGKJQ)u${' . "\0" . 'ᚮ) *"S¥' . "\0" . 'Hp=oga-(:m#go  =s^+z3 1L)uàp\':{$YI?P' . "\0" . '=L[҆L|::K.[ V L\'fBGmޮLSRN U;[pIɠeˈh+;BEP H+,DH +jt+0T* p_sh+ r("RQlG&7ǩR,Yʎzt틆BHӦR[iћqf6OݣP$@Fe x\\W +DI$ЄCGC죗b7\' +2gK:>JS?ɯ޼ tI[^;_z 5qSrtzH򑝝__|wo^w#~C=x?VG<M=?H00(+l\\+Ŝc@hV44bY0V܋xZڹ.xwzgvOI |f5^GՇZJ8X^G`H l.ư0 +Ix^3YH÷8 +W]! eF}{\'S$M F/ug˓}&A5x|5 \\Jʫ()Z/ZŇF$CLDoy#]a`+ DQx4sJю2xV:!]WڵzQ(LL~ +1GK!hm-Z-)_BW quU>Ͱ zdl#%[U +)QWCPZe$kj*\'l;V/l +R@Ll"5 }5Ty$yU>ռAqh+Ar!* rǺz޲' . "\0" . ' wWAa@5i({d Zt[@ +P2J2Ro|쐋0WVkw~"L' . "\0" . 'ɦÂZZVv*4E(- PM!0)J=**;8mLҮ(PDjF"JLK)4X.źŢT' . "\0" . '=9 8?;>x _͘V ܮ5:v%2;47XLZ*CF< 2>[Qݝ=;|<8!}3\'I:L\'eoxf͝*/8͒Gόbe6V,,|K≚Fx20LL|Sb? +1GB% !ls ƛx8 eoqh1ύSEu{k~dta]M.b`Cت-Rx5r2ZF5^hT +~ʁ\'%%iNz9 +U4.xgj%Xyϐ5+@uJ/wyi%|vY8CYQFRbmf%D ɫ5X{1F#,S8 o,).S0&Cd{F07 ʼgVRyV\\T2eK8RzZWB_q]|emJwq|2 ϼNc*xC)1->hX\' 2 ow>1[' . "\0" . ' Z+ʰB5=)x " 39*A +DWQ׍>&|)7]+1.+\'=ydN<:;\\*8呟+q3vıZ+WT(z|Tgb.t_9FTD48FE#]1:$h-/ۊ4ycqG$r߭w4' . "\0" . 'e 8CU҉%maTdDePmDk蟕Z;WNr1+, o)*5#x:vEއEpihKkP{j%YXIJbeums-V}%EɨC~Y=K׶B9EWş!«H!d[ٍ2֍>aBp}9N 4n2tqV%PK47ɱǚ 2]C:ωo$ 1ҢL1io[6~Rd!d}U19w$m0ک JWt^]7ѻ"G8dG>ңxQ0Mu +՗oO믩ޜJs/M>WxЈR~]bNK/ \\.gbx^2hRJAI6 Ld' . "\0" . 's0z({wr~xf glX&aIR @䓼R[THJv5>0- !`< 5˲ǎ*}勯~=WHYɰ#=yFC[4Pj/騧oѐTwf *vXԖ&9`:cA +sTs9z4B-fq6R"twQ.ֳ2p' . "\0" . 'ꮴ~JpE-bq_l <Ȥ7k' . "\0" . '/{!YA{0o>Q%ߦM}y&h4Si$e0Hn`߁z N*q`bnI/`y|PU#An2f.t)WxYH)mZ;]!Z\\KLOY&qwBj +6{OsӂQ!s2ADt}\'\'͆LHԲt3ܢ?xJv?,@nSdU7nt6p(WjLF{ +4ض62#XM/k趟l5Ŕ#-)issaKEiCwn;Ou:*\'O"S@^ӭ\'Iր^PjolwM1Po!s2`fBb\'LOٻ똓q9guN/Vm=tr\'gkδS9zt˳ē\'OiI$Ms:ע_*>Ԓ@-aZ/ew]va0ZLPd8z)t[[.ч؈r o\'4L7,tI6yxwOVY-ӔeaaX #7P6h3nn|#{v>]}(%TyZq ŞSLwJ/YIoc-C̒]Yf;_ m0kYyҖNpD8ٔ m 3D V9N#4&NL6Jt[e8DTe:!t-d|Jƚ.p {Tp"`~JTu|./:rx0&.vnrJqN#T0Z_\'OLET<098y2ub.n' . "\0" . 'h} ܉ 6 M_b9Wߤ =Xt Gb' . "\0" . ' +W, j\\n5(0\\۱&QMG]i)]TZ+Z=֫esZ[н iI&pd6jN"J[*G}MכQ2`D74Aі(MWb$DUb^9bmJ9[9\\1yUW.\\˝ABL``0t0IJ("CfiH;HDgԗ +iI:mS? EMp\\(JhY1B(ê2Ѓ]Y> KL$%n pՎɚmei*L *MM-\\Y-+V*[im|x c*2ЫJV -sx(Y52c;WA^P Vh GhU0S[ᵌ+<[)gYZG7j*7i>=]>QM|=gC 80= QӡcҬh8-&౑4(z_@Ln7`MjupLf3P+L G]N1G2vy~|u!P>f o5ߊFﱫG1޺2uyy9GkKwcjތSPDܔ(Ccú̡/hni)ƶ<~Oe.G0yUQtWN C"3A3Lc\'=/ X-n΋g-+T`)jYS6[;9c"~!F͎ U*|Cr>ekgꭗ +K[|[QU?A4?pfTC7L_F c M h\'Idvr7mCh^&S\'? & nP\\.8EAI&}jTCc\'&)/>z+F}ˣ@ +-ıZ&t"վB>Z$%@u#o- z[3ϻW_"fߞ/~u6G Ί4lT|SbBeAoD˓S +rTww3\\ː"Jp()yE^5^R.)O]% yDk/mh8nBRS;S6j`Z @AUe}!nA \\#E䎳@X: +hkz6 _~d2.BWZߌtEu ϪpQM&:"ZihDԛle-@홢 K|}mJOyVؠΥJ-{+,Cc +&O@F' . "\0" . ' e 19)Nlf vЈ 6I4h6I6W98k_u>%"0q$,G.գl 2DZvBz6q!m1OO/>\\fPvAC,e<ϥ̦+VVɩn\\OP#+/>/' . "\0" . ';r"{|Pͻ㸩-f‡u-q^7H[ť w%.BFjӫs [U\'tQ?!oTl<0 zd[eC2VдeU-HFmXAMbJSիٛW , ֫㋿$+oU5{evߣ*`je Z' . "\0" . ']b+:t;jw"XHdt74.%Vdc2hr\';!...{.iޏy(({"]pwڐ17:`!@.ctD#D`d%v9?Xb/-2Q,#$$3E1o8$' . "\0" . ';f$S75+bcX8I&T]сԞ̅"%KՆ1@VaJ1`*T3$۵b)$tL2ead$ʯH)I~]oIm:jA"Qh:7Ҭ,uc|AϔGyٯk6uZ53%KR^fIf u[r,ߗ' . "\0" . '}nɛC o/&;hcqgIYl?]R[ivh ݶu5t־N=$7|EH9`dT)s9N$}iG9qm Y' . "\0" . 'Q655 oܥ x2f\'.L:w.jDzf[c +=YBS-מa.Qj馀?\'lPV$\'pɼNFXȮׇTO}uSk]$["yN>HEꦃ[["N">+~H~TKq`*ETm!C iqD#b7Uk㔖Mme@+z9(98 +M˚}5>cՆJ՟ j{eNuIz[q"˭fYԟd +wZD_v?7aşbrvI-ڨ ``5n(C;[,**i7loE4COTFǪ/,H9 tлZy[N15?mϙy ̾pS~c"=n;[pΆl}Zj@)(.B&)$ [dn7z P=!` J[6)?L6F9X1Z( 0z  O$m3۠BmÈ`P{B 4E)vPJ, +@9.W5pَ V7 czDRGt捷*kF.Cn, ,qSS\'sЗO~ +U/j2$ +' . "\0" . 'F$X6CEk9nUՄ@>PZc@|ujV.~Kzl$P +*v~1=&HٔMrZ:Gl5z?SʪŪ{4AVB7_;{ScLD~R&l5ѹ\'_fB* ?.=Vi\'Ȗa~ +a^^n^$d_NTWD鞱8p#  8Ǟ5qXԢZBC:B{UQ_Bl-[e k0)QSb!Lil;5pAm0z `c1=t*CCEL=NdOJ5KIOBo8+47!T3~-|0H,OpT98' . "\0" . 'E>J.0b60,H>TaxY)l!dh-re!QDve>(s8]Sbutex\'l |x"(;kK&b8&qӾڼ!CL' . "\0" . 'k#2E R Dr1{%O 0"|GGU ݋;֋R̝n)QNj(-6? + txh%^Ԛ,u{RT*W q"jW>N~AE\'}\\[k_Ɨ%}l?G v1U;\\s,8r Mжʤʞ? JTgTAM31yEQspFQD%_kGrR +h4YW5[F 4xfꦖ }{GJ{u@qy' . "\0" . 'jCΆmizֆ9YoltEÖX[rr"@olzJ{9 ^J7G?xx:{Bh' . "\0" . 'Zy6z/z +ƹIĀKO' . "\0" . 'T7/W;]}x]wt➻{!1pqiu +<' . "\0" . 'T14KC1>|^Z7Jֽ,#P%!wPst_%yFRU2+&JIl桐DUCRmZ.% T"6tнF%Iֆ5t+K)B (B@2BkMMtE\'uT_[_"6^-!Eʧ1G/- +!n676+[{K{P*2d}х(eUx =o{' . "\0" . '3AeHoZ7N v)!%|("0+0ҳ +ͻO1w eAϚ$2 㵀MM69}O)!Y*-wyZVWMg+' . "\0" . 'YCJlIRй G@ A1*$wS`hּ S1t@?X%`!;`o3Ծ1HH(anP܅ڥȣ>>I!y딳o4k Ct' . "\0" . 'or\\ٞ3>rec)0B,PxIIy51Re$4cr| $"k TJ,޺zmBKϪWQg +-2U4+yb&VJ +-y:]gZzccuNR&1Y?Owk3E(9a[ZȜ8ޖQc8Ȍ\'/{jy2vեeLG/LꇥV&9vBlhK<RŏGRWjX78"pAHEjUBD +#< +&v@Q]̓@Ŕ几kjigmK 6r9Gv]JDW_9]_s#\\C"w*ZH\\˽_zyvwYwp=Ypy}W]~tcE,-H0ۅfY ;>8_>]ݷoe:8y0z ӳʬ$[50 n4-' . "\0" . ' tŢvrrԪ6i/L!~ ?e`ؽ941=\'B,AˆǹŠr~ ' . "\0" . 'Tp8LB:+8"QbWXi1x`U + #\\:^>Y($JQf,OGԏ=bs`lz Ÿ=$tԅ{TV=U[F"#O U pWYnzkh*8HvAyMƼ_X8+l0|%~pNH~HFo;\'\\zuz̟!w|?w|=ws`[۵ߘ]P\'Lh]="]T|[.2_}DoؓF +s̊ʗKvخ"1.*L2?]E]IT +HZv7Q9s 6VbU4{eT%ESא/e|偲3Q;i|6zP*̃zSToրA5N%@B:NoGxbhS-}N&gj*{]URhJG/Dg4tō@nߊI8PgCFu3˴ 5DwQ-ֿ1җ(dH/oz"{X׷=F)oU=%/8]{XֿXvsdBMjq!qWf;D]lyN؃-,$Fj[IkQ51X.1DX;O QraWwʿl=)n t܏Mh{fx(6K/) \'>1SoxDK뜻=G5r;n%3iofTMTVU[?빠c+!b7[ ҂}l%|hgBuwL[`g6' . "\0" . '^gx}$O]_獆:RvG~Cܟ~l 8$MF_bYTx~2o۱+9X ljN̤d1eT?̒5N>$^ˤ]q"`ḍ>@6/]e+0܍oB*&X -KW.,,y/6ޥ(^DNwzeU*/P,uAWǘ6oQWҮDsDNYr!a6 +Eѿ>*\\Jg' . "\0" . 'fb2#ڿovQ:K)w9,T\'ZLZF*ŧTf b8IiJODI&ʥ So;$Ivᢉz96!0mLGK,S[Qdz>8M/' . "\0" . '0UmtTnf GEE*' . "\0" . ';`S^r&J_"PZeӕ@  nɂ(Ljk˰˲F35 ="&2<\\Nz6hZL&̀L]CuHp%}ĘUB]d+uIocw%SBIn#U +%.ldp7k<| WMu$D [DZ}njԾjU^˿ͳQVr}؟{ikCf:{*$Q63HUW0g,ʝgq]Onb* Aw25Ch/ypUn(4 taSFd~Y݋:xRX mC}s|C0٠c2}1?tH<ˡU~|Ĩ@GɠEb{EnٙEVE `ly˩K`H]{NZ=!1\'Hx ק<ڊ%y~fQKF9$玠L' . "\0" . '^HX +(DK(D=kH +#a-\\bJs|Io/]:KWgoaP:ik:m{> +On]J 7,GLZԥ9}VmʦtKdeB)cL 04X&ʺ\'WC v5k ^펙=;' . "\0" . ')ʮ yI?JՊQdW֔բK*uHId.ǖz,+AuВq}+JU?/_E?WeˏdݓӿwC=]Zu3?k=x3X$%#j]m_ K#jl /GJ7ZWKal9aC7wVj*ݷ۶vknݧ9XjNC7ɠ@ s@U)- |+qpאVtcJZtb^#G]ŏ،uvGZ\\[4 -rSA~N}P.E \'i^`*% AQyrkcz &\\cRE/}%JNQ+B0CeMnokRX|LĐEL U;b^3Kc +U 9-֛p' . "\0" . '&rz)[[ㇿ{>' . "\0" . 'O Ne RJBz=~X|"qBmh.QF2 #z(rQ[-{>-C^?V~N6 cmAd' . "\0" . '4HS0z-Z[uZdž+8, NFʞ CC[ T' . "\0" . 'l`.~BUAݳ-Q{-78X@K˃[iC5̄W%4;(ݥbPu4dKJtX!F +)>NHe(V¶aިʶp1VTڇRXR "ً,p}oR7Y.vJ{&Rr M CoVɊi' . "\0" . 'x^~U/VEtb(88މ~WTYRL&qU=\\Q+и(8w&Y}&w7y۽\\].}͹{8[ +B +0j&f•,_avD1i%gGGۘ+d-n\\N?tn0>+ěQAAk/n%?|Yi 6t])}qQ{xzB?v߹ >U&@1 +\\R +}' . "\0" . ',' . "\0" . ' +S9 wʀ_̱s2!ԓ772wOi^hǏ' . "\0" . '𩆨ח A£r\'$iT0n|<){1L\'I/t"5\\kwQ) )\\KIxjqWˡ-ςm摃Y`Wx?;' . "\0" . ';V(&`(%(6\'w/6\'$- (>9U7PxY - Xo[\\FJp>TG>M܀!_iCvfj7HxXsW60P! \'"OW,/ЮDP ĜigׄepFg1IfruS%Zo P\\")DU 4,ɖ +S\'1S*,TZ Fm\'`?JUuY^ fٌfش$bHd5}U Ϻ*HA`moOQ֨2Oi-8xdV\\Mk]}݋(\'t6ĪUBj:!^+i)kwL42' . "\0" . '| γǀa6 +x::n t,{dtYXamg\'y/0}K{/+(|HrB;wBF,3t{#u[dE\'eqG" 2f2={ȲySnDuR ?:PKq}H*A#yZR\' @˩:_  ثstT +0L.Q9;Yq^^ 9ʓT +݈m`@| +0̓eDx&&Q&+mrdTL) ñED];NPļcH4ǣHXz_3Ę,I .X' . "\0" . 'ѕSk/}&:C:;ͤ!KE)7Dyc4J`Otu/]vk@rfuׇ\\"r _zIŔDMWGKDʂ}_ KV@z .\'ZWu83Z>,-QЦ##P,DJi!uJe~9\\.?\\}^( 4|OnP.Kjhc)SFQ%vX!=/]=%Ɔh9y}Jqw=M_>_t 3oA3%tL|Rpz}=ڸH4ieqh%"- wZ$ub͙q#3t4g*Dc, &0 GW@GRFydi??)*TWC.{DTW1m1ZX@I ѧf26<t4oA|nX,̈́q +RAN6퀤4ӲW/-:ïB ݨe3Db +/Hz,f #/Z^H@&/ϊ Ch~ S;)hWX+A`+ǜW@ѣ&AW%g\'̌GuS˫<*| +]B@w#' . "\0" . 'D=eyv,;WX˄\\μ,Eį}T(;u>׸W/Ju؁a~fYN̉17ӳ' . "\0" . 'auv)Z"t;bKJ]{pI +>,!Q?44j~̅PL䋙gNyv#WTƶ&FZL<)\\M +A?`V\\' . "\0" . 'QB-}F +L}V>&d/Z1T#Au>T4)wZ@/:$7p/:,)~_6jއ$ \'Kסܡ`1&e_' . "\0" . '݀s.s8^<}-eeAG\'y0H(2z>W&n!O(KAÎ*S񵗎pӒhER\'k?:% c5_v!G2Nw\'>a-{ M3r=-VruFWϰveYwf`TB6%f5)d#T_њa!n\'ȶ1IoR&+L 숡r~M#9J&_7Ea:FYc$gI|A[fִ-$](伹wX4AظetB"' . "\0" . 'oTNA\'7`pfs-j+Oo0wB_}3̙ѻZ:s' . "\0" . 'Gfu}MKP;\\tgo/3w.mG~}~&hK$~{uSk׮p`|T7cY!N_g>ڎO7?H-1꜌W m<3EJGu]GGFr 5bY ANH1ֹ.k`ϋtu*QGRgJ=:۾"Pz0UC&D>5.iA3DOu[Ʈe]T=wJJJG7BUzuc4DHHg0' . "\0" . '3;DUU6zU@ҫOj+_tjT5 +=(5-\\I;p[qn2' . "\0" . '^tW~ez,S»V6#Y{atv7!u-uKRuTriojBQ|y1&`ngT1G 6p=O<^;??:y#.z^+j([<+/~ h\'ޝBEtXIZ ĕzL"A ~b*8' . "\0" . ' ]:c{/Wd #o 9=k +d,V]hjĠ8MaSReA}{h D9Ԥ~Iu!ÂUے4""Yqcax)-nt}[ٳd7X_+` HӤ3)oalY-`o/`{DU -ordl{!y DqlĴ"_[u}XI߾txfM_ٌG , tOfxfOٳ#Wb , \'}~&It偾b+W&@MBo[cTXMF"f,v!!Rm_Tx} zH un ʼ}vs=zޝ^==V}9ZN$0RݵL2>(pp]M@b+O?*pyЮetk~}3tB퀁 Pg EtM" {eC͚Сjݴz%AD:ʐժ^ ؋lӟz!9Rdt-t>HO~-2Mn22|qsބ&;\\"з\'1A,a]x@r)-/^?J/8g*T3Ͱt#ZR{+ +Ǫ;Mms@}KQX\'/lˎՂt ӭVP-oI$qי$[o5dZup!ӘJ! %;)ﶻ ay:uު6SWF#!Sk`h= ;^:KUyœuܐ_Z[4^_o|)&f@[@B(/:8WR藼KwtU5D Ђ1 ʢ#]HԃX`_fr-|yUt|(9d5l(*o݆F +&#XvQ7X ~sGpߦmõ, +TBYKs>isԫGG/dQX<i{z]Mi"~yr5,6 ~h(.- ԔV/]UZaRCⓂ"bvkr(E|a:C-ą-P"[n\\޶iyD\\m .ؑgMP' . "\0" . '*P듗ß٠Y7lZ 2CAE/頵bwWU,WK8l%c䐊HXT@<E&D dY5m+ʀÊR$: +b3YQHҸGԐi3Nv8~C0DSKvY7feNQ:a{#@EIBU38xF6`(%Ib6~a]!Zэ4?Q\\օ\\:+&)@rӈ>x 0Bqš1Q7R 8\'qRUX(-%"76Z!!8k]m]_RpRK_\'՝ Q֒iS5Ӻ?Ը*Jfuu:2+qeU>due5\'' . "\0" . 'K~{2s):ZTkn.#%>;NU75(DsTkV?#B6kTFr uZ\\dW\\Nϓl<`g}]*w|vCKUi$>tw*H:ÇG5];ɮբt2֎"r!zQiM8 Fjn(vnSZS;KC3.͋l6j7z)j+i!KGZ@ey&%j} BvVP]yzr=\'W) s!LON`% oAT{"HJ\'1F U[A}2Zlԫ63b̄@A߁q\'gGAI6C"Տhg0' . "\0" . 'vd"5<4@c!XoC 1F*Ѩ~T KvY hJ.B#Nx@{eiЗؒV j6G{FP@C|&l{K5o+"BYeb{} +ˈ)B\\ +o#aY>fz{!ޫF2sya_s* -~z}j2b9YI~<<_ Z.8VSTsƎ!*,' . "\0" . '-tBd0p6 ;TTQs#xU.{]g%i:yc& +0YW0-P;lKfKZ85f#X=XՋK6h)lJ $`RO!Xfk%0H ]1Z(S;zV=uFB:kE F T.n= &V+[\'ºm4DBDxIo#p/ez%2ݺpuqC/ yYCZJؕ|Z5z3U7-2Y4C)ˎZ֟>`p>zSF6;qc-&6b]ix *ժѩe\\t !+E.-tyeq*x޴t Rz(TQyFI?v}Jƃ,鲝q塿6"ҐWKS3j ' . "\0" . '8JKwi"ݞNR+VԚSŪpH|ɇkEz%*VN}W%(|>tQA\'1#@oєN ^l`51CD#ǾFS֑? . ӡ(UDlw+JR}\\kyl^t])pzXh|V-(mm~W]]n7d%TCl勺"ݧf %?7jvRYܬ?>RN#&2\'Ss2cD] plVsn t@8\\1vpOo(lg]&<\\GdTJ!k;bM{vc3N2"6O;xg%ԕa۴?1|~$Ɏjw5]MYeu}\\)np{hnEkOvYv +VRjK>e KQ[q1|#I?- 7;= 8уpd4 P0jt4?(~7(?vJ e,2E"4F<@P?ͣ7m-ҼOhgn + +}@X,ab5iohYBohN84|8[9\' I7WE}4b] >0ہ;z:ɦ 1WմI1h7y3O' . "\0" . '% h`; eE4@خ?6 []s{Xg=8ehi>\\ưd6:}\\|~q8ؙNz۵g+_Bɸmv׏/E ًuѲ3l$֜[ѓS!@Xbյv=\\4oDb.}تkum+TOWwTGٻVWDg +@ǰNFa$sӡSq4G8MdY/oʔ~;HGص|;ZqkDkߕck0v;B5]<èP9C=I!zh%|=+gGg-\\zZ,.qݤIsH"^HlwjghuWZ+ktfu{^ ++UE_zt;B)$ZN܊+x,@9 yM",Y`l`kThA9՗ҦIICsx18;|Ϛ"rjn' . "\0" . '`fbKd!"8JZ}V¬aY#XC6 ~^g/7^X.䱅fK&lHMOೣt *4ϋK`y%[(v!TQFHK &j܃gja\\ʎ9kh6bp=vM;er +EIA3M\'7m g C`Gb84nfOUg⭠ky +[({AN[o@ǣ~uy,Y jmᩨlZTo#K k7\\};WyyhtQKN JS-"dɸHF[;VX^Qxg,sѝz^.Z| +a=ӖE_~MnO\\ʆ.KwR*n>K!WCm9ϒp +%$gN7' . "\0" . 'l \\1C@K&}l`$餼0lZ[s\'7p-Olvđ*-Q XiܧL&y%Ըt+W$shoE:wh[۶VfYfi+rܷןn<ݪIFП^ +˖ +zlzV mmgTV[;V36YɈgetMethods() as $method ) { - $params = array(); - - // Access type - $access = implode( ' ', Reflection::getModifierNames( $method->getModifiers() ) ); - - // Method parameters - foreach ( $method->getParameters() as $param ) { - $paramString = ''; - - if ( $param->isArray() ) { - $paramString .= 'array '; - } else { - try { - if ( $paramClassName = $param->getClass() ) { - $paramString .= $paramClassName->name . ' '; - } - } catch ( ReflectionException $e ) { - preg_match( '/\[\s\<\w+?>\s([\w]+)/s', $param->__toString(), $matches ); - $paramClassName = isset( $matches[1] ) ? $matches[1] : ''; - - $paramString .= ' UNDEFINED CLASS (' . $paramClassName . ') '; - } - } - - $paramString .= ( $param->isPassedByReference() ? '&' : '' ) . '$' . $param->getName(); - - if ( $param->isDefaultValueAvailable() ) { - if ( is_array( $param->getDefaultValue() ) ) { - $arrayValues = array(); - foreach ( $param->getDefaultValue() as $key => $value ) { - $arrayValues[] = $key . ' => ' . $value; - } - - $defaultValue = 'array(' . implode( ', ', $arrayValues ) . ')'; - } elseif ( $param->getDefaultValue() === null ) { - $defaultValue = 'NULL'; - } elseif ( $param->getDefaultValue() === false ) { - $defaultValue = 'false'; - } elseif ( $param->getDefaultValue() === true ) { - $defaultValue = 'true'; - } elseif ( $param->getDefaultValue() === '' ) { - $defaultValue = '""'; - } else { - $defaultValue = $param->getDefaultValue(); - } - - $paramString .= ' = ' . $defaultValue; - } - - $params[] = $paramString; - } - - $output = new kintVariableData; - - // Simple DocBlock parser, look for @return - if ( ( $docBlock = $method->getDocComment() ) ) { - $matches = array(); - if ( preg_match_all( '/@(\w+)\s+(.*)\r?\n/m', $docBlock, $matches ) ) { - $lines = array_combine( $matches[1], $matches[2] ); - if ( isset( $lines['return'] ) ) { - $output->operator = '->'; - # since we're outputting code, assumption that the string is utf8 is most likely correct - # and saves resources - $output->type = self::escape( $lines['return'], 'UTF-8' ); - } - } - } - - $output->name = ( $method->returnsReference() ? '&' : '' ) . $method->getName() . '(' - . implode( ', ', $params ) . ')'; - $output->access = $access; - - if ( is_string( $docBlock ) ) { - $lines = array(); - foreach ( explode( "\n", $docBlock ) as $line ) { - $line = trim( $line ); - - if ( in_array( $line, array( '/**', '/*', '*/' ) ) ) { - continue; - } elseif ( strpos( $line, '*' ) === 0 ) { - $line = substr( $line, 1 ); - } - - $lines[] = self::escape( trim( $line ), 'UTF-8' ); - } - - $output->extendedValue = implode( "\n", $lines ) . "\n\n"; - } - - $declaringClass = $method->getDeclaringClass(); - $declaringClassName = $declaringClass->getName(); - - if ( $declaringClassName !== $className ) { - $output->extendedValue .= "Inherited from {$declaringClassName}\n"; - } - - $fileName = Kint::shortenPath( $method->getFileName() ) . ':' . $method->getStartLine(); - $output->extendedValue .= "Defined in {$fileName}"; - - $sortName = $access . $method->getName(); - - if ( $method->isPrivate() ) { - $private[ $sortName ] = $output; - } elseif ( $method->isProtected() ) { - $protected[ $sortName ] = $output; - } else { - $public[ $sortName ] = $output; - } - } - - if ( !$private && !$protected && !$public ) { - self::$cache[ $className ] = false; - } - - ksort( $public ); - ksort( $protected ); - ksort( $private ); - - self::$cache[ $className ] = $public + $protected + $private; - } - - if ( count( self::$cache[ $className ] ) === 0 ) { - return false; - } - - $this->value = self::$cache[ $className ]; - $this->type = 'Available methods'; - $this->size = count( self::$cache[ $className ] ); - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/classstatics.php b/system/ThirdParty/Kint/parsers/custom/classstatics.php deleted file mode 100755 index cde23c650b0d..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/classstatics.php +++ /dev/null @@ -1,49 +0,0 @@ -getProperties( ReflectionProperty::IS_STATIC ) as $property ) { - if ( $property->isPrivate() ) { - if ( !method_exists( $property, 'setAccessible' ) ) { - break; - } - $property->setAccessible( true ); - $access = "private"; - } elseif ( $property->isProtected() ) { - $property->setAccessible( true ); - $access = "protected"; - } else { - $access = 'public'; - } - - $_ = $property->getValue(); - $output = kintParser::factory( $_, '$' . $property->getName() ); - - $output->access = $access; - $output->operator = '::'; - $extendedValue[] = $output; - } - - foreach ( $reflection->getConstants() as $constant => $val ) { - $output = kintParser::factory( $val, $constant ); - - $output->access = 'constant'; - $output->operator = '::'; - $extendedValue[] = $output; - } - - if ( empty( $extendedValue ) ) return false; - - $this->value = $extendedValue; - $this->type = 'Static class properties'; - $this->size = count( $extendedValue ); - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/color.php b/system/ThirdParty/Kint/parsers/custom/color.php deleted file mode 100755 index 5ca44d88b06d..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/color.php +++ /dev/null @@ -1,400 +0,0 @@ -'#f0f8ff','antiquewhite'=>'#faebd7','aqua'=>'#00ffff','aquamarine'=>'#7fffd4','azure'=>'#f0ffff', - 'beige'=>'#f5f5dc','bisque'=>'#ffe4c4','black'=>'#000000','blanchedalmond'=>'#ffebcd','blue'=>'#0000ff', - 'blueviolet'=>'#8a2be2','brown'=>'#a52a2a','burlywood'=>'#deb887','cadetblue'=>'#5f9ea0','chartreuse'=>'#7fff00', - 'chocolate'=>'#d2691e','coral'=>'#ff7f50','cornflowerblue'=>'#6495ed','cornsilk'=>'#fff8dc','crimson'=>'#dc143c', - 'cyan'=>'#00ffff','darkblue'=>'#00008b','darkcyan'=>'#008b8b','darkgoldenrod'=>'#b8860b','darkgray'=>'#a9a9a9', - 'darkgrey'=>'#a9a9a9','darkgreen'=>'#006400','darkkhaki'=>'#bdb76b','darkmagenta'=>'#8b008b', - 'darkolivegreen'=>'#556b2f','darkorange'=>'#ff8c00','darkorchid'=>'#9932cc','darkred'=>'#8b0000', - 'darksalmon'=>'#e9967a','darkseagreen'=>'#8fbc8f','darkslateblue'=>'#483d8b','darkslategray'=>'#2f4f4f', - 'darkslategrey'=>'#2f4f4f','darkturquoise'=>'#00ced1','darkviolet'=>'#9400d3','deeppink'=>'#ff1493', - 'deepskyblue'=>'#00bfff','dimgray'=>'#696969','dimgrey'=>'#696969','dodgerblue'=>'#1e90ff', - 'firebrick'=>'#b22222','floralwhite'=>'#fffaf0','forestgreen'=>'#228b22','fuchsia'=>'#ff00ff', - 'gainsboro'=>'#dcdcdc','ghostwhite'=>'#f8f8ff','gold'=>'#ffd700','goldenrod'=>'#daa520','gray'=>'#808080', - 'grey'=>'#808080','green'=>'#008000','greenyellow'=>'#adff2f','honeydew'=>'#f0fff0','hotpink'=>'#ff69b4', - 'indianred'=>'#cd5c5c','indigo'=>'#4b0082','ivory'=>'#fffff0','khaki'=>'#f0e68c','lavender'=>'#e6e6fa', - 'lavenderblush'=>'#fff0f5','lawngreen'=>'#7cfc00','lemonchiffon'=>'#fffacd','lightblue'=>'#add8e6', - 'lightcoral'=>'#f08080','lightcyan'=>'#e0ffff','lightgoldenrodyellow'=>'#fafad2','lightgray'=>'#d3d3d3', - 'lightgrey'=>'#d3d3d3','lightgreen'=>'#90ee90','lightpink'=>'#ffb6c1','lightsalmon'=>'#ffa07a', - 'lightseagreen'=>'#20b2aa','lightskyblue'=>'#87cefa','lightslategray'=>'#778899','lightslategrey'=>'#778899', - 'lightsteelblue'=>'#b0c4de','lightyellow'=>'#ffffe0','lime'=>'#00ff00','limegreen'=>'#32cd32','linen'=>'#faf0e6', - 'magenta'=>'#ff00ff','maroon'=>'#800000','mediumaquamarine'=>'#66cdaa','mediumblue'=>'#0000cd', - 'mediumorchid'=>'#ba55d3','mediumpurple'=>'#9370d8','mediumseagreen'=>'#3cb371','mediumslateblue'=>'#7b68ee', - 'mediumspringgreen'=>'#00fa9a','mediumturquoise'=>'#48d1cc','mediumvioletred'=>'#c71585', - 'midnightblue'=>'#191970','mintcream'=>'#f5fffa','mistyrose'=>'#ffe4e1','moccasin'=>'#ffe4b5', - 'navajowhite'=>'#ffdead','navy'=>'#000080','oldlace'=>'#fdf5e6','olive'=>'#808000','olivedrab'=>'#6b8e23', - 'orange'=>'#ffa500','orangered'=>'#ff4500','orchid'=>'#da70d6','palegoldenrod'=>'#eee8aa','palegreen'=>'#98fb98', - 'paleturquoise'=>'#afeeee','palevioletred'=>'#d87093','papayawhip'=>'#ffefd5','peachpuff'=>'#ffdab9', - 'peru'=>'#cd853f','pink'=>'#ffc0cb','plum'=>'#dda0dd','powderblue'=>'#b0e0e6','purple'=>'#800080', - 'red'=>'#ff0000','rosybrown'=>'#bc8f8f','royalblue'=>'#4169e1','saddlebrown'=>'#8b4513','salmon'=>'#fa8072', - 'sandybrown'=>'#f4a460','seagreen'=>'#2e8b57','seashell'=>'#fff5ee','sienna'=>'#a0522d','silver'=>'#c0c0c0', - 'skyblue'=>'#87ceeb','slateblue'=>'#6a5acd','slategray'=>'#708090','slategrey'=>'#708090','snow'=>'#fffafa', - 'springgreen'=>'#00ff7f','steelblue'=>'#4682b4','tan'=>'#d2b48c','teal'=>'#008080','thistle'=>'#d8bfd8', - 'tomato'=>'#ff6347','turquoise'=>'#40e0d0','violet'=>'#ee82ee','wheat'=>'#f5deb3','white'=>'#ffffff', - 'whitesmoke'=>'#f5f5f5','yellow'=>'#ffff00','yellowgreen'=>'#9acd32' - ); - - - protected function _parse( & $variable ) - { - if ( !self::_fits( $variable ) ) return false; - - $this->type = 'CSS color'; - $variants = self::_convert( $variable ); - $this->value = - "
{$variable}
" - . "hex : {$variants['hex']}\n" - . "rgb : {$variants['rgb']}\n" - . ( isset( $variants['name'] ) ? "name: {$variants['name']}\n" : '' ) - . "hsl : {$variants['hsl']}"; - } - - - private static function _fits( $variable ) - { - if ( !is_string( $variable ) ) return false; - - $var = strtolower( trim( $variable ) ); - - return isset( self::$_css3Named[$var] ) - || preg_match( - '/^(?:#[0-9A-Fa-f]{3}|#[0-9A-Fa-f]{6}|(?:rgb|hsl)a?\s*\((?:\s*[0-9.%]+\s*,?){3,4}\))$/', - $var - ); - } - - private static function _convert( $color ) - { - $color = strtolower( $color ); - $decimalColors = array(); - $variants = array( - 'hex' => null, - 'rgb' => null, - 'name' => null, - 'hsl' => null, - ); - - if ( isset( self::$_css3Named[ $color ] ) ) { - $variants['name'] = $color; - $color = self::$_css3Named[ $color ]; - } - - if ( $color{0} === '#' ) { - $variants['hex'] = $color; - $color = substr( $color, 1 ); - if ( strlen( $color ) === 6 ) { - $colors = str_split( $color, 2 ); - } else { - $colors = array( - $color{0} . $color{0}, - $color{1} . $color{1}, - $color{2} . $color{2}, - ); - } - - $decimalColors = array_map( 'hexdec', $colors ); - } elseif ( substr( $color, 0, 3 ) === 'rgb' ) { - $variants['rgb'] = $color; - preg_match_all( '#([0-9.%]+)#', $color, $matches ); - $decimalColors = $matches[1]; - foreach ( $decimalColors as &$color ) { - if ( strpos( $color, '%' ) !== false ) { - $color = str_replace( '%', '', $color ) * 2.55; - } - } - - - } elseif ( substr( $color, 0, 3 ) === 'hsl' ) { - $variants['hsl'] = $color; - preg_match_all( '#([0-9.%]+)#', $color, $matches ); - - $colors = $matches[1]; - $colors[0] /= 360; - $colors[1] = str_replace( '%', '', $colors[1] ) / 100; - $colors[2] = str_replace( '%', '', $colors[2] ) / 100; - - $decimalColors = self::_HSLtoRGB( $colors ); - if ( isset( $colors[3] ) ) { - $decimalColors[] = $colors[3]; - } - } - - if ( isset( $decimalColors[3] ) ) { - $alpha = $decimalColors[3]; - unset( $decimalColors[3] ); - } else { - $alpha = null; - } - foreach ( $variants as $type => &$variant ) { - if ( isset( $variant ) ) continue; - - switch ( $type ) { - case 'hex': - $variant = '#'; - foreach ( $decimalColors as &$color ) { - $variant .= str_pad( dechex( $color ), 2, "0", STR_PAD_LEFT ); - } - $variant .= isset( $alpha ) ? ' (alpha omitted)' : ''; - break; - case 'rgb': - $rgb = $decimalColors; - if ( isset( $alpha ) ) { - $rgb[] = $alpha; - $a = 'a'; - } else { - $a = ''; - } - $variant = "rgb{$a}( " . implode( ', ', $rgb ) . " )"; - break; - case 'hsl': - $rgb = self::_RGBtoHSL( $decimalColors ); - if ( $rgb === null ) { - unset( $variants[ $type ] ); - break; - } - if ( isset( $alpha ) ) { - $rgb[] = $alpha; - $a = 'a'; - } else { - $a = ''; - } - - $variant = "hsl{$a}( " . implode( ', ', $rgb ) . " )"; - break; - case 'name': - // [!] name in initial variants array must go after hex - if ( ( $key = array_search( $variants['hex'], self::$_css3Named, true ) ) !== false ) { - $variant = $key; - } else { - unset( $variants[ $type ] ); - } - break; - } - - } - - return $variants; - } - - - private static function _HSLtoRGB( array $hsl ) - { - list( $h, $s, $l ) = $hsl; - $m2 = ( $l <= 0.5 ) ? $l * ( $s + 1 ) : $l + $s - $l * $s; - $m1 = $l * 2 - $m2; - return array( - round( self::_hue2rgb( $m1, $m2, $h + 0.33333 ) * 255 ), - round( self::_hue2rgb( $m1, $m2, $h ) * 255 ), - round( self::_hue2rgb( $m1, $m2, $h - 0.33333 ) * 255 ), - ); - } - - - /** - * Helper function for _color_hsl2rgb(). - */ - private static function _hue2rgb( $m1, $m2, $h ) - { - $h = ( $h < 0 ) ? $h + 1 : ( ( $h > 1 ) ? $h - 1 : $h ); - if ( $h * 6 < 1 ) return $m1 + ( $m2 - $m1 ) * $h * 6; - if ( $h * 2 < 1 ) return $m2; - if ( $h * 3 < 2 ) return $m1 + ( $m2 - $m1 ) * ( 0.66666 - $h ) * 6; - return $m1; - } - - - private static function _RGBtoHSL( array $rgb ) - { - list( $clrR, $clrG, $clrB ) = $rgb; - - $clrMin = min( $clrR, $clrG, $clrB ); - $clrMax = max( $clrR, $clrG, $clrB ); - $deltaMax = $clrMax - $clrMin; - - $L = ( $clrMax + $clrMin ) / 510; - - if ( 0 == $deltaMax ) { - $H = 0; - $S = 0; - } else { - if ( 0.5 > $L ) { - $S = $deltaMax / ( $clrMax + $clrMin ); - } else { - $S = $deltaMax / ( 510 - $clrMax - $clrMin ); - } - - if ( $clrMax == $clrR ) { - $H = ( $clrG - $clrB ) / ( 6.0 * $deltaMax ); - } else if ( $clrMax == $clrG ) { - $H = 1 / 3 + ( $clrB - $clrR ) / ( 6.0 * $deltaMax ); - } else { - $H = 2 / 3 + ( $clrR - $clrG ) / ( 6.0 * $deltaMax ); - } - - if ( 0 > $H ) $H += 1; - if ( 1 < $H ) $H -= 1; - } - return array( - round( $H * 360 ), - round( $S * 100 ) . '%', - round( $L * 100 ) . '%' - ); - - } -} - -/* ************* - * TEST DATA - * -dd(array( -'hsl(0, 100%,50%)', -'hsl(30, 100%,50%)', -'hsl(60, 100%,50%)', -'hsl(90, 100%,50%)', -'hsl(120,100%,50%)', -'hsl(150,100%,50%)', -'hsl(180,100%,50%)', -'hsl(210,100%,50%)', -'hsl(240,100%,50%)', -'hsl(270,100%,50%)', -'hsl(300,100%,50%)', -'hsl(330,100%,50%)', -'hsl(360,100%,50%)', -'hsl(120,100%,25%)', -'hsl(120,100%,50%)', -'hsl(120,100%,75%)', -'hsl(120,100%,50%)', -'hsl(120, 67%,50%)', -'hsl(120, 33%,50%)', -'hsl(120, 0%,50%)', -'hsl(120, 60%,70%)', -'#f03', -'#F03', -'#ff0033', -'#FF0033', -'rgb(255,0,51)', -'rgb(255, 0, 51)', -'rgb(100%,0%,20%)', -'rgb(100%, 0%, 20%)', -'hsla(240,100%,50%,0.05)', -'hsla(240,100%,50%, 0.4)', -'hsla(240,100%,50%, 0.7)', -'hsla(240,100%,50%, 1)', -'rgba(255,0,0,0.1)', -'rgba(255,0,0,0.4)', -'rgba(255,0,0,0.7)', -'rgba(255,0,0, 1)', -'black', -'silver', -'gray', -'white', -'maroon', -'red', -'purple', -'fuchsia', -'green', -'lime', -'olive', -'yellow', -'navy', -'blue', -'teal', -'aqua', -'orange', -'aliceblue', -'antiquewhite', -'aquamarine', -'azure', -'beige', -'bisque', -'blanchedalmond', -'blueviolet', -'brown', -'burlywood', -'cadetblue', -'chartreuse', -'chocolate', -'coral', -'cornflowerblue', -'cornsilk', -'crimson', -'darkblue', -'darkcyan', -'darkgoldenrod', -'darkgray', -'darkgreen', -'darkgrey', -'darkkhaki', -'darkmagenta', -'darkolivegreen', -'darkorange', -'darkorchid', -'darkred', -'darksalmon', -'darkseagreen', -'darkslateblue', -'darkslategray', -'darkslategrey', -'darkturquoise', -'darkviolet', -'deeppink', -'deepskyblue', -'dimgray', -'dimgrey', -'dodgerblue', -'firebrick', -'floralwhite', -'forestgreen', -'gainsboro', -'ghostwhite', -'gold', -'goldenrod', -'greenyellow', -'grey', -'honeydew', -'hotpink', -'indianred', -'indigo', -'ivory', -'khaki', -'lavender', -'lavenderblush', -'lawngreen', -'lemonchiffon', -'lightblue', -'lightcoral', -'lightcyan', -'lightgoldenrodyellow', -'lightgray', -'lightgreen', -'lightgrey', -'lightpink', -'lightsalmon', -'lightseagreen', -'lightskyblue', -'lightslategray', -'lightslategrey', -'lightsteelblue', -'lightyellow', -'limegreen', -'linen', -'mediumaquamarine', -'mediumblue', -'mediumorchid', -'mediumpurple', -'mediumseagreen', -'mediumslateblue', -'mediumspringgreen', -'mediumturquoise', -'mediumvioletred', -'midnightblue', -'mintcream', -'mistyrose', -'moccasin', -'navajowhite', -'oldlace', -'olivedrab', -));*/ \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/fspath.php b/system/ThirdParty/Kint/parsers/custom/fspath.php deleted file mode 100755 index 0610391cdffe..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/fspath.php +++ /dev/null @@ -1,69 +0,0 @@ - 2048 - || preg_match( '[[:?<>"*|]]', $variable ) - || !@is_readable( $variable ) # f@#! PHP and its random warnings - ) return false; - - try { - $fileInfo = new SplFileInfo( $variable ); - $flags = array(); - $perms = $fileInfo->getPerms(); - - if ( ( $perms & 0xC000 ) === 0xC000 ) { - $type = 'File socket'; - $flags[] = 's'; - } elseif ( ( $perms & 0xA000 ) === 0xA000 ) { - $type = 'File symlink'; - $flags[] = 'l'; - } elseif ( ( $perms & 0x8000 ) === 0x8000 ) { - $type = 'File'; - $flags[] = '-'; - } elseif ( ( $perms & 0x6000 ) === 0x6000 ) { - $type = 'Block special file'; - $flags[] = 'b'; - } elseif ( ( $perms & 0x4000 ) === 0x4000 ) { - $type = 'Directory'; - $flags[] = 'd'; - } elseif ( ( $perms & 0x2000 ) === 0x2000 ) { - $type = 'Character special file'; - $flags[] = 'c'; - } elseif ( ( $perms & 0x1000 ) === 0x1000 ) { - $type = 'FIFO pipe file'; - $flags[] = 'p'; - } else { - $type = 'Unknown file'; - $flags[] = 'u'; - } - - // owner - $flags[] = ( ( $perms & 0x0100 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0080 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0040 ) ? ( ( $perms & 0x0800 ) ? 's' : 'x' ) : ( ( $perms & 0x0800 ) ? 'S' : '-' ) ); - - // group - $flags[] = ( ( $perms & 0x0020 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0010 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0008 ) ? ( ( $perms & 0x0400 ) ? 's' : 'x' ) : ( ( $perms & 0x0400 ) ? 'S' : '-' ) ); - - // world - $flags[] = ( ( $perms & 0x0004 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0002 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0001 ) ? ( ( $perms & 0x0200 ) ? 't' : 'x' ) : ( ( $perms & 0x0200 ) ? 'T' : '-' ) ); - - $this->type = $type; - $this->size = sprintf( '%.2fK', $fileInfo->getSize() / 1024 ); - $this->value = implode( $flags ); - - } catch ( Exception $e ) { - return false; - } - - } -} diff --git a/system/ThirdParty/Kint/parsers/custom/json.php b/system/ThirdParty/Kint/parsers/custom/json.php deleted file mode 100755 index f752a214cea8..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/json.php +++ /dev/null @@ -1,19 +0,0 @@ -value = kintParser::factory( $val )->extendedValue; - $this->type = 'JSON'; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/microtime.php b/system/ThirdParty/Kint/parsers/custom/microtime.php deleted file mode 100755 index 2c5fe68fa94e..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/microtime.php +++ /dev/null @@ -1,60 +0,0 @@ -value = @date( 'Y-m-d H:i:s', $sec ) . '.' . substr( $usec, 2, 4 ); - - $numberOfCalls = count( self::$_times ); - if ( $numberOfCalls > 0 ) { # meh, faster than count($times) > 1 - $lap = $time - end( self::$_times ); - self::$_laps[] = $lap; - - $this->value .= "\nSINCE LAST CALL: " . round( $lap, 4 ) . 's.'; - if ( $numberOfCalls > 1 ) { - $this->value .= "\nSINCE START: " . round( $time - self::$_times[0], 4 ) . 's.'; - $this->value .= "\nAVERAGE DURATION: " - . round( array_sum( self::$_laps ) / $numberOfCalls, 4 ) . 's.'; - } - } - - $unit = array( 'B', 'KB', 'MB', 'GB', 'TB' ); - if ( KINT_PHP53 ) { - $this->value .= "\nMEMORY USAGE: " . $size . " bytes (" - . round( $size / pow( 1024, ( $i = floor( log( $size, 1024 ) ) ) ), 3 ) . ' ' . $unit[ $i ] . ")"; - } - - self::$_times[] = $time; - $this->type = 'Stats'; - } - - /* - function test() { - d( 'start', microtime() ); - for ( $i = 0; $i < 10; $i++ ) { - d( - $duration = mt_rand( 0, 200000 ), // the reported duration will be larger because of Kint overhead - usleep( $duration ), - microtime() - ); - } - dd( ); - } - */ -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/objectiterateable.php b/system/ThirdParty/Kint/parsers/custom/objectiterateable.php deleted file mode 100755 index bbc0de070c57..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/objectiterateable.php +++ /dev/null @@ -1,22 +0,0 @@ -value = kintParser::factory( $arrayCopy )->extendedValue; - $this->type = 'Iterator contents'; - $this->size = count( $arrayCopy ); - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/splobjectstorage.php b/system/ThirdParty/Kint/parsers/custom/splobjectstorage.php deleted file mode 100755 index 69da91fd9eed..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/splobjectstorage.php +++ /dev/null @@ -1,24 +0,0 @@ -count(); - if ( $count === 0 ) return false; - - $variable->rewind(); - while ( $variable->valid() ) { - $current = $variable->current(); - $this->value[] = kintParser::factory( $current ); - $variable->next(); - } - - $this->type = 'Storage contents'; - $this->size = $count; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/timestamp.php b/system/ThirdParty/Kint/parsers/custom/timestamp.php deleted file mode 100755 index d1a3ce3232bd..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/timestamp.php +++ /dev/null @@ -1,29 +0,0 @@ -type = 'timestamp'; - # avoid dreaded "Timezone must be set" error - $this->value = @date( 'Y-m-d H:i:s', $var ); - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/parsers/custom/xml.php b/system/ThirdParty/Kint/parsers/custom/xml.php deleted file mode 100755 index 63283a0fa7c9..000000000000 --- a/system/ThirdParty/Kint/parsers/custom/xml.php +++ /dev/null @@ -1,25 +0,0 @@ -value = kintParser::factory( $xml )->extendedValue; - $this->type = 'XML'; - } -} diff --git a/system/ThirdParty/Kint/parsers/objects/closure.php b/system/ThirdParty/Kint/parsers/objects/closure.php deleted file mode 100755 index 25fb852b1c5c..000000000000 --- a/system/ThirdParty/Kint/parsers/objects/closure.php +++ /dev/null @@ -1,33 +0,0 @@ -name = 'Closure'; - $reflection = new ReflectionFunction( $variable ); - $ret = array( - 'Parameters' => array() - ); - if ( $val = $reflection->getParameters() ) { - foreach ( $val as $parameter ) { - // todo http://php.net/manual/en/class.reflectionparameter.php - $ret['Parameters'][] = $parameter->name; - } - - } - if ( $val = $reflection->getStaticVariables() ) { - $ret['Uses'] = $val; - } - if ( method_exists($reflection, 'getClousureThis') && $val = $reflection->getClosureThis() ) { - $ret['Uses']['$this'] = $val; - } - if ( $val = $reflection->getFileName() ) { - $this->value = Kint::shortenPath( $val ) . ':' . $reflection->getStartLine(); - } - - return $ret; - } -} diff --git a/system/ThirdParty/Kint/parsers/objects/smarty.php b/system/ThirdParty/Kint/parsers/objects/smarty.php deleted file mode 100755 index 52b07505efca..000000000000 --- a/system/ThirdParty/Kint/parsers/objects/smarty.php +++ /dev/null @@ -1,35 +0,0 @@ -name = 'object Smarty (v' . substr( Smarty::SMARTY_VERSION, 7 ) . ')'; # trim 'Smarty-' - - $assigned = $globalAssigns = array(); - foreach ( $variable->tpl_vars as $name => $var ) { - $assigned[ $name ] = $var->value; - } - foreach ( Smarty::$global_tpl_vars as $name => $var ) { - if ( $name === 'SCRIPT_NAME' ) continue; - - $globalAssigns[ $name ] = $var->value; - } - - return array( - 'Assigned' => $assigned, - 'Assigned globally' => $globalAssigns, - 'Configuration' => array( - 'Compiled files stored in' => isset($variable->compile_dir) - ? $variable->compile_dir - : $variable->getCompileDir(), - ) - ); - - } -} diff --git a/system/ThirdParty/Kint/parsers/objects/splfileinfo.php b/system/ThirdParty/Kint/parsers/objects/splfileinfo.php deleted file mode 100755 index 964dd61509a6..000000000000 --- a/system/ThirdParty/Kint/parsers/objects/splfileinfo.php +++ /dev/null @@ -1,70 +0,0 @@ -name = 'SplFileInfo'; - $this->value = $variable->getBasename(); - - - $flags = array(); - $perms = $variable->getPerms(); - - if ( ( $perms & 0xC000 ) === 0xC000 ) { - $type = 'File socket'; - $flags[] = 's'; - } elseif ( ( $perms & 0xA000 ) === 0xA000 ) { - $type = 'File symlink'; - $flags[] = 'l'; - } elseif ( ( $perms & 0x8000 ) === 0x8000 ) { - $type = 'File'; - $flags[] = '-'; - } elseif ( ( $perms & 0x6000 ) === 0x6000 ) { - $type = 'Block special file'; - $flags[] = 'b'; - } elseif ( ( $perms & 0x4000 ) === 0x4000 ) { - $type = 'Directory'; - $flags[] = 'd'; - } elseif ( ( $perms & 0x2000 ) === 0x2000 ) { - $type = 'Character special file'; - $flags[] = 'c'; - } elseif ( ( $perms & 0x1000 ) === 0x1000 ) { - $type = 'FIFO pipe file'; - $flags[] = 'p'; - } else { - $type = 'Unknown file'; - $flags[] = 'u'; - } - - // owner - $flags[] = ( ( $perms & 0x0100 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0080 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0040 ) ? ( ( $perms & 0x0800 ) ? 's' : 'x' ) : ( ( $perms & 0x0800 ) ? 'S' : '-' ) ); - - // group - $flags[] = ( ( $perms & 0x0020 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0010 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0008 ) ? ( ( $perms & 0x0400 ) ? 's' : 'x' ) : ( ( $perms & 0x0400 ) ? 'S' : '-' ) ); - - // world - $flags[] = ( ( $perms & 0x0004 ) ? 'r' : '-' ); - $flags[] = ( ( $perms & 0x0002 ) ? 'w' : '-' ); - $flags[] = ( ( $perms & 0x0001 ) ? ( ( $perms & 0x0200 ) ? 't' : 'x' ) : ( ( $perms & 0x0200 ) ? 'T' : '-' ) ); - - $size = sprintf( '%.2fK', $variable->getSize() / 1024 ); - $flags = implode( $flags ); - $path = $variable->getRealPath(); - - return array( - 'File information' => array( - 'Full path' => $path, - 'Type' => $type, - 'Size' => $size, - 'Flags' => $flags - ) - ); - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/base.js b/system/ThirdParty/Kint/view/base.js deleted file mode 100755 index b8087a7635f4..000000000000 --- a/system/ThirdParty/Kint/view/base.js +++ /dev/null @@ -1,437 +0,0 @@ -/** - java -jar compiler.jar --js $FileName$ --js_output_file compiled/kint.js --compilation_level ADVANCED_OPTIMIZATIONS --output_wrapper "(function(){%output%})()" - */ - -if ( typeof kintInitialized === 'undefined' ) { - kintInitialized = 1; - var kint = { - visiblePluses : [], // all visible toggle carets - currentPlus : -1, // currently selected caret - - selectText : function( element ) { - var selection = window.getSelection(), - range = document.createRange(); - - range.selectNodeContents(element); - selection.removeAllRanges(); - selection.addRange(range); - }, - - each : function( selector, callback ) { - Array.prototype.slice.call(document.querySelectorAll(selector), 0).forEach(callback) - }, - - hasClass : function( target, className ) { - if ( !target.classList ) return false; - - if ( typeof className === 'undefined' ) { - className = 'kint-show'; - } - return target.classList.contains(className); - }, - - addClass : function( target, className ) { - if ( typeof className === 'undefined' ) { - className = 'kint-show'; - } - target.classList.add(className); - }, - - removeClass : function( target, className ) { - if ( typeof className === 'undefined' ) { - className = 'kint-show'; - } - target.classList.remove(className); - return target; - }, - - next : function( element ) { - do { - element = element.nextElementSibling; - } while ( element.nodeName.toLowerCase() !== 'dd' ); - - return element; - }, - - toggle : function( element, hide ) { - var parent = kint.next(element); - - if ( typeof hide === 'undefined' ) { - hide = kint.hasClass(element); - } - - if ( hide ) { - kint.removeClass(element); - } else { - kint.addClass(element); - } - - if ( parent.childNodes.length === 1 ) { - parent = parent.childNodes[0].childNodes[0]; // reuse variable cause I can - - // parent is checked in case of empty
 when array("\n") is dumped
-				if ( parent && kint.hasClass(parent, 'kint-parent') ) {
-					kint.toggle(parent, hide)
-				}
-			}
-		},
-
-		toggleChildren : function( element, hide ) {
-			var parent = kint.next(element)
-				, nodes = parent.getElementsByClassName('kint-parent')
-				, i = nodes.length;
-
-			if ( typeof hide === 'undefined' ) {
-				hide = kint.hasClass(element);
-			}
-
-			while ( i-- ) {
-				kint.toggle(nodes[i], hide);
-			}
-			kint.toggle(element, hide);
-		},
-
-		toggleAll : function( caret ) {
-			var elements = document.getElementsByClassName('kint-parent')
-				, i = elements.length
-				, visible = kint.hasClass(caret.parentNode);
-
-			while ( i-- ) {
-				kint.toggle(elements[i], visible);
-			}
-		},
-
-		switchTab : function( target ) {
-			var lis, el = target, index = 0;
-
-			target.parentNode.getElementsByClassName('kint-active-tab')[0].className = '';
-			target.className = 'kint-active-tab';
-
-			// take the index of clicked title tab and make the same n-th content tab visible
-			while ( el = el.previousSibling ) el.nodeType === 1 && index++;
-			lis = target.parentNode.nextSibling.childNodes;
-			for ( var i = 0; i < lis.length; i++ ) {
-				if ( i === index ) {
-					lis[i].style.display = 'block';
-
-					if ( lis[i].childNodes.length === 1 ) {
-						el = lis[i].childNodes[0].childNodes[0];
-
-						if ( kint.hasClass(el, 'kint-parent') ) {
-							kint.toggle(el, false)
-						}
-					}
-				} else {
-					lis[i].style.display = 'none';
-				}
-			}
-		},
-
-		isSibling : function( el ) {
-			for ( ; ; ) {
-				el = el.parentNode;
-				if ( !el || kint.hasClass(el, 'kint') ) break;
-			}
-
-			return !!el;
-		},
-
-		fetchVisiblePluses : function() {
-			kint.visiblePluses = [];
-			kint.each('.kint nav, .kint-tabs>li:not(.kint-active-tab)', function( el ) {
-				if ( el.offsetWidth !== 0 || el.offsetHeight !== 0 ) {
-					kint.visiblePluses.push(el)
-				}
-			});
-		},
-
-		openInNewWindow : function( kintContainer ) {
-			var newWindow;
-
-			if ( newWindow = window.open() ) {
-				newWindow.document.open();
-				newWindow.document.write(
-					''
-					+ ''
-					+ 'Kint (' + new Date().toISOString() + ')'
-					+ ''
-					+ document.getElementsByClassName('-kint-js')[0].outerHTML
-					+ document.getElementsByClassName('-kint-css')[0].outerHTML
-					+ ''
-					+ ''
-					+ ''
-					+ '
' - + kintContainer.parentNode.outerHTML - + '
' - ); - newWindow.document.close(); - } - }, - - sortTable : function( table, column ) { - var tbody = table.tBodies[0]; - - var format = function( s ) { - var n = column === 1 ? s.replace(/^#/, '') : s; - if ( isNaN(n) ) { - return s.trim().toLocaleLowerCase(); - } else { - n = parseFloat(n); - return isNaN(n) ? s.trim() : n; - } - }; - - - [].slice.call(table.tBodies[0].rows) - .sort(function( a, b ) { - a = format(a.cells[column].textContent); - b = format(b.cells[column].textContent); - if ( a < b ) return -1; - if ( a > b ) return 1; - - return 0; - }) - .forEach(function( el ) { - tbody.appendChild(el); - }); - }, - - keyCallBacks : { - cleanup : function( i ) { - var focusedClass = 'kint-focused'; - var prevElement = document.querySelector('.' + focusedClass); - prevElement && kint.removeClass(prevElement, focusedClass); - - if ( i !== -1 ) { - var el = kint.visiblePluses[i]; - kint.addClass(el, focusedClass); - - - var offsetTop = function( el ) { - return el.offsetTop + ( el.offsetParent ? offsetTop(el.offsetParent) : 0 ); - }; - - var top = offsetTop(el) - (window.innerHeight / 2 ); - window.scrollTo(0, top); - } - - kint.currentPlus = i; - }, - - moveCursor : function( up, i ) { - // todo make the first VISIBLE plus active - if ( up ) { - if ( --i < 0 ) { - i = kint.visiblePluses.length - 1; - } - } else { - if ( ++i >= kint.visiblePluses.length ) { - i = 0; - } - } - - kint.keyCallBacks.cleanup(i); - return false; - } - } - }; - - window.addEventListener("click", function( e ) { - var target = e.target - , nodeName = target.nodeName.toLowerCase(); - - if ( !kint.isSibling(target) ) return; - - // auto-select name of variable - if ( nodeName === 'dfn' ) { - kint.selectText(target); - target = target.parentNode; - } else if ( nodeName === 'var' ) { // stupid workaround for misc elements - target = target.parentNode; // to not stop event from further propagating - nodeName = target.nodeName.toLowerCase() - } else if ( nodeName === 'th' ) { - if ( !e.ctrlKey ) { - kint.sortTable(target.parentNode.parentNode.parentNode, target.cellIndex) - } - return false; - } - - // switch tabs - if ( nodeName === 'li' && target.parentNode.className === 'kint-tabs' ) { - if ( target.className !== 'kint-active-tab' ) { - kint.switchTab(target); - if ( kint.currentPlus !== -1 ) kint.fetchVisiblePluses(); - } - return false; - } - - // handle clicks on the navigation caret - if ( nodeName === 'nav' ) { - // special case for nav in footer - if ( target.parentNode.nodeName.toLowerCase() === 'footer' ) { - target = target.parentNode; - if ( kint.hasClass(target) ) { - kint.removeClass(target) - } else { - kint.addClass(target) - } - } else { - // ensure doubleclick has different behaviour, see below - setTimeout(function() { - var timer = parseInt(target.kintTimer, 10); - if ( timer > 0 ) { - target.kintTimer--; - } else { - kint.toggleChildren(target.parentNode); //
- if ( kint.currentPlus !== -1 ) kint.fetchVisiblePluses(); - } - }, 300); - } - - e.stopPropagation(); - return false; - } else if ( kint.hasClass(target, 'kint-parent') ) { - kint.toggle(target); - if ( kint.currentPlus !== -1 ) kint.fetchVisiblePluses(); - return false; - } else if ( kint.hasClass(target, 'kint-ide-link') ) { - e.preventDefault(); - var ajax = new XMLHttpRequest(); // add ajax call to contact editor but prevent link default action - ajax.open('GET', target.href); - ajax.send(null); - return false; - } else if ( kint.hasClass(target, 'kint-popup-trigger') ) { - var kintContainer = target.parentNode; - if ( kintContainer.nodeName.toLowerCase() === 'footer' ) { - kintContainer = kintContainer.previousSibling; - } else { - while ( kintContainer && !kint.hasClass(kintContainer, 'kint-parent') ) { - kintContainer = kintContainer.parentNode; - } - } - - kint.openInNewWindow(kintContainer); - } else if ( nodeName === 'pre' && e.detail === 3 ) { // triple click pre to select it all - kint.selectText(target); - } - }, false); - - window.addEventListener("dblclick", function( e ) { - var target = e.target; - if ( !kint.isSibling(target) ) return; - - if ( target.nodeName.toLowerCase() === 'nav' ) { - target.kintTimer = 2; - kint.toggleAll(target); - if ( kint.currentPlus !== -1 ) kint.fetchVisiblePluses(); - e.stopPropagation(); - } - }, false); - - // keyboard navigation - window.onkeydown = function( e ) { // direct assignment is used to have priority over ex FAYT - - // do nothing if alt/ctrl key is pressed or if we're actually typing somewhere - if ( e.target !== document.body || e.altKey || e.ctrlKey ) return; - - var keyCode = e.keyCode - , shiftKey = e.shiftKey - , i = kint.currentPlus; - - - if ( keyCode === 68 ) { // 'd' : toggles navigation on/off - if ( i === -1 ) { - kint.fetchVisiblePluses(); - return kint.keyCallBacks.moveCursor(false, i); - } else { - kint.keyCallBacks.cleanup(-1); - return false; - } - } else { - if ( i === -1 ) return; - - if ( keyCode === 9 ) { // TAB : moves up/down depending on shift key - return kint.keyCallBacks.moveCursor(shiftKey, i); - } else if ( keyCode === 38 ) { // ARROW UP : moves up - return kint.keyCallBacks.moveCursor(true, i); - } else if ( keyCode === 40 ) { // ARROW DOWN : down - return kint.keyCallBacks.moveCursor(false, i); - } - } - - - var kintNode = kint.visiblePluses[i]; - if ( kintNode.nodeName.toLowerCase() === 'li' ) { // we're on a trace tab - if ( keyCode === 32 || keyCode === 13 ) { // SPACE/ENTER - kint.switchTab(kintNode); - kint.fetchVisiblePluses(); - return kint.keyCallBacks.moveCursor(true, i); - } else if ( keyCode === 39 ) { // arrows - return kint.keyCallBacks.moveCursor(false, i); - } else if ( keyCode === 37 ) { - return kint.keyCallBacks.moveCursor(true, i); - } - } - - kintNode = kintNode.parentNode; // simple dump - if ( keyCode === 32 || keyCode === 13 ) { // SPACE/ENTER : toggles - kint.toggle(kintNode); - kint.fetchVisiblePluses(); - return false; - } else if ( keyCode === 39 || keyCode === 37 ) { // ARROW LEFT/RIGHT : respectively hides/shows and traverses - var visible = kint.hasClass(kintNode); - var hide = keyCode === 37; - - if ( visible ) { - kint.toggleChildren(kintNode, hide); // expand/collapse all children if immediate ones are showing - } else { - if ( hide ) { // LEFT - // traverse to parent and THEN hide - do {kintNode = kintNode.parentNode} while ( kintNode && kintNode.nodeName.toLowerCase() !== 'dd' ); - - if ( kintNode ) { - kintNode = kintNode.previousElementSibling; - - i = -1; - var parentPlus = kintNode.querySelector('nav'); - while ( parentPlus !== kint.visiblePluses[++i] ) {} - kint.keyCallBacks.cleanup(i) - } else { // we are at root - kintNode = kint.visiblePluses[i].parentNode; - } - } - kint.toggle(kintNode, hide); - } - kint.fetchVisiblePluses(); - return false; - } - }; - - window.addEventListener("load", function( e ) { // colorize microtime results relative to others - var elements = Array.prototype.slice.call(document.querySelectorAll('.kint-microtime'), 0); - elements.forEach(function( el ) { - var value = parseFloat(el.innerHTML) - , min = Infinity - , max = -Infinity - , ratio; - - elements.forEach(function( el ) { - var val = parseFloat(el.innerHTML); - - if ( min > val ) min = val; - if ( max < val ) max = val; - }); - - ratio = 1 - (value - min) / (max - min); - - el.style.background = 'hsl(' + Math.round(ratio * 120) + ',60%,70%)'; - }); - }); -} - -// debug purposes only, removed in minified source -function clg( i ) { - if ( !window.console )return; - var l = arguments.length, o = 0; - while ( o < l )console.log(arguments[o++]) -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/base.less b/system/ThirdParty/Kint/view/base.less deleted file mode 100755 index 24c68f766fb6..000000000000 --- a/system/ThirdParty/Kint/view/base.less +++ /dev/null @@ -1,413 +0,0 @@ -// -// VARIABLES FOR THEMES TO OVERRIDE -// -------------------------------------------------- - -@spacing : 4; - -// caret taken from solarized -@caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg=="); - -// -// SET UP HELPER VARIABLES -// -------------------------------------------------- - -@border: 1px solid @border-color; - -.selection() { - background : @border-color-hover; - color : @text-color; -} - -// -// BASE STYLES -// -------------------------------------------------- - -.kint::selection { .selection() } - -.kint::-moz-selection { .selection() } - -.kint::-webkit-selection { .selection() } - -.kint, -.kint::before, -.kint::after, -.kint *, -.kint *::before, -.kint *::after { - box-sizing : border-box; - border-radius : 0; - color : @text-color; - float : none !important; - font-family : Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; - line-height : 15px; - margin : 0; - padding : 0; - text-align : left; -} - -.kint { - font-size : 13px; - margin : @spacing * 2px 0; - overflow-x : auto; - white-space : nowrap; - - dt { - background : @main-background; - border : @border; - color : @text-color; - display : block; - font-weight : bold; - list-style : none outside none; - overflow : auto; - padding : @spacing * 1px; - - &:hover { - border-color : @border-color-hover; - } - - > var { - margin-right : 5px; - } - } - - > dl dl { - padding : 0 0 0 @spacing * 3px; - } - - // - // DROPDOWN CARET - // -------------------------------------------------- - - nav { - background : @caret-image no-repeat scroll 0 0 transparent; - cursor : pointer; - display : inline-block; - height : 15px; - width : 15px; - margin-right : 3px; - vertical-align : middle; - } - - dt.kint-parent:hover nav { - background-position : 0 -15px; - } - - dt.kint-parent.kint-show:hover > nav { - background-position : 0 -45px; - } - - dt.kint-show > nav { - background-position : 0 -30px; - } - - dt.kint-parent + dd { - display : none; - border-left : 1px dashed @border-color; - } - - dt.kint-parent.kint-show + dd { - display : block; - } - - // - // INDIVIDUAL ITEMS - // -------------------------------------------------- - - var, - var a { - color : @variable-type-color; - font-style : normal; - } - - dt:hover var, - dt:hover var a { - color : @variable-type-color-hover; - } - - dfn { - font-style : normal; - font-family : monospace; - color : @variable-name-color; - } - - pre { - color : @text-color; - margin : 0 0 0 @spacing * 3px; - padding : 5px; - overflow-y : hidden; - border-top : 0; - border : @border; - background : @main-background; - display : block; - word-break : normal; - } - - .kint-popup-trigger { - float : right !important; - cursor : pointer; - color : @border-color-hover; - - &:hover { - color : @border-color; - } - } - - dt.kint-parent > .kint-popup-trigger { - font-size : 13px; - } - - footer { - padding : 0 3px 3px; - font-size : 9px; - - > .kint-popup-trigger { - font-size : 12px; - } - - nav { - background-size : 10px; - height : 10px; - width : 10px; - - &:hover { - background-position : 0 -10px; - } - } - - > ol { - display : none; - margin-left : 32px; - } - - &.kint-show { - > ol { - display : block; - } - - nav { - background-position : 0 -20px; - - &:hover { - background-position : 0 -30px; - } - } - } - } - - a { - color : @text-color; - text-shadow : none; - - &:hover { - color : @variable-name-color; - border-bottom : 1px dotted @variable-name-color; - } - } - - // - // TABS - // -------------------------------------------------- - - ul { - list-style : none; - padding-left : @spacing * 3px; - - &:not(.kint-tabs) { - li { - border-left : 1px dashed @border-color; - - > dl { - border-left : none; - } - } - } - - &.kint-tabs { - margin : 0 0 0 @spacing * 3px; - padding-left : 0; - background : @main-background; - border : @border; - border-top : 0; - - li { - background : @secondary-background; - border : @border; - cursor : pointer; - display : inline-block; - height : @spacing * 6px; - margin : round(@spacing / 2) * 1px; - padding : 0 2px + round(@spacing * 2.5px); - vertical-align : top; - - &:hover, - &.kint-active-tab:hover { - border-color : @border-color-hover; - color : @variable-type-color-hover; - } - - &.kint-active-tab { - background : @main-background; - border-top : 0; - margin-top : -1px; - height : 27px; - line-height : 24px; - } - - &:not(.kint-active-tab) { - line-height : @spacing * 5px; - } - } - - li + li { - margin-left : 0 - } - } - - &:not(.kint-tabs) > li:not(:first-child) { - display : none; - } - } - - dt:hover + dd > ul > li.kint-active-tab { - border-color : @border-color-hover; - color : @variable-type-color-hover; - } -} - -// -// REPORT -// -------------------------------------------------- - -.kint-report { - border-collapse : collapse; - empty-cells : show; - border-spacing : 0; - - * { - font-size : 12px; - } - - dt { - background : none; - padding : (@spacing/2) * 1px; - - .kint-parent { - min-width : 100%; - overflow : hidden; - text-overflow : ellipsis; - white-space : nowrap; - } - } - - td, - th { - border : @border; - padding : (@spacing/2) * 1px; - vertical-align : center; - } - - th { - cursor : alias; - } - - td:first-child, - th { - font-weight : bold; - background : @secondary-background; - color : @variable-name-color; - } - - td { - background : @main-background; - white-space : pre; - - > dl { - padding : 0; - } - } - - pre { - border-top : 0; - border-right : 0; - } - - th:first-child { - background : none; - border : 0; - } - - td.kint-empty { - background : #d33682 !important; - } - - tr:hover { - > td { - box-shadow : 0 0 1px 0 @border-color-hover inset; - } - - var { - color : @variable-type-color-hover; - } - } - ul.kint-tabs li.kint-active-tab { - height : 20px; - line-height : 17px; - } -} - -// -// TRACE -// -------------------------------------------------- -.kint-trace { - .kint-source { - line-height : round(@spacing * 3.5) * 1px; - - span { - padding-right : 1px; - border-right : 3px inset @variable-type-color; - } - - .kint-highlight { - background : @secondary-background; - } - } - - .kint-parent { - > b { - min-width : 18px; - display : inline-block; - text-align : right; - color : @variable-name-color; - } - - > var { - > a { - color : @variable-type-color; - } - } - } -} - -// -// MISC -// -------------------------------------------------- - -// keyboard navigation caret -.kint-focused { - .keyboard-caret -} - -.kint-microtime, -.kint-color-preview { - box-shadow : 0 0 2px 0 #b6cedb; - height : 16px; - text-align : center; - text-shadow : -1px 0 #839496, 0 1px #839496, 1px 0 #839496, 0 -1px #839496; - width : 230px; - color : #fdf6e3; -} - -// mini trace -.kint footer li { - color : #ddd; -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/compiled/aante-light.css b/system/ThirdParty/Kint/view/compiled/aante-light.css deleted file mode 100755 index b22faddc346a..000000000000 --- a/system/ThirdParty/Kint/view/compiled/aante-light.css +++ /dev/null @@ -1,397 +0,0 @@ -/** - * @author Ante Aljinovic https://github.com/aljinovic - */ -.kint::selection { - background: #aaaaaa; - color: #1d1e1e; -} -.kint::-moz-selection { - background: #aaaaaa; - color: #1d1e1e; -} -.kint::-webkit-selection { - background: #aaaaaa; - color: #1d1e1e; -} -.kint, -.kint::before, -.kint::after, -.kint *, -.kint *::before, -.kint *::after { - box-sizing: border-box; - border-radius: 0; - color: #1d1e1e; - float: none !important; - font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; - line-height: 15px; - margin: 0; - padding: 0; - text-align: left; -} -.kint { - font-size: 13px; - margin: 8px 0; - overflow-x: auto; - white-space: nowrap; -} -.kint dt { - background: #f8f8f8; - border: 1px solid #d7d7d7; - color: #1d1e1e; - display: block; - font-weight: bold; - list-style: none outside none; - overflow: auto; - padding: 4px; -} -.kint dt:hover { - border-color: #aaaaaa; -} -.kint dt > var { - margin-right: 5px; -} -.kint > dl dl { - padding: 0 0 0 12px; -} -.kint nav { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAYAAAC5OOBJAAAAvklEQVR42mNgGMFAS0uLLSQk5HxoaOh/XDg4OLgdpwEBAQEGQAN+YtMIFH/i4ODAg9cFQNMbsWkOCgrKJMv5ID5Qipko/6M7PzAw0ImkAIQ5H0hvISv0gZpP+fn5qZAVfcBAkmEYBaNZcjRLjmbJUUCvqAIlDlAiASUWkjWDkiU0eTaSpBGUEZBy1E9QRiFWLzO2LEmU80GZHkcRhN/5oGIGVNzgKIbwOx9UwOErAIl2/igYzZKjWXI0S9IHAAASJijo0Ypj8AAAAABJRU5ErkJggg==") no-repeat scroll 0 0 transparent; - cursor: pointer; - display: inline-block; - height: 15px; - width: 15px; - margin-right: 3px; - vertical-align: middle; -} -.kint dt.kint-parent:hover nav { - background-position: 0 -15px; -} -.kint dt.kint-parent.kint-show:hover > nav { - background-position: 0 -45px; -} -.kint dt.kint-show > nav { - background-position: 0 -30px; -} -.kint dt.kint-parent + dd { - display: none; - border-left: 1px dashed #d7d7d7; -} -.kint dt.kint-parent.kint-show + dd { - display: block; -} -.kint var, -.kint var a { - color: #0066ff; - font-style: normal; -} -.kint dt:hover var, -.kint dt:hover var a { - color: #ff0000; -} -.kint dfn { - font-style: normal; - font-family: monospace; - color: #1d1e1e; -} -.kint pre { - color: #1d1e1e; - margin: 0 0 0 12px; - padding: 5px; - overflow-y: hidden; - border-top: 0; - border: 1px solid #d7d7d7; - background: #f8f8f8; - display: block; - word-break: normal; -} -.kint .kint-popup-trigger { - float: right !important; - cursor: pointer; - color: #aaaaaa; -} -.kint .kint-popup-trigger:hover { - color: #d7d7d7; -} -.kint dt.kint-parent > .kint-popup-trigger { - font-size: 13px; -} -.kint footer { - padding: 0 3px 3px; - font-size: 9px; -} -.kint footer > .kint-popup-trigger { - font-size: 12px; -} -.kint footer nav { - background-size: 10px; - height: 10px; - width: 10px; -} -.kint footer nav:hover { - background-position: 0 -10px; -} -.kint footer > ol { - display: none; - margin-left: 32px; -} -.kint footer.kint-show > ol { - display: block; -} -.kint footer.kint-show nav { - background-position: 0 -20px; -} -.kint footer.kint-show nav:hover { - background-position: 0 -30px; -} -.kint a { - color: #1d1e1e; - text-shadow: none; -} -.kint a:hover { - color: #1d1e1e; - border-bottom: 1px dotted #1d1e1e; -} -.kint ul { - list-style: none; - padding-left: 12px; -} -.kint ul:not(.kint-tabs) li { - border-left: 1px dashed #d7d7d7; -} -.kint ul:not(.kint-tabs) li > dl { - border-left: none; -} -.kint ul.kint-tabs { - margin: 0 0 0 12px; - padding-left: 0; - background: #f8f8f8; - border: 1px solid #d7d7d7; - border-top: 0; -} -.kint ul.kint-tabs li { - background: #f8f8f8; - border: 1px solid #d7d7d7; - cursor: pointer; - display: inline-block; - height: 24px; - margin: 2px; - padding: 0 12px; - vertical-align: top; -} -.kint ul.kint-tabs li:hover, -.kint ul.kint-tabs li.kint-active-tab:hover { - border-color: #aaaaaa; - color: #ff0000; -} -.kint ul.kint-tabs li.kint-active-tab { - background: #f8f8f8; - border-top: 0; - margin-top: -1px; - height: 27px; - line-height: 24px; -} -.kint ul.kint-tabs li:not(.kint-active-tab) { - line-height: 20px; -} -.kint ul.kint-tabs li + li { - margin-left: 0; -} -.kint ul:not(.kint-tabs) > li:not(:first-child) { - display: none; -} -.kint dt:hover + dd > ul > li.kint-active-tab { - border-color: #aaaaaa; - color: #ff0000; -} -.kint-report { - border-collapse: collapse; - empty-cells: show; - border-spacing: 0; -} -.kint-report * { - font-size: 12px; -} -.kint-report dt { - background: none; - padding: 2px; -} -.kint-report dt .kint-parent { - min-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.kint-report td, -.kint-report th { - border: 1px solid #d7d7d7; - padding: 2px; - vertical-align: center; -} -.kint-report th { - cursor: alias; -} -.kint-report td:first-child, -.kint-report th { - font-weight: bold; - background: #f8f8f8; - color: #1d1e1e; -} -.kint-report td { - background: #f8f8f8; - white-space: pre; -} -.kint-report td > dl { - padding: 0; -} -.kint-report pre { - border-top: 0; - border-right: 0; -} -.kint-report th:first-child { - background: none; - border: 0; -} -.kint-report td.kint-empty { - background: #d33682 !important; -} -.kint-report tr:hover > td { - box-shadow: 0 0 1px 0 #aaaaaa inset; -} -.kint-report tr:hover var { - color: #ff0000; -} -.kint-report ul.kint-tabs li.kint-active-tab { - height: 20px; - line-height: 17px; -} -.kint-trace .kint-source { - line-height: 14px; -} -.kint-trace .kint-source span { - padding-right: 1px; - border-right: 3px inset #0066ff; -} -.kint-trace .kint-source .kint-highlight { - background: #f8f8f8; -} -.kint-trace .kint-parent > b { - min-width: 18px; - display: inline-block; - text-align: right; - color: #1d1e1e; -} -.kint-trace .kint-parent > var > a { - color: #0066ff; -} -.kint-focused { - box-shadow: 0 0 3px 2px #ff0000; -} -.kint-microtime, -.kint-color-preview { - box-shadow: 0 0 2px 0 #b6cedb; - height: 16px; - text-align: center; - text-shadow: -1px 0 #839496, 0 1px #839496, 1px 0 #839496, 0 -1px #839496; - width: 230px; - color: #fdf6e3; -} -.kint footer li { - color: #ddd; -} -.kint dt { - font-weight: normal; -} -.kint dt.kint-parent { - margin-top: 4px; -} -.kint > dl { - background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, #ffffff 15px); -} -.kint dl dl { - margin-top: 4px; - padding-left: 25px; - border-left: none; -} -.kint > dl > dt { - background: #f8f8f8; -} -.kint ul { - margin: 0; - padding-left: 0; -} -.kint ul:not(.kint-tabs) > li { - border-left: 0; -} -.kint ul.kint-tabs { - background: #f8f8f8; - border: 1px solid #d7d7d7; - border-width: 0 1px 1px 1px; - padding: 4px 0 0 12px; - margin-left: -1px; - margin-top: -1px; -} -.kint ul.kint-tabs li, -.kint ul.kint-tabs li + li { - margin: 0 0 0 4px; -} -.kint ul.kint-tabs li { - border-bottom-width: 0; - height: 25px; -} -.kint ul.kint-tabs li:first-child { - margin-left: 0; -} -.kint ul.kint-tabs li.kint-active-tab { - border-top: 1px solid #d7d7d7; - background: #fff; - font-weight: bold; - padding-top: 0; - border-bottom: 1px solid #fff !important; - margin-bottom: -1px; -} -.kint ul.kint-tabs li.kint-active-tab:hover { - border-bottom: 1px solid #fff; -} -.kint ul > li > pre { - border: 1px solid #d7d7d7; -} -.kint dt:hover + dd > ul { - border-color: #aaaaaa; -} -.kint pre { - background: #fff; - margin-top: 4px; - margin-left: 25px; -} -.kint .kint-popup-trigger:hover { - color: #ff0000; -} -.kint .kint-source .kint-highlight { - background: #cfc; -} -.kint-report td { - background: #fff; -} -.kint-report td > dl { - padding: 0; - margin: 0; -} -.kint-report td > dl > dt.kint-parent { - margin: 0; -} -.kint-report td:first-child, -.kint-report td, -.kint-report th { - padding: 2px 4px; -} -.kint-report td.kint-empty { - background: #d7d7d7 !important; -} -.kint-report dd, -.kint-report dt { - background: #fff; -} -.kint-report tr:hover > td { - box-shadow: none; - background: #cfc; -} diff --git a/system/ThirdParty/Kint/view/compiled/kint.js b/system/ThirdParty/Kint/view/compiled/kint.js deleted file mode 100755 index a7eea2de51fd..000000000000 --- a/system/ThirdParty/Kint/view/compiled/kint.js +++ /dev/null @@ -1,10 +0,0 @@ -(function(){if("undefined"===typeof kintInitialized){kintInitialized=1;var e=[],f=-1,g=function(a){var b=window.getSelection(),c=document.createRange();c.selectNodeContents(a);b.removeAllRanges();b.addRange(c)},h=function(a){Array.prototype.slice.call(document.querySelectorAll(".kint nav, .kint-tabs>li:not(.kint-active-tab)"),0).forEach(a)},k=function(a,b){if(!a.classList)return!1;"undefined"===typeof b&&(b="kint-show");return a.classList.contains(b)},l=function(a,b){"undefined"===typeof b&&(b="kint-show");a.classList.add(b)}, -m=function(a,b){"undefined"===typeof b&&(b="kint-show");a.classList.remove(b)},n=function(a){do a=a.nextElementSibling;while("dd"!==a.nodeName.toLowerCase());return a},q=function(a,b){var c=n(a);"undefined"===typeof b&&(b=k(a));b?m(a):l(a);1===c.childNodes.length&&(c=c.childNodes[0].childNodes[0])&&k(c,"kint-parent")&&q(c,b)},r=function(a,b){var c=n(a).getElementsByClassName("kint-parent"),d=c.length;for("undefined"===typeof b&&(b=k(a));d--;)q(c[d],b);q(a,b)},t=function(a){var b=a,c=0;a.parentNode.getElementsByClassName("kint-active-tab")[0].className= -"";for(a.className="kint-active-tab";b=b.previousSibling;)1===b.nodeType&&c++;a=a.parentNode.nextSibling.childNodes;for(var d=0;dKint ("+(new Date).toISOString()+')'+document.getElementsByClassName("-kint-js")[0].outerHTML+document.getElementsByClassName("-kint-css")[0].outerHTML+'
'+a.parentNode.outerHTML+"
"),b.document.close()},x=function(a,b){function c(a){var c=1===b?a.replace(/^#/,""):a;if(isNaN(c))return a.trim().toLocaleLowerCase();c=parseFloat(c);return isNaN(c)? -a.trim():c}var d=a.tBodies[0];[].slice.call(a.tBodies[0].rows).sort(function(a,d){a=c(a.cells[b].textContent);d=c(d.cells[b].textContent);return ad?1:0}).forEach(function(a){d.appendChild(a)})},y=function(a){var b=document.querySelector(".kint-focused");b&&m(b,"kint-focused");if(-1!==a){b=e[a];l(b,"kint-focused");var c=function(a){return a.offsetTop+(a.offsetParent?c(a.offsetParent):0)};window.scrollTo(0,c(b)-window.innerHeight/2)}f=a},z=function(a,b){a?0>--b&&(b=e.length-1):++b>=e.length&& -(b=0);y(b);return!1};window.addEventListener("click",function(a){var b=a.target,c=b.nodeName.toLowerCase();if(u(b)){if("dfn"===c)g(b),b=b.parentNode;else if("var"===c)b=b.parentNode,c=b.nodeName.toLowerCase();else if("th"===c)return a.ctrlKey||x(b.parentNode.parentNode.parentNode,b.cellIndex),!1;if("li"===c&&"kint-tabs"===b.parentNode.className)return"kint-active-tab"!==b.className&&(t(b),-1!==f&&v()),!1;if("nav"===c)return"footer"===b.parentNode.nodeName.toLowerCase()?(b=b.parentNode,k(b)?m(b):l(b)): -setTimeout(function(){0a&&(d=a);pvar{margin-right:5px}.kint>dl dl{padding:0 0 0 12px}.kint nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC") no-repeat scroll 0 0 transparent;cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}.kint dt.kint-parent:hover nav{background-position:0 -15px}.kint dt.kint-parent.kint-show:hover>nav{background-position:0 -45px}.kint dt.kint-show>nav{background-position:0 -30px}.kint dt.kint-parent+dd{display:none;border-left:1px dashed #b6cedb}.kint dt.kint-parent.kint-show+dd{display:block}.kint var,.kint var a{color:#0092db;font-style:normal}.kint dt:hover var,.kint dt:hover var a{color:#5cb730}.kint dfn{font-style:normal;font-family:monospace;color:#1d1e1e}.kint pre{color:#1d1e1e;margin:0 0 0 12px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #b6cedb;background:#e0eaef;display:block;word-break:normal}.kint .kint-popup-trigger{float:right !important;cursor:pointer;color:#0092db}.kint .kint-popup-trigger:hover{color:#b6cedb}.kint dt.kint-parent>.kint-popup-trigger{font-size:13px}.kint footer{padding:0 3px 3px;font-size:9px}.kint footer>.kint-popup-trigger{font-size:12px}.kint footer nav{background-size:10px;height:10px;width:10px}.kint footer nav:hover{background-position:0 -10px}.kint footer>ol{display:none;margin-left:32px}.kint footer.kint-show>ol{display:block}.kint footer.kint-show nav{background-position:0 -20px}.kint footer.kint-show nav:hover{background-position:0 -30px}.kint a{color:#1d1e1e;text-shadow:none}.kint a:hover{color:#1d1e1e;border-bottom:1px dotted #1d1e1e}.kint ul{list-style:none;padding-left:12px}.kint ul:not(.kint-tabs) li{border-left:1px dashed #b6cedb}.kint ul:not(.kint-tabs) li>dl{border-left:none}.kint ul.kint-tabs{margin:0 0 0 12px;padding-left:0;background:#e0eaef;border:1px solid #b6cedb;border-top:0}.kint ul.kint-tabs li{background:#c1d4df;border:1px solid #b6cedb;cursor:pointer;display:inline-block;height:24px;margin:2px;padding:0 12px;vertical-align:top}.kint ul.kint-tabs li:hover,.kint ul.kint-tabs li.kint-active-tab:hover{border-color:#0092db;color:#5cb730}.kint ul.kint-tabs li.kint-active-tab{background:#e0eaef;border-top:0;margin-top:-1px;height:27px;line-height:24px}.kint ul.kint-tabs li:not(.kint-active-tab){line-height:20px}.kint ul.kint-tabs li+li{margin-left:0}.kint ul:not(.kint-tabs)>li:not(:first-child){display:none}.kint dt:hover+dd>ul>li.kint-active-tab{border-color:#0092db;color:#5cb730}.kint-report{border-collapse:collapse;empty-cells:show;border-spacing:0}.kint-report *{font-size:12px}.kint-report dt{background:none;padding:2px}.kint-report dt .kint-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kint-report td,.kint-report th{border:1px solid #b6cedb;padding:2px;vertical-align:center}.kint-report th{cursor:alias}.kint-report td:first-child,.kint-report th{font-weight:bold;background:#c1d4df;color:#1d1e1e}.kint-report td{background:#e0eaef;white-space:pre}.kint-report td>dl{padding:0}.kint-report pre{border-top:0;border-right:0}.kint-report th:first-child{background:none;border:0}.kint-report td.kint-empty{background:#d33682 !important}.kint-report tr:hover>td{box-shadow:0 0 1px 0 #0092db inset}.kint-report tr:hover var{color:#5cb730}.kint-report ul.kint-tabs li.kint-active-tab{height:20px;line-height:17px}.kint-trace .kint-source{line-height:14px}.kint-trace .kint-source span{padding-right:1px;border-right:3px inset #0092db}.kint-trace .kint-source .kint-highlight{background:#c1d4df}.kint-trace .kint-parent>b{min-width:18px;display:inline-block;text-align:right;color:#1d1e1e}.kint-trace .kint-parent>var>a{color:#0092db}.kint-focused{box-shadow:0 0 3px 2px #5cb730}.kint-microtime,.kint-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}.kint footer li{color:#ddd}.kint>dl>dt{background:linear-gradient(to bottom, #e3ecf0 0, #c0d4df 100%)}.kint ul.kint-tabs{background:linear-gradient(to bottom, #9dbed0 0, #b2ccda 100%)}.kint>dl:not(.kint-trace)>dd>ul.kint-tabs li{background:#e0eaef}.kint>dl:not(.kint-trace)>dd>ul.kint-tabs li.kint-active-tab{background:#c1d4df}.kint>dl.kint-trace>dt{background:linear-gradient(to bottom, #c0d4df 0, #e3ecf0 100%)}.kint .kint-source .kint-highlight{background:#f0eb96} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/compiled/solarized-dark.css b/system/ThirdParty/Kint/view/compiled/solarized-dark.css deleted file mode 100755 index d7c68abaa909..000000000000 --- a/system/ThirdParty/Kint/view/compiled/solarized-dark.css +++ /dev/null @@ -1 +0,0 @@ -.kint::selection{background:#268bd2;color:#839496}.kint::-moz-selection{background:#268bd2;color:#839496}.kint::-webkit-selection{background:#268bd2;color:#839496}.kint,.kint::before,.kint::after,.kint *,.kint *::before,.kint *::after{box-sizing:border-box;border-radius:0;color:#839496;float:none !important;font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;line-height:15px;margin:0;padding:0;text-align:left}.kint{font-size:13px;margin:10px 0;overflow-x:auto;white-space:nowrap}.kint dt{background:#002b36;border:1px solid #586e75;color:#839496;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:5px}.kint dt:hover{border-color:#268bd2}.kint dt>var{margin-right:5px}.kint>dl dl{padding:0 0 0 15px}.kint nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg==") no-repeat scroll 0 0 transparent;cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}.kint dt.kint-parent:hover nav{background-position:0 -15px}.kint dt.kint-parent.kint-show:hover>nav{background-position:0 -45px}.kint dt.kint-show>nav{background-position:0 -30px}.kint dt.kint-parent+dd{display:none;border-left:1px dashed #586e75}.kint dt.kint-parent.kint-show+dd{display:block}.kint var,.kint var a{color:#268bd2;font-style:normal}.kint dt:hover var,.kint dt:hover var a{color:#2aa198}.kint dfn{font-style:normal;font-family:monospace;color:#93a1a1}.kint pre{color:#839496;margin:0 0 0 15px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #586e75;background:#002b36;display:block;word-break:normal}.kint .kint-popup-trigger{float:right !important;cursor:pointer;color:#268bd2}.kint .kint-popup-trigger:hover{color:#586e75}.kint dt.kint-parent>.kint-popup-trigger{font-size:13px}.kint footer{padding:0 3px 3px;font-size:9px}.kint footer>.kint-popup-trigger{font-size:12px}.kint footer nav{background-size:10px;height:10px;width:10px}.kint footer nav:hover{background-position:0 -10px}.kint footer>ol{display:none;margin-left:32px}.kint footer.kint-show>ol{display:block}.kint footer.kint-show nav{background-position:0 -20px}.kint footer.kint-show nav:hover{background-position:0 -30px}.kint a{color:#839496;text-shadow:none}.kint a:hover{color:#93a1a1;border-bottom:1px dotted #93a1a1}.kint ul{list-style:none;padding-left:15px}.kint ul:not(.kint-tabs) li{border-left:1px dashed #586e75}.kint ul:not(.kint-tabs) li>dl{border-left:none}.kint ul.kint-tabs{margin:0 0 0 15px;padding-left:0;background:#002b36;border:1px solid #586e75;border-top:0}.kint ul.kint-tabs li{background:#073642;border:1px solid #586e75;cursor:pointer;display:inline-block;height:30px;margin:3px;padding:0 15px;vertical-align:top}.kint ul.kint-tabs li:hover,.kint ul.kint-tabs li.kint-active-tab:hover{border-color:#268bd2;color:#2aa198}.kint ul.kint-tabs li.kint-active-tab{background:#002b36;border-top:0;margin-top:-1px;height:27px;line-height:24px}.kint ul.kint-tabs li:not(.kint-active-tab){line-height:25px}.kint ul.kint-tabs li+li{margin-left:0}.kint ul:not(.kint-tabs)>li:not(:first-child){display:none}.kint dt:hover+dd>ul>li.kint-active-tab{border-color:#268bd2;color:#2aa198}.kint-report{border-collapse:collapse;empty-cells:show;border-spacing:0}.kint-report *{font-size:12px}.kint-report dt{background:none;padding:2.5px}.kint-report dt .kint-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kint-report td,.kint-report th{border:1px solid #586e75;padding:2.5px;vertical-align:top}.kint-report th{cursor:alias}.kint-report td{background:#002b36;white-space:pre}.kint-report td>dl{padding:0}.kint-report pre{border-top:0;border-right:0}.kint-report th:first-child{background:none;border:0}.kint-report td:first-child,.kint-report th{font-weight:bold;background:#073642;color:#93a1a1;padding:2.5px 0}.kint-report td.kint-empty{background:#d33682 !important}.kint-report tr:hover>td{box-shadow:0 0 1px 0 #268bd2 inset}.kint-report tr:hover var{color:#2aa198}.kint-report ul.kint-tabs li.kint-active-tab{height:20px;line-height:17px}.kint-trace .kint-source{line-height:18px}.kint-trace .kint-source span{padding-right:1px;border-right:3px inset #268bd2}.kint-trace .kint-source .kint-highlight{background:#073642}.kint-trace .kint-parent>b{min-width:18px;display:inline-block;text-align:right;color:#93a1a1}.kint-trace .kint-parent>var>a{color:#268bd2}.kint-focused{box-shadow:0 0 3px 2px #859900 inset;border-radius:7px}.kint-microtime,.kint-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}.kint footer li{color:#ddd}body{background:#073642;color:#fff}.kint{background:#073642;box-shadow:0 0 5px 3px #073642}.kint>dl>dt,.kint ul.kint-tabs{box-shadow:4px 0 2px -3px #268bd2 inset}.kint ul.kint-tabs li.kint-active-tab{padding-top:7px;height:34px} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/compiled/solarized.css b/system/ThirdParty/Kint/view/compiled/solarized.css deleted file mode 100755 index 118daf36bb50..000000000000 --- a/system/ThirdParty/Kint/view/compiled/solarized.css +++ /dev/null @@ -1 +0,0 @@ -.kint::selection{background:#268bd2;color:#657b83}.kint::-moz-selection{background:#268bd2;color:#657b83}.kint::-webkit-selection{background:#268bd2;color:#657b83}.kint,.kint::before,.kint::after,.kint *,.kint *::before,.kint *::after{box-sizing:border-box;border-radius:0;color:#657b83;float:none !important;font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;line-height:15px;margin:0;padding:0;text-align:left}.kint{font-size:13px;margin:10px 0;overflow-x:auto;white-space:nowrap}.kint dt{background:#fdf6e3;border:1px solid #93a1a1;color:#657b83;display:block;font-weight:bold;list-style:none outside none;overflow:auto;padding:5px}.kint dt:hover{border-color:#268bd2}.kint dt>var{margin-right:5px}.kint>dl dl{padding:0 0 0 15px}.kint nav{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAA7VBMVEVYbnWToaH///9YbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaFYbnVYbnWToaGToaFYbnWToaFYbnWToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnVYbnWToaGToaGToaGToaGToaGToaGToaGToaGToaFYbnWToaEeRtj1AAAATXRSTlMAAACLab0B7iP2R7CL9iPuaUewAb36F0/QnPoX0JxPYQaurepi6QV8FhV7MzFhM6586RYFMa3qYnsVBpNQ+ZGOGJ3PkM9QkZAY+Y6TnV7gnYsAAAEcSURBVHhebdHVbgMxEEBRu8sUZmqapBBoSuGUmWb+/3NayxtrLOe+Hck745XZgd6/eb3CVcKt7HmR+gKxlCGOETG5vFbOoahR6NxI5zHt6nuq+2dqnqfzzH3mfcz70vb8H6MJt5u6a96hS30E4PjEFgAEp2fKNojKUfVEOoS044ex7u3YPE/nmfvM+5j3pe17T9oe/77O41w+n4vnrbrwZxbTshVhvtx5Kb8vliRLRWmeSQSTjJq/El5x5fUXYmNN9hcQC5y4g/hGvVksNtT8451rns2IScb7mn567ll2GNpWr9YWfvQgzWsKs8HOA/m960g6rjTzA8HAV/NHwiOmPLwDKA/J/gggYsRVgFvqbr/fpWYv90zzZKKs9Qfx00Jx65oxLAAAAABJRU5ErkJggg==") no-repeat scroll 0 0 transparent;cursor:pointer;display:inline-block;height:15px;width:15px;margin-right:3px;vertical-align:middle}.kint dt.kint-parent:hover nav{background-position:0 -15px}.kint dt.kint-parent.kint-show:hover>nav{background-position:0 -45px}.kint dt.kint-show>nav{background-position:0 -30px}.kint dt.kint-parent+dd{display:none;border-left:1px dashed #93a1a1}.kint dt.kint-parent.kint-show+dd{display:block}.kint var,.kint var a{color:#268bd2;font-style:normal}.kint dt:hover var,.kint dt:hover var a{color:#2aa198}.kint dfn{font-style:normal;font-family:monospace;color:#586e75}.kint pre{color:#657b83;margin:0 0 0 15px;padding:5px;overflow-y:hidden;border-top:0;border:1px solid #93a1a1;background:#fdf6e3;display:block;word-break:normal}.kint .kint-popup-trigger{float:right !important;cursor:pointer;color:#268bd2}.kint .kint-popup-trigger:hover{color:#93a1a1}.kint dt.kint-parent>.kint-popup-trigger{font-size:13px}.kint footer{padding:0 3px 3px;font-size:9px}.kint footer>.kint-popup-trigger{font-size:12px}.kint footer nav{background-size:10px;height:10px;width:10px}.kint footer nav:hover{background-position:0 -10px}.kint footer>ol{display:none;margin-left:32px}.kint footer.kint-show>ol{display:block}.kint footer.kint-show nav{background-position:0 -20px}.kint footer.kint-show nav:hover{background-position:0 -30px}.kint a{color:#657b83;text-shadow:none}.kint a:hover{color:#586e75;border-bottom:1px dotted #586e75}.kint ul{list-style:none;padding-left:15px}.kint ul:not(.kint-tabs) li{border-left:1px dashed #93a1a1}.kint ul:not(.kint-tabs) li>dl{border-left:none}.kint ul.kint-tabs{margin:0 0 0 15px;padding-left:0;background:#fdf6e3;border:1px solid #93a1a1;border-top:0}.kint ul.kint-tabs li{background:#eee8d5;border:1px solid #93a1a1;cursor:pointer;display:inline-block;height:30px;margin:3px;padding:0 15px;vertical-align:top}.kint ul.kint-tabs li:hover,.kint ul.kint-tabs li.kint-active-tab:hover{border-color:#268bd2;color:#2aa198}.kint ul.kint-tabs li.kint-active-tab{background:#fdf6e3;border-top:0;margin-top:-1px;height:27px;line-height:24px}.kint ul.kint-tabs li:not(.kint-active-tab){line-height:25px}.kint ul.kint-tabs li+li{margin-left:0}.kint ul:not(.kint-tabs)>li:not(:first-child){display:none}.kint dt:hover+dd>ul>li.kint-active-tab{border-color:#268bd2;color:#2aa198}.kint-report{border-collapse:collapse;empty-cells:show;border-spacing:0}.kint-report *{font-size:12px}.kint-report dt{background:none;padding:2.5px}.kint-report dt .kint-parent{min-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kint-report td,.kint-report th{border:1px solid #93a1a1;padding:2.5px;vertical-align:top}.kint-report th{cursor:alias}.kint-report td{background:#fdf6e3;white-space:pre}.kint-report td>dl{padding:0}.kint-report pre{border-top:0;border-right:0}.kint-report th:first-child{background:none;border:0}.kint-report td:first-child,.kint-report th{font-weight:bold;background:#eee8d5;color:#586e75;padding:2.5px 0}.kint-report td.kint-empty{background:#d33682 !important}.kint-report tr:hover>td{box-shadow:0 0 1px 0 #268bd2 inset}.kint-report tr:hover var{color:#2aa198}.kint-report ul.kint-tabs li.kint-active-tab{height:20px;line-height:17px}.kint-trace .kint-source{line-height:18px}.kint-trace .kint-source span{padding-right:1px;border-right:3px inset #268bd2}.kint-trace .kint-source .kint-highlight{background:#eee8d5}.kint-trace .kint-parent>b{min-width:18px;display:inline-block;text-align:right;color:#586e75}.kint-trace .kint-parent>var>a{color:#268bd2}.kint-focused{box-shadow:0 0 3px 2px #859900 inset;border-radius:7px}.kint-microtime,.kint-color-preview{box-shadow:0 0 2px 0 #b6cedb;height:16px;text-align:center;text-shadow:-1px 0 #839496,0 1px #839496,1px 0 #839496,0 -1px #839496;width:230px;color:#fdf6e3}.kint footer li{color:#ddd}.kint>dl>dt,.kint ul.kint-tabs{box-shadow:4px 0 2px -3px #268bd2 inset}.kint ul.kint-tabs li.kint-active-tab{padding-top:7px;height:34px} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/themes/aante-light.less b/system/ThirdParty/Kint/view/themes/aante-light.less deleted file mode 100755 index f342d757dc6e..000000000000 --- a/system/ThirdParty/Kint/view/themes/aante-light.less +++ /dev/null @@ -1,154 +0,0 @@ -/** - * @author Ante Aljinovic https://github.com/aljinovic - */ -@import "../base"; - -@main-background: #f8f8f8; -@secondary-background : #f8f8f8; - -@text-color: #1d1e1e; -@variable-name-color: #1d1e1e; -@variable-type-color: #06f; -@variable-type-color-hover: #f00; - -@border-color: #d7d7d7; -@border-color-hover: #aaa; - -@caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAYAAAC5OOBJAAAAvklEQVR42mNgGMFAS0uLLSQk5HxoaOh/XDg4OLgdpwEBAQEGQAN+YtMIFH/i4ODAg9cFQNMbsWkOCgrKJMv5ID5Qipko/6M7PzAw0ImkAIQ5H0hvISv0gZpP+fn5qZAVfcBAkmEYBaNZcjRLjmbJUUCvqAIlDlAiASUWkjWDkiU0eTaSpBGUEZBy1E9QRiFWLzO2LEmU80GZHkcRhN/5oGIGVNzgKIbwOx9UwOErAIl2/igYzZKjWXI0S9IHAAASJijo0Ypj8AAAAABJRU5ErkJggg=="); - -.keyboard-caret() { - box-shadow : 0 0 3px 2px @variable-type-color-hover; -} - -.kint { - dt { - font-weight : normal; - - &.kint-parent { - margin-top : 4px; - } - } - - > dl { - background : linear-gradient(90deg, rgba(255,255,255,0) 0, #fff 15px); - } - - dl dl { - margin-top : 4px; - padding-left : 25px; - border-left : none; - } - - > dl > dt { - background : @secondary-background; - } - - // - // TABS - // -------------------------------------------------- - - ul { - margin : 0; - padding-left : 0; - - &:not(.kint-tabs) > li { - border-left : 0; - } - - &.kint-tabs { - background : @secondary-background; - border : @border; - border-width : 0 1px 1px 1px; - padding : 4px 0 0 12px; - margin-left : -1px; - margin-top : -1px; - - li, - li + li { - margin : 0 0 0 4px; - } - - li { - border-bottom-width : 0; - height : @spacing * 6px + 1px; - - - &:first-child { - margin-left : 0; - } - - &.kint-active-tab { - border-top : @border; - background : #fff; - font-weight : bold; - padding-top : 0; - border-bottom : 1px solid #fff !important; - margin-bottom : -1px; - } - - &.kint-active-tab:hover { - border-bottom : 1px solid #fff; - } - } - } - - > li > pre { - border : @border; - } - } - - dt:hover + dd > ul { - border-color : @border-color-hover; - } - - pre { - background : #fff; - margin-top : 4px; - margin-left : 25px; - } - - .kint-popup-trigger:hover { - color : @variable-type-color-hover; - } - - .kint-source .kint-highlight { - background : #cfc; - } -} - -// -// REPORT -// -------------------------------------------------- - -.kint-report { - td { - background : #fff; - - > dl { - padding : 0; - margin : 0; - > dt.kint-parent { - margin: 0; - } - } - } - - td:first-child, - td, - th { - padding : 2px 4px; - } - - td.kint-empty { - background : @border-color !important; - } - - dd, dt { - background: #fff; - } - - tr:hover > td { - box-shadow : none; - background : #cfc; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/themes/original.less b/system/ThirdParty/Kint/view/themes/original.less deleted file mode 100755 index 839a9de47322..000000000000 --- a/system/ThirdParty/Kint/view/themes/original.less +++ /dev/null @@ -1,43 +0,0 @@ -@import "../base"; - -@main-background: #e0eaef; -@secondary-background: #c1d4df; - -@text-color: #1d1e1e; -@variable-name-color: #1d1e1e; -@variable-type-color: #0092db; -@variable-type-color-hover: #5cb730; - -@border-color: #b6cedb; -@border-color-hover: #0092db; - -@caret-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAA8CAMAAACO5hB7AAAACVBMVEX///82b4tctzBls0NMAAAAQklEQVR42rWRIQ4AAAgCwf8/2qgER2BeuwJsgAqIzekMfMryPL9XQSkobE6vwKcsz/N7FfPvh09lnPf/b+7673+f0uHuAZ/EdkNQAAAAAElFTkSuQmCC"); - -.keyboard-caret() { - box-shadow : 0 0 3px 2px #5cb730; -} - -.kint { - > dl > dt { - background : linear-gradient(to bottom, #e3ecf0 0, #c0d4df 100%); - } - - ul.kint-tabs { - background : linear-gradient(to bottom, #9dbed0 0px, #b2ccda 100%); - } - - & > dl:not(.kint-trace) > dd > ul.kint-tabs li { - background : @main-background; - &.kint-active-tab { - background : @secondary-background; - } - } - - & > dl.kint-trace > dt { - background : linear-gradient(to bottom, #c0d4df 0px, #e3ecf0 100%); - } - - .kint-source .kint-highlight { - background : #f0eb96; - } -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/themes/solarized-dark.less b/system/ThirdParty/Kint/view/themes/solarized-dark.less deleted file mode 100755 index f8467e13a923..000000000000 --- a/system/ThirdParty/Kint/view/themes/solarized-dark.less +++ /dev/null @@ -1,39 +0,0 @@ -@import "../base"; - -@spacing : 5; - -@main-background: #002b36; // base 03 -@secondary-background : #073642; // base 02 - -@text-color: #839496; // base 0 -@variable-name-color: #93a1a1; // base 1 -@variable-type-color: #268bd2; // blue -@variable-type-color-hover: #2aa198; // cyan - -@border-color: #586e75; // base 01 -@border-color-hover: #268bd2; // blue - -.keyboard-caret() { - box-shadow : 0 0 3px 2px #859900 inset; // green - border-radius : 7px; -} - -body { - background : @secondary-background; - color : #fff; // for non-kint elements to remain at least semi-readable -} - -.kint { - background : @secondary-background; - box-shadow : 0 0 5px 3px @secondary-background; - - > dl > dt, - ul.kint-tabs { - box-shadow : 4px 0 2px -3px @variable-type-color inset; - } -} - -.kint ul.kint-tabs li.kint-active-tab { - padding-top: 7px; - height: 34px; -} \ No newline at end of file diff --git a/system/ThirdParty/Kint/view/themes/solarized.less b/system/ThirdParty/Kint/view/themes/solarized.less deleted file mode 100755 index ded8748bab59..000000000000 --- a/system/ThirdParty/Kint/view/themes/solarized.less +++ /dev/null @@ -1,29 +0,0 @@ -@import "../base"; - -@spacing : 5; - -@main-background: #fdf6e3; // base 3 -@secondary-background : #eee8d5; // base 2 - -@text-color: #657b83; // base 00 -@variable-name-color: #586e75; // base 01 -@variable-type-color: #268bd2; // blue -@variable-type-color-hover: #2aa198; // cyan - -@border-color: #93a1a1; // base 1 -@border-color-hover: #268bd2; // blue - -.keyboard-caret() { - box-shadow : 0 0 3px 2px #859900 inset; // green - border-radius : 7px; -} - -.kint > dl > dt, -.kint ul.kint-tabs { - box-shadow : 4px 0 2px -3px @variable-type-color inset; -} - -.kint ul.kint-tabs li.kint-active-tab { - padding-top: 7px; - height: 34px; -} \ No newline at end of file diff --git a/system/ThirdParty/PSR/Log/AbstractLogger.php b/system/ThirdParty/PSR/Log/AbstractLogger.php index 00f9034521b4..d5106da5c649 100644 --- a/system/ThirdParty/PSR/Log/AbstractLogger.php +++ b/system/ThirdParty/PSR/Log/AbstractLogger.php @@ -18,7 +18,7 @@ abstract class AbstractLogger implements LoggerInterface * @param array $context * @return null */ - public function emergency($message, array $context = array()) + public function emergency($message, array $context = []) { $this->log(LogLevel::EMERGENCY, $message, $context); } @@ -33,7 +33,7 @@ public function emergency($message, array $context = array()) * @param array $context * @return null */ - public function alert($message, array $context = array()) + public function alert($message, array $context = []) { $this->log(LogLevel::ALERT, $message, $context); } @@ -47,7 +47,7 @@ public function alert($message, array $context = array()) * @param array $context * @return null */ - public function critical($message, array $context = array()) + public function critical($message, array $context = []) { $this->log(LogLevel::CRITICAL, $message, $context); } @@ -60,7 +60,7 @@ public function critical($message, array $context = array()) * @param array $context * @return null */ - public function error($message, array $context = array()) + public function error($message, array $context = []) { $this->log(LogLevel::ERROR, $message, $context); } @@ -75,7 +75,7 @@ public function error($message, array $context = array()) * @param array $context * @return null */ - public function warning($message, array $context = array()) + public function warning($message, array $context = []) { $this->log(LogLevel::WARNING, $message, $context); } @@ -87,7 +87,7 @@ public function warning($message, array $context = array()) * @param array $context * @return null */ - public function notice($message, array $context = array()) + public function notice($message, array $context = []) { $this->log(LogLevel::NOTICE, $message, $context); } @@ -101,7 +101,7 @@ public function notice($message, array $context = array()) * @param array $context * @return null */ - public function info($message, array $context = array()) + public function info($message, array $context = []) { $this->log(LogLevel::INFO, $message, $context); } @@ -113,7 +113,7 @@ public function info($message, array $context = array()) * @param array $context * @return null */ - public function debug($message, array $context = array()) + public function debug($message, array $context = []) { $this->log(LogLevel::DEBUG, $message, $context); } diff --git a/system/ThirdParty/PSR/Log/LoggerInterface.php b/system/ThirdParty/PSR/Log/LoggerInterface.php index 476bb962af78..20c7ff0b3a27 100644 --- a/system/ThirdParty/PSR/Log/LoggerInterface.php +++ b/system/ThirdParty/PSR/Log/LoggerInterface.php @@ -1,6 +1,4 @@ -log(LogLevel::EMERGENCY, $message, $context); } @@ -34,7 +34,7 @@ public function emergency($message, array $context = array()) * @param array $context * @return null */ - public function alert($message, array $context = array()) + public function alert($message, array $context = []) { $this->log(LogLevel::ALERT, $message, $context); } @@ -48,7 +48,7 @@ public function alert($message, array $context = array()) * @param array $context * @return null */ - public function critical($message, array $context = array()) + public function critical($message, array $context = []) { $this->log(LogLevel::CRITICAL, $message, $context); } @@ -61,7 +61,7 @@ public function critical($message, array $context = array()) * @param array $context * @return null */ - public function error($message, array $context = array()) + public function error($message, array $context = []) { $this->log(LogLevel::ERROR, $message, $context); } @@ -76,7 +76,7 @@ public function error($message, array $context = array()) * @param array $context * @return null */ - public function warning($message, array $context = array()) + public function warning($message, array $context = []) { $this->log(LogLevel::WARNING, $message, $context); } @@ -88,7 +88,7 @@ public function warning($message, array $context = array()) * @param array $context * @return null */ - public function notice($message, array $context = array()) + public function notice($message, array $context = []) { $this->log(LogLevel::NOTICE, $message, $context); } @@ -102,7 +102,7 @@ public function notice($message, array $context = array()) * @param array $context * @return null */ - public function info($message, array $context = array()) + public function info($message, array $context = []) { $this->log(LogLevel::INFO, $message, $context); } @@ -114,7 +114,7 @@ public function info($message, array $context = array()) * @param array $context * @return null */ - public function debug($message, array $context = array()) + public function debug($message, array $context = []) { $this->log(LogLevel::DEBUG, $message, $context); } @@ -127,5 +127,5 @@ public function debug($message, array $context = array()) * @param array $context * @return null */ - abstract public function log($level, $message, array $context = array()); + abstract public function log($level, $message, array $context = []); } diff --git a/system/ThirdParty/PSR/Log/NullLogger.php b/system/ThirdParty/PSR/Log/NullLogger.php index 553a3c593ae5..e47d4b96f769 100644 --- a/system/ThirdParty/PSR/Log/NullLogger.php +++ b/system/ThirdParty/PSR/Log/NullLogger.php @@ -20,7 +20,7 @@ class NullLogger extends AbstractLogger * @param array $context * @return null */ - public function log($level, $message, array $context = array()) + public function log($level, $message, array $context = []) { // noop } diff --git a/system/ThirdParty/ZendEscaper/Escaper.php b/system/ThirdParty/ZendEscaper/Escaper.php index 072d543f711d..5cec9684ef46 100644 --- a/system/ThirdParty/ZendEscaper/Escaper.php +++ b/system/ThirdParty/ZendEscaper/Escaper.php @@ -24,12 +24,12 @@ class Escaper * * @var array */ - protected static $htmlNamedEntityMap = array( + protected static $htmlNamedEntityMap = [ 34 => 'quot', // quotation mark 38 => 'amp', // ampersand 60 => 'lt', // less-than sign 62 => 'gt', // greater-than sign - ); + ]; /** * Current encoding for escaping. If not UTF-8, we convert strings from this encoding @@ -41,13 +41,11 @@ class Escaper /** * Holds the value of the special flags passed as second parameter to - * htmlspecialchars(). We modify these for PHP 5.4 to take advantage - * of the new ENT_SUBSTITUTE flag for correctly dealing with invalid - * UTF-8 sequences. + * htmlspecialchars(). * - * @var string + * @var int */ - protected $htmlSpecialCharsFlags = ENT_QUOTES; + protected $htmlSpecialCharsFlags; /** * Static Matcher which escapes characters for HTML Attribute contexts @@ -75,7 +73,7 @@ class Escaper * * @var array */ - protected $supportedEncodings = array( + protected $supportedEncodings = [ 'iso-8859-1', 'iso8859-1', 'iso-8859-5', 'iso8859-5', 'iso-8859-15', 'iso8859-15', 'utf-8', 'cp866', 'ibm866', '866', 'cp1251', 'windows-1251', @@ -85,12 +83,11 @@ class Escaper 'big5-hkscs', 'shift_jis', 'sjis', 'sjis-win', 'cp932', '932', 'euc-jp', 'eucjp', 'eucjp-win', 'macroman' - ); + ]; /** * Constructor: Single parameter allows setting of global encoding for use by - * the current object. If PHP 5.4 is detected, additional ENT_SUBSTITUTE flag - * is set for htmlspecialchars() calls. + * the current object. * * @param string $encoding * @throws Exception\InvalidArgumentException @@ -98,7 +95,11 @@ class Escaper public function __construct($encoding = null) { if ($encoding !== null) { - $encoding = (string) $encoding; + if (! is_string($encoding)) { + throw new Exception\InvalidArgumentException( + get_class($this) . ' constructor parameter must be a string, received ' . gettype($encoding) + ); + } if ($encoding === '') { throw new Exception\InvalidArgumentException( get_class($this) . ' constructor parameter does not allow a blank value' @@ -106,7 +107,7 @@ public function __construct($encoding = null) } $encoding = strtolower($encoding); - if (!in_array($encoding, $this->supportedEncodings)) { + if (! in_array($encoding, $this->supportedEncodings)) { throw new Exception\InvalidArgumentException( 'Value of \'' . $encoding . '\' passed to ' . get_class($this) . ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()' @@ -116,14 +117,13 @@ public function __construct($encoding = null) $this->encoding = $encoding; } - if (defined('ENT_SUBSTITUTE')) { - $this->htmlSpecialCharsFlags|= ENT_SUBSTITUTE; - } + // We take advantage of ENT_SUBSTITUTE flag to correctly deal with invalid UTF-8 sequences. + $this->htmlSpecialCharsFlags = ENT_QUOTES | ENT_SUBSTITUTE; // set matcher callbacks - $this->htmlAttrMatcher = array($this, 'htmlAttrMatcher'); - $this->jsMatcher = array($this, 'jsMatcher'); - $this->cssMatcher = array($this, 'cssMatcher'); + $this->htmlAttrMatcher = [$this, 'htmlAttrMatcher']; + $this->jsMatcher = [$this, 'jsMatcher']; + $this->cssMatcher = [$this, 'cssMatcher']; } /** @@ -248,7 +248,7 @@ protected function htmlAttrMatcher($matches) * replace it with while grabbing the integer value of the character. */ if (strlen($chr) > 1) { - $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8'); } $hex = bin2hex($chr); @@ -281,7 +281,13 @@ protected function jsMatcher($matches) return sprintf('\\x%02X', ord($chr)); } $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); - return sprintf('\\u%04s', strtoupper(bin2hex($chr))); + $hex = strtoupper(bin2hex($chr)); + if (strlen($hex) <= 4) { + return sprintf('\\u%04s', $hex); + } + $highSurrogate = substr($hex, 0, 4); + $lowSurrogate = substr($hex, 4, 4); + return sprintf('\\u%04s\\u%04s', $highSurrogate, $lowSurrogate); } /** @@ -297,7 +303,7 @@ protected function cssMatcher($matches) if (strlen($chr) == 1) { $ord = ord($chr); } else { - $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8'); + $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8'); $ord = hexdec(bin2hex($chr)); } return sprintf('\\%X ', $ord); @@ -319,7 +325,7 @@ protected function toUtf8($string) $result = $this->convertEncoding($string, 'UTF-8', $this->getEncoding()); } - if (!$this->isUtf8($result)) { + if (! $this->isUtf8($result)) { throw new Exception\RuntimeException( sprintf('String to be escaped was not valid UTF-8 or could not be converted: %s', $result) ); diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php new file mode 100644 index 000000000000..9bdc635f3b9c --- /dev/null +++ b/system/Throttle/Throttler.php @@ -0,0 +1,201 @@ +cache = $cache; + } + + //-------------------------------------------------------------------- + + /** + * Returns the number of seconds until the next available token will + * be released for usage. + * + * @return int + */ + public function getTokenTime() + { + return $this->tokenTime; + } + + //-------------------------------------------------------------------- + + /** + * Restricts the number of requests made by a single IP address within + * a set number of seconds. + * + * Example: + * + * if (! $throttler->check($request->ipAddress(), 60, MINUTE)) + * { + * die('You submitted over 60 requests within a minute.'); + * } + * + * @param string $key The name to use as the "bucket" name. + * @param int $capacity The number of requests the "bucket" can hold + * @param int $seconds The time it takes the "bucket" to completely refill + * @param int $cost The number of tokens this action uses. + * + * @return bool + * @internal param int $maxRequests + */ + public function check(string $key, int $capacity, int $seconds, int $cost = 1) + { + $tokenName = $this->prefix . $key; + + // Check to see if the bucket has even been created yet. + if (($tokens = $this->cache->get($tokenName)) === false) + { + // If it hasn't been created, then we'll set it to the maximum + // capacity - 1, and save it to the cache. + $this->cache->save($tokenName, $capacity - $cost, $seconds); + $this->cache->save($tokenName . 'Time', time()); + + return true; + } + + // If $tokens > 0, then we need to replenish the bucket + // based on how long it's been since the last update. + $throttleTime = $this->cache->get($tokenName . 'Time'); + $elapsed = $this->time() - $throttleTime; + // Number of tokens to add back per second + $rate = $capacity / $seconds; + + // We must have a minimum wait of 1 second for a new token + // Primarily stored to allow devs to report back to users. + $this->tokenTime = max(1, $rate); + + // Add tokens based up on number per second that + // should be refilled, then checked against capacity + // to be sure the bucket didn't overflow. + $tokens += $rate * $elapsed; + $tokens = $tokens > $capacity ? $capacity : $tokens; + + // If $tokens > 0, then we are save to perform the action, but + // we need to decrement the number of available tokens. + $response = false; + + if ($tokens > 0) + { + $response = true; + + $this->cache->save($tokenName, $tokens - $cost, $elapsed); + $this->cache->save($tokenName . 'Time', time()); + } + + return $response; + } + + //-------------------------------------------------------------------- + + /** + * Used during testing to set the current timestamp to use. + * + * @param int $time + * + * @return $this + */ + public function setTestTime(int $time) + { + $this->testTime = $time; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * + * + * @return int + */ + public function time() + { + return $this->testTime ?? time(); + } + + +} diff --git a/system/Throttle/ThrottlerInterface.php b/system/Throttle/ThrottlerInterface.php new file mode 100644 index 000000000000..3fd95875532e --- /dev/null +++ b/system/Throttle/ThrottlerInterface.php @@ -0,0 +1,70 @@ +checkIPAddress($request->ipAddress(), 60, MINUTE)) + * { + * die('You submitted over 60 requests within a minute.'); + * } + * + * @param string $key The name to use as the "bucket" name. + * @param int $capacity The number of requests the "bucket" can hold + * @param int $seconds The time it takes the "bucket" to completely refill + * @param int $cost The number of tokens this action uses. + * + * @return bool + */ + public function check(string $key, int $capacity, int $seconds, int $cost); + + //-------------------------------------------------------------------- + + /** + * Returns the number of seconds until the next available token will + * be released for usage. + * + * @return int + */ + public function getTokenTime(); +} diff --git a/system/Typography/Typography.php b/system/Typography/Typography.php new file mode 100644 index 000000000000..0fa787d4f9c9 --- /dev/null +++ b/system/Typography/Typography.php @@ -0,0 +1,406 @@ + tags + * + * @var string + */ + public $blockElements = 'address|blockquote|div|dl|fieldset|form|h\d|hr|noscript|object|ol|p|pre|script|table|ul'; + + /** + * Elements that should not have

and
tags within them. + * + * @var string + */ + public $skipElements = 'p|pre|ol|ul|dl|object|table|h\d'; + + /** + * Tags we want the parser to completely ignore when splitting the string. + * + * @var string + */ + public $inlineElements = 'a|abbr|acronym|b|bdo|big|br|button|cite|code|del|dfn|em|i|img|ins|input|label|map|kbd|q|samp|select|small|span|strong|sub|sup|textarea|tt|var'; + + /** + * array of block level elements that require inner content to be within another block level element + * + * @var array + */ + public $innerBlockRequired = ['blockquote']; + + /** + * the last block element parsed + * + * @var string + */ + public $lastBlockElement = ''; + + /** + * whether or not to protect quotes within { curly braces } + * + * @var bool + */ + public $protectBracedQuotes = false; + + /** + * Auto Typography + * + * This function converts text, making it typographically correct: + * - Converts double spaces into paragraphs. + * - Converts single line breaks into
tags + * - Converts single and double quotes into correctly facing curly quote entities. + * - Converts three dots into ellipsis. + * - Converts double dashes into em-dashes. + * - Converts two spaces into entities + * + * @param string $str + * @param bool $reduce_linebreaks whether to reduce more then two consecutive newlines to two + * + * @return string + */ + public function autoTypography(string $str, bool $reduce_linebreaks = false): string + { + if ($str === '') + { + return ''; + } + + // Standardize Newlines to make matching easier + if (strpos($str, "\r") !== false) + { + $str = str_replace(["\r\n", "\r"], "\n", $str); + } + + // Reduce line breaks. If there are more than two consecutive linebreaks + // we'll compress them down to a maximum of two since there's no benefit to more. + if ($reduce_linebreaks === false) + { + $str = preg_replace("/\n\n+/", "\n\n", $str); + } + + // HTML comment tags don't conform to patterns of normal tags, so pull them out separately, only if needed + $html_comments = []; + if (strpos($str, '' . PHP_EOL + . $output . PHP_EOL + . '' . PHP_EOL; + } + } + } + + // Should we cache? + if (isset($this->renderVars['options']['cache'])) + { + cache()->save($this->renderVars['cacheName'], $output, (int) $this->renderVars['options']['cache']); + } + + return $output; + } + + //-------------------------------------------------------------------- + + /** + * Builds the output based upon a string and any + * data that has already been set. + * Cache does not apply, because there is no "key". + * + * @param string $view The view contents + * @param array $options Reserved for 3rd-party uses since + * it might be needed to pass additional info + * to other template engines. + * @param bool $saveData If true, will save data for use with any other calls, + * if false, will clean the data after displaying the view, + * if not specified, use the config setting. + * + * @return string + */ + public function renderString(string $view, array $options = null, $saveData = null): string + { + $start = microtime(true); + if (is_null($saveData)) + { + $saveData = $this->config->saveData; + } + + extract($this->data); + + if ( ! $saveData) + { + $this->data = []; + } + + ob_start(); + $incoming = "?>" . $view; + eval($incoming); $output = ob_get_contents(); @ob_end_clean(); - $this->logPerformance($start, microtime(true), $view); + $this->logPerformance($start, microtime(true), $this->excerpt($view)); return $output; } //-------------------------------------------------------------------- + /** + * Extract first bit of a long string and add ellipsis + * + * @param string $string + * @param int $length + * @return string + */ + public function excerpt(string $string, int $length = 20): string + { + return (strlen($string) > $length) ? substr($string, 0, $length - 3) . '...' : $string; + } + + //-------------------------------------------------------------------- + /** * Sets several pieces of view data at once. * @@ -169,11 +311,11 @@ public function render(string $view, array $options=null, $saveData=false): stri * @param string $context The context to escape it for: html, css, js, url * If null, no escaping will happen * - * @return RenderableInterface + * @return RendererInterface */ - public function setData(array $data=[], string $context=null): RenderableInterface + public function setData(array $data = [], string $context = null): RendererInterface { - if (! empty($context)) + if ( ! empty($context)) { $data = \esc($data, $context); } @@ -189,15 +331,15 @@ public function setData(array $data=[], string $context=null): RenderableInterfa * Sets a single piece of view data. * * @param string $name - * @param null $value + * @param mixed $value * @param string $context The context to escape it for: html, css, js, url * If null, no escaping will happen * - * @return RenderableInterface + * @return RendererInterface */ - public function setVar(string $name, $value=null, string $context=null): RenderableInterface + public function setVar(string $name, $value = null, string $context = null): RendererInterface { - if (! empty($context)) + if ( ! empty($context)) { $value = \esc($value, $context); } @@ -212,7 +354,7 @@ public function setVar(string $name, $value=null, string $context=null): Rendera /** * Removes all of the view data from the system. * - * @return RenderableInterface + * @return RendererInterface */ public function resetData() { @@ -230,7 +372,7 @@ public function resetData() */ public function getData() { - return $this->data; + return $this->data; } //-------------------------------------------------------------------- @@ -243,7 +385,7 @@ public function getData() */ public function getPerformanceData(): array { - return $this->performanceData; + return $this->performanceData; } //-------------------------------------------------------------------- @@ -257,15 +399,15 @@ public function getPerformanceData(): array */ protected function logPerformance(float $start, float $end, string $view) { - if (! $this->debug) return; + if ( ! $this->debug) + return; $this->performanceData[] = [ - 'start' => $start, - 'end' => $end, - 'view' => $view + 'start' => $start, + 'end' => $end, + 'view' => $view ]; } //-------------------------------------------------------------------- - } diff --git a/system/bootstrap.php b/system/bootstrap.php new file mode 100644 index 000000000000..b8059ad857fc --- /dev/null +++ b/system/bootstrap.php @@ -0,0 +1,158 @@ +publicDirectory, '/'); + +$pos = strrpos(FCPATH, $public.DIRECTORY_SEPARATOR); + +/** + * The path to the main application directory. Just above public. + */ +if (! defined('ROOTPATH')) +{ + define('ROOTPATH', substr_replace(FCPATH, '', $pos, strlen($public.DIRECTORY_SEPARATOR))); +} + +/** + * The path to the application directory. + */ +if (! defined('APPPATH')) +{ + define('APPPATH', realpath(ROOTPATH . $paths->applicationDirectory).DIRECTORY_SEPARATOR); +} + +/** + * The path to the system directory. + */ +if (! defined('BASEPATH')) +{ + define('BASEPATH', realpath(ROOTPATH . $paths->systemDirectory).DIRECTORY_SEPARATOR); +} + +/** + * The path to the writable directory. + */ +if (! defined('WRITEPATH')) +{ + define('WRITEPATH', realpath(ROOTPATH . $paths->writableDirectory).DIRECTORY_SEPARATOR); +} + +/** + * The path to the tests directory + */ +if (! defined('TESTPATH')) +{ + define('TESTPATH', realpath(ROOTPATH . $paths->testsDirectory).DIRECTORY_SEPARATOR); +} + +/* + * --------------------------------------------------------------- + * GRAB OUR CONSTANTS & COMMON + * --------------------------------------------------------------- + */ +require_once APPPATH.'Config/Constants.php'; + +require_once BASEPATH.'Common.php'; + +/* + * --------------------------------------------------------------- + * LOAD OUR AUTOLOADER + * --------------------------------------------------------------- + * + * The autoloader allows all of the pieces to work together + * in the framework. We have to load it here, though, so + * that the config files can use the path constants. + */ + +require_once BASEPATH.'Autoloader/Autoloader.php'; +require_once APPPATH .'Config/Autoload.php'; +require_once BASEPATH .'Config/BaseService.php'; +require_once APPPATH .'Config/Services.php'; + +// Use Config\Services as CodeIgniter\Services +if (! class_exists('CodeIgniter\Services', false)) +{ + class_alias('Config\Services', 'CodeIgniter\Services'); +} + +$loader = CodeIgniter\Services::autoloader(); +$loader->initialize(new Config\Autoload()); +$loader->register(); // Register the loader with the SPL autoloader stack. + +// Now load Composer's if it's available +if (file_exists(COMPOSER_PATH)) +{ + require_once COMPOSER_PATH; +} + +// Load environment settings from .env files +// into $_SERVER and $_ENV +require_once BASEPATH . 'Config/DotEnv.php'; + +$env = new \CodeIgniter\Config\DotEnv(ROOTPATH); +$env->load(); + +// Always load the URL helper - +// it should be used in 90% of apps. +helper('url'); + +/* + * --------------------------------------------------------------- + * GRAB OUR CODEIGNITER INSTANCE + * --------------------------------------------------------------- + * + * The CodeIgniter class contains the core functionality to make + * the application run, and does all of the dirty work to get + * the pieces all working together. + */ + +$appConfig = config(\Config\App::class); +$app = new \CodeIgniter\CodeIgniter($appConfig); +$app->initialize(); + +return $app; diff --git a/tests/.htaccess b/tests/.htaccess new file mode 100644 index 000000000000..f24db0accc73 --- /dev/null +++ b/tests/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/tests/README.md b/tests/README.md index 90159a8f3d0f..0856e484fbe3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,7 +40,7 @@ You can run the tests without running the live database tests. 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/ 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. diff --git a/tests/_support/Autoloader/MockAutoloader.php b/tests/_support/Autoloader/MockAutoloader.php index 6b0987536e91..8a722a571235 100644 --- a/tests/_support/Autoloader/MockAutoloader.php +++ b/tests/_support/Autoloader/MockAutoloader.php @@ -1,4 +1,6 @@ -files) ? $file : false; } //-------------------------------------------------------------------- - } diff --git a/tests/_support/Autoloader/MockFileLocator.php b/tests/_support/Autoloader/MockFileLocator.php index dd53bebc8623..c683bbb9d8c6 100644 --- a/tests/_support/Autoloader/MockFileLocator.php +++ b/tests/_support/Autoloader/MockFileLocator.php @@ -1,7 +1,10 @@ -prefix.$key; + + return array_key_exists($key, $this->cache) + ? $this->cache[$key] + : false; + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * The $raw parameter is only utilized by Mamcache in order to + * allow usage of increment() and decrement(). + * + * @param string $key Cache item name + * @param $value the data to save + * @param null $ttl Time To Live, in seconds (default 60) + * @param bool $raw Whether to store the raw value. + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60, bool $raw = false) + { + $key = $this->prefix.$key; + + $this->cache[$key] = $value; + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + unset($this->cache[$key]); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + $key = $this->prefix.$key; + + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data+$offset); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param int $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + $key = $this->prefix.$key; + + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data-$offset); + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + $this->cache = []; + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return []; + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + return false; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return true; + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Commands/CommandsTestStreamFilter.php b/tests/_support/Commands/CommandsTestStreamFilter.php new file mode 100644 index 000000000000..961850730561 --- /dev/null +++ b/tests/_support/Commands/CommandsTestStreamFilter.php @@ -0,0 +1,17 @@ +data; + $consumed += $bucket->datalen; + } + return PSFS_PASS_ON; + } +} + +stream_filter_register('CommandsTestStreamFilter', 'CodeIgniter\Commands\CommandsTestStreamFilter'); diff --git a/tests/_support/Config/BadRegistrar.php b/tests/_support/Config/BadRegistrar.php new file mode 100644 index 000000000000..3ac14fe72cde --- /dev/null +++ b/tests/_support/Config/BadRegistrar.php @@ -0,0 +1,17 @@ + [ - + 'Tests\Support\Log\Handlers\TestHandler' => [ /* * The log levels that this handler will handle. */ 'handles' => ['critical', 'alert', 'emergency', 'debug', - 'error', 'info', 'notice', 'warning'], + 'error', 'info', 'notice', 'warning'], ] ]; + } diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php new file mode 100644 index 000000000000..b571cdb3c22e --- /dev/null +++ b/tests/_support/Config/Registrar.php @@ -0,0 +1,20 @@ + ['first', 'second'], + 'format' => 'nice', + 'fruit' => ['apple', 'banana'] + ]; + } + +} diff --git a/tests/_support/Config/Routes.php b/tests/_support/Config/Routes.php new file mode 100644 index 000000000000..2369e33b22b7 --- /dev/null +++ b/tests/_support/Config/Routes.php @@ -0,0 +1,7 @@ +add('testing', 'TestController::index'); diff --git a/tests/_support/DOM/DOMParser.php b/tests/_support/DOM/DOMParser.php new file mode 100644 index 000000000000..d8154e7f37f0 --- /dev/null +++ b/tests/_support/DOM/DOMParser.php @@ -0,0 +1,291 @@ +dom = new \DOMDocument('1.0', 'utf-8'); + } + + /** + * Returns the body of the current document. + * + * @return string + */ + public function getBody(): string + { + return $this->dom->saveHTML(); + } + + /** + * Sets a string as the body that we want to work with. + * + * @param string $content + * + * @return $this + */ + public function withString(string $content) + { + // converts all special characters to utf-8 + $content = mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'); + + //turning off some errors + libxml_use_internal_errors(true); + + if (! $this->dom->loadHTML($content)) + { + throw new \BadMethodCallException('Invalid HTML'); + } + + // ignore the whitespace. + $this->dom->preserveWhiteSpace = false; + + return $this; + } + + /** + * Loads the contents of a file as a string + * so that we can work with it. + * + * @param string $path + * + * @return \Tests\Support\DOM\DOMParser + */ + public function withFile(string $path) + { + if (! is_file($path)) + { + throw new \InvalidArgumentException(basename($path).' is not a valid file.'); + } + + $content = file_get_contents($path); + + return $this->withString($content); + } + + /** + * Checks to see if the text is found within the result. + * + * @param string $search + * + * @return bool + */ + public function see(string $search=null, string $element=null): bool + { + // If Element is null, we're just scanning for text + if (is_null($element)) + { + $content = $this->dom->saveHTML(); + return strpos($content, $search) !== false; + } + + $result = $this->doXPath($search, $element); + + return (bool)$result->length; + } + + /** + * Checks to see if the text is NOT found within the result. + * + * @param string $search + * @param string|null $element + * + * @return bool + */ + public function dontSee(string $search=null, string $element=null): bool + { + return ! $this->see($search, $element); + } + + /** + * Checks to see if an element with the matching CSS specifier + * is found within the current DOM. + * + * @param string $element + * + * @return bool + */ + public function seeElement(string $element): bool + { + return $this->see(null, $element); + } + + /** + * Checks to see if the element is available within the result. + * + * @param string $element + * + * @return bool + */ + public function dontSeeElement(string $element): bool + { + return $this->dontSee(null, $element); + } + + /** + * Determines if a link with the specified text is found + * within the results. + * + * @param string $text + * @param string|null $details + * + * @return bool + */ + public function seeLink(string $text, string $details=null): bool + { + return $this->see($text, 'a'.$details); + } + + /** + * Checks for an input named $field with a value of $value. + * + * @param string $field + * @param string $value + * + * @return bool + */ + public function seeInField(string $field, string $value): bool + { + $result = $this->doXPath(null, "input", ["[@value=\"{$value}\"][@name=\"{$field}\"]"]); + + return (bool)$result->length; + } + + /** + * Checks for checkboxes that are currently checked. + * + * @param string $element + * + * @return bool + */ + public function seeCheckboxIsChecked(string $element): bool + { + $result = $this->doXPath(null, "input".$element, [ + '[@type="checkbox"]', + '[@checked="checked"]' + ]); + + return (bool)$result->length; + } + + + //-------------------------------------------------------------------- + + protected function doXPath(string $search=null, string $element, array $paths=[]) + { + // Otherwise, grab any elements that match + // the selector + $selector = $this->parseSelector($element); + + $path = ''; + + // By ID + if (! empty($selector['id'])) + { + $path = empty($selector['tag']) + ? "id(\"{$selector['id']}\")" + : "//body//{$selector['tag']}[@id=\"{$selector['id']}\"]"; + } + // By Class + else if (! empty($selector['class'])) + { + $path = empty($selector['tag']) + ? "//*[@class=\"{$selector['class']}\"]" + : "//body//{$selector['tag']}[@class=\"{$selector['class']}\"]"; + } + // By tag only + else if (! empty($selector['tag'])) + { + $path = "//body//{$selector['tag']}"; + } + + if (! empty($selector['attr'])) + { + foreach ($selector['attr'] as $key => $value) + { + $path .= "[{$key}={$value}]"; + } + } + + // $paths might contain a number of different + // ready to go xpath portions to tack on. + if (! empty($paths) && is_array($paths)) + { + foreach ($paths as $extra) + { + $path .= $extra; + } + } + + if (! is_null($search)) + { + $path .= "[contains(., \"{$search}\")]"; + } + + $xpath = new \DOMXPath($this->dom); + + $result = $xpath->query($path); + + return $result; + } + + public function parseSelector(string $selector) + { + $tag = null; + $id = null; + $class = null; + $attr = null; + + // ID? + if ($pos = strpos($selector, '#') !== false) + { + list($tag, $id) = explode('#', $selector); + } + // Attribute + elseif (strpos($selector, '[') !== false && strpos($selector, ']') !== false) + { + $open = strpos($selector, '['); + $close = strpos($selector, ']'); + + $tag = substr($selector, 0, $open); + $text = substr($selector, $open+1, $close-2); + + // We only support a single attribute currently + $text = explode(',', $text); + $text = trim(array_shift($text)); + + list($name, $value) = explode('=', $text); + $name = trim($name); + $value = trim($value); + $attr = [$name => trim($value, '] ')]; + } + // Class? + elseif ($pos = strpos($selector, '.') !== false) + { + list($tag, $class) = explode('.', $selector); + } + // Otherwise, assume the entire string is our tag + else + { + $tag = $selector; + } + + return [ + 'tag' => $tag, + 'id' => $id, + 'class' => $class, + 'attr' => $attr + ]; + } + +} diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php new file mode 100644 index 000000000000..cea0a54d3ccb --- /dev/null +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -0,0 +1,61 @@ +db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; + + // User Table + $this->forge->addField([ + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 80,], + 'email' => ['type' => 'VARCHAR', 'constraint' => 100], + 'country' => ['type' => 'VARCHAR', 'constraint' => 40,], + 'deleted' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => '0'], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('user', true); + + // Job Table + $this->forge->addField([ + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 40], + 'description' => ['type' => 'TEXT'], + 'created_at' => ['type' => 'DATETIME', 'null' => true] + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('job', true); + + // Misc Table + $this->forge->addField([ + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], + 'key' => ['type' => 'VARCHAR', 'constraint' => 40], + 'value' => ['type' => 'TEXT'], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('misc', true); + + // Empty Table + $this->forge->addField([ + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 40,], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('empty', true); + } + + //-------------------------------------------------------------------- + + public function down() + { + $this->forge->dropTable('user'); + $this->forge->dropTable('job'); + $this->forge->dropTable('misc'); + $this->forge->dropTable('empty'); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Database/MockBuilder.php b/tests/_support/Database/MockBuilder.php index c24dcb1f6a78..3aeb7e95ac6a 100644 --- a/tests/_support/Database/MockBuilder.php +++ b/tests/_support/Database/MockBuilder.php @@ -1,4 +1,7 @@ -lastQuery = $query; + // Run the query if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { @@ -41,21 +45,11 @@ public function query(string $sql, $binds = null) // @todo deal with errors - if ($this->saveQueries) - { - $this->queries[] = $query; - } - return false; } $query->setDuration($startTime); - if ($this->saveQueries) - { - $this->queries[] = $query; - } - $resultClass = str_replace('Connection', 'Result', get_class($this)); return new $resultClass($this->connID, $this->resultID); @@ -174,7 +168,7 @@ public function error() */ public function insertID() { - return $this->conn_id->insert_id; + return $this->connID->insert_id; } //-------------------------------------------------------------------- @@ -206,4 +200,51 @@ protected function _listColumns(string $table = ''): string } //-------------------------------------------------------------------- + + /** + * Close the connection. + */ + protected function _close() + { + return; + } + + //-------------------------------------------------------------------- + + + /** + * Begin Transaction + * + * @return bool + */ + protected function _transBegin(): bool + { + return true; + } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _transCommit(): bool + { + return true; + } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _transRollback(): bool + { + return true; + } + + //-------------------------------------------------------------------- } diff --git a/tests/_support/Database/MockQuery.php b/tests/_support/Database/MockQuery.php index 97658d1e9af3..0008104a1adc 100644 --- a/tests/_support/Database/MockQuery.php +++ b/tests/_support/Database/MockQuery.php @@ -1,6 +1,8 @@ - [ + ['name' => 'Derek Jones', 'email' => 'derek@world.com', 'country' => 'US'], + ['name' => 'Ahmadinejad', 'email' => 'ahmadinejad@world.com', 'country' => 'Iran'], + ['name' => 'Richard A Causey', 'email' => 'richard@world.com', 'country' => 'US'], + ['name' => 'Chris Martin', 'email' => 'chris@world.com', 'country' => 'UK'] + ], + 'job' => [ + ['name' => 'Developer', 'description' => 'Awesome job, but sometimes makes you bored'], + ['name' => 'Politician', 'description' => 'This is not really a job'], + ['name' => 'Accountant', 'description' => 'Boring job, but you will get free snack at lunch'], + ['name' => 'Musician', 'description' => 'Only Coldplay can actually called Musician'] + ], + 'misc' => [ + ['key' => '\\xxxfoo456', 'value' => 'Entry with \\xxx'], + ['key' => '\\%foo456', 'value' => 'Entry with \\%'], + ['key' => 'spaces and tabs', 'value' => ' One two three tab'] + ] + ]; + + foreach ($data as $table => $dummy_data) + { + $this->db->table($table)->truncate(); + + foreach ($dummy_data as $single_dummy_data) + { + $this->db->table($table)->insert($single_dummy_data); + } + } + } + + //-------------------------------------------------------------------- + + +} diff --git a/tests/_support/Events/MockEvents.php b/tests/_support/Events/MockEvents.php new file mode 100644 index 000000000000..6e4550807159 --- /dev/null +++ b/tests/_support/Events/MockEvents.php @@ -0,0 +1,66 @@ +dom = new DOMParser(); + } + + //-------------------------------------------------------------------- + // Getters / Setters + //-------------------------------------------------------------------- + + /** + * @param string $body + * + * @return $this + */ + public function setBody(string $body) + { + $this->body = $body; + + if (! empty($body)) + { + $this->dom = $this->dom->withString($body); + } + + return $this; + } + + /** + * @return string + */ + public function getBody() + { + return $this->body; + } + + + /** + * @param \CodeIgniter\HTTP\RequestInterface $request + * + * @return $this + */ + public function setRequest(RequestInterface $request) + { + $this->request = $request; + + return $this; + } + + /** + * @param \CodeIgniter\HTTP\ResponseInterface $response + * + * @return $this + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + + $this->setBody($response->getBody() ?? ''); + + return $this; + } + + /** + * @return \CodeIgniter\HTTP\IncomingRequest + */ + public function request() + { + return $this->request; + } + + /** + * @return \CodeIgniter\HTTP\Response + */ + public function response() + { + return $this->response; + } + + //-------------------------------------------------------------------- + // Simple Response Checks + //-------------------------------------------------------------------- + + /** + * Boils down the possible responses into a bolean valid/not-valid + * response type. + * + * @return bool + */ + public function isOK(): bool + { + // Only 200 and 300 range status codes + // are considered valid. + if ($this->response->getStatusCode() >= 400 || $this->response->getStatusCode() < 200) + { + return false; + } + + // Empty bodies are not considered valid. + if (empty($this->response->getBody())) + { + return false; + } + + return true; + } + + /** + * Returns whether or not the Response was a redirect response + * + * @return bool + */ + public function isRedirect(): bool + { + return $this->response instanceof RedirectResponse; + } + + //-------------------------------------------------------------------- + // Utility + //-------------------------------------------------------------------- + + public function __call($function, $params) + { + if (method_exists($this->dom, $function)) + { + return $this->dom->{$function}(...$params); + } + } + +} diff --git a/tests/_support/Helpers/ControllerTester.php b/tests/_support/Helpers/ControllerTester.php new file mode 100644 index 000000000000..bf97190ac26e --- /dev/null +++ b/tests/_support/Helpers/ControllerTester.php @@ -0,0 +1,189 @@ +withRequest($request) + * ->withResponse($response) + * ->withURI($uri) + * ->withBody($body) + * ->controller('App\Controllers\Home') + * ->run('methodName'); + */ +trait ControllerTester +{ + protected $appConfig; + + protected $request; + + protected $response; + + protected $controller; + + protected $uri = 'http://example.com'; + + protected $body; + + /** + * Loads the specified controller, and generates any needed dependencies. + * + * @param string $name + * + * @return mixed + */ + public function controller(string $name) + { + if (! class_exists($name)) + { + throw new \InvalidArgumentException('Invalid Controller: '.$name); + } + + if (empty($this->appConfig)) + { + $this->appConfig = new App(); + } + + if (empty($this->request)) + { + $this->request = new IncomingRequest($this->appConfig, $this->uri, $this->body); + } + + if (empty($this->response)) + { + $this->response = new Response($this->appConfig); + } + + $this->controller = new $name($this->request, $this->response); + + return $this; + } + + /** + * Runs the specified method on the controller and returns the results. + * + * @param string $method + * @param array $params + * + * @return \Tests\Support\Helpers\ControllerResponse + */ + public function execute(string $method, ...$params) + { + if (! method_exists($this->controller, $method) || ! is_callable([$this->controller, $method])) + { + throw new \InvalidArgumentException('Method does not exist or is not callable in controller: '.$method); + } + + // The URL helper is always loaded by the system + // so ensure it's available. + helper('url'); + + $result = (new ControllerResponse()) + ->setRequest($this->request) + ->setResponse($this->response); + + try + { + ob_start(); + + $response = $this->controller->{$method}(...$params); + } + catch (\Throwable $e) + { + $result->response() + ->setStatusCode($e->getCode()); + } + finally + { + $output = ob_get_clean(); + + // If the controller returned a redirect response + // then we need to use that... + if (isset($response) && $response instanceof Response) + { + $result->setResponse($response); + } + + $result->response()->setBody($output); + $result->setBody($output); + } + + // If not response code has been sent, assume a success + if (empty($result->response()->getStatusCode())) + { + $result->response()->setStatusCode(200); + } + + return $result; + } + + /** + * @param mixed $appConfig + * + * @return mixed + */ + public function withConfig($appConfig) + { + $this->appConfig = $appConfig; + + return $this; + } + + /** + * @param mixed $request + * + * @return mixed + */ + public function withRequest($request) + { + $this->request = $request; + + return $this; + } + + /** + * @param mixed $response + * + * @return mixed + */ + public function withResponse($response) + { + $this->response = $response; + + return $this; + } + + /** + * @param string $uri + * + * @return mixed + */ + public function withUri(string $uri) + { + $this->uri = new URI($uri); + + return $this; + } + + /** + * @param mixed $body + * + * @return mixed + */ + public function withBody($body) + { + $this->body = $body; + + return $this; + } + + +} diff --git a/tests/_support/Images/EXIFsamples/down-mirrored.jpg b/tests/_support/Images/EXIFsamples/down-mirrored.jpg new file mode 100644 index 000000000000..34a7b1d39524 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/down-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/down.jpg b/tests/_support/Images/EXIFsamples/down.jpg new file mode 100644 index 000000000000..9077a7c92b0f Binary files /dev/null and b/tests/_support/Images/EXIFsamples/down.jpg differ diff --git a/tests/_support/Images/EXIFsamples/left-mirrored.jpg b/tests/_support/Images/EXIFsamples/left-mirrored.jpg new file mode 100644 index 000000000000..1832702492e7 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/left-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/left.jpg b/tests/_support/Images/EXIFsamples/left.jpg new file mode 100644 index 000000000000..ad1f89850f5a Binary files /dev/null and b/tests/_support/Images/EXIFsamples/left.jpg differ diff --git a/tests/_support/Images/EXIFsamples/right-mirrored.jpg b/tests/_support/Images/EXIFsamples/right-mirrored.jpg new file mode 100644 index 000000000000..cc8a29aebea7 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/right-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/right.jpg b/tests/_support/Images/EXIFsamples/right.jpg new file mode 100644 index 000000000000..183ffebb8eaf Binary files /dev/null and b/tests/_support/Images/EXIFsamples/right.jpg differ diff --git a/tests/_support/Images/EXIFsamples/up-mirrored.jpg b/tests/_support/Images/EXIFsamples/up-mirrored.jpg new file mode 100644 index 000000000000..e1865a5f0ea3 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/up-mirrored.jpg differ diff --git a/tests/_support/Images/EXIFsamples/up.jpg b/tests/_support/Images/EXIFsamples/up.jpg new file mode 100644 index 000000000000..70fc26ff2de8 Binary files /dev/null and b/tests/_support/Images/EXIFsamples/up.jpg differ diff --git a/tests/_support/Images/Steveston_dusk.JPG b/tests/_support/Images/Steveston_dusk.JPG new file mode 100644 index 000000000000..c3b9b121b7c9 Binary files /dev/null and b/tests/_support/Images/Steveston_dusk.JPG differ diff --git a/tests/_support/Images/ci-logo.gif b/tests/_support/Images/ci-logo.gif new file mode 100644 index 000000000000..3001b2f75262 Binary files /dev/null and b/tests/_support/Images/ci-logo.gif differ diff --git a/tests/_support/Images/ci-logo.jpeg b/tests/_support/Images/ci-logo.jpeg new file mode 100644 index 000000000000..1b0178bba3fe Binary files /dev/null and b/tests/_support/Images/ci-logo.jpeg differ diff --git a/tests/_support/Images/ci-logo.png b/tests/_support/Images/ci-logo.png new file mode 100644 index 000000000000..34fb01082893 Binary files /dev/null and b/tests/_support/Images/ci-logo.png differ diff --git a/tests/_support/Language/MockLanguage.php b/tests/_support/Language/MockLanguage.php new file mode 100644 index 000000000000..08f80b0dc6af --- /dev/null +++ b/tests/_support/Language/MockLanguage.php @@ -0,0 +1,59 @@ +data = $data; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Provides an override that allows us to set custom + * data to be returned easily during testing. + * + * @param string $path + * + * @return array|mixed + */ + protected function requireFile(string $path): array + { + return $this->data ?? []; + } + + //-------------------------------------------------------------------- + + /** + * Arbitrarily turnoff internationalization support for testing + */ + public function disableIntlSupport() + { + $this->intlSupport = false; + } + +} diff --git a/tests/_support/Language/SecondMockLanguage.php b/tests/_support/Language/SecondMockLanguage.php new file mode 100644 index 000000000000..7142885434ed --- /dev/null +++ b/tests/_support/Language/SecondMockLanguage.php @@ -0,0 +1,27 @@ +load($file, $locale, $return); + } + + //-------------------------------------------------------------------- + + /** + * Expose the loaded language files + */ + public function loaded(string $locale = 'en') + { + return $this->loadedFiles[$locale]; + } + +} diff --git a/tests/_support/Language/en/More.php b/tests/_support/Language/en/More.php new file mode 100644 index 000000000000..57d3d8abf1e1 --- /dev/null +++ b/tests/_support/Language/en/More.php @@ -0,0 +1,7 @@ + 'These are not the droids you are looking for', + 'notaMoon' => "That's no moon... it's a space station", + 'cannotMove' => "I have a very bad feeling about this", +]; diff --git a/tests/_support/Log/Handlers/MockChromeHandler.php b/tests/_support/Log/Handlers/MockChromeHandler.php new file mode 100644 index 000000000000..7164e324c1ae --- /dev/null +++ b/tests/_support/Log/Handlers/MockChromeHandler.php @@ -0,0 +1,24 @@ +json['rows'][0]; + } + +} diff --git a/tests/_support/Log/Handlers/MockFileHandler.php b/tests/_support/Log/Handlers/MockFileHandler.php new file mode 100644 index 000000000000..b674d175e7d5 --- /dev/null +++ b/tests/_support/Log/Handlers/MockFileHandler.php @@ -0,0 +1,25 @@ +handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; + } + +} \ No newline at end of file diff --git a/tests/_support/Log/Handlers/TestHandler.php b/tests/_support/Log/Handlers/TestHandler.php index de34d92abdbf..b62b47830f88 100644 --- a/tests/_support/Log/Handlers/TestHandler.php +++ b/tests/_support/Log/Handlers/TestHandler.php @@ -1,4 +1,6 @@ -handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; self::$logs = []; } //-------------------------------------------------------------------- - /** - * Checks whether the Handler will handle logging items of this - * log Level. - * - * @param $level - * - * @return bool - */ - public function canHandle(string $level): bool - { - return in_array($level, $this->handles); - } - - //-------------------------------------------------------------------- - - /** - * Stores the date format to use while logging messages. - * - * @param string $format - * - * @return HandlerInterface - */ - public function setDateFormat(string $format) - { - $this->dateFormat = $format; - - return $this; - } - - //-------------------------------------------------------------------- - /** * Handles logging the message. * If the handler returns false, then execution of handlers @@ -81,7 +48,7 @@ public function handle($level, $message): bool { $date = date($this->dateFormat); - self::$logs[] = strtoupper($level).' - '.$date.' --> '.$message; + self::$logs[] = strtoupper($level) . ' - ' . $date . ' --> ' . $message; return true; } @@ -90,10 +57,8 @@ public function handle($level, $message): bool public static function getLogs() { - return self::$logs; + return self::$logs; } //-------------------------------------------------------------------- - - -} \ No newline at end of file +} diff --git a/tests/_support/Log/TestLogger.php b/tests/_support/Log/TestLogger.php index 363f3195a4f5..613398222aef 100644 --- a/tests/_support/Log/TestLogger.php +++ b/tests/_support/Log/TestLogger.php @@ -1,4 +1,6 @@ - $level, - 'message' => $log_message, - 'file' => $file, + 'level' => $level, + 'message' => $log_message, + 'file' => $file, ]; // Let the parent do it's thing. @@ -52,16 +54,16 @@ public function log($level, $message, array $context = []): bool /** * Used by CIUnitTestCase class to provide ->assertLogged() methods. * - * @param string $level - * @param $message - * @param string|null $file + * @param string $level + * @param string $message + * + * @return bool */ public static function didLog(string $level, $message) { foreach (self::$op_logs as $log) { - if (strtolower($log['level']) == strtolower($level) - && $message == $log['message']) + if (strtolower($log['level']) == strtolower($level) && $message == $log['message']) { return true; } @@ -71,5 +73,10 @@ public static function didLog(string $level, $message) } //-------------------------------------------------------------------- + // Expose cleanFileNames() + public function cleanup($file) + { + return $this->cleanFileNames($file); + } } diff --git a/tests/_support/MockCodeIgniter.php b/tests/_support/MockCodeIgniter.php index 9e2db886e358..7bb0cee7d9a9 100644 --- a/tests/_support/MockCodeIgniter.php +++ b/tests/_support/MockCodeIgniter.php @@ -1,4 +1,6 @@ -tokens[] = 'beforeInsert'; + + return $data; + } + + protected function afterInsertMethod(array $data) + { + $this->tokens[] = 'afterInsert'; + + return $data; + } + + protected function beforeUpdateMethod(array $data) + { + $this->tokens[] = 'beforeUpdate'; + + return $data; + } + + protected function afterUpdateMethod(array $data) + { + $this->tokens[] = 'afterUpdate'; + + return $data; + } + + protected function afterFindMethod(array $data) + { + $this->tokens[] = 'afterFind'; + + return $data; + } + + protected function afterDeleteMethod(array $data) + { + $this->tokens[] = 'afterDelete'; + + return $data; + } + + public function hasToken(string $token) + { + return in_array($token, $this->tokens); + } + +} diff --git a/tests/_support/Models/JobModel.php b/tests/_support/Models/JobModel.php index 7130ec9cafc4..c9b3fcb1f59b 100644 --- a/tests/_support/Models/JobModel.php +++ b/tests/_support/Models/JobModel.php @@ -11,4 +11,6 @@ class JobModel extends Model protected $useSoftDeletes = false; protected $dateFormat = 'integer'; + + protected $allowedFields = ['name', 'description']; } diff --git a/tests/_support/Models/SimpleEntity.php b/tests/_support/Models/SimpleEntity.php new file mode 100644 index 000000000000..2664fb7294f1 --- /dev/null +++ b/tests/_support/Models/SimpleEntity.php @@ -0,0 +1,20 @@ + ['required', 'min_length[3]'], + 'token' => 'in_list[{id}]' + ]; + + protected $validationMessages = [ + 'name' => [ + 'required' => 'You forgot to name the baby.', + 'min_length' => 'Too short, man!', + ] + ]; +} diff --git a/tests/_support/Security/MockSecurity.php b/tests/_support/Security/MockSecurity.php index 106cdc11b10f..7a9239f53b80 100644 --- a/tests/_support/Security/MockSecurity.php +++ b/tests/_support/Security/MockSecurity.php @@ -1,5 +1,6 @@ -driver, true); + } + + //-------------------------------------------------------------------- + + /** + * Starts the session. + * Extracted for testing reasons. + */ + protected function startSession() + { +// session_start(); + } + + //-------------------------------------------------------------------- + + /** + * Takes care of setting the cookie on the client side. + * Extracted for testing reasons. + */ + protected function setCookie() + { + $this->cookies[] = [ + $this->sessionCookieName, + session_id(), + (empty($this->sessionExpiration) ? 0 : time()+$this->sessionExpiration), + $this->cookiePath, + $this->cookieDomain, + $this->cookieSecure, + true + ]; + } + + //-------------------------------------------------------------------- + + public function regenerate(bool $destroy = false) + { + $this->didRegenerate = true; + $_SESSION['__ci_last_regenerate'] = time(); + } + + //-------------------------------------------------------------------- +} diff --git a/tests/_support/Validation/TestRules.php b/tests/_support/Validation/TestRules.php new file mode 100644 index 000000000000..6c5cc46356c4 --- /dev/null +++ b/tests/_support/Validation/TestRules.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/tests/_support/View/Views/simpler.php b/tests/_support/View/Views/simpler.php new file mode 100644 index 000000000000..0588b62cd136 --- /dev/null +++ b/tests/_support/View/Views/simpler.php @@ -0,0 +1 @@ +

{testString}

\ No newline at end of file diff --git a/tests/_support/_Services.php b/tests/_support/_Services.php deleted file mode 100644 index 1b81c7399f5d..000000000000 --- a/tests/_support/_Services.php +++ /dev/null @@ -1,10 +0,0 @@ -load(); -unset($env); - -/* - * ------------------------------------------------------ - * Load the framework constants - * ------------------------------------------------------ - */ -if (file_exists(APPPATH.'Config/'.ENVIRONMENT.'/Constants.php')) -{ - require_once APPPATH.'Config/'.ENVIRONMENT.'/Constants.php'; -} - -require_once(APPPATH.'Config/Constants.php'); - -/* - * ------------------------------------------------------ - * Setup the autoloader - * ------------------------------------------------------ - */ -// The autoloader isn't initialized yet, so load the file manually. -require BASEPATH.'Autoloader/Autoloader.php'; -require APPPATH.'Config/Autoload.php'; -require APPPATH.'Config/Services.php'; -// Use special Services for testing. -require SUPPORTPATH.'Services.php'; - -// The Autoloader class only handles namespaces -// and "legacy" support. -$loader = CodeIgniter\Services::autoloader(); -$loader->initialize(new Config\Autoload()); - -// The register function will prepend -// the psr4 loader. -$loader->register(); - -// Add namespace paths to autoload mocks for testing. -$loader->addNamespace('CodeIgniter', SUPPORTPATH); -$loader->addNamespace('Config', SUPPORTPATH.'Config'); - -/* - * ------------------------------------------------------ - * Load the global functions - * ------------------------------------------------------ - */ - -// Use special global functions for testing. -require_once SUPPORTPATH.'MockCommon.php'; -require_once BASEPATH.'Common.php'; - -/* - * ------------------------------------------------------ - * Set custom exception handling - * ------------------------------------------------------ - */ -$config = new \Config\App(); - -Config\Services::exceptions($config, true) - ->initialize(); - -//-------------------------------------------------------------------- -// Should we use a Composer autoloader? -//-------------------------------------------------------------------- - -if ($composer_autoload = $config->composerAutoload) +// Set environment values that would otherwise stop the framework from functioning during tests. +if (! isset($_SERVER['app.baseURL'])) { - if ($composer_autoload === TRUE) - { - file_exists(APPPATH.'vendor/autoload.php') - ? require_once(APPPATH.'vendor/autoload.php') - : log_message('error', '$config->\'composerAutoload\' is set to TRUE but '.APPPATH.'vendor/autoload.php was not found.'); - } - elseif (file_exists($composer_autoload)) - { - require_once($composer_autoload); - } - else - { - log_message('error', 'Could not find the specified $config->\'composerAutoload\' path: '.$composer_autoload); - } + $_SERVER['app.baseURL'] = 'http://example.com'; } //-------------------------------------------------------------------- // Load our TestCase //-------------------------------------------------------------------- -require_once __DIR__ .'/CIUnitTestCase.php'; +require __DIR__.'/CIUnitTestCase.php'; diff --git a/tests/_support/_database/migrations/20160428212500_Create_test_tables.php b/tests/_support/_database/migrations/20160428212500_Create_test_tables.php deleted file mode 100644 index 60603d1e7939..000000000000 --- a/tests/_support/_database/migrations/20160428212500_Create_test_tables.php +++ /dev/null @@ -1,99 +0,0 @@ -forge->addField([ - 'id' => [ - 'type' => 'INTEGER', - 'constraint' => 3, - 'auto_increment' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 40, - ], - 'email' => [ - 'type' => 'VARCHAR', - 'constraint' => 100, - ], - 'country' => [ - 'type' => 'VARCHAR', - 'constraint' => 40, - ], - 'deleted' => [ - 'type' => 'TINYINT', - 'constraint' => 1, - 'default' => '0' - ], - ]); - $this->forge->addKey('id', true); - $this->forge->createTable('user', true); - - // Job Table - $this->forge->addField([ - 'id' => [ - 'type' => 'INTEGER', - 'constraint' => 3, - 'auto_increment' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 40, - ], - 'description' => [ - 'type' => 'TEXT', - ], - ]); - $this->forge->addKey('id', true); - $this->forge->createTable('job', true); - - // Misc Table - $this->forge->addField([ - 'id' => [ - 'type' => 'INTEGER', - 'constraint' => 3, - 'auto_increment' => true, - ], - 'key' => [ - 'type' => 'VARCHAR', - 'constraint' => 40, - ], - 'value' => [ - 'type' => 'TEXT', - ], - ]); - $this->forge->addKey('id', true); - $this->forge->createTable('misc', true); - - // Empty Table - $this->forge->addField([ - 'id' => [ - 'type' => 'INTEGER', - 'constraint' => 3, - 'auto_increment' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 40, - ], - ]); - $this->forge->addKey('id', true); - $this->forge->createTable('empty', true); - } - - //-------------------------------------------------------------------- - - public function down() - { - $this->forge->dropTable('user'); - $this->forge->dropTable('job'); - $this->forge->dropTable('misc'); - $this->forge->dropTable('empty'); - } - - //-------------------------------------------------------------------- - -} diff --git a/tests/_support/_database/seeds/CITestSeeder.php b/tests/_support/_database/seeds/CITestSeeder.php deleted file mode 100644 index 5174084047be..000000000000 --- a/tests/_support/_database/seeds/CITestSeeder.php +++ /dev/null @@ -1,42 +0,0 @@ - array( - array('name' => 'Derek Jones', 'email' => 'derek@world.com', 'country' => 'US'), - array('name' => 'Ahmadinejad', 'email' => 'ahmadinejad@world.com', 'country' => 'Iran'), - array('name' => 'Richard A Causey', 'email' => 'richard@world.com', 'country' => 'US'), - array('name' => 'Chris Martin', 'email' => 'chris@world.com', 'country' => 'UK') - ), - 'job' => array( - array('name' => 'Developer', 'description' => 'Awesome job, but sometimes makes you bored'), - array('name' => 'Politician', 'description' => 'This is not really a job'), - array('name' => 'Accountant', 'description' => 'Boring job, but you will get free snack at lunch'), - array('name' => 'Musician', 'description' => 'Only Coldplay can actually called Musician') - ), - 'misc' => array( - array('key' => '\\xxxfoo456', 'value' => 'Entry with \\xxx'), - array('key' => '\\%foo456', 'value' => 'Entry with \\%'), - array('key' => 'spaces and tabs', 'value' => ' One two three tab') - ) - ); - - foreach ($data as $table => $dummy_data) - { - $this->db->table($table)->truncate(); - - foreach ($dummy_data as $single_dummy_data) - { - $this->db->table($table)->insert($single_dummy_data); - } - } - } - - //-------------------------------------------------------------------- - - -} diff --git a/tests/bin/php-coveralls.phar b/tests/bin/php-coveralls.phar new file mode 100755 index 000000000000..30523379b445 Binary files /dev/null and b/tests/bin/php-coveralls.phar differ diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php new file mode 100644 index 000000000000..9ac5356fa315 --- /dev/null +++ b/tests/system/API/ResponseTraitTest.php @@ -0,0 +1,377 @@ +formatter = new JSONFormatter(); + } + + protected function makeController(array $userConfig = [], string $uri = 'http://example.com', array $userHeaders = []) + { + $config = [ + 'baseURL' => 'http://example.com', + 'uriProtocol' => 'REQUEST_URI', + 'defaultLocale' => 'en', + 'negotiateLocale' => false, + 'supportedLocales' => ['en'], + 'CSPEnabled' => false, + 'cookiePrefix' => '', + 'cookieDomain' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + 'cookieHTTPOnly' => false, + 'proxyIPs' => [] + ]; + + $config = array_merge($config, $userConfig); + + if (is_null($this->request)) + { + $this->request = new MockIncomingRequest((object)$config, new URI($uri), null, new UserAgent()); + $this->response = new MockResponse((object)$config); + } + + // Insert headers into request. + $headers = [ + 'Accept' => 'text/html' + ]; + $headers = array_merge($headers, $userHeaders); + + foreach ($headers as $key => $value) + { + $this->request->setHeader($key, $value); + if (($key == 'Accept') && ! is_array($value)) + $this->response->setContentType($value); + } + + // Create the controller class finally. + $controller = new class($this->request, $this->response, $this->formatter) + { + + use ResponseTrait; + + protected $request; + protected $response; + protected $formatter; + + public function __construct(&$request, &$response, &$formatter) + { + $this->request = $request; + $this->response = $response; + $this->formatter = $formatter; + } + }; + + return $controller; + } + + public function testNoFormatterJSON() + { + $this->formatter = null; + $controller = $this->makeController([], 'http://codeigniter.com', ['Accept' => 'application/json']); + $controller->respondCreated(['id' => 3], 'A Custom Reason'); + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(201, $this->response->getStatusCode()); + + $expected = <<assertEquals($expected, $this->response->getBody()); + } + + public function testNoFormatterHTML() + { + $this->formatter = null; + $controller = $this->makeController(); + $controller->respondCreated('A Custom Reason'); + + $this->assertEquals('A Custom Reason', $this->response->getBody()); + } + + public function testRespondSets404WithNoData() + { + $controller = $this->makeController(); + $controller->respond(null, null); + + $this->assertEquals(404, $this->response->getStatusCode()); + $this->assertNull($this->response->getBody()); + } + + public function testRespondSetsStatusWithEmptyData() + { + $controller = $this->makeController(); + $controller->respond(null, 201); + + $this->assertEquals(201, $this->response->getStatusCode()); + $this->assertNull($this->response->getBody()); + } + + public function testRespondSetsCorrectBodyAndStatus() + { + $controller = $this->makeController(); + $controller->respond('something', 201); + + $this->assertEquals(201, $this->response->getStatusCode()); + $this->assertEquals('something', $this->response->getBody()); + $this->assertStringStartsWith('text/html', $this->response->getHeaderLine('Content-Type')); + $this->assertEquals('Created', $this->response->getReason()); + } + + public function testRespondWithCustomReason() + { + $controller = $this->makeController(); + $controller->respond('something', 201, 'A Custom Reason'); + + $this->assertEquals(201, $this->response->getStatusCode()); + $this->assertEquals('A Custom Reason', $this->response->getReason()); + } + + public function testFailSingleMessage() + { + $controller = $this->makeController(); + + $controller->fail('Failure to Launch', 500, 'WHAT!', 'A Custom Reason'); + + // Will use the JSON formatter by default + $expected = [ + 'status' => 500, + 'error' => 'WHAT!', + 'messages' => [ + 'error' => 'Failure to Launch' + ] + ]; + + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + $this->assertEquals(500, $this->response->getStatusCode()); + $this->assertEquals('A Custom Reason', $this->response->getReason()); + } + + public function testCreated() + { + $controller = $this->makeController(); + $controller->respondCreated(['id' => 3], 'A Custom Reason'); + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(201, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format(['id' => 3]), $this->response->getBody()); + } + + public function testDeleted() + { + $controller = $this->makeController(); + $controller->respondDeleted(['id' => 3], 'A Custom Reason'); + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format(['id' => 3]), $this->response->getBody()); + } + + public function testUnauthorized() + { + $controller = $this->makeController(); + $controller->failUnauthorized('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 401, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(401, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testForbidden() + { + $controller = $this->makeController(); + $controller->failForbidden('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 403, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(403, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testNotFound() + { + $controller = $this->makeController(); + $controller->failNotFound('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 404, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(404, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testValidationError() + { + $controller = $this->makeController(); + $controller->failValidationError('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 400, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(400, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testResourceExists() + { + $controller = $this->makeController(); + $controller->failResourceExists('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 409, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(409, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testResourceGone() + { + $controller = $this->makeController(); + $controller->failResourceGone('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 410, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(410, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testTooManyRequests() + { + $controller = $this->makeController(); + $controller->failTooManyRequests('Nope', 'FAT CHANCE', 'A Custom Reason'); + + $expected = [ + 'status' => 429, + 'error' => 'FAT CHANCE', + 'messages' => [ + 'error' => 'Nope' + ] + ]; + + $this->assertEquals('A Custom Reason', $this->response->getReason()); + $this->assertEquals(429, $this->response->getStatusCode()); + $this->assertEquals($this->formatter->format($expected), $this->response->getBody()); + } + + public function testServerError() + { + $controller = $this->makeController(); + $controller->failServerError('Nope.', 'FAT-CHANCE', 'A custom reason.'); + + $this::assertEquals('A custom reason.', $this->response->getReason()); + $this::assertEquals(500, $this->response->getStatusCode()); + $this::assertEquals($this->formatter->format([ + 'status' => 500, + 'error' => 'FAT-CHANCE', + 'messages' => [ + 'error' => 'Nope.' + ] + ]), $this->response->getBody()); + } + + public function testValidContentTypes() + { + $chars = '; charset=UTF-8'; + $goodMimes = ['text/xml', 'text/html', 'application/json', 'application/xml']; + for ($i = 0; $i < count($goodMimes); $i ++ ) + $this->tryValidContentType($goodMimes[$i], $goodMimes[$i] . $chars); + } + + private function tryValidContentType($mimeType, $contentType) + { + $original = $_SERVER; + $_SERVER['CONTENT_TYPE'] = $mimeType; + + $controller = $this->makeController([], 'http://codeigniter.com', ['Accept' => $mimeType]); + $this->assertEquals($mimeType, $this->request->getHeaderLine('Accept'), 'Request header...'); + $this->response->setContentType($contentType); + $this->assertEquals($contentType, $this->response->getHeaderLine('Content-Type'), 'Response header pre-response...'); + + $_SERVER = $original; + } + + public function testValidResponses() + { + $chars = '; charset=UTF-8'; + $goodMimes = ['text/xml', 'text/html', 'application/json', 'application/xml']; + for ($i = 0; $i < count($goodMimes); $i ++ ) + $this->tryValidContentType($goodMimes[$i], $goodMimes[$i] . $chars); + } + + public function testXMLFormatter() + { + $this->formatter = new XMLFormatter(); + $controller = $this->makeController(); + + $this->assertEquals('CodeIgniter\Format\XMLFormatter', get_class($this->formatter)); + + $controller->respondCreated(['id' => 3], 'A Custom Reason'); + $expected = << +3 + +EOH; + $this->assertEquals($expected, $this->response->getBody()); + } + +} diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index 211d89dcdb4e..e19cd81e7d11 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -1,6 +1,7 @@ classmap = [ - 'FirstClass' => '/app/dir/First.php', - 'Name\Spaced\Class' => '/app/namespace/Class.php', + 'FirstClass' => '/app/dir/First.php', + 'Name\Spaced\Class' => '/app/namespace/Class.php', ]; $config->psr4 = [ - 'App\Controllers' => '/application/Controllers', - 'App\Libraries' => '/application/somewhere', + 'App\Controllers' => '/application/Controllers', + 'App\Libraries' => '/application/somewhere', ]; $this->loader = new MockAutoloader(); @@ -32,25 +35,53 @@ protected function setUp() '/application/somewhere/Classname.php', '/app/dir/First.php', '/app/namespace/Class.php', - '/my/app/Class.php', - APPPATH.'Libraries/someLibrary.php', - APPPATH.'Models/someModel.php', + '/my/app/Class.php', + APPPATH . 'Libraries/someLibrary.php', + APPPATH . 'Models/someModel.php', + APPPATH . 'Models/Some/CoolModel.php', ]); } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- // PSR4 Namespacing //-------------------------------------------------------------------- + public function testServiceAutoLoaderFromShareInstances() + { + + $auto_loader = \CodeIgniter\Config\Services::autoloader(); + // $auto_loader->register(); + // look for Home controller, as that should be in base repo + $actual = $auto_loader->loadClass('App\Controllers\Home'); + $expected = APPPATH . 'Controllers/Home.php'; + $this->assertSame($expected, $actual); + } + + //-------------------------------------------------------------------- + + public function testServiceAutoLoader() + { + + $getShared = false; + $auto_loader = \CodeIgniter\Config\Services::autoloader($getShared); + $auto_loader->initialize(new Autoload()); + $auto_loader->register(); + // look for Home controller, as that should be in base repo + $actual = $auto_loader->loadClass('App\Controllers\Home'); + $expected = APPPATH . 'Controllers/Home.php'; + $this->assertSame($expected, $actual); + } + + //-------------------------------------------------------------------- + public function testExistingFile() { - $actual = $this->loader->loadClass('App\Controllers\Classname'); + $actual = $this->loader->loadClass('App\Controllers\Classname'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); - $actual = $this->loader->loadClass('App\Libraries\Classname'); + $actual = $this->loader->loadClass('App\Libraries\Classname'); $expected = '/application/somewhere/Classname.php'; $this->assertSame($expected, $actual); } @@ -59,7 +90,7 @@ public function testExistingFile() public function testMatchesWithPreceedingSlash() { - $actual = $this->loader->loadClass('\App\Controllers\Classname'); + $actual = $this->loader->loadClass('\App\Controllers\Classname'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); } @@ -68,7 +99,7 @@ public function testMatchesWithPreceedingSlash() public function testMatchesWithFileExtension() { - $actual = $this->loader->loadClass('\App\Controllers\Classname.php'); + $actual = $this->loader->loadClass('\App\Controllers\Classname.php'); $expected = '/application/Controllers/Classname.php'; $this->assertSame($expected, $actual); } @@ -77,13 +108,26 @@ public function testMatchesWithFileExtension() public function testMissingFile() { - $this->assertFalse($this->loader->loadClass('App\Missing\Classname')); + $this->assertFalse($this->loader->loadClass('\App\Missing\Classname')); } //-------------------------------------------------------------------- - //-------------------------------------------------------------------- + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Config array must contain either the 'psr4' key or the 'classmap' key. + */ + public function testInitializeException() + { + $config = new Autoload(); + $config->classmap = []; + $config->psr4 = []; + + $this->loader = new MockAutoloader(); + $this->loader->initialize($config); + } + public function testAddNamespaceWorks() { $this->assertFalse($this->loader->loadClass('My\App\Class')); @@ -114,13 +158,39 @@ public function testAddNamespaceMultiplePathsWorks() $this->assertSame($expected, $actual); } + public function testAddNamespaceStingToArray() + { + $this->loader->addNamespace('App\Controllers', '/application/Controllers'); + + $this->assertSame('/application/Controllers/Classname.php', $this->loader->loadClass('App\Controllers\Classname')); + } + + //-------------------------------------------------------------------- + + public function testRemoveNamespace() + { + $this->loader->addNamespace('My\App', '/my/app'); + $this->assertSame('/my/app/Class.php', $this->loader->loadClass('My\App\Class')); + + $this->loader->removeNamespace('My\App'); + $this->assertFalse((bool) $this->loader->loadClass('My\App\Class')); + } + //-------------------------------------------------------------------- public function testLoadLegacy() { - $this->assertFalse((bool)$this->loader->loadClass('someLibraries')); - $this->assertTrue((bool)$this->loader->loadClass('someLibrary')); - $this->assertTrue((bool)$this->loader->loadClass('someModel')); + // should not be able to find a folder + $this->assertFalse((bool) $this->loader->loadClass('someLibraries')); + // should be able to find these because we said so in the MockAutoloader + $this->assertTrue((bool) $this->loader->loadClass('someLibrary')); + $this->assertTrue((bool) $this->loader->loadClass('someModel')); + // should not be able to find these - don't exist + $this->assertFalse((bool) $this->loader->loadClass('anotherLibrary')); + $this->assertFalse((bool) $this->loader->loadClass('\nester\anotherLibrary')); + $this->assertFalse((bool) $this->loader->loadClass('\Shouldnt\Find\This')); + // should not be able to find these legacy classes - namespaced + $this->assertFalse($this->loader->loadClass('\Some\CoolModel')); } //-------------------------------------------------------------------- @@ -134,7 +204,7 @@ public function testSanitizationSimply() } //-------------------------------------------------------------------- - + public function testSanitizationAllowsWindowsFilepaths() { $test = 'C:\path\to\some/file.php'; diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index 17f482ea3011..82ca6349638f 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -1,9 +1,11 @@ psr4 = [ - 'App\Libraries' => '/application/somewhere', - 'App' => '/application', - 'Blog' => '/modules/blog' + 'App\Libraries' => '/application/somewhere', + 'App' => '/application', + 'Sys' => BASEPATH, + 'Blog' => '/modules/blog', + 'Tests/Support' => TESTPATH.'_support/', ]; $this->loader = new MockFileLocator($config); $this->loader->setFiles([ - APPPATH.'index.php', - APPPATH.'Views/index.php', - APPPATH.'Views/admin/users/create.php', + APPPATH . 'index.php', + APPPATH . 'Views/index.php', + APPPATH . 'Views/admin/users/create.php', '/modules/blog/Views/index.php', '/modules/blog/Views/admin/posts.php' ]); @@ -37,7 +43,7 @@ public function testLocateFileWorksInApplicationDirectory() { $file = 'index'; - $expected = APPPATH.'Views/index.php'; + $expected = APPPATH . 'Views/index.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -48,7 +54,7 @@ public function testLocateFileWorksInApplicationDirectoryWithoutFolder() { $file = 'index'; - $expected = APPPATH.'index.php'; + $expected = APPPATH . 'index.php'; $this->assertEquals($expected, $this->loader->locateFile($file)); } @@ -59,7 +65,7 @@ public function testLocateFileWorksInNestedApplicationDirectory() { $file = 'admin/users/create'; - $expected = APPPATH.'Views/admin/users/create.php'; + $expected = APPPATH . 'Views/admin/users/create.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -81,7 +87,7 @@ public function testLocateFileReplacesFolderNameLegacy() { $file = 'Views/index.php'; - $expected = APPPATH.'Views/index.php'; + $expected = APPPATH . 'Views/index.php'; $this->assertEquals($expected, $this->loader->locateFile($file, 'Views')); } @@ -118,4 +124,129 @@ public function testLocateFileReturnsEmptyWithBadNamespace() } //-------------------------------------------------------------------- + + public function testSearchSimple() + { + $expected = rtrim(APPPATH, '/') . '/Config/App.php'; + + $foundFiles = $this->loader->search('Config/App.php'); + + $this->assertEquals($expected, $foundFiles[0]); + } + + //-------------------------------------------------------------------- + + public function testSearchWithFileExtension() + { + $expected = rtrim(APPPATH, '/') . '/Config/App.php'; + + $foundFiles = $this->loader->search('Config/App', 'php'); + + $this->assertEquals($expected, $foundFiles[0]); + } + + //-------------------------------------------------------------------- + + public function testSearchWithMultipleFilesFound() + { + $foundFiles = $this->loader->search('index', 'html'); + + $expected = rtrim(APPPATH, '/') . '/index.html'; + $this->assertContains($expected, $foundFiles); + + $expected = rtrim(BASEPATH, '/') . '/index.html'; + $this->assertContains($expected, $foundFiles); + } + + //-------------------------------------------------------------------- + + public function testSearchForFileNotExist() + { + $foundFiles = $this->loader->search('Views/Fake.html'); + + $this->assertArrayNotHasKey(0, $foundFiles); + } + + //-------------------------------------------------------------------- + + public function testListFilesSimple() + { + $files = $this->loader->listFiles('Config/'); + + $expectedWin = APPPATH . 'Config\App.php'; + $expectedLin = APPPATH . 'Config/App.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); + } + + //-------------------------------------------------------------------- + + public function testListFilesWithFileAsInput() + { + $files = $this->loader->listFiles('Config/App.php'); + + $this->assertEmpty($files); + } + + //-------------------------------------------------------------------- + + public function testListFilesFromMultipleDir() + { + $files = $this->loader->listFiles('Filters/'); + + $expectedWin = APPPATH . 'Filters\DebugToolbar.php'; + $expectedLin = APPPATH . 'Filters/DebugToolbar.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); + + $expectedWin = BASEPATH . 'Filters\Filters.php'; + $expectedLin = BASEPATH . 'Filters/Filters.php'; + $this->assertTrue(in_array($expectedWin, $files) || in_array($expectedLin, $files)); + } + + //-------------------------------------------------------------------- + + public function testListFilesWithPathNotExist() + { + $files = $this->loader->listFiles('Fake/'); + + $this->assertEmpty($files); + } + + //-------------------------------------------------------------------- + + public function testListFilesWithoutPath() + { + $files = $this->loader->listFiles(''); + + $this->assertEmpty($files); + } + + public function testFindQNameFromPathSimple() + { + $ClassName = $this->loader->findQualifiedNameFromPath('system/HTTP/Header.php'); + $expected = '\Sys\HTTP\Header'; + + $this->assertEquals($expected, $ClassName); + } + + public function testFindQNameFromPathWithNumericNamespace() + { + $ClassName = $this->loader->findQualifiedNameFromPath('application/Config/App.php'); + + $this->assertNull($ClassName); + } + + public function testFindQNameFromPathWithFileNotExist() + { + $ClassName = $this->loader->findQualifiedNameFromPath('modules/blog/Views/index.php'); + + $this->assertNull($ClassName); + } + + public function testFindQNameFromPathWithoutCorrespondingNamespace() + { + $ClassName = $this->loader->findQualifiedNameFromPath('tests/system/CodeIgniterTest.php'); + + $this->assertNull($ClassName); + } + } diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 4c9ee830ddee..0b4f34ed6da9 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -1,10 +1,318 @@ stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + } + + public function tearDown() + { + stream_filter_remove($this->stream_filter); + } + public function testNew() { $actual = new CLI(); $this->assertInstanceOf(CLI::class, $actual); } + + public function testBeep() + { + $this->expectOutputString("\x07"); + CLI::beep(); + } + + public function testBeep4() + { + $this->expectOutputString("\x07\x07\x07\x07"); + CLI::beep(4); + } + + public function testWait() + { + $time = time(); + CLI::wait(1, true); + $this->assertEquals(1, time() - $time); + + $time = time(); + CLI::wait(1); + $this->assertEquals(1, time() - $time); + + // Leaving the code fragment below in, to remind myself (or others) + // of what appears to be the most likely path to test this last + // bit of wait() functionality. + // The problem: if the block below is enabled, the phpunit tests + // go catatonic when it is executed, presumably because of + // the CLI::input() waiting for a key press +// // test the press any key to continue... +// stream_filter_register('CLITestKeyboardFilter', 'CodeIgniter\CLI\CLITestKeyboardFilter'); +// $spoofer = stream_filter_append(STDIN, 'CLITestKeyboardFilter'); +// $time = time(); +// CLITestKeyboardFilter::$spoofed = ' '; +// CLI::wait(0); +// stream_filter_remove($spoofer); +// $this->assertEquals(0, time() - $time); + } + + public function testIsWindows() + { + $this->assertEquals(('\\' === DIRECTORY_SEPARATOR), CLI::isWindows()); + $this->assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), CLI::isWindows()); + } + + public function testNewLine() + { + $this->expectOutputString(''); + CLI::newLine(); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Invalid foreground color: Foreground + */ + public function testColorExceptionForeground() + { + CLI::color('test', 'Foreground'); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage Invalid background color: Background + */ + public function testColorExceptionBackground() + { + CLI::color('test', 'white', 'Background'); + } + + public function testColor() + { + $this->assertEquals("\033[1;37m\033[42m\033[4mtest\033[0m", CLI::color('test', 'white', 'green', 'underline')); + } + + public function testWrite() + { + CLI::write('test'); + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testWriteForeground() + { + CLI::write('test', 'red'); + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testWriteBackground() + { + CLI::write('test', 'red', 'green'); + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testError() + { + $this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter'); + CLI::error('test'); + // red expected cuz stderr + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testErrorForeground() + { + $this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter'); + CLI::error('test', 'purple'); + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testErrorBackground() + { + $this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter'); + CLI::error('test', 'purple', 'green'); + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testShowProgress() + { + CLI::write('first.'); + CLI::showProgress(1, 20); + CLI::showProgress(10, 20); + CLI::showProgress(20, 20); + CLI::write('second.'); + CLI::showProgress(1, 20); + CLI::showProgress(10, 20); + CLI::showProgress(20, 20); + CLI::write('third.'); + CLI::showProgress(1, 20); + + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testShowProgressWithoutBar() + { + CLI::write('first.'); + CLI::showProgress(false, 20); + CLI::showProgress(false, 20); + CLI::showProgress(false, 20); + + $expected = <<assertEquals($expected, CITestStreamFilter::$buffer); + } + + public function testWrap() + { + $this->assertEquals('', CLI::wrap('')); + $this->assertEquals('1234' . PHP_EOL . ' 5678' . PHP_EOL . ' 90' . PHP_EOL . ' abc' . PHP_EOL . ' de' . PHP_EOL . ' fghij' . PHP_EOL . ' 0987654321', CLI::wrap('1234 5678 90' . PHP_EOL . 'abc de fghij' . PHP_EOL . '0987654321', 5, 1)); + $this->assertEquals('1234 5678 90' . PHP_EOL . ' abc de fghij' . PHP_EOL . ' 0987654321', CLI::wrap('1234 5678 90' . PHP_EOL . 'abc de fghij' . PHP_EOL . '0987654321', 999, 2)); + $this->assertEquals('1234 5678 90' . PHP_EOL . 'abc de fghij' . PHP_EOL . '0987654321', CLI::wrap('1234 5678 90' . PHP_EOL . 'abc de fghij' . PHP_EOL . '0987654321')); + } + + public function testParseCommand() + { + $_SERVER['argv'] = ['ignored', 'b', 'c']; + $_SERVER['argc'] = 3; + CLI::init(); + $this->assertEquals(null, CLI::getSegment(3)); + $this->assertEquals('b', CLI::getSegment(1)); + $this->assertEquals('c', CLI::getSegment(2)); + $this->assertEquals('b/c', CLI::getURI()); + $this->assertEquals([], CLI::getOptions()); + $this->assertEmpty(CLI::getOptionString()); + $this->assertEquals(['b', 'c'], CLI::getSegments()); + } + + public function testParseCommandMixed() + { + $_SERVER['argv'] = ['ignored', 'b', 'c', 'd', '-parm', 'pvalue', 'd2']; + $_SERVER['argc'] = 7; + CLI::init(); + $this->assertEquals(null, CLI::getSegment(7)); + $this->assertEquals('b', CLI::getSegment(1)); + $this->assertEquals('c', CLI::getSegment(2)); + $this->assertEquals('d', CLI::getSegment(3)); + $this->assertEquals(['b', 'c', 'd', 'd2'], CLI::getSegments()); + } + + public function testParseCommandOption() + { + $_SERVER['argv'] = ['ignored', 'b', 'c', '-parm', 'pvalue', 'd']; + $_SERVER['argc'] = 6; + CLI::init(); + $this->assertEquals(['parm' => 'pvalue'], CLI::getOptions()); + $this->assertEquals('pvalue', CLI::getOption('parm')); + $this->assertEquals('-parm pvalue ', CLI::getOptionString()); + $this->assertNull(CLI::getOption('bogus')); + $this->assertEquals(['b', 'c', 'd'], CLI::getSegments()); + } + + public function testParseCommandMultipleOptions() + { + $_SERVER['argv'] = ['ignored', 'b', 'c', '-parm', 'pvalue', 'd', '-p2', '-p3', 'value 3']; + $_SERVER['argc'] = 9; + CLI::init(); + $this->assertEquals(['parm' => 'pvalue', 'p2' => null, 'p3' => 'value 3'], CLI::getOptions()); + $this->assertEquals('pvalue', CLI::getOption('parm')); + $this->assertEquals('-parm pvalue -p2 -p3 "value 3" ', CLI::getOptionString()); + $this->assertEquals(['b', 'c', 'd'], CLI::getSegments()); + } + + public function testWindow() + { + $this->assertTrue(is_int(CLI::getHeight())); + $this->assertTrue(is_int(CLI::getWidth())); + } + + /** + * @dataProvider tableProvider + * + * @param array $tbody + * @param array $thead + * @param array $expected + */ + public function testTable($tbody, $thead, $expected) + { + CLI::table($tbody, $thead); + $this->assertEquals(CITestStreamFilter::$buffer, $expected); + } + + public function tableProvider() + { + $head = ['ID', 'Title']; + $one_row = [['id' => 1, 'foo' => 'bar']]; + $many_rows = [ + ['id' => 1, 'foo' => 'bar'], + ['id' => 2, 'foo' => 'bar * 2'], + ['id' => 3, 'foo' => 'bar + bar + bar'], + ]; + + return [ + [$one_row, [], "+---+-----+\n" . + "| 1 | bar |\n" . + "+---+-----+\n"], + [$one_row, $head, "+----+-------+\n" . + "| ID | Title |\n" . + "+----+-------+\n" . + "| 1 | bar |\n" . + "+----+-------+\n"], + [$many_rows, [], "+---+-----------------+\n" . + "| 1 | bar |\n" . + "| 2 | bar * 2 |\n" . + "| 3 | bar + bar + bar |\n" . + "+---+-----------------+\n"], + [$many_rows, $head, "+----+-----------------+\n" . + "| ID | Title |\n" . + "+----+-----------------+\n" . + "| 1 | bar |\n" . + "| 2 | bar * 2 |\n" . + "| 3 | bar + bar + bar |\n" . + "+----+-----------------+\n"], + ]; + } } diff --git a/tests/system/CLI/CommandRunnerTest.php b/tests/system/CLI/CommandRunnerTest.php new file mode 100644 index 000000000000..16f7b82566da --- /dev/null +++ b/tests/system/CLI/CommandRunnerTest.php @@ -0,0 +1,93 @@ +stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + + $this->env = new \CodeIgniter\Config\DotEnv(ROOTPATH); + $this->env->load(); + + // Set environment values that would otherwise stop the framework from functioning during tests. + if ( ! isset($_SERVER['app.baseURL'])) + { + $_SERVER['app.baseURL'] = 'http://example.com'; + } + + $_SERVER['argv'] = ['spark', 'list']; + $_SERVER['argc'] = 2; + CLI::init(); + + $this->config = new MockCLIConfig(); + $this->request = new \CodeIgniter\HTTP\IncomingRequest($this->config, new \CodeIgniter\HTTP\URI('https://somwhere.com'), null, new UserAgent()); + $this->response = new \CodeIgniter\HTTP\Response($this->config); + $this->logger = Services::logger(); + $this->runner = new CommandRunner(); + $this->runner->initController($this->request, $this->response, $this->logger); + } + + public function tearDown() + { + stream_filter_remove($this->stream_filter); + } + + public function testGoodCommand() + { + $this->runner->index(['list']); + $result = CITestStreamFilter::$buffer; + + // make sure the result looks like a command list + $this->assertContains('Lists the available commands.', $result); + $this->assertContains('Displays basic usage information.', $result); + } + + public function testDefaultCommand() + { + $this->runner->index([]); + $result = CITestStreamFilter::$buffer; + + // make sure the result looks like basic help + $this->assertContains('Displays basic usage information.', $result); + $this->assertContains('help command_name', $result); + } + + public function testEmptyCommand() + { + $this->runner->index([null,'list']); + $result = CITestStreamFilter::$buffer; + + // make sure the result looks like a command list + $this->assertContains('Lists the available commands.', $result); + } + + public function testBadCommand() + { + $this->error_filter = stream_filter_append(STDERR, 'CITestStreamFilter'); + $this->runner->index(['bogus']); + $result = CITestStreamFilter::$buffer; + stream_filter_remove($this->error_filter); + + // make sure the result looks like a command list + $this->assertContains("Command 'bogus' not found", $result); + } + +} diff --git a/tests/system/CLI/ConsoleTest.php b/tests/system/CLI/ConsoleTest.php new file mode 100644 index 000000000000..0a7cfd1f177a --- /dev/null +++ b/tests/system/CLI/ConsoleTest.php @@ -0,0 +1,68 @@ +stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + + $this->env = new \CodeIgniter\Config\DotEnv(ROOTPATH); + $this->env->load(); + + // Set environment values that would otherwise stop the framework from functioning during tests. + if ( ! isset($_SERVER['app.baseURL'])) + { + $_SERVER['app.baseURL'] = 'http://example.com'; + } + + $_SERVER['argv'] = ['spark', 'list']; + $_SERVER['argc'] = 2; + CLI::init(); + + $this->app = new MockCodeIgniter(new MockCLIConfig()); + } + + public function tearDown() + { + stream_filter_remove($this->stream_filter); + } + + public function testNew() + { + $console = new \CodeIgniter\CLI\Console($this->app); + $this->assertInstanceOf(Console::class, $console); + } + + public function testHeader() + { + $console = new \CodeIgniter\CLI\Console($this->app); + $console->showHeader(); + $result = CITestStreamFilter::$buffer; + $this->assertTrue(strpos($result, 'CodeIgniter CLI Tool') > 0); + } + + public function testRun() + { + $console = new \CodeIgniter\CLI\Console($this->app); + $console->run(true); + $result = CITestStreamFilter::$buffer; + + // close open buffer + ob_end_clean(); + + // make sure the result looks like a command list + $this->assertContains('Lists the available commands.', $result); + $this->assertContains('Displays basic usage information.', $result); + } + +} diff --git a/tests/system/Cache/CacheFactoryTest.php b/tests/system/Cache/CacheFactoryTest.php new file mode 100644 index 000000000000..88cf179596cc --- /dev/null +++ b/tests/system/Cache/CacheFactoryTest.php @@ -0,0 +1,91 @@ +cacheFactory = new CacheFactory(); + + //Initialize path + $this->config = new \Config\Cache(); + $this->config->storePath .= self::$directory; + } + + public function tearDown() + { + if (is_dir($this->config->storePath)) { + chmod($this->config->storePath, 0777); + rmdir($this->config->storePath); + } + } + + public function testNew() + { + $this->assertInstanceOf(CacheFactory::class, $this->cacheFactory); + } + + /** + * @expectedException \CodeIgniter\Cache\Exceptions\CacheException + * @expectedExceptionMessage Cache config must have an array of $validHandlers. + */ + public function testGetHandlerExceptionCacheInvalidHandlers() + { + $this->config->validHandlers = null; + + $this->cacheFactory->getHandler($this->config); + } + + /** + * @expectedException \CodeIgniter\Cache\Exceptions\CacheException + * @expectedExceptionMessage Cache config must have a handler and backupHandler set. + */ + public function testGetHandlerExceptionCacheNoBackup() + { + $this->config->backupHandler = null; + + $this->cacheFactory->getHandler($this->config); + } + + /** + * @expectedException \CodeIgniter\Cache\Exceptions\CacheException + * @expectedExceptionMessage Cache config must have a handler and backupHandler set. + */ + public function testGetHandlerExceptionCacheNoHandler() + { + $this->config->handler = null; + + $this->cacheFactory->getHandler($this->config); + } + + /** + * @expectedException \CodeIgniter\Cache\Exceptions\CacheException + * @expectedExceptionMessage Cache config has an invalid handler or backup handler specified. + */ + public function testGetHandlerExceptionCacheHandlerNotFound() + { + unset($this->config->validHandlers[$this->config->handler]); + + $this->cacheFactory->getHandler($this->config); + } + + public function testGetDummyHandler() + { + if (!is_dir($this->config->storePath)) { + mkdir($this->config->storePath, 0555, true); + } + + $this->config->handler = 'dummy'; + + $this->assertInstanceOf(\CodeIgniter\Cache\Handlers\DummyHandler::class, $this->cacheFactory->getHandler($this->config)); + + //Initialize path + $this->config = new \Config\Cache(); + $this->config->storePath .= self::$directory; + } +} diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php new file mode 100644 index 000000000000..8c19a6ebc5f2 --- /dev/null +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -0,0 +1,62 @@ +dummyHandler = new DummyHandler(); + $this->dummyHandler->initialize(); + } + + public function testNew() + { + $this->assertInstanceOf(DummyHandler::class, $this->dummyHandler); + } + + public function testGet() + { + $this->assertNull($this->dummyHandler->get('key')); + } + + public function testSave() + { + $this->assertTrue($this->dummyHandler->save('key', 'value')); + } + + public function testDelete() + { + $this->assertTrue($this->dummyHandler->delete('key')); + } + + public function testIncrement() + { + $this->assertTrue($this->dummyHandler->increment('key')); + } + + public function testDecrement() + { + $this->assertTrue($this->dummyHandler->decrement('key')); + } + + public function testClean() + { + $this->assertTrue($this->dummyHandler->clean()); + } + + public function testGetCacheInfo() + { + $this->assertNull($this->dummyHandler->getCacheInfo()); + } + + public function testGetMetaData() + { + $this->assertNull($this->dummyHandler->getMetaData('key')); + } + + public function testIsSupported() + { + $this->assertTrue($this->dummyHandler->isSupported()); + } +} diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php new file mode 100644 index 000000000000..9433822a0170 --- /dev/null +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -0,0 +1,201 @@ +config = new \Config\Cache(); + $this->config->storePath .= self::$directory; + + if (! is_dir($this->config->storePath)) + { + mkdir($this->config->storePath, 0777, true); + } + + $this->fileHandler = new FileHandler($this->config); + $this->fileHandler->initialize(); + } + + public function tearDown() + { + if (is_dir($this->config->storePath)) + { + chmod($this->config->storePath, 0777); + + foreach (self::getKeyArray() as $key) + { + if (is_file($this->config->storePath.DIRECTORY_SEPARATOR.$key)) + { + chmod($this->config->storePath.DIRECTORY_SEPARATOR.$key, 0777); + unlink($this->config->storePath.DIRECTORY_SEPARATOR.$key); + } + } + + rmdir($this->config->storePath); + } + } + + public function testNew() + { + $this->assertInstanceOf(FileHandler::class, $this->fileHandler); + } + + public function testSetDefaultPath() + { + //Initialize path + $config = new \Config\Cache(); + $config->storePath = null; + + $this->fileHandler = new FileHandler($config); + $this->fileHandler->initialize(); + + $this->assertInstanceOf(FileHandler::class, $this->fileHandler); + } + + public function testGet() + { + $this->fileHandler->save(self::$key1, 'value', 1); + + $this->assertSame('value', $this->fileHandler->get(self::$key1)); + $this->assertFalse($this->fileHandler->get(self::$dummy)); + + \CodeIgniter\CLI\CLI::wait(2); + $this->assertFalse($this->fileHandler->get(self::$key1)); + } + + public function testSave() + { + $this->assertTrue($this->fileHandler->save(self::$key1, 'value')); + + chmod($this->config->storePath, 0444); + $this->assertFalse($this->fileHandler->save(self::$key2, 'value')); + } + + public function testDelete() + { + $this->fileHandler->save(self::$key1, 'value'); + + $this->assertTrue($this->fileHandler->delete(self::$key1)); + $this->assertFalse($this->fileHandler->delete(self::$dummy)); + } + + public function testIncrement() + { + $this->fileHandler->save(self::$key1, 1); + $this->fileHandler->save(self::$key2, 'value'); + + $this->assertSame(11, $this->fileHandler->increment(self::$key1, 10)); + $this->assertFalse($this->fileHandler->increment(self::$key2, 10)); + $this->assertSame(10, $this->fileHandler->increment(self::$key3, 10)); + } + + public function testDecrement() + { + $this->fileHandler->save(self::$key1, 10); + $this->fileHandler->save(self::$key2, 'value'); + $this->fileHandler->save(self::$key3, 0); + + $this->assertSame(9, $this->fileHandler->decrement(self::$key1, 1)); + $this->assertFalse($this->fileHandler->decrement(self::$key2, 1)); + $this->assertSame(-1, $this->fileHandler->decrement(self::$key3, 1)); + } + + public function testClean() + { + $this->fileHandler->save(self::$key1, 1); + $this->fileHandler->save(self::$key2, 'value'); + + $this->assertTrue($this->fileHandler->clean()); + + $this->fileHandler->save(self::$key1, 1); + $this->fileHandler->save(self::$key2, 'value'); + } + + public function testGetMetaData() + { + $time = time(); + $this->fileHandler->save(self::$key1, 'value'); + + $this->assertFalse($this->fileHandler->getMetaData(self::$dummy)); + + $actual = $this->fileHandler->getMetaData(self::$key1); + $this->assertLessThanOrEqual(60, $actual['expire']-$time); + $this->assertLessThanOrEqual(0, $actual['mtime']-$time); + $this->assertSame('value', $actual['data']); + } + + public function testIsSupported() + { + $this->assertTrue($this->fileHandler->isSupported()); + } + + //-------------------------------------------------------------------- + + public function testFileHandler() + { + $fileHandler = new BaseTestFileHandler(); + + $actual = $fileHandler->getFileInfoTest(); + + $this->assertArrayHasKey('server_path', $actual); + $this->assertArrayHasKey('size', $actual); + $this->assertArrayHasKey('date', $actual); + $this->assertArrayHasKey('readable', $actual); + $this->assertArrayHasKey('writable', $actual); + $this->assertArrayHasKey('executable', $actual); + $this->assertArrayHasKey('fileperms', $actual); + } +} + +final class BaseTestFileHandler extends FileHandler +{ + private static $directory = 'FileHandler'; + private $config; + + public function __construct() + { + $this->config = new \Config\Cache(); + $this->config->storePath .= self::$directory; + + parent::__construct($this->config); + } + + public function getFileInfoTest() + { + return $this->getFileInfo($this->config->storePath, [ + 'name', + 'server_path', + 'size', + 'date', + 'readable', + 'writable', + 'executable', + 'fileperms', + ]); + } +} diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php new file mode 100644 index 000000000000..63718aa46b79 --- /dev/null +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -0,0 +1,139 @@ +config = new \Config\Cache(); + + $this->memcachedHandler = new MemcachedHandler($this->config->memcached); + if (!$this->memcachedHandler->isSupported()) { + $this->markTestSkipped('Not support memcached and memcache'); + } + + $this->memcachedHandler->initialize(); + } + + public function tearDown() + { + foreach (self::getKeyArray() as $key) { + $this->memcachedHandler->delete($key); + } + } + + public function testNew() + { + $this->assertInstanceOf(MemcachedHandler::class, $this->memcachedHandler); + } + + public function testGet() + { + $this->memcachedHandler->save(self::$key1, 'value', 1); + + $this->assertSame('value', $this->memcachedHandler->get(self::$key1)); + $this->assertFalse($this->memcachedHandler->get(self::$dummy)); + + \CodeIgniter\CLI\CLI::wait(2); + $this->assertFalse($this->memcachedHandler->get(self::$key1)); + } + + public function testSave() + { + $this->assertTrue($this->memcachedHandler->save(self::$key1, 'value')); + } + + public function testDelete() + { + $this->memcachedHandler->save(self::$key1, 'value'); + + $this->assertTrue($this->memcachedHandler->delete(self::$key1)); + $this->assertFalse($this->memcachedHandler->delete(self::$dummy)); + } + + public function testIncrement() + { + $this->memcachedHandler->save(self::$key1, 1); + + $this->assertFalse($this->memcachedHandler->increment(self::$key1, 10)); + + $config = new \Config\Cache(); + $config->memcached['raw'] = true; + $memcachedHandler = new MemcachedHandler($config->memcached); + $memcachedHandler->initialize(); + + $memcachedHandler->save(self::$key1, 1); + $memcachedHandler->save(self::$key2, 'value'); + + $this->assertSame(11, $memcachedHandler->increment(self::$key1, 10)); + $this->assertFalse($memcachedHandler->increment(self::$key2, 10)); + $this->assertSame(10, $memcachedHandler->increment(self::$key3, 10)); + } + + public function testDecrement() + { + $this->memcachedHandler->save(self::$key1, 10); + + $this->assertFalse($this->memcachedHandler->decrement(self::$key1, 1)); + + $config = new \Config\Cache(); + $config->memcached['raw'] = true; + $memcachedHandler = new MemcachedHandler($config->memcached); + $memcachedHandler->initialize(); + + $memcachedHandler->save(self::$key1, 10); + $memcachedHandler->save(self::$key2, 'value'); + + $this->assertSame(9, $memcachedHandler->decrement(self::$key1, 1)); + $this->assertFalse($memcachedHandler->decrement(self::$key2, 1)); + $this->assertSame(1, $memcachedHandler->decrement(self::$key3, 1)); + } + + public function testClean() + { + $this->memcachedHandler->save(self::$key1, 1); + $this->memcachedHandler->save(self::$key2, 'value'); + + $this->assertTrue($this->memcachedHandler->clean()); + } + + public function testGetCacheInfo() + { + $this->memcachedHandler->save(self::$key1, 'value'); + + $this->assertInternalType('array', $this->memcachedHandler->getCacheInfo()); + } + + public function testGetMetaData() + { + $time = time(); + $this->memcachedHandler->save(self::$key1, 'value'); + + $this->assertFalse($this->memcachedHandler->getMetaData(self::$dummy)); + + $actual = $this->memcachedHandler->getMetaData(self::$key1); + $this->assertLessThanOrEqual(60, $actual['expire'] - $time); + $this->assertLessThanOrEqual(0, $actual['mtime'] - $time); + $this->assertSame('value', $actual['data']); + } + + public function testIsSupported() + { + $this->assertTrue($this->memcachedHandler->isSupported()); + } +} diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php new file mode 100644 index 000000000000..88c39bf57cf6 --- /dev/null +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -0,0 +1,154 @@ +config = new \Config\Cache(); + + $this->redisHandler = new RedisHandler($this->config); + if (!$this->redisHandler->isSupported()) { + $this->markTestSkipped('Not support redis'); + } + + $this->redisHandler->initialize(); + } + + public function tearDown() + { + foreach (self::getKeyArray() as $key) { + $this->redisHandler->delete($key); + } + } + + public function testNew() + { + $this->assertInstanceOf(RedisHandler::class, $this->redisHandler); + } + + public function testDestruct() + { + $this->redisHandler = new RedisHandler($this->config); + $this->redisHandler->initialize(); + + $this->assertInstanceOf(RedisHandler::class, $this->redisHandler); + } + + + public function testGet() + { + $this->redisHandler->save(self::$key1, 'value', 1); + + $this->assertSame('value', $this->redisHandler->get(self::$key1)); + $this->assertFalse($this->redisHandler->get(self::$dummy)); + + \CodeIgniter\CLI\CLI::wait(2); + $this->assertFalse($this->redisHandler->get(self::$key1)); + } + + public function testSave() + { + $this->assertTrue($this->redisHandler->save(self::$key1, 'value')); + } + + public function testDelete() + { + $this->redisHandler->save(self::$key1, 'value'); + + $this->assertTrue($this->redisHandler->delete(self::$key1)); + $this->assertFalse($this->redisHandler->delete(self::$dummy)); + } + + //FIXME: I don't like all Hash logic very much. It's wasting memory. + //public function testIncrement() + //{ + //} + + //public function testDecrement() + //{ + //} + + public function testClean() + { + $this->redisHandler->save(self::$key1, 1); + $this->redisHandler->save(self::$key2, 'value'); + + $this->assertTrue($this->redisHandler->clean()); + } + + public function testGetCacheInfo() + { + $this->redisHandler->save(self::$key1, 'value'); + + $this->assertInternalType('array', $this->redisHandler->getCacheInfo()); + } + + public function testGetMetaData() + { + $time = time(); + $this->redisHandler->save(self::$key1, 'value'); + + $this->assertFalse($this->redisHandler->getMetaData(self::$dummy)); + + $actual = $this->redisHandler->getMetaData(self::$key1); + $this->assertLessThanOrEqual(60, $actual['expire'] - $time); + $this->assertLessThanOrEqual(0, $actual['mtime'] - $time); + $this->assertSame('value', $actual['data']); + } + + public function testIsSupported() + { + $this->assertTrue($this->redisHandler->isSupported()); + } +} diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 47ba2607a695..a56457800d31 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -1,8 +1,13 @@ codeigniter = new MockCodeIgniter(memory_get_usage(), microtime(true), $config); + $this->codeigniter = new MockCodeIgniter($config); + } + + public function tearDown() + { + parent::tearDown(); + + if( count( ob_list_handlers() ) > 1 ) + { + ob_end_clean(); + } } //-------------------------------------------------------------------- @@ -31,7 +52,7 @@ public function testRunDefaultRoute() $_SERVER['argc'] = 2; ob_start(); - $this->codeigniter->run(); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); $this->assertContains('

Welcome to CodeIgniter

', $output); @@ -47,7 +68,7 @@ public function testRunEmptyDefaultRoute() $_SERVER['argc'] = 1; ob_start(); - $this->codeigniter->run(); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); $this->assertContains('

Welcome to CodeIgniter

', $output); @@ -55,7 +76,33 @@ public function testRunEmptyDefaultRoute() //-------------------------------------------------------------------- - public function testRunDefaultRouteNoAutoRoute() + public function testRunClosureRoute() + { + $_SERVER['argv'] = [ + 'index.php', + 'pages/about', + ]; + $_SERVER['argc'] = 2; + $_SERVER['REQUEST_URI'] = '/pages/about'; + + // Inject mock router. + $routes = Services::routes(); + $routes->add('pages/(:segment)', function($segment) { + echo 'You want to see "' . esc($segment) . '" page.'; + }); + $router = Services::router($routes); + Services::injectMock('router', $router); + + ob_start(); + $this->codeigniter->useSafeOutput(true)->run(); + $output = ob_get_clean(); + + $this->assertContains('You want to see "about" page.', $output); + } + + //-------------------------------------------------------------------- + + public function testRun404Override() { $_SERVER['argv'] = [ 'index.php', @@ -66,69 +113,115 @@ public function testRunDefaultRouteNoAutoRoute() // Inject mock router. $routes = Services::routes(); $routes->setAutoRoute(false); + $routes->set404Override('Home::index'); $router = Services::router($routes); Services::injectMock('router', $router); ob_start(); - $this->codeigniter->run($routes); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); - $this->assertContains("Can't find a route for '/'.", $output); + $this->assertContains('

Welcome to CodeIgniter

', $output); } //-------------------------------------------------------------------- - public function testRunClosureRoute() + public function testRun404OverrideByClosure() + { + $_SERVER['argv'] = [ + 'index.php', + '/', + ]; + $_SERVER['argc'] = 2; + + // Inject mock router. + $routes = new RouteCollection(new MockFileLocator(new \Config\Autoload()), new \Config\Modules()); + $routes->setAutoRoute(false); + $routes->set404Override(function() { + echo '404 Override by Closure.'; + }); + $router = Services::router($routes); + Services::injectMock('router', $router); + + ob_start(); + $this->codeigniter->useSafeOutput(true)->run($routes); + $output = ob_get_clean(); + + $this->assertContains('404 Override by Closure.', $output); + } + + //-------------------------------------------------------------------- + + public function testControllersCanReturnString() { $_SERVER['argv'] = [ 'index.php', 'pages/about', ]; $_SERVER['argc'] = 2; + $_SERVER['REQUEST_URI'] = '/pages/about'; // Inject mock router. $routes = Services::routes(); - $routes->add('pages/(:segment)', function($segment) - { - echo 'You want to see "'.esc($segment).'" page.'; + $routes->add('pages/(:segment)', function($segment) { + return 'You want to see "' . esc($segment) . '" page.'; }); $router = Services::router($routes); Services::injectMock('router', $router); ob_start(); - $this->codeigniter->run(); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); $this->assertContains('You want to see "about" page.', $output); } - //-------------------------------------------------------------------- + //-------------------------------------------------------------------- - public function testRun404Override() + public function testControllersCanReturnResponseObject() { $_SERVER['argv'] = [ 'index.php', - '/', + 'pages/about', ]; $_SERVER['argc'] = 2; + $_SERVER['REQUEST_URI'] = '/pages/about'; // Inject mock router. $routes = Services::routes(); - $routes->setAutoRoute(false); - $routes->set404Override('Home::index'); + $routes->add('pages/(:segment)', function($segment) { + $response = Services::response(); + $string = "You want to see 'about' page."; + return $response->setBody($string); + }); $router = Services::router($routes); Services::injectMock('router', $router); ob_start(); - $this->codeigniter->run(); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); - $this->assertContains('

Welcome to CodeIgniter

', $output); + $this->assertContains("You want to see 'about' page.", $output); } //-------------------------------------------------------------------- - public function testRun404OverrideByClosure() + public function testResponseConfigEmpty() + { + $_SERVER['argv'] = [ + 'index.php', + '/', + ]; + $_SERVER['argc'] = 2; + + $response = Config\Services::response(null, false); + + $this->assertInstanceOf('\CodeIgniter\HTTP\Response', $response); + } + + //-------------------------------------------------------------------- + + public function testRoutesIsEmpty() { $_SERVER['argv'] = [ 'index.php', @@ -137,22 +230,33 @@ public function testRun404OverrideByClosure() $_SERVER['argc'] = 2; // Inject mock router. - $routes = new RouteCollection(); - $routes->setAutoRoute(false); - $routes->set404Override(function() - { - echo '404 Override by Closure.'; - }); - $router = Services::router($routes); + $router = Services::router(null, false); Services::injectMock('router', $router); ob_start(); - $this->codeigniter->run($routes); + $this->codeigniter->useSafeOutput(true)->run(); $output = ob_get_clean(); - $this->assertContains('404 Override by Closure.', $output); + $this->assertContains('

Welcome to CodeIgniter

', $output); + } + + public function testTransfersCorrectHTTPVersion() + { + $_SERVER['argv'] = [ + 'index.php', + '/', + ]; + $_SERVER['argc'] = 2; + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/2'; + + ob_start(); + $this->codeigniter->useSafeOutput(true)->run(); + $output = ob_get_clean(); + + $response = $this->getPrivateProperty($this->codeigniter, 'response'); + + $this->assertEquals(2, $response->getProtocolVersion()); } - //-------------------------------------------------------------------- } diff --git a/tests/system/Commands/CommandsTest.php b/tests/system/Commands/CommandsTest.php new file mode 100644 index 000000000000..e51f77fe52a1 --- /dev/null +++ b/tests/system/Commands/CommandsTest.php @@ -0,0 +1,76 @@ +stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + + $this->env = new \CodeIgniter\Config\DotEnv(ROOTPATH); + $this->env->load(); + + // Set environment values that would otherwise stop the framework from functioning during tests. + if ( ! isset($_SERVER['app.baseURL'])) + { + $_SERVER['app.baseURL'] = 'http://example.com'; + } + + $_SERVER['argv'] = ['spark', 'list']; + $_SERVER['argc'] = 2; + CLI::init(); + + $this->config = new MockAppConfig(); + $this->request = new \CodeIgniter\HTTP\IncomingRequest($this->config, new \CodeIgniter\HTTP\URI('https://somwhere.com'), null, new UserAgent()); + $this->response = new \CodeIgniter\HTTP\Response($this->config); + $this->logger = Services::logger(); + $this->runner = new CommandRunner(); + $this->runner->initController($this->request, $this->response, $this->logger); + } + + public function tearDown() + { + stream_filter_remove($this->stream_filter); + } + + public function testHelpCommand() + { + $this->runner->index(['help']); + $result = CITestStreamFilter::$buffer; + + // make sure the result looks like a command list + $this->assertContains('Displays basic usage information.', $result); + $this->assertContains('command_name', $result); + } + + public function testListCommands() + { + $this->runner->index(['list']); + $result = CITestStreamFilter::$buffer; + + // make sure the result looks like a command list + $this->assertContains('Lists the available commands.', $result); + $this->assertContains('Displays basic usage information.', $result); + } + +} diff --git a/tests/system/Commands/SessionsCommandsTest.php b/tests/system/Commands/SessionsCommandsTest.php new file mode 100644 index 000000000000..f4b619566928 --- /dev/null +++ b/tests/system/Commands/SessionsCommandsTest.php @@ -0,0 +1,83 @@ +stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + + $this->env = new \CodeIgniter\Config\DotEnv(ROOTPATH); + $this->env->load(); + + // Set environment values that would otherwise stop the framework from functioning during tests. + if ( ! isset($_SERVER['app.baseURL'])) + { + $_SERVER['app.baseURL'] = 'http://example.com'; + } + + $_SERVER['argv'] = ['spark', 'list']; + $_SERVER['argc'] = 2; + CLI::init(); + + $this->config = new MockAppConfig(); + $this->request = new \CodeIgniter\HTTP\IncomingRequest($this->config, new \CodeIgniter\HTTP\URI('https://somwhere.com'), null, new UserAgent()); + $this->response = new \CodeIgniter\HTTP\Response($this->config); + $this->logger = Services::logger(); + $this->runner = new CommandRunner(); + $this->runner->initController($this->request, $this->response, $this->logger); + } + + public function tearDown() + { + stream_filter_remove($this->stream_filter); + } + + public function testCreateMigrationCommand() + { + $this->runner->index(['session:migration']); + $result = CITestStreamFilter::$buffer; + + // make sure we end up with a migration class in the right place + // or at least that we claim to have done so + // separate assertions avoid console color codes + $this->assertContains('Created file:', $result); + $this->assertContains('APPPATH/Database/Migrations/', $result); + $this->assertContains('_create_ci_sessions_table.php', $result); + } + + public function testOverriddenCreateMigrationCommand() + { + $_SERVER['argv'] = ['spark','session:migration', '-t', 'mygoodies']; + $_SERVER['argc'] = 4; + CLI::init(); + + $this->runner->index(['session:migration']); + $result = CITestStreamFilter::$buffer; + + // make sure we end up with a migration class in the right place + $this->assertContains('Created file:', $result); + $this->assertContains('APPPATH/Database/Migrations/', $result); + $this->assertContains('_create_mygoodies_table.php', $result); + } + + +} diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php new file mode 100644 index 000000000000..c6fb56c33d21 --- /dev/null +++ b/tests/system/CommonFunctionsTest.php @@ -0,0 +1,329 @@ +assertEquals(' class="foo" id="bar"', stringify_attributes(['class' => 'foo', 'id' => 'bar'])); + + $atts = new stdClass; + $atts->class = 'foo'; + $atts->id = 'bar'; + $this->assertEquals(' class="foo" id="bar"', stringify_attributes($atts)); + + $atts = new stdClass; + $this->assertEquals('', stringify_attributes($atts)); + + $this->assertEquals(' class="foo" id="bar"', stringify_attributes('class="foo" id="bar"')); + + $this->assertEquals('', stringify_attributes([])); + } + + // ------------------------------------------------------------------------ + + public function testStringifyJsAttributes() + { + $this->assertEquals('width=800,height=600', stringify_attributes(['width' => '800', 'height' => '600'], TRUE)); + + $atts = new stdClass; + $atts->width = 800; + $atts->height = 600; + $this->assertEquals('width=800,height=600', stringify_attributes($atts, TRUE)); + } + + // ------------------------------------------------------------------------ + + public function testEnvReturnsDefault() + { + $this->assertEquals('baz', env('foo', 'baz')); + } + + public function testEnvGetsFromSERVER() + { + $_SERVER['foo'] = 'bar'; + + $this->assertEquals('bar', env('foo', 'baz')); + } + + public function testEnvGetsFromENV() + { + $_ENV['foo'] = 'bar'; + + $this->assertEquals('bar', env('foo', 'baz')); + } + + public function testEnvBooleans() + { + $_ENV['p1'] = 'true'; + $_ENV['p2'] = 'false'; + $_ENV['p3'] = 'empty'; + $_ENV['p4'] = 'null'; + + $this->assertTrue(env('p1')); + $this->assertFalse(env('p2')); + $this->assertEmpty(env('p3')); + $this->assertNull(env('p4')); + } + + // ------------------------------------------------------------------------ + + public function testRedirectReturnsRedirectResponse() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $response = $this->createMock(\CodeIgniter\HTTP\Response::class); + $routes = new \CodeIgniter\Router\RouteCollection(new \Tests\Support\Autoloader\MockFileLocator(new \Config\Autoload()), new \Config\Modules()); + \CodeIgniter\Services::injectMock('response', $response); + \CodeIgniter\Services::injectMock('routes', $routes); + + $routes->add('home/base', 'Controller::index', ['as' => 'base']); + + $response->method('redirect') + ->will($this->returnArgument(0)); + + $this->assertInstanceOf(\CodeIgniter\HTTP\RedirectResponse::class, redirect('base')); + } + + public function testRedirectDefault() + { + $this->assertInstanceOf(\CodeIgniter\HTTP\RedirectResponse::class, redirect()); + } + + // ------------------------------------------------------------------------ + + public function testView() + { + $data = [ + 'testString' => 'bar', + 'bar' => 'baz', + ]; + $expected = '

bar

'; + $this->assertContains($expected, view('\Tests\Support\View\Views\simple', $data, [])); + } + + public function testViewSavedData() + { + $data = [ + 'testString' => 'bar', + 'bar' => 'baz', + ]; + $expected = '

bar

'; + $this->assertContains($expected, view('\Tests\Support\View\Views\simple', $data, ['saveData' => true])); + $this->assertContains($expected, view('\Tests\Support\View\Views\simple')); + } + + // ------------------------------------------------------------------------ + + public function testViewCell() + { + $expected = 'Hello'; + $this->assertEquals($expected, view_cell('\Tests\Support\View\SampleClass::hello')); + } + + // ------------------------------------------------------------------------ + + public function testEscapeBadContext() + { + $this->expectException(InvalidArgumentException::class); + esc(['width' => '800', 'height' => '600'], 'bogus'); + } + + // ------------------------------------------------------------------------ + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSessionInstance() + { + $this->injectSessionMock(); + + $this->assertInstanceOf(CodeIgniter\Session\Session::class, session()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSessionVariable() + { + $this->injectSessionMock(); + + $_SESSION['notbogus'] = 'Hi there'; + + $this->assertEquals('Hi there', session('notbogus')); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSessionVariableNotThere() + { + $this->injectSessionMock(); + + $_SESSION['bogus'] = 'Hi there'; + $this->assertEquals(null, session('notbogus')); + } + + // ------------------------------------------------------------------------ + + public function testSingleService() + { + $timer1 = single_service('timer'); + $timer2 = single_service('timer'); + $this->assertFalse($timer1 === $timer2); + } + + // ------------------------------------------------------------------------ + + public function testRouteTo() + { + // prime the pump + $routes = service('routes'); + $routes->add('path/(:any)/to/(:num)', 'myController::goto/$1/$2'); + + $this->assertEquals('/path/string/to/13', route_to('myController::goto', 'string', 13)); + } + + // ------------------------------------------------------------------------ + + public function testInvisible() + { + $this->assertEquals('Javascript', remove_invisible_characters("Java\0script")); + } + + public function testInvisibleEncoded() + { + $this->assertEquals('Javascript', remove_invisible_characters("Java%0cscript", true)); + } + + // ------------------------------------------------------------------------ + + public function testAppTimezone() + { + $this->assertEquals('America/Chicago', app_timezone()); + } + + // ------------------------------------------------------------------------ + + public function testCSRFToken() + { + $this->assertEquals('csrf_test_name', csrf_token()); + } + + public function testHash() + { + $this->assertEquals(32, strlen(csrf_hash())); + } + + public function testCSRFField() + { + $this->assertContains('injectSessionMock(); + // setup from RedirectResponseTest... + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $this->config = new App(); + $this->config->baseURL = 'http://example.com'; + + $this->routes = new RouteCollection(new MockFileLocator(new Autoload()), new \Config\Modules()); + Services::injectMock('routes', $this->routes); + + $this->request = new MockIncomingRequest($this->config, new URI('http://example.com'), null, new UserAgent()); + Services::injectMock('request', $this->request); + + // setup & ask for a redirect... + $_SESSION = []; + $_GET = ['foo' => 'bar']; + $_POST = ['bar' => 'baz', 'zibble' => serialize('fritz')]; + + $response = new RedirectResponse(new App()); + $returned = $response->withInput(); + + $this->assertEquals('bar', old('foo')); // regular parameter + $this->assertEquals('doo', old('yabba dabba', 'doo')); // non-existing parameter + $this->assertEquals('fritz', old('zibble')); // serialized parameter + } + + // ------------------------------------------------------------------------ + + public function testReallyWritable() + { + // cannot test fully on *nix + $this->assertTrue(is_really_writable(WRITEPATH)); + } + + // ------------------------------------------------------------------------ + + public function testSlashItem() + { + $this->assertEquals('/', slash_item('cookiePath')); // slash already there + $this->assertEquals('', slash_item('cookieDomain')); // empty, so untouched + $this->assertEquals('en/', slash_item('defaultLocale')); // slash appended + } + + protected function injectSessionMock() + { + $defaults = [ + 'sessionDriver' => 'CodeIgniter\Session\Handlers\FileHandler', + 'sessionCookieName' => 'ci_session', + 'sessionExpiration' => 7200, + 'sessionSavePath' => null, + 'sessionMatchIP' => false, + 'sessionTimeToUpdate' => 300, + 'sessionRegenerateDestroy' => false, + 'cookieDomain' => '', + 'cookiePrefix' => '', + 'cookiePath' => '/', + 'cookieSecure' => false, + ]; + + $config = (object)$defaults; + + $session = new MockSession(new FileHandler($config), $config); + $session->setLogger(new TestLogger(new Logger())); + \CodeIgniter\Config\BaseService::injectMock('session', $session); + } + +} diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index ca118315a84e..936338ca17e5 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -4,17 +4,24 @@ class BaseConfigTest extends CIUnitTestCase { + protected $fixturesFolder; //-------------------------------------------------------------------- public function setup() { - $this->fixturesFolder = __DIR__.'/fixtures'; + parent::setUp(); + + $this->fixturesFolder = __DIR__ . '/fixtures'; - if (! class_exists('SimpleConfig', false)) + if ( ! class_exists('SimpleConfig', false)) + { + require $this->fixturesFolder . '/SimpleConfig.php'; + } + if ( ! class_exists('RegistrarConfig', false)) { - require $this->fixturesFolder.'/SimpleConfig.php'; + require $this->fixturesFolder . '/RegistrarConfig.php'; } } @@ -24,10 +31,36 @@ public function testBasicValues() { $dotenv = new DotEnv($this->fixturesFolder, '.env'); $dotenv->load(); - $config = new \SimpleConfig(); $this->assertEquals('bar', $config->FOO); + // empty treated as boolean false + $this->assertEquals(false, $config->echo); + // 'true' should be treated as boolean true + $this->assertTrue($config->foxtrot); + // numbers should be treated properly + $this->assertEquals(18, $config->golf); + } + + //-------------------------------------------------------------------- + + public function testEnvironmentOverrides() + { + $dotenv = new DotEnv($this->fixturesFolder, '.env', 'z'); + $dotenv->load(); + + $config = new \SimpleConfig(); + + // override config with ENV var + $this->assertEquals('pow', $config->alpha); + // config should not be over-written by wrongly named ENV var + $this->assertEquals('three', $config->charlie); + // override config with shortPrefix ENV var + $this->assertEquals('hubbahubba', $config->delta); + // incorrect env name should not inject property + $this->assertObjectNotHasAttribute('notthere', $config); + // same ENV var as property, but not namespaced, still over-rides + $this->assertEquals('kazaam', $config->bravo); } //-------------------------------------------------------------------- @@ -52,6 +85,11 @@ public function testPrefixedArrayValues() $config = new \SimpleConfig(); $this->assertEquals('ci4', $config->default['name']); + $this->assertEquals('Malcolm', $config->crew['captain']); + $this->assertEquals('Spock', $config->crew['science']); + $this->assertFalse(array_key_exists('pilot', $config->crew)); + $this->assertTrue($config->crew['comms']); + $this->assertFalse($config->crew['doctor']); } //-------------------------------------------------------------------- @@ -63,7 +101,9 @@ public function testArrayValues() $config = new \SimpleConfig(); - $this->assertEquals('simpleton', $config->simple['name']); + $this->assertEquals('complex', $config->simple['name']); + $this->assertEquals('foo', $config->first); + $this->assertEquals('bar', $config->second); } //-------------------------------------------------------------------- @@ -96,4 +136,34 @@ public function testRecognizesLooseValues() //-------------------------------------------------------------------- + public function testRegistrars() + { + $config = new \RegistrarConfig(); + $config::$registrars = ['\Tests\Support\Config\Registrar']; + $this->setPrivateProperty($config, 'didDiscovery', true); + $method = $this->getPrivateMethodInvoker($config, 'registerProperties'); + $method(); + + // no change to unmodified property + $this->assertEquals('bar', $config->foo); + // add to an existing array property + $this->assertEquals(['baz', 'first', 'second'], $config->bar); + // add a new property + $this->assertEquals('nice', $config->format); + // add a new array property + $this->assertEquals(['apple', 'banana'], $config->fruit); + } + + public function testBadRegistrar() + { + // Shouldn't change any values. + $config = new \RegistrarConfig(); + $config::$registrars = ['\Tests\Support\Config\BadRegistrar']; + $this->setPrivateProperty($config, 'didDiscovery', true); + $method = $this->getPrivateMethodInvoker($config, 'registerProperties'); + $method(); + + $this->assertEquals('bar', $config->foo); + } + } diff --git a/tests/system/Config/ConfigTest.php b/tests/system/Config/ConfigTest.php new file mode 100644 index 000000000000..ea08b26e981f --- /dev/null +++ b/tests/system/Config/ConfigTest.php @@ -0,0 +1,38 @@ +assertInstanceOf(Email::class, $Config); + $this->assertInstanceOf(Email::class, $NamespaceConfig); + } + + public function testCreateInvalidInstance() + { + $Config = Config::get('gfnusvjai', false); + + $this->assertNull($Config); + } + + public function testCreateSharedInstance() + { + $Config = Config::get('Email' ); + $Config2 = Config::get('Config\\Email'); + + $this->assertTrue($Config === $Config2); + } + + public function testCreateNonConfig() + { + $Config = Config::get('Constants', false); + + $this->assertNull($Config); + } +} diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index 752aa9ab90c8..050ae1e5c0f0 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -1,24 +1,47 @@ -fixturesFolder = __DIR__.'/fixtures'; + parent::setUp(); + + $this->root = vfsStream::setup(); + $this->fixturesFolder = $this->root->url(); + $this->path = TESTPATH . 'system/Config/fixtures'; + vfsStream::copyFromFileSystem($this->path, $this->root); + + $file = "unreadable.env"; + $path = rtrim($this->fixturesFolder, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file; + chmod($path, 0644); + } + + public function tearDown() + { + parent::tearDown(); + + $this->root = null; } - + //-------------------------------------------------------------------- - + public function testReturnsFalseIfCannotFindFile() { - $dotenv = new DotEnv(__DIR__); + $dotenv = new DotEnv($this->fixturesFolder, 'bogus'); $this->assertFalse($dotenv->load()); } @@ -36,13 +59,25 @@ public function testLoadsVars() //-------------------------------------------------------------------- + public function testLoadsNoneStringFiles() + { + $dotenv = new DotEnv($this->fixturesFolder, 2); + $dotenv->load(); + $this->assertEquals('bar', getenv('FOO')); + $this->assertEquals('baz', getenv('BAR')); + $this->assertEquals('with spaces', getenv('SPACED')); + $this->assertEquals('', getenv('NULL')); + } + + //-------------------------------------------------------------------- + public function testCommentedLoadsVars() { $dotenv = new DotEnv($this->fixturesFolder, 'commented.env'); $dotenv->load(); $this->assertEquals('bar', getenv('CFOO')); - $this->assertEquals(false, getenv('CBAR')); - $this->assertEquals(false, getenv('CZOO')); + $this->assertFalse(getenv('CBAR')); + $this->assertFalse(getenv('CZOO')); $this->assertEquals('with spaces', getenv('CSPACED')); $this->assertEquals('a value with a # character', getenv('CQUOTES')); $this->assertEquals('a value with a # character & a quote " character inside quotes', getenv('CQUOTESWITHQUOTE')); @@ -51,6 +86,19 @@ public function testCommentedLoadsVars() //-------------------------------------------------------------------- + public function testLoadsUnreadableFile() + { + $file = "unreadable.env"; + $path = rtrim($this->fixturesFolder, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file; + chmod($path, 0000); + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage("The .env file is not readable: {$path}"); + $dotenv = new DotEnv($this->fixturesFolder, $file); + $dotenv->load(); + } + + //-------------------------------------------------------------------- + public function testQuotedDotenvLoadsEnvironmentVars() { $dotenv = new Dotenv($this->fixturesFolder, 'quoted.env'); @@ -67,7 +115,8 @@ public function testQuotedDotenvLoadsEnvironmentVars() public function testSpacedValuesWithoutQuotesThrowsException() { - $this->setExpectedException('InvalidArgumentException', '.env values containing spaces must be surrounded by quotes.'); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('.env values containing spaces must be surrounded by quotes.'); $dotenv = new Dotenv($this->fixturesFolder, 'spaced-wrong.env'); $dotenv->load(); @@ -88,6 +137,27 @@ public function testLoadsServerGlobals() //-------------------------------------------------------------------- + public function testNamespacedVariables() + { + $dotenv = new Dotenv($this->fixturesFolder, '.env'); + $dotenv->load(); + + $this->assertEquals('complex', $_SERVER['simple.name']); + } + + //-------------------------------------------------------------------- + + public function testLoadsGetServerVar() + { + $_SERVER['SER_VAR'] = 'TT'; + $dotenv = new Dotenv($this->fixturesFolder, 'nested.env'); + $dotenv->load(); + + $this->assertEquals('TT', $_ENV['NVAR7']); + } + + //-------------------------------------------------------------------- + public function testLoadsEnvGlobals() { $dotenv = new Dotenv($this->fixturesFolder); @@ -123,5 +193,4 @@ public function testDotenvAllowsSpecialCharacters() } //-------------------------------------------------------------------- - } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php new file mode 100644 index 000000000000..bc569434d187 --- /dev/null +++ b/tests/system/Config/ServicesTest.php @@ -0,0 +1,166 @@ +original = $_SERVER; +// $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'es; q=1.0, en; q=0.5'; + $this->config = new App(); +// $this->config->negotiateLocale = true; +// $this->config->supportedLocales = ['en', 'es']; + } + + public function tearDown() + { + $_SERVER = $this->original; + } + + public function testNewCurlRequest() + { + $actual = Services::curlrequest(); + $this->assertInstanceOf(\CodeIgniter\HTTP\CURLRequest::class, $actual); + } + + public function testNewExceptions() + { + $actual = Services::exceptions(new Exceptions(), Services::request(), Services::response()); + $this->assertInstanceOf(\CodeIgniter\Debug\Exceptions::class, $actual); + } + + public function testNewExceptionsWithNullConfig() + { + $actual = Services::exceptions(null, null, null, false); + $this->assertInstanceOf(\CodeIgniter\Debug\Exceptions::class, $actual); + } + + public function testNewIterator() + { + $actual = Services::iterator(); + $this->assertInstanceOf(\CodeIgniter\Debug\Iterator::class, $actual); + } + + public function testNewImage() + { + $actual = Services::image(); + $this->assertInstanceOf(\CodeIgniter\Images\ImageHandlerInterface::class, $actual); + } + +// public function testNewMigrationRunner() +// { +// //FIXME - docs aren't clear about setting this up to just make sure that the service +// // returns a MigrationRunner +// $config = new \Config\Migrations(); +// $db = new \CodeIgniter\Database\MockConnection([]); +// $this->expectException('InvalidArgumentException'); +// $actual = Services::migrations($config, $db); +// $this->assertInstanceOf(\CodeIgniter\Database\MigrationRunner::class, $actual); +// } +// + public function testNewNegotiatorWithNullConfig() + { + $actual = Services::negotiator(null); + $this->assertInstanceOf(\CodeIgniter\HTTP\Negotiate::class, $actual); + } + + public function testNewClirequest() + { + $actual = Services::clirequest(null); + $this->assertInstanceOf(\CodeIgniter\HTTP\CLIRequest::class, $actual); + } + + public function testNewUnsharedClirequest() + { + $actual = Services::clirequest(null, false); + $this->assertInstanceOf(\CodeIgniter\HTTP\CLIRequest::class, $actual); + } + + public function testNewPager() + { + $actual = Services::pager(null); + $this->assertInstanceOf(\CodeIgniter\Pager\Pager::class, $actual); + } + + public function testNewThrottlerFromShared() + { + $actual = Services::throttler(); + $this->assertInstanceOf(\CodeIgniter\Throttle\Throttler::class, $actual); + } + + public function testNewThrottler() + { + $actual = Services::throttler(false); + $this->assertInstanceOf(\CodeIgniter\Throttle\Throttler::class, $actual); + } + + public function testNewToolbar() + { + $actual = Services::toolbar(null); + $this->assertInstanceOf(\CodeIgniter\Debug\Toolbar::class, $actual); + } + + public function testNewUri() + { + $actual = Services::uri(null); + $this->assertInstanceOf(\CodeIgniter\HTTP\URI::class, $actual); + } + + public function testNewValidation() + { + $actual = Services::validation(null); + $this->assertInstanceOf(\CodeIgniter\Validation\Validation::class, $actual); + } + + public function testNewViewcellFromShared() + { + $actual = Services::viewcell(); + $this->assertInstanceOf(\CodeIgniter\View\Cell::class, $actual); + } + + public function testNewViewcell() + { + $actual = Services::viewcell(false); + $this->assertInstanceOf(\CodeIgniter\View\Cell::class, $actual); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testNewSession() + { + $actual = Services::session($this->config); + $this->assertInstanceOf(\CodeIgniter\Session\Session::class, $actual); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testNewSessionWithNullConfig() + { + $actual = Services::session(null, false); + $this->assertInstanceOf(\CodeIgniter\Session\Session::class, $actual); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testCallStatic() + { + // __callStatic should kick in for this but fail + $actual = \CodeIgniter\Config\Services::SeSsIoNs(null, false); + $this->assertNull($actual); + // __callStatic should kick in for this + $actual = \CodeIgniter\Config\Services::SeSsIoN(null, false); + $this->assertInstanceOf(\CodeIgniter\Session\Session::class, $actual); + } + +} diff --git a/tests/system/Config/fixtures/.env b/tests/system/Config/fixtures/.env index 02e25e02962e..f8608bdcd0bc 100644 --- a/tests/system/Config/fixtures/.env +++ b/tests/system/Config/fixtures/.env @@ -6,4 +6,21 @@ NULL= SimpleConfig.onedeep=baz SimpleConfig.default.name=ci4 -simple.name=simpleton \ No newline at end of file +simple.name=complex + +# for environment override testing +SimpleConfig.alpha=pow +SimpleConfig.charliewrong=null +SimpleConfig.notthere=missing +simpleconfig.delta=hubbahubba +simpleconfig.foxtrot="true" + +bravo=kazaam + +simpleconfig.default.something=else +simpleconfig.name = ci123 + +SimpleConfig.crew.captain = Malcolm +SimpleConfig.crew.pilot = Wash +SimpleConfig.crew.comms = true +SimpleConfig.crew.doctor = false diff --git a/tests/system/Config/fixtures/RegistrarConfig.php b/tests/system/Config/fixtures/RegistrarConfig.php new file mode 100644 index 000000000000..a5f3bdcb7cdd --- /dev/null +++ b/tests/system/Config/fixtures/RegistrarConfig.php @@ -0,0 +1,10 @@ + null ]; - public $simple = [ 'name' => null ]; + // properties for environment over-ride testing + public $alpha = 'one'; + public $bravo = 'two'; + public $charlie = 'three'; + public $delta = 'four'; + public $echo = ''; + public $foxtrot = 'false'; + public $golf = 18; + public $crew = [ + 'captain' => 'Kirk', + 'science' => 'Spock', + 'doctor' => 'Bones', + 'comms' => 'Uhuru' + ]; + } diff --git a/tests/system/Config/fixtures/nested.env b/tests/system/Config/fixtures/nested.env index 24bfa8ff971a..01d1ea6d7054 100644 --- a/tests/system/Config/fixtures/nested.env +++ b/tests/system/Config/fixtures/nested.env @@ -2,4 +2,6 @@ NVAR1="Hello" NVAR2="World!" NVAR3="{$NVAR1} {$NVAR2}" NVAR4="${NVAR1} ${NVAR2}" -NVAR5="$NVAR1 {NVAR2}" \ No newline at end of file +NVAR5="$NVAR1 {NVAR2}" +NVAR6="${NVAR_X}" +NVAR7="${SER_VAR}" \ No newline at end of file diff --git a/tests/system/Config/fixtures/unreadable.env b/tests/system/Config/fixtures/unreadable.env new file mode 100644 index 000000000000..418558c076d6 --- /dev/null +++ b/tests/system/Config/fixtures/unreadable.env @@ -0,0 +1,10 @@ +FOO=bar +BAR=baz +SPACED="with spaces" + +NULL= + +SimpleConfig.onedeep=baz +SimpleConfig.default.name=ci4 +simple.name=simpleton +impleconfig.shortprefix=wht \ No newline at end of file diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php new file mode 100644 index 000000000000..39941b3af868 --- /dev/null +++ b/tests/system/ControllerTest.php @@ -0,0 +1,114 @@ +config = new App(); + $this->request = new \CodeIgniter\HTTP\IncomingRequest($this->config, new \CodeIgniter\HTTP\URI('https://somwhere.com'), null, new UserAgent()); + $this->response = new \CodeIgniter\HTTP\Response($this->config); + $this->logger = \Config\Services::logger(); + $this->codeigniter = new MockCodeIgniter($this->config); + } + + //-------------------------------------------------------------------- + + public function testConstructor() + { + // make sure we can instantiate one + $this->controller = new Controller(); + $this->controller->initController($this->request, $this->response, $this->logger); + $this->assertInstanceOf(Controller::class, $this->controller); + } + + public function testConstructorHTTPS() + { + $original = $_SERVER; + $_SERVER = ['HTTPS' => 'on']; + // make sure we can instantiate one + $this->controller = new Class() extends Controller + { + protected $forceHTTPS = 1; + }; + $this->controller->initController($this->request, $this->response, $this->logger); + + $this->assertInstanceOf(Controller::class, $this->controller); + $_SERVER = $original; // restore so code coverage doesn't break + } + + //-------------------------------------------------------------------- + public function testCachePage() + { + $this->controller = new Controller(); + $this->controller->initController($this->request, $this->response, $this->logger); + + $this->assertNull($this->controller->cachePage(10)); + } + + public function testValidate() + { + // make sure we can instantiate one + $this->controller = new Controller(); + $this->controller->initController($this->request, $this->response, $this->logger); + + // and that we can attempt validation, with no rules + $this->assertFalse($this->controller->validate([])); + } + + //-------------------------------------------------------------------- + public function testHelpers() + { + $this->controller = new Class() extends Controller + { + protected $helpers = ['cookie', 'text']; + }; + $this->controller->initController($this->request, $this->response, $this->logger); + + $this->assertInstanceOf(Controller::class, $this->controller); + } + +} diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index aa581de77680..5a7f259e5ac7 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -1,6 +1,7 @@ false, 'strictOn' => true, 'failover' => [], - 'saveQueries' => true, ]; protected $failoverOptions = [ @@ -45,48 +45,47 @@ class BaseConnectionTest extends \CIUnitTestCase 'compress' => false, 'strictOn' => true, 'failover' => [], - 'saveQueries' => true, ]; - + //-------------------------------------------------------------------- - - public function testSavesConfigOptions() + + public function testSavesConfigOptions() { $db = new MockConnection($this->options); - + $this->assertSame('localhost', $db->hostname); $this->assertSame('first', $db->username); $this->assertSame('last', $db->password); $this->assertSame('dbname', $db->database); $this->assertSame('MockDriver', $db->DBDriver); - $this->assertSame(true, $db->pConnect); - $this->assertSame(true, $db->DBDebug); - $this->assertSame(false, $db->cacheOn); + $this->assertTrue($db->pConnect); + $this->assertTrue($db->DBDebug); + $this->assertFalse($db->cacheOn); $this->assertSame('my/cacheDir', $db->cacheDir); $this->assertSame('utf8', $db->charset); $this->assertSame('utf8_general_ci', $db->DBCollat); $this->assertSame('', $db->swapPre); - $this->assertSame(false, $db->encrypt); - $this->assertSame(false, $db->compress); - $this->assertSame(true, $db->strictOn); + $this->assertFalse($db->encrypt); + $this->assertFalse($db->compress); + $this->assertTrue($db->strictOn); $this->assertSame([], $db->failover); - $this->assertSame(true, $db->saveQueries); } - + //-------------------------------------------------------------------- - - public function testConnectionThrowExceptionWhenCannotConnect() + + public function testConnectionThrowExceptionWhenCannotConnect() { $db = new MockConnection($this->options); - - $this->setExpectedException('CodeIgniter\DatabaseException', 'Unable to connect to the database.'); - + + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('Unable to connect to the database.'); + $db->shouldReturn('connect', false) ->initialize(); } - + //-------------------------------------------------------------------- - + public function testCanConnectAndStoreConnection() { $db = new MockConnection($this->options); @@ -100,7 +99,7 @@ public function testCanConnectAndStoreConnection() //-------------------------------------------------------------------- /** - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException * @group single */ public function testCanConnectToFailoverWhenNoConnectionAvailable() @@ -127,11 +126,25 @@ public function testStoresConnectionTimings() $db->initialize(); - $this->assertTrue($db->getConnectStart() > $start); - $this->assertTrue($db->getConnectDuration() > 0); + $this->assertGreaterThan($start, $db->getConnectStart()); + $this->assertGreaterThan(0.0, $db->getConnectDuration()); } //-------------------------------------------------------------------- + /** + * Ensures we don't have escaped - values... + * + * @see https://github.com/bcit-ci/CodeIgniter4/issues/606 + */ + public function testEscapeProtectsNegativeNumbers() + { + $db = new MockConnection($this->options); + + $db->initialize(); + + $this->assertEquals("'-100'", $db->escape(-100)); + } + } diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index 9ca7e51934c7..f861a6feaf0b 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -1,14 +1,17 @@ db = new MockConnection([]); } @@ -160,7 +163,7 @@ public function testBindingAutoEscapesParameters() $query->setQuery('SELECT * FROM users WHERE name = ?', ["O'Reilly"]); - $expected = "SELECT * FROM users WHERE name = 'O\'Reilly'"; + $expected = "SELECT * FROM users WHERE name = 'O''Reilly'"; $this->assertEquals($expected, $query->getQuery()); } @@ -171,7 +174,7 @@ public function testNamedBinds() { $query = new Query($this->db); - $query->setQuery('SELECT * FROM users WHERE id = :id OR name = :name', ['id' => 13, 'name' => 'Geoffrey']); + $query->setQuery('SELECT * FROM users WHERE id = :id: OR name = :name:', ['id' => 13, 'name' => 'Geoffrey']); $expected = "SELECT * FROM users WHERE id = 13 OR name = 'Geoffrey'"; @@ -179,4 +182,22 @@ public function testNamedBinds() } //-------------------------------------------------------------------- + + /** + * @group single + * + * @see https://github.com/bcit-ci/CodeIgniter4/issues/201 + */ + public function testSimilarNamedBinds() + { + $query = new Query($this->db); + + $query->setQuery('SELECT * FROM users WHERE sitemap = :sitemap: OR site = :site:', ['sitemap' => 'sitemap', 'site' => 'site']); + + $expected = "SELECT * FROM users WHERE sitemap = 'sitemap' OR site = 'site'"; + + $this->assertEquals($expected, $query->getQuery()); + } + + //-------------------------------------------------------------------- } diff --git a/tests/system/Database/Builder/AliasTest.php b/tests/system/Database/Builder/AliasTest.php index da45489857e6..83e96e8408c9 100644 --- a/tests/system/Database/Builder/AliasTest.php +++ b/tests/system/Database/Builder/AliasTest.php @@ -1,7 +1,6 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/CountTest.php b/tests/system/Database/Builder/CountTest.php index 77f719159589..898843c04db1 100644 --- a/tests/system/Database/Builder/CountTest.php +++ b/tests/system/Database/Builder/CountTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } @@ -33,7 +35,7 @@ public function testCountAllResults() $answer = $builder->where('id >', 3)->countAllResults(null, true); - $expectedSQL = "SELECT COUNT(*) AS \"numrows\" FROM \"jobs\" WHERE \"id\" > :id"; + $expectedSQL = "SELECT COUNT(*) AS \"numrows\" FROM \"jobs\" WHERE \"id\" > :id:"; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $answer)); } diff --git a/tests/system/Database/Builder/DeleteTest.php b/tests/system/Database/Builder/DeleteTest.php index 49a90f8f9771..3a5f83d92e7b 100644 --- a/tests/system/Database/Builder/DeleteTest.php +++ b/tests/system/Database/Builder/DeleteTest.php @@ -1,7 +1,6 @@ db = new MockConnection([]); } @@ -22,7 +23,7 @@ public function testDelete() $answer = $builder->delete(['id' => 1], null, true, true); - $expectedSQL = "DELETE FROM \"jobs\" WHERE \"id\" = :id"; + $expectedSQL = "DELETE FROM \"jobs\" WHERE \"id\" = :id:"; $expectedBinds = ['id' => 1]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $answer)); diff --git a/tests/system/Database/Builder/DistinctTest.php b/tests/system/Database/Builder/DistinctTest.php index 8104693f86c3..f6b6f042993d 100644 --- a/tests/system/Database/Builder/DistinctTest.php +++ b/tests/system/Database/Builder/DistinctTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/EmptyTest.php b/tests/system/Database/Builder/EmptyTest.php index 679bb1f90668..bf5d12483a86 100644 --- a/tests/system/Database/Builder/EmptyTest.php +++ b/tests/system/Database/Builder/EmptyTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/FromTest.php b/tests/system/Database/Builder/FromTest.php index ece76b48c457..d399040bba63 100644 --- a/tests/system/Database/Builder/FromTest.php +++ b/tests/system/Database/Builder/FromTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/GroupTest.php b/tests/system/Database/Builder/GroupTest.php index 4fd9e58fa474..79cb89aec4ad 100644 --- a/tests/system/Database/Builder/GroupTest.php +++ b/tests/system/Database/Builder/GroupTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } @@ -54,7 +56,7 @@ public function testOrHavingBy() ->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\" > :id: OR SUM(id) > 2"; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -71,7 +73,7 @@ public function testAndGroups() ->groupEnd() ->where('name', 'Darth'); - $expectedSQL = "SELECT * FROM \"user\" WHERE ( \"id\" > :id AND \"name\" != :name ) AND \"name\" = :name0"; + $expectedSQL = "SELECT * FROM \"user\" WHERE ( \"id\" > :id: AND \"name\" != :name: ) AND \"name\" = :name0:"; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -88,7 +90,7 @@ public function testOrGroups() ->where('name !=', 'Luke') ->groupEnd(); - $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name OR ( \"id\" > :id AND \"name\" != :name0 )"; + $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name: OR ( \"id\" > :id: AND \"name\" != :name0: )"; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -105,7 +107,7 @@ public function testNotGroups() ->where('name !=', 'Luke') ->groupEnd(); - $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name AND NOT ( \"id\" > :id AND \"name\" != :name0 )"; + $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name: AND NOT ( \"id\" > :id: AND \"name\" != :name0: )"; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -122,7 +124,7 @@ public function testOrNotGroups() ->where('name !=', 'Luke') ->groupEnd(); - $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name OR NOT ( \"id\" > :id AND \"name\" != :name0 )"; + $expectedSQL = "SELECT * FROM \"user\" WHERE \"name\" = :name: OR NOT ( \"id\" > :id: AND \"name\" != :name0: )"; $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 0d7c42f6c598..3e14a2e803e2 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -1,8 +1,7 @@ db = new MockConnection([]); } @@ -27,7 +28,7 @@ public function testSimpleInsert() ]; $builder->insert($insertData, true, true); - $expectedSQL = "INSERT INTO \"jobs\" (\"id\", \"name\") VALUES (:id, :name)"; + $expectedSQL = "INSERT INTO \"jobs\" (\"id\", \"name\") VALUES (:id:, :name:)"; $expectedBinds = $insertData; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledInsert())); @@ -40,46 +41,39 @@ public function testThrowsExceptionOnNoValuesSet() { $builder = $this->db->table('jobs'); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('You must use the "set" method to update an entry.'); $builder->insert(null, true, true); } //-------------------------------------------------------------------- - + public function testInsertBatch() { $builder = $this->db->table('jobs'); - $insertData = array( + $insertData = [ ['id' => 2, 'name' => 'Commedian', 'description' => 'Theres something in your teeth'], ['id' => 3, 'name' => 'Cab Driver', 'description' => 'Iam yellow'], - ); + ]; $this->db->shouldReturn('execute', 1) ->shouldReturn('affectedRows', 1); $builder->insertBatch($insertData, true, true); - $queries = $this->db->getQueries(); - - $q1 = $queries[0]; - $q2 = $queries[1]; + $query = $this->db->getLastQuery(); - $this->assertTrue($q1 instanceof Query); - $this->assertTrue($q2 instanceof Query); + $this->assertInstanceOf(Query::class, $query); - $raw1 = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES (:description,:id,:name)"; - $raw2 = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES (:description0,:id0,:name0)"; + $raw = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES (:description0:,:id0:,:name0:)"; - $this->assertEquals($raw1, str_replace("\n", ' ', $q1->getOriginalQuery() )); - $this->assertEquals($raw2, str_replace("\n", ' ', $q2->getOriginalQuery() )); + $this->assertEquals($raw, str_replace("\n", ' ', $query->getOriginalQuery() )); - $expected1 = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES ('Theres something in your teeth',2,'Commedian')"; - $expected2 = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES ('Iam yellow',3,'Cab Driver')"; + $expected = "INSERT INTO \"jobs\" (\"description\", \"id\", \"name\") VALUES ('Iam yellow',3,'Cab Driver')"; - $this->assertEquals($expected1, str_replace("\n", ' ', $q1->getQuery() )); - $this->assertEquals($expected2, str_replace("\n", ' ', $q2->getQuery() )); + $this->assertEquals($expected, str_replace("\n", ' ', $query->getQuery() )); } //-------------------------------------------------------------------- @@ -88,7 +82,8 @@ public function testInsertBatchThrowsExceptionOnNoData() { $builder = $this->db->table('jobs'); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectExceptionMessage('You must use the "set" method to update an entry.'); $builder->insertBatch(); } @@ -98,7 +93,8 @@ public function testInsertBatchThrowsExceptionOnEmptData() { $builder = $this->db->table('jobs'); - $this->setExpectedException('CodeIgniter\DatabaseException', 'insertBatch() called with no data'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('insertBatch() called with no data'); $builder->insertBatch([]); } diff --git a/tests/system/Database/Builder/JoinTest.php b/tests/system/Database/Builder/JoinTest.php index b40bf0245e75..b2eeaee4dc61 100644 --- a/tests/system/Database/Builder/JoinTest.php +++ b/tests/system/Database/Builder/JoinTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/LikeTest.php b/tests/system/Database/Builder/LikeTest.php index 46648259e02b..792ecb197844 100644 --- a/tests/system/Database/Builder/LikeTest.php +++ b/tests/system/Database/Builder/LikeTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } @@ -22,7 +24,7 @@ public function testSimpleLike() $builder->like('name', 'veloper'); - $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name ESCAPE '!'"; + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'"; $expectedBinds = ['name' => '%veloper%']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -37,7 +39,7 @@ public function testLikeNoSide() $builder->like('name', 'veloper', 'none'); - $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name ESCAPE '!'"; + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'"; $expectedBinds = ['name' => 'veloper']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -52,7 +54,7 @@ public function testLikeBeforeOnly() $builder->like('name', 'veloper', 'before'); - $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name ESCAPE '!'"; + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'"; $expectedBinds = ['name' => '%veloper']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -67,7 +69,7 @@ public function testLikeAfterOnly() $builder->like('name', 'veloper', 'after'); - $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name ESCAPE '!'"; + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" LIKE :name: ESCAPE '!'"; $expectedBinds = ['name' => 'veloper%']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -82,7 +84,7 @@ public function testOrLike() $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 :name: ESCAPE '!' OR \"name\" LIKE :name0: ESCAPE '!'"; $expectedBinds = ['name' => '%veloper%', 'name0' => '%ian%']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -97,7 +99,7 @@ public function testNotLike() $builder->notLike('name', 'veloper'); - $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" NOT LIKE :name ESCAPE '!'"; + $expectedSQL = "SELECT * FROM \"job\" WHERE \"name\" NOT LIKE :name: ESCAPE '!'"; $expectedBinds = ['name' => '%veloper%']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -112,7 +114,7 @@ public function testOrNotLike() $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 :name: ESCAPE '!' OR \"name\" NOT LIKE :name0: ESCAPE '!'"; $expectedBinds = ['name' => '%veloper%', 'name0' => '%ian%']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -120,4 +122,22 @@ public function testOrNotLike() } //-------------------------------------------------------------------- + + /** + * @group single + */ + public function testCaseInsensitiveLike() + { + $builder = new BaseBuilder('job', $this->db); + + $builder->like('name', 'VELOPER', 'both', null, true); + + $expectedSQL = "SELECT * FROM \"job\" WHERE LOWER(name) LIKE :name: ESCAPE '!'"; + $expectedBinds = ['name' => '%veloper%']; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + //-------------------------------------------------------------------- } diff --git a/tests/system/Database/Builder/LimitTest.php b/tests/system/Database/Builder/LimitTest.php index 5eca7cd9a6ad..9f16a9c20d40 100644 --- a/tests/system/Database/Builder/LimitTest.php +++ b/tests/system/Database/Builder/LimitTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/OrderTest.php b/tests/system/Database/Builder/OrderTest.php index 5af4724cfc80..f5a4e6b3a8c6 100644 --- a/tests/system/Database/Builder/OrderTest.php +++ b/tests/system/Database/Builder/OrderTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/PrefixTest.php b/tests/system/Database/Builder/PrefixTest.php index 605f31622071..4338950da1fe 100644 --- a/tests/system/Database/Builder/PrefixTest.php +++ b/tests/system/Database/Builder/PrefixTest.php @@ -1,7 +1,6 @@ db = new MockConnection(['DBPrefix' => 'ci_']); } diff --git a/tests/system/Database/Builder/ReplaceTest.php b/tests/system/Database/Builder/ReplaceTest.php index b05013d4f15b..28cf354f2ebd 100644 --- a/tests/system/Database/Builder/ReplaceTest.php +++ b/tests/system/Database/Builder/ReplaceTest.php @@ -1,6 +1,6 @@ db = new MockConnection([]); } //-------------------------------------------------------------------- - public function testSimpleReplace() + public function testSimpleReplace() { $builder = $this->db->table('jobs'); - - $expected = "REPLACE INTO \"jobs\" (\"title\", \"name\", \"date\") VALUES (:title, :name, :date)"; - $data = array( + $expected = "REPLACE INTO \"jobs\" (\"title\", \"name\", \"date\") VALUES (:title:, :name:, :date:)"; + + $data = [ 'title' => 'My title', 'name' => 'My Name', 'date' => 'My date' - ); + ]; $this->assertSame($expected, $builder->replace($data, true)); } - + //-------------------------------------------------------------------- public function testReplaceThrowsExceptionWithNoData() { $builder = $this->db->table('jobs'); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('You must use the "set" method to update an entry.'); $builder->replace(); } @@ -44,5 +47,5 @@ public function testReplaceThrowsExceptionWithNoData() //-------------------------------------------------------------------- - -} \ No newline at end of file + +} diff --git a/tests/system/Database/Builder/SelectTest.php b/tests/system/Database/Builder/SelectTest.php index 663d185fd494..8d1be2a8c6aa 100644 --- a/tests/system/Database/Builder/SelectTest.php +++ b/tests/system/Database/Builder/SelectTest.php @@ -1,21 +1,23 @@ db = new MockConnection([]); } - + //-------------------------------------------------------------------- - + public function testSimpleSelect() { $builder = new BaseBuilder('users', $this->db); @@ -24,7 +26,7 @@ public function testSimpleSelect() $this->assertEquals($expected, str_replace("\n", ' ', $builder->getCompiledSelect())); } - + //-------------------------------------------------------------------- public function testSelectOnlyOneColumn() @@ -200,7 +202,8 @@ public function testSelectMinThrowsExceptionOnEmptyValue() { $builder = new BaseBuilder('invoices', $this->db); - $this->setExpectedException('CodeIgniter\DatabaseException', 'The query you submitted is not valid.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('The query you submitted is not valid.'); $builder->selectSum(''); } @@ -219,4 +222,4 @@ public function testSelectMaxWithDotNameAndNoAlias() } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Builder/TruncateTest.php b/tests/system/Database/Builder/TruncateTest.php index ce279a803988..fd148d628810 100644 --- a/tests/system/Database/Builder/TruncateTest.php +++ b/tests/system/Database/Builder/TruncateTest.php @@ -1,7 +1,7 @@ db = new MockConnection([]); } diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index 47945f31e24b..3ba2c0ee90a6 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -1,8 +1,8 @@ db = new MockConnection([]); } //-------------------------------------------------------------------- - - public function testUpdate() + + public function testUpdate() { $builder = new BaseBuilder('jobs', $this->db); - + $builder->where('id', 1)->update(['name' => 'Programmer'], null, null, true); - $expectedSQL = "UPDATE \"jobs\" SET \"name\" = :name WHERE \"id\" = :id"; + $expectedSQL = "UPDATE \"jobs\" SET \"name\" = :name: WHERE \"id\" = :id:"; $expectedBinds = ['id' => 1, 'name' => 'Programmer']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); $this->assertEquals($expectedBinds, $builder->getBinds()); } - + //-------------------------------------------------------------------- public function testUpdateInternalWhereAndLimit() @@ -38,7 +40,7 @@ public function testUpdateInternalWhereAndLimit() $builder->update(['name' => 'Programmer'], ['id' => 1], 5, true); - $expectedSQL = "UPDATE \"jobs\" SET \"name\" = :name WHERE \"id\" = :id LIMIT 5"; + $expectedSQL = "UPDATE \"jobs\" SET \"name\" = :name: WHERE \"id\" = :id: LIMIT 5"; $expectedBinds = ['id' => 1, 'name' => 'Programmer']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); @@ -53,7 +55,7 @@ public function testUpdateWithSet() $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\" = :name: WHERE \"id\" = :id:"; $expectedBinds = ['id' => 1, 'name' => 'Programmer']; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); @@ -66,7 +68,7 @@ public function testUpdateThrowsExceptionWithNoData() { $builder = new BaseBuilder('jobs', $this->db); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectException('CodeIgniter\Database\Exceptions\DatabaseException', 'You must use the "set" method to update an entry.'); $builder->update(null, null, null, true); } @@ -77,32 +79,28 @@ public function testUpdateBatch() { $builder = new BaseBuilder('jobs', $this->db); - $updateData = array( + $updateData = [ ['id' => 2, 'name' => 'Comedian', 'description' => 'Theres something in your teeth'], ['id' => 3, 'name' => 'Cab Driver', 'description' => 'Iam yellow'], - ); + ]; $this->db->shouldReturn('execute', 1) ->shouldReturn('affectedRows', 1); $builder->updateBatch($updateData, 'id'); - $query = $this->db->getQueries(); - - $this->assertTrue(is_array($query)); - - $query = $query[0]; + $query = $this->db->getLastQuery(); - $this->assertTrue($query instanceof MockQuery); + $this->assertInstanceOf(MockQuery::class, $query); $expected = 'UPDATE "jobs" SET "name" = CASE -WHEN "id" = :id THEN :name -WHEN "id" = :id0 THEN :name0 +WHEN "id" = :id: THEN :name: +WHEN "id" = :id0: THEN :name0: ELSE "name" END, "description" = CASE -WHEN "id" = :id THEN :description -WHEN "id" = :id0 THEN :description0 +WHEN "id" = :id: THEN :description: +WHEN "id" = :id0: THEN :description0: ELSE "description" END -WHERE "id" IN(:id,:id0)'; +WHERE "id" IN(:id:,:id0:)'; $this->assertEquals($expected, $query->getOriginalQuery() ); @@ -124,7 +122,8 @@ public function testUpdateBatchThrowsExceptionWithNoData() { $builder = new BaseBuilder('jobs', $this->db); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must use the "set" method to update an entry.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('You must use the "set" method to update an entry.'); $builder->updateBatch(null, 'id'); } @@ -135,7 +134,8 @@ public function testUpdateBatchThrowsExceptionWithNoID() { $builder = new BaseBuilder('jobs', $this->db); - $this->setExpectedException('CodeIgniter\DatabaseException', 'You must specify an index to match on for batch updates.'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('You must specify an index to match on for batch updates.'); $builder->updateBatch([]); } @@ -146,11 +146,77 @@ public function testUpdateBatchThrowsExceptionWithEmptySetArray() { $builder = new BaseBuilder('jobs', $this->db); - $this->setExpectedException('CodeIgniter\DatabaseException', 'updateBatch() called with no data'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); + $this->expectExceptionMessage('updateBatch() called with no data'); $builder->updateBatch([], 'id'); } //-------------------------------------------------------------------- + public function testUpdateWithWhereSameColumn() + { + $builder = new BaseBuilder('jobs', $this->db); + + $builder->update(['name' => 'foobar'], ['name' => 'Programmer'], null, true); + + $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "name" = :name0:'; + $expectedBinds = ['name' => 'foobar', 'name0' => 'Programmer']; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertEquals($expectedBinds, $builder->getBinds()); + } + + //-------------------------------------------------------------------- + + public function testUpdateWithWhereSameColumn2() + { + // calling order: set() -> where() + $builder = new BaseBuilder('jobs', $this->db); + + $builder->set('name', 'foobar') + ->where('name', 'Programmer') + ->update(null, null, null, true); + + $expectedSQL = 'UPDATE "jobs" SET "name" = :name: WHERE "name" = :name0:'; + $expectedBinds = ['name' => 'foobar', 'name0' => 'Programmer']; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertEquals($expectedBinds, $builder->getBinds()); + } + + //-------------------------------------------------------------------- + + public function testUpdateWithWhereSameColumn3() + { + // calling order: where() -> set() in update() + $builder = new BaseBuilder('jobs', $this->db); + + $builder->where('name', 'Programmer') + ->update(['name' => 'foobar'], null, null, true); + + $expectedSQL = 'UPDATE "jobs" SET "name" = :name0: WHERE "name" = :name:'; + $expectedBinds = ['name' => 'Programmer', 'name0' => 'foobar']; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); + $this->assertEquals($expectedBinds, $builder->getBinds()); + } + + //-------------------------------------------------------------------- + + // @see https://bcit-ci.github.io/CodeIgniter4/database/query_builder.html#updating-data + public function testSetWithoutEscape() + { + $builder = new BaseBuilder('mytable', $this->db); + + $builder->set('field', 'field+1', false) + ->where('id', 2) + ->update(null, null, null, true); + + $expectedSQL = 'UPDATE "mytable" SET field = field+1 WHERE "id" = :id:'; + $expectedBinds = ['id' => 2]; + + $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 3a5c1c1537f7..ab986ebe611f 100644 --- a/tests/system/Database/Builder/WhereTest.php +++ b/tests/system/Database/Builder/WhereTest.php @@ -1,7 +1,6 @@ db = new MockConnection([]); } @@ -20,7 +21,7 @@ public function testSimpleWhere() { $builder = $this->db->table('users'); - $expectedSQL = "SELECT * FROM \"users\" WHERE \"id\" = :id"; + $expectedSQL = "SELECT * FROM \"users\" WHERE \"id\" = :id:"; $expectedBinds = ['id' => 3]; $builder->where('id', 3); @@ -34,7 +35,7 @@ public function testWhereNoEscape() { $builder = $this->db->table('users'); - $expectedSQL = "SELECT * FROM \"users\" WHERE id = :id"; + $expectedSQL = "SELECT * FROM \"users\" WHERE id = :id:"; $expectedBinds = ['id' => 3]; $builder->where('id', 3, false); @@ -48,7 +49,7 @@ public function testWhereCustomKeyOperator() { $builder = $this->db->table('users'); - $expectedSQL = "SELECT * FROM \"users\" WHERE \"id\" != :id"; + $expectedSQL = "SELECT * FROM \"users\" WHERE \"id\" != :id:"; $expectedBinds = ['id' => 3]; $builder->where('id !=', 3); @@ -67,7 +68,7 @@ public function testWhereAssociateArray() 'name !=' => 'Accountant' ]; - $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"id\" = :id AND \"name\" != :name"; + $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"id\" = :id: AND \"name\" != :name:"; $expectedBinds = ['id' => 2, 'name' => 'Accountant']; @@ -102,7 +103,7 @@ public function testOrWhere() $builder->where('name !=', 'Accountant') ->orWhere('id >', 3); - $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" != :name OR \"id\" > :id"; + $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" != :name: OR \"id\" > :id:"; $expectedBinds = ['name' => 'Accountant', 'id' => 3]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -111,13 +112,29 @@ public function testOrWhere() //-------------------------------------------------------------------- + public function testOrWhereSameColumn() + { + $builder = $this->db->table('jobs'); + + $builder->where('name', 'Accountant') + ->orWhere('name', 'foobar'); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = :name: OR "name" = :name0:'; + $expectedBinds = ['name' => 'Accountant', 'name0' => 'foobar']; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + + //-------------------------------------------------------------------- + public function testWhereIn() { $builder = $this->db->table('jobs'); $builder->whereIn('name', ['Politician', 'Accountant']); - $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" IN :name"; + $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" IN :name:"; $expectedBinds = ['name' => ['Politician', 'Accountant']]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -132,7 +149,7 @@ public function testWhereNotIn() $builder->whereNotIn('name', ['Politician', 'Accountant']); - $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" NOT IN :name"; + $expectedSQL = "SELECT * FROM \"jobs\" WHERE \"name\" NOT IN :name:"; $expectedBinds = ['name' => ['Politician', 'Accountant']]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -147,7 +164,7 @@ public function testOrWhereIn() $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\" = :id: OR \"name\" IN :name:"; $expectedBinds = ['id' => 2, 'name' => ['Politician', 'Accountant']]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -162,7 +179,7 @@ public function testOrWhereNotIn() $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\" = :id: OR \"name\" NOT IN :name:"; $expectedBinds = ['id' => 2, 'name' => ['Politician', 'Accountant']]; $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -170,4 +187,4 @@ public function testOrWhereNotIn() } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/AliasTest.php b/tests/system/Database/Live/AliasTest.php new file mode 100644 index 000000000000..f4d1f588c891 --- /dev/null +++ b/tests/system/Database/Live/AliasTest.php @@ -0,0 +1,24 @@ +db->table('job j'); + + $jobs = $builder + ->where('j.name', 'Developer') + ->get(); + + $this->assertEquals(1, count($jobs->getResult())); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/system/Database/Live/CIDbTestCaseTest.php b/tests/system/Database/Live/CIDbTestCaseTest.php index 27da620ba3bf..3cd083edd1f1 100644 --- a/tests/system/Database/Live/CIDbTestCaseTest.php +++ b/tests/system/Database/Live/CIDbTestCaseTest.php @@ -1,13 +1,15 @@ seeInDatabase('user', ['name' => 'Ricky', 'email' => 'sofine@example.com', 'country' => 'US']); } - + //-------------------------------------------------------------------- public function testDontSeeInDatabase() @@ -43,5 +45,5 @@ public function testGrabFromDatabase() //-------------------------------------------------------------------- - -} \ No newline at end of file + +} diff --git a/tests/system/Database/Live/CountTest.php b/tests/system/Database/Live/CountTest.php index 6813d36c2ba4..510061d7b9fa 100644 --- a/tests/system/Database/Live/CountTest.php +++ b/tests/system/Database/Live/CountTest.php @@ -1,13 +1,15 @@ setExpectedException('CodeIgniter\DatabaseException'); + $this->expectException('\CodeIgniter\Database\Exceptions\DatabaseException'); $this->db->table('job')->delete(); } @@ -44,7 +45,7 @@ public function testDeleteWithInternalWhere() /** * @group single - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function testDeleteWithLimit() { @@ -68,4 +69,4 @@ public function testDeleteWithLimit() //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/EmptyTest.php b/tests/system/Database/Live/EmptyTest.php index 8f5520e94c08..32575a99a9b7 100644 --- a/tests/system/Database/Live/EmptyTest.php +++ b/tests/system/Database/Live/EmptyTest.php @@ -1,21 +1,23 @@ db->table('misc')->emptyTable(); $this->assertEquals(0, $this->db->table('misc')->countAll()); } - + //-------------------------------------------------------------------- public function testTruncate() @@ -26,4 +28,4 @@ public function testTruncate() } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php new file mode 100644 index 000000000000..bf289e0e9009 --- /dev/null +++ b/tests/system/Database/Live/ForgeTest.php @@ -0,0 +1,327 @@ +forge = \Config\Database::forge($this->DBGroup); + } + + public function testCreateTable() + { + $this->forge->dropTable('forge_test_table', true); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'unsigned' => false, + 'auto_increment' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->createTable('forge_test_table'); + + $exist = $this->db->tableExists('forge_test_table'); + $this->forge->dropTable('forge_test_table', true); + + $this->assertTrue($exist); + } + + public function testCreateTableWithAttributes() + { + if ($this->db->DBDriver == 'SQLite3') + { + $this->markTestSkipped('SQLite3 does not support comments on tables or columns.'); + } + + $this->forge->dropTable('forge_test_attributes', true); + + $this->forge->addField('id'); + + $attributes = [ + 'comment' => "Forge's Test" + ]; + + $this->forge->createTable('forge_test_attributes', false, $attributes); + + $exist = $this->db->tableExists('forge_test_attributes'); + $this->forge->dropTable('forge_test_attributes', true, true); + + $this->assertTrue($exist); + } + + public function testAddFields() + { + + $this->forge->dropTable('forge_test_fields', true); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'unsigned' => false, + 'auto_increment' => true, + ], + 'username' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'unique' => false, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + 'active' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'default' => 0, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['username', 'active']); + $create = $this->forge->createTable('forge_test_fields', true); + + //Check Field names + $fieldsNames = $this->db->getFieldNames('forge_test_fields'); + $this->assertContains('id', $fieldsNames); + $this->assertContains('username', $fieldsNames); + $this->assertContains('name', $fieldsNames); + $this->assertContains('active', $fieldsNames); + + $fieldsData = $this->db->getFieldData('forge_test_fields'); + + $this->assertContains($fieldsData[0]->name, ['id', 'name', 'username', 'active']); + $this->assertContains($fieldsData[1]->name, ['id', 'name', 'username', 'active']); + + if ($this->db->DBDriver === 'MySQLi') + { + //Check types + $this->assertEquals($fieldsData[0]->type, 'int'); + $this->assertEquals($fieldsData[1]->type, 'varchar'); + + $this->assertEquals($fieldsData[0]->max_length, 11); + + $this->assertNull($fieldsData[0]->default); + $this->assertNull($fieldsData[1]->default); + + $this->assertEquals($fieldsData[0]->primary_key, 1); + + $this->assertEquals($fieldsData[1]->max_length, 255); + + } + elseif ($this->db->DBDriver === 'Postgre') + { + //Check types + $this->assertEquals($fieldsData[0]->type, 'integer'); + $this->assertEquals($fieldsData[1]->type, 'character varying'); + + $this->assertEquals($fieldsData[0]->max_length, 32); + $this->assertNull($fieldsData[1]->default); + + $this->assertEquals($fieldsData[1]->max_length, 255); + } + elseif ($this->db->DBDriver === 'SQLite3') + { + $this->assertEquals(strtolower($fieldsData[0]->type), 'integer'); + $this->assertEquals(strtolower($fieldsData[1]->type), 'varchar'); + + $this->assertEquals($fieldsData[1]->default, null); + } + else + { + $this->assertTrue(false, "DB Driver not supported"); + } + + $this->forge->dropTable('forge_test_fields', true); + + } + + public function testCompositeKey() + { + // SQLite3 uses auto increment different + $unique_or_auto = $this->db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'code' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'company' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'active' => [ + 'type' => 'INTEGER', + 'constraint' => 1, + ], + ]); + $this->forge->addPrimaryKey('id'); + $this->forge->addKey(['code', 'company']); + $this->forge->addUniqueKey(['code', 'active']); + $this->forge->createTable('forge_test_1', true); + + $keys = $this->db->getIndexData('forge_test_1'); + + if ($this->db->DBDriver == 'MySQLi') + { + $this->assertEquals($keys[0]->name, 'PRIMARY KEY'); + $this->assertEquals($keys[0]->fields, ['id']); + $this->assertEquals($keys[0]->type, 'PRIMARY'); + $this->assertEquals($keys[2]->name, 'code_company'); + $this->assertEquals($keys[2]->fields, ['code', 'company']); + $this->assertEquals($keys[2]->type, 'INDEX'); + $this->assertEquals($keys[1]->name, 'code_active'); + $this->assertEquals($keys[1]->fields, ['code', 'active']); + $this->assertEquals($keys[1]->type, 'UNIQUE'); + } + elseif($this->db->DBDriver == 'Postgre') + { + $this->assertEquals($keys[0]->name, 'pk_db_forge_test_1'); + $this->assertEquals($keys[0]->fields, ['id']); + $this->assertEquals($keys[0]->type, 'PRIMARY'); + $this->assertEquals($keys[1]->name, 'db_forge_test_1_code_company'); + $this->assertEquals($keys[1]->fields, ['code', 'company']); + $this->assertEquals($keys[1]->type, 'INDEX'); + $this->assertEquals($keys[2]->name, 'db_forge_test_1_code_active'); + $this->assertEquals($keys[2]->fields, ['code', 'active']); + $this->assertEquals($keys[2]->type, 'UNIQUE'); + } + + $this->forge->dropTable('forge_test_1', true); + } + + public function testForeignKey() + { + $attributes = []; + + if ($this->db->DBDriver == 'MySQLi') + { + $attributes = ['ENGINE' => 'InnoDB']; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('forge_test_users', true, $attributes); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE'); + + $this->forge->createTable('forge_test_invoices', true, $attributes); + + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); + + if ($this->db->DBDriver == 'SQLite3') + { + $this->assertEquals($foreignKeyData[0]->constraint_name, 'users_id to db_forge_test_users.id'); + } + else + { + $this->assertEquals($foreignKeyData[0]->constraint_name,$this->db->DBPrefix.'forge_test_invoices_users_id_foreign'); + } + $this->assertEquals($foreignKeyData[0]->table_name, $this->db->DBPrefix.'forge_test_invoices'); + $this->assertEquals($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix.'forge_test_users'); + + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_users', true); + + } + + public function testDropForeignKey() + { + + $attributes = []; + + if ($this->db->DBDriver == 'MySQLi') + { + $attributes = ['ENGINE' => 'InnoDB']; + } + if ($this->db->DBDriver == 'SQLite3') + { + $this->expectException(DatabaseException::class); + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('forge_test_users', true, $attributes); + + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addForeignKey('users_id', 'forge_test_users', 'id', 'CASCADE', 'CASCADE'); + + $this->forge->createTable('forge_test_invoices', true, $attributes); + + $this->forge->dropForeignKey('forge_test_invoices', 'forge_test_invoices_users_id_foreign'); + + $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); + + $this->assertEmpty($foreignKeyData); + + $this->forge->dropTable('forge_test_invoices', true); + $this->forge->dropTable('forge_test_users', true); + + } +} diff --git a/tests/system/Database/Live/FromTest.php b/tests/system/Database/Live/FromTest.php index dc9f157e3ceb..1df87f02b6fe 100644 --- a/tests/system/Database/Live/FromTest.php +++ b/tests/system/Database/Live/FromTest.php @@ -1,19 +1,21 @@ db->table('job')->from('misc')->get()->getResult(); - $this->assertEquals(12, count($result)); + $this->assertCount(12, $result); } //-------------------------------------------------------------------- @@ -23,7 +25,7 @@ public function testFromCanOverride() { $result = $this->db->table('job')->from('misc', true)->get()->getResult(); - $this->assertEquals(3, count($result)); + $this->assertCount(3, $result); } //-------------------------------------------------------------------- @@ -32,10 +34,10 @@ public function testFromWithWhere() { $result = $this->db->table('job')->from('user')->where('user.id', 1)->get()->getResult(); - $this->assertEquals(4, count($result)); + $this->assertCount(4, $result); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/GetTest.php b/tests/system/Database/Live/GetTest.php index 286cc3a3d813..785a4753a89c 100644 --- a/tests/system/Database/Live/GetTest.php +++ b/tests/system/Database/Live/GetTest.php @@ -1,13 +1,15 @@ get() ->getResult(); - $this->assertEquals(4, count($result)); + $this->assertCount(4, $result); } //-------------------------------------------------------------------- @@ -31,7 +33,7 @@ public function testHavingBy() ->get() ->getResultArray(); - $this->assertEquals(2, count($result)); + $this->assertCount(2, $result); } //-------------------------------------------------------------------- @@ -45,7 +47,7 @@ public function testOrHavingBy() ->get() ->getResult(); - $this->assertEquals(2, count($result)); + $this->assertCount(2, $result); } //-------------------------------------------------------------------- @@ -61,7 +63,7 @@ public function testAndGroups() ->get() ->getResult(); - $this->assertEquals(1, count($result)); + $this->assertCount(1, $result); $this->assertEquals('Richard A Causey', $result[0]->name); } @@ -78,7 +80,7 @@ public function testOrGroups() ->get() ->getResult(); - $this->assertEquals(2, count($result)); + $this->assertCount(2, $result); $this->assertEquals('Ahmadinejad', $result[0]->name); $this->assertEquals('Chris Martin', $result[1]->name); } @@ -96,7 +98,7 @@ public function testNotGroups() ->get() ->getResult(); - $this->assertEquals(1, count($result)); + $this->assertCount(1, $result); $this->assertEquals('Derek Jones', $result[0]->name); } @@ -113,7 +115,7 @@ public function testOrNotGroups() ->get() ->getResult(); - $this->assertEquals(3, count($result)); + $this->assertCount(3, $result); $this->assertEquals('Derek Jones', $result[0]->name); $this->assertEquals('Richard A Causey', $result[1]->name); $this->assertEquals('Chris Martin', $result[2]->name); @@ -121,4 +123,4 @@ public function testOrNotGroups() //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/IncrementTest.php b/tests/system/Database/Live/IncrementTest.php index f85495064820..69d1f6f5997f 100644 --- a/tests/system/Database/Live/IncrementTest.php +++ b/tests/system/Database/Live/IncrementTest.php @@ -1,13 +1,15 @@ 'Grocery Sales', 'description' => 'Discount!'); + $job_data = ['name' => 'Grocery Sales', 'description' => 'Discount!']; $this->db->table('job')->insert($job_data); @@ -22,10 +24,10 @@ public function testInsert() public function testInsertBatch() { - $job_data = array( - array('name' => 'Comedian', 'description' => 'Theres something in your teeth'), - array('name' => 'Cab Driver', 'description' => 'Iam yellow'), - ); + $job_data = [ + ['name' => 'Comedian', 'description' => 'Theres something in your teeth'], + ['name' => 'Cab Driver', 'description' => 'Iam yellow'], + ]; $this->db->table('job')->insertBatch($job_data); @@ -34,10 +36,10 @@ public function testInsertBatch() } //-------------------------------------------------------------------- - + public function testReplaceWithNoMatchingData() { - $data = array('id' => 5, 'name' => 'Cab Driver', 'description' => 'Iam yellow'); + $data = ['id' => 5, 'name' => 'Cab Driver', 'description' => 'Iam yellow']; $this->db->table('job')->replace($data); @@ -52,7 +54,7 @@ public function testReplaceWithNoMatchingData() public function testReplaceWithMatchingData() { - $data = array('id' => 1, 'name' => 'Cab Driver', 'description' => 'Iam yellow'); + $data = ['id' => 1, 'name' => 'Cab Driver', 'description' => 'Iam yellow']; $this->db->table('job')->replace($data); @@ -64,4 +66,31 @@ public function testReplaceWithMatchingData() } //-------------------------------------------------------------------- -} \ No newline at end of file + + public function testBug302() + { + $code = "my code \'CodeIgniter\Autoloader\'"; + + $this->db->table('misc')->insert([ + 'key' => 'test', + 'value' => $code + ]); + + $this->seeInDatabase('misc', ['key' => 'test']); + $this->seeInDatabase('misc', ['value' => $code]); + } + + public function testInsertPasswordHash() + { + $hash = '$2y$10$tNevVVMwW52V2neE3H79a.wp8ZoItrwosk54.Siz5Fbw55X9YIBsW'; + + $this->db->table('misc')->insert([ + 'key' => 'password', + 'value' => $hash + ]); + + $this->seeInDatabase('misc', ['value' => $hash]); + } + + +} diff --git a/tests/system/Database/Live/JoinTest.php b/tests/system/Database/Live/JoinTest.php index 003baa138376..da3cd0702664 100644 --- a/tests/system/Database/Live/JoinTest.php +++ b/tests/system/Database/Live/JoinTest.php @@ -1,13 +1,15 @@ db->table('job')->like('name', 'VELOPER', 'both', null, true)->get(); + $job = $job->getRow(); + + $this->assertEquals(1, $job->id); + $this->assertEquals('Developer', $job->name); + } + + //-------------------------------------------------------------------- + public function testOrLike() { $jobs = $this->db->table('job')->like('name', 'ian') @@ -60,7 +73,7 @@ public function testOrLike() ->get() ->getResult(); - $this->assertEquals(3, count($jobs)); + $this->assertCount(3, $jobs); $this->assertEquals('Developer', $jobs[0]->name); $this->assertEquals('Politician', $jobs[1]->name); $this->assertEquals('Musician', $jobs[2]->name); @@ -75,7 +88,7 @@ public function testNotLike() ->get() ->getResult(); - $this->assertEquals(3, count($jobs)); + $this->assertCount(3, $jobs); $this->assertEquals('Politician', $jobs[0]->name); $this->assertEquals('Accountant', $jobs[1]->name); $this->assertEquals('Musician', $jobs[2]->name); @@ -91,7 +104,7 @@ public function testOrNotLike() ->get() ->getResult(); - $this->assertEquals(3, count($jobs)); + $this->assertCount(3, $jobs); $this->assertEquals('Politician', $jobs[0]->name); $this->assertEquals('Accountant', $jobs[1]->name); $this->assertEquals('Musician', $jobs[2]->name); @@ -105,11 +118,11 @@ public function testLikeSpacesOrTabs() $spaces = $builder->like('value', ' ')->get()->getResult(); $tabs = $builder->like('value', "\t")->get()->getResult(); - $this->assertEquals(1, count($spaces)); - $this->assertEquals(1, count($tabs)); + $this->assertCount(1, $spaces); + $this->assertCount(1, $tabs); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/LimitTest.php b/tests/system/Database/Live/LimitTest.php index 3cdcc7013169..ad5359b41e73 100644 --- a/tests/system/Database/Live/LimitTest.php +++ b/tests/system/Database/Live/LimitTest.php @@ -1,13 +1,15 @@ get() ->getResult(); - $this->assertEquals(2, count($jobs)); + $this->assertCount(2, $jobs); $this->assertEquals('Developer', $jobs[0]->name); $this->assertEquals('Politician', $jobs[1]->name); } @@ -30,11 +32,11 @@ public function testLimitAndOffset() ->get() ->getResult(); - $this->assertEquals(2, count($jobs)); + $this->assertCount(2, $jobs); $this->assertEquals('Accountant', $jobs[0]->name); $this->assertEquals('Musician', $jobs[1]->name); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/ModelTest.php b/tests/system/Database/Live/ModelTest.php index df3c7fd0bccf..baa8387e8484 100644 --- a/tests/system/Database/Live/ModelTest.php +++ b/tests/system/Database/Live/ModelTest.php @@ -1,17 +1,26 @@ model->encodeID($expected); - - $this->assertNotEquals($expected, $str); - - $this->assertEquals($expected, $this->model->decodeID($str)); - } - - //-------------------------------------------------------------------- - - public function testHashIDsWithString() - { - $expected = 'my test hash'; - - $str = $this->model->encodeID($expected); - - $this->assertNotEquals($expected, $str); - - $this->assertEquals($expected, $this->model->decodeID($str)); - } - - //-------------------------------------------------------------------- - - public function testHashedIdsWithFind() - { - $hash = $this->model->encodeId(4); - - $this->model->setTable('job') - ->withDeleted(); - - $user = $this->model->asObject() - ->findByHashedID($hash); - - $this->assertNotEmpty($user); - $this->assertEquals(4, $user->id); - } - - //-------------------------------------------------------------------- - public function testFindReturnsRow() { $model = new JobModel($this->db); @@ -87,83 +54,60 @@ public function testFindReturnsMultipleRows() //-------------------------------------------------------------------- - public function testFindRespectsReturnArray() + public function testFindActsAsGetWithNoParams() { $model = new JobModel($this->db); - $job = $model->asArray()->find(4); + $jobs = $model->asArray()->find(); - $this->assertTrue(is_array($job)); - } + $this->assertCount(4, $jobs); - //-------------------------------------------------------------------- - - public function testFindRespectsReturnObject() - { - $model = new JobModel($this->db); - - $job = $model->asObject()->find(4); - - $this->assertTrue(is_object($job)); + $names = array_column($jobs, 'name'); + $this->assertTrue(in_array('Developer', $names)); + $this->assertTrue(in_array('Politician', $names)); + $this->assertTrue(in_array('Accountant', $names)); + $this->assertTrue(in_array('Musician', $names)); } //-------------------------------------------------------------------- - public function testFindRespectsSoftDeletes() - { - $this->db->table('user')->where('id', 4)->update(['deleted' => 1]); - - $model = new UserModel($this->db); - - $user = $model->asObject()->find(4); - - $this->assertTrue(empty($user)); - - $user = $model->withDeleted()->find(4); - - $this->assertEquals(1, count($user)); - } - - //-------------------------------------------------------------------- - - public function testFindWhereSimple() + public function testFindRespectsReturnArray() { - $model = new JobModel($this->db); + $model = new JobModel($this->db); - $jobs = $model->asObject()->findWhere('id >', 2); + $job = $model->asArray()->find(4); - $this->assertEquals(2, count($jobs)); - $this->assertEquals('Accountant', $jobs[0]->name); - $this->assertEquals('Musician', $jobs[1]->name); + $this->assertInternalType('array', $job); } //-------------------------------------------------------------------- - public function testFindWhereWithArrayWhere() + public function testFindRespectsReturnObject() { $model = new JobModel($this->db); - $jobs = $model->asArray()->findWhere(['id' => 1]); + $job = $model->asObject()->find(4); - $this->assertEquals(1, count($jobs)); - $this->assertEquals('Developer', $jobs[0]['name']); + $this->assertInternalType('object', $job); } //-------------------------------------------------------------------- - public function testFindWhereRespectsSoftDeletes() + public function testFindRespectsSoftDeletes() { $this->db->table('user')->where('id', 4)->update(['deleted' => 1]); $model = new UserModel($this->db); - $user = $model->findWhere('id >', '2'); + $user = $model->asObject()->find(4); - $this->assertEquals(1, count($user)); + $this->assertEmpty($user); - $user = $model->withDeleted()->findWhere('id >', 2); + $user = $model->withDeleted()->find(4); - $this->assertEquals(2, count($user)); + // fix for PHP7.2 + $count = is_array($user) ? count($user) : 1; + $this->assertEquals(1, $count); } //-------------------------------------------------------------------- @@ -174,7 +118,7 @@ public function testFindAllReturnsAllRecords() $users = $model->findAll(); - $this->assertEquals(4, count($users)); + $this->assertCount(4, $users); } //-------------------------------------------------------------------- @@ -185,7 +129,7 @@ public function testFindAllRespectsLimits() $users = $model->findAll(2); - $this->assertEquals(2, count($users)); + $this->assertCount(2, $users); $this->assertEquals('Derek Jones', $users[0]->name); } @@ -197,7 +141,7 @@ public function testFindAllRespectsLimitsAndOffset() $users = $model->findAll(2, 2); - $this->assertEquals(2, count($users)); + $this->assertCount(2, $users); $this->assertEquals('Richard A Causey', $users[0]->name); } @@ -211,11 +155,11 @@ public function testFindAllRespectsSoftDeletes() $user = $model->findAll(); - $this->assertEquals(3, count($user)); + $this->assertCount(3, $user); $user = $model->withDeleted()->findAll(); - $this->assertEquals(4, count($user)); + $this->assertCount(4, $user); } //-------------------------------------------------------------------- @@ -226,7 +170,9 @@ public function testFirst() $user = $model->where('id >', 2)->first(); - $this->assertEquals(1, count($user)); + // fix for PHP7.2 + $count = is_array($user) ? count($user) : 1; + $this->assertEquals(1, $count); $this->assertEquals(3, $user->id); } @@ -240,7 +186,9 @@ public function testFirstRespectsSoftDeletes() $user = $model->first(); - $this->assertEquals(1, count($user)); + // fix for PHP7.2 + $count = is_array($user) ? count($user) : 1; + $this->assertEquals(1, $count); $this->assertEquals(2, $user->id); $user = $model->withDeleted()->first(); @@ -250,9 +198,6 @@ public function testFirstRespectsSoftDeletes() //-------------------------------------------------------------------- - /** - * @group single - */ public function testSaveNewRecordObject() { $model = new JobModel(); @@ -260,7 +205,7 @@ public function testSaveNewRecordObject() $data = new \stdClass(); $data->name = 'Magician'; $data->description = 'Makes peoples things dissappear.'; - + $model->protect(false)->save($data); $this->seeInDatabase('job', ['name' => 'Magician']); @@ -327,10 +272,11 @@ public function testSaveProtected() $data->id = 1; $data->name = 'Engineer'; $data->description = 'A fancier term for Developer.'; + $data->random_thing = 'Something wicked'; // If not protected, this would kill the script. - $this->setExpectedException('CodeIgniter\DatabaseException'); + $result = $model->protect(true)->save($data); - $model->protect(true)->save($data); + $this->assertTrue($result); } //-------------------------------------------------------------------- @@ -374,28 +320,32 @@ public function testDeleteWithSoftDeletesPurge() //-------------------------------------------------------------------- - public function testDeleteWhereWithSoftDeletes() + public function testDeleteMultiple() { - $model = new UserModel(); + $model = new JobModel(); - $this->seeInDatabase('user', ['name' =>'Derek Jones', 'deleted' => 0]); - $model->deleteWhere('name', 'Derek Jones'); + $this->seeInDatabase('job', ['name' =>'Developer']); + $this->seeInDatabase('job', ['name' =>'Politician']); - $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted' => 1]); + $model->delete([1, 2]); + + $this->dontSeeInDatabase('job', ['name' => 'Developer']); + $this->dontSeeInDatabase('job', ['name' => 'Politician']); + $this->seeInDatabase('job', ['name' =>'Accountant']); } //-------------------------------------------------------------------- - public function testDeleteWhereWithSoftDeletesPurge() + public function testDeleteNoParams() { - $model = new UserModel(); + $model = new JobModel(); - $this->seeInDatabase('user', ['name' =>'Derek Jones', 'deleted' => 0]); + $this->seeInDatabase('job', ['name' =>'Developer']); - $model->deleteWhere('name', 'Derek Jones', true); + $model->where('id', 1)->delete(); - $this->dontSeeInDatabase('user', ['name' => 'Derek Jones']); + $this->dontSeeInDatabase('job', ['name' => 'Developer']); } //-------------------------------------------------------------------- @@ -410,7 +360,7 @@ public function testPurgeDeleted() $users = $model->withDeleted()->findAll(); - $this->assertEquals(3, count($users)); + $this->assertCount(3, $users); } //-------------------------------------------------------------------- @@ -423,7 +373,7 @@ public function testOnlyDeleted() $users = $model->onlyDeleted()->findAll(); - $this->assertEquals(1, count($users)); + $this->assertCount(1, $users); } //-------------------------------------------------------------------- @@ -443,5 +393,323 @@ public function testChunk() //-------------------------------------------------------------------- + public function testValidationBasics() + { + $model = new ValidModel($this->db); + + $data = [ + 'description' => 'some great marketing stuff' + ]; + + $this->assertFalse($model->insert($data)); + + $errors = $model->errors(); + + $this->assertEquals('You forgot to name the baby.', $errors['name']); + } + + //-------------------------------------------------------------------- + + public function testValidationPlaceholdersSuccess() + { + $model = new ValidModel($this->db); + + $data = [ + 'name' => 'abc', + 'id' => 13, + 'token' => 13 + ]; + + $this->assertTrue($model->validate($data)); + } + + public function testValidationPlaceholdersFail() + { + $model = new ValidModel($this->db); + + $data = [ + 'name' => 'abc', + 'id' => 13, + 'token' => 12 + ]; + + $this->assertFalse($model->validate($data)); + } + + public function testSkipValidation() + { + $model = new ValidModel($this->db); + + $data = [ + 'name' => '2', + 'description' => 'some great marketing stuff' + ]; + + $this->assertInternalType('numeric', $model->skipValidation(true)->insert($data)); + } + + //-------------------------------------------------------------------- -} \ No newline at end of file + public function testCanCreateAndSaveEntityClasses() + { + $model = new EntityModel($this->db); + + $entity = $model->where('name', 'Developer')->first(); + + $this->assertInstanceOf(SimpleEntity::class, $entity); + $this->assertEquals('Developer', $entity->name); + $this->assertEquals('Awesome job, but sometimes makes you bored', $entity->description); + + $entity->name = 'Senior Developer'; + $entity->created_at = '2017-07-15'; + + $date = $this->getPrivateProperty($entity, 'created_at'); + $this->assertInstanceOf(Time::class, $date); + + $this->assertTrue($model->save($entity)); + + $this->seeInDatabase('job', ['name' => 'Senior Developer', 'created_at' => '2017-07-15 00:00:00']); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/580 + */ + public function testPasswordsStoreCorrectly() + { + $model = new UserModel(); + + $pass = password_hash('secret123', PASSWORD_BCRYPT); + + $data = [ + 'name' => $pass, + 'email' => 'foo@example.com', + 'country' => 'US', + 'deleted' => 0 + ]; + + $model->insert($data); + + $this->seeInDatabase('user', $data); + } + + public function testInsertEvent() + { + $model = new EventModel(); + + $data = [ + 'name' => 'Foo', + 'email' => 'foo@example.com', + 'country' => 'US', + 'deleted' => 0 + ]; + + $model->insert($data); + + $this->assertTrue($model->hasToken('beforeInsert')); + $this->assertTrue($model->hasToken('afterInsert')); + } + + public function testUpdateEvent() + { + $model = new EventModel(); + + $data = [ + 'name' => 'Foo', + 'email' => 'foo@example.com', + 'country' => 'US', + 'deleted' => 0 + ]; + + $id = $model->insert($data); + $model->update($id, $data); + + $this->assertTrue($model->hasToken('beforeUpdate')); + $this->assertTrue($model->hasToken('afterUpdate')); + } + + public function testFindEvent() + { + $model = new EventModel(); + + $model->find(1); + + $this->assertTrue($model->hasToken('afterFind')); + } + + public function testDeleteEvent() + { + $model = new EventModel(); + + $model->delete(1); + + $this->assertTrue($model->hasToken('afterDelete')); + } + + public function testSetWorksWithInsert() + { + $model = new EventModel(); + + $this->dontSeeInDatabase('user', [ + 'email' => 'foo@example.com' + ]); + + $model->set([ + 'email' => 'foo@example.com', + 'name' => 'Foo Bar', + 'country' => 'US' + ])->insert(); + + $this->seeInDatabase('user', [ + 'email' => 'foo@example.com' + ]); + } + + public function testSetWorksWithUpdate() + { + $model = new EventModel(); + + $this->dontSeeInDatabase('user', [ + 'email' => 'foo@example.com' + ]); + + $userId = $model->insert([ + 'email' => 'foo@example.com', + 'name' => 'Foo Bar', + 'country' => 'US' + ]); + + $model->set([ + 'name' => 'Fred Flintstone' + ])->update($userId); + + $this->seeInDatabase('user', [ + 'id' => $userId, + 'email' => 'foo@example.com', + 'name' => 'Fred Flintstone' + ]); + } + + public function testSetWorksWithUpdateNoId() + { + $model = new EventModel(); + + $this->dontSeeInDatabase('user', [ + 'email' => 'foo@example.com' + ]); + + $userId = $model->insert([ + 'email' => 'foo@example.com', + 'name' => 'Foo Bar', + 'country' => 'US' + ]); + + $model + ->where('id', $userId) + ->set([ + 'name' => 'Fred Flintstone' + ])->update(); + + $this->seeInDatabase('user', [ + 'id' => $userId, + 'email' => 'foo@example.com', + 'name' => 'Fred Flintstone' + ]); + } + + public function testUpdateArray() + { + $model = new EventModel(); + + $data = [ + 'name' => 'Foo', + 'email' => 'foo@example.com', + 'country' => 'US', + 'deleted' => 0 + ]; + + $id = $model->insert($data); + $model->update([1,2], ['name' => 'Foo Bar']); + + $this->seeInDatabase('user', ['id' => 1, 'name' => 'Foo Bar']); + $this->seeInDatabase('user', ['id' => 2, 'name' => 'Foo Bar']); + } + + public function testInsertBatchSuccess() + { + $job_data = [ + ['name' => 'Comedian', 'description' => 'Theres something in your teeth'], + ['name' => 'Cab Driver', 'description' => 'Iam yellow'], + ]; + + $model = new JobModel($this->db); + $model->insertBatch($job_data); + + $this->seeInDatabase('job', ['name' => 'Comedian']); + $this->seeInDatabase('job', ['name' => 'Cab Driver']); + } + + public function testInsertBatchValidationFail() + { + $job_data = [ + ['name' => 'Comedian', 'description' => null], + ]; + + $model = new JobModel($this->db); + + $this->setPrivateProperty($model, 'validationRules', ['description' => 'required']); + + $this->assertFalse($model->insertBatch($job_data)); + + $error = $model->errors(); + $this->assertTrue(isset($error['description'])); + } + + public function testUpdateBatchSuccess() + { + $data = [ + [ + 'name' => 'Derek Jones', + 'country' => 'Greece' + ], + [ + 'name' => 'Ahmadinejad', + 'country' => 'Greece' + ], + ]; + + $model = new EventModel($this->db); + + $model->updateBatch($data, 'name'); + + $this->seeInDatabase('user', [ + 'name' => 'Derek Jones', + 'country' => 'Greece' + ]); + $this->seeInDatabase('user', [ + 'name' => 'Ahmadinejad', + 'country' => 'Greece' + ]); + } + + //-------------------------------------------------------------------- + + public function testUpdateBatchValidationFail() + { + $data = [ + [ + 'name' => 'Derek Jones', + 'country' => null + ], + ]; + + $model = new EventModel($this->db); + $this->setPrivateProperty($model, 'validationRules', ['country' => 'required']); + + $this->assertFalse($model->updateBatch($data, 'name')); + + $error = $model->errors(); + $this->assertTrue(isset($error['country'])); + } + + //-------------------------------------------------------------------- +} diff --git a/tests/system/Database/Live/OrderTest.php b/tests/system/Database/Live/OrderTest.php index 05ca4e167f81..c239a10e8764 100644 --- a/tests/system/Database/Live/OrderTest.php +++ b/tests/system/Database/Live/OrderTest.php @@ -1,13 +1,15 @@ get() ->getResult(); - $this->assertEquals(4, count($jobs)); + $this->assertCount(4, $jobs); $this->assertEquals('Accountant', $jobs[0]->name); $this->assertEquals('Developer', $jobs[1]->name); $this->assertEquals('Musician', $jobs[2]->name); @@ -32,7 +34,7 @@ public function testOrderDescending() ->get() ->getResult(); - $this->assertEquals(4, count($jobs)); + $this->assertCount(4, $jobs); $this->assertEquals('Accountant', $jobs[3]->name); $this->assertEquals('Developer', $jobs[2]->name); $this->assertEquals('Musician', $jobs[1]->name); @@ -49,7 +51,7 @@ public function testMultipleOrderValues() ->get() ->getResult(); - $this->assertEquals(4, count($users)); + $this->assertCount(4, $users); $this->assertEquals('Ahmadinejad', $users[0]->name); $this->assertEquals('Chris Martin', $users[1]->name); $this->assertEquals('Richard A Causey', $users[2]->name); @@ -59,4 +61,4 @@ public function testMultipleOrderValues() //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php new file mode 100644 index 000000000000..f283f3cc6597 --- /dev/null +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -0,0 +1,67 @@ +db->prepare(function($db){ + return $db->table('user')->insert([ + 'name' => 'a', + 'email' => 'b@example.com' + ]); + }); + + $this->assertInstanceOf(BasePreparedQuery::class, $query); + + $ec = $this->db->escapeChar; + $pre = $this->db->DBPrefix; + + $placeholders = '?, ?'; + + if ($this->db->DBDriver == 'Postgre') + { + $placeholders = '$1, $2'; + } + + $expected = "INSERT INTO {$ec}{$pre}user{$ec} ({$ec}name{$ec}, {$ec}email{$ec}) VALUES ({$placeholders})"; + $this->assertEquals($expected, $query->getQueryString()); + + $query->close(); + } + + //-------------------------------------------------------------------- + + public function testExecuteRunsQueryAndReturnsResultObject() + { + $query = $this->db->prepare(function($db){ + return $db->table('user')->insert([ + 'name' => 'a', + 'email' => 'b@example.com', + 'country' => 'x' + ]); + }); + + $query->execute('foo', 'foo@example.com', 'US'); + $query->execute('bar', 'bar@example.com', 'GB'); + + $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'foo', 'email' => 'foo@example.com']); + $this->seeInDatabase($this->db->DBPrefix.'user', ['name' => 'bar', 'email' => 'bar@example.com']); + + $query->close(); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/system/Database/Live/PretendTest.php b/tests/system/Database/Live/PretendTest.php new file mode 100644 index 000000000000..5ae96dfb2ff7 --- /dev/null +++ b/tests/system/Database/Live/PretendTest.php @@ -0,0 +1,34 @@ +db` in testing, so we need to restore the state. + $this->db->pretend(false); + } + + public function testPretendReturnsQueryObject() + { + $result = $this->db->pretend(false) + ->table('user') + ->get(); + + $this->assertFalse($result instanceof Query); + + $result = $this->db->pretend(true) + ->table('user') + ->get(); + + $this->assertInstanceOf(Query::class, $result); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/system/Database/Live/SelectTest.php b/tests/system/Database/Live/SelectTest.php index 997165cb140c..39c1d47abb19 100644 --- a/tests/system/Database/Live/SelectTest.php +++ b/tests/system/Database/Live/SelectTest.php @@ -1,19 +1,22 @@ db->table('job')->get()->getRowArray(); + $row = $this->db->table('job')->get()->getRowArray(); $this->assertArrayHasKey('id', $row); $this->assertArrayHasKey('name', $row); @@ -26,9 +29,9 @@ public function testSelectSingleColumn() { $row = $this->db->table('job')->select('name')->get()->getRowArray(); - $this->assertFalse(array_key_exists('id', $row)); + $this->assertArrayNotHasKey('id', $row); $this->assertArrayHasKey('name', $row); - $this->assertFalse(array_key_exists('description', $row)); + $this->assertArrayNotHasKey('description', $row); } //-------------------------------------------------------------------- @@ -37,7 +40,7 @@ public function testSelectMultipleColumns() { $row = $this->db->table('job')->select('name, description')->get()->getRowArray(); - $this->assertFalse(array_key_exists('id', $row)); + $this->assertArrayNotHasKey('id', $row); $this->assertArrayHasKey('name', $row); $this->assertArrayHasKey('description', $row); } @@ -120,7 +123,7 @@ public function testSelectDistinctWorkTogether() { $users = $this->db->table('user')->select('country')->distinct()->get()->getResult(); - $this->assertEquals(3, count($users)); + $this->assertCount(3, $users); } //-------------------------------------------------------------------- @@ -129,8 +132,8 @@ public function testSelectDistinctCanBeTurnedOff() { $users = $this->db->table('user')->select('country')->distinct(false)->get()->getResult(); - $this->assertEquals(4, count($users)); + $this->assertCount(4, $users); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index 6c939fecc8cb..729f1ba96afd 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -1,15 +1,16 @@ assertTrue(true); return; } } @@ -68,7 +72,7 @@ public function testUpdateWithWhere() } } - $this->assertEquals(2, count($rows)); + $this->assertCount(2, $rows); } //-------------------------------------------------------------------- @@ -91,6 +95,9 @@ public function testUpdateWithWhereAndLimit() } catch (DatabaseException $e) { + // This DB doesn't support Where and Limit together + // but we don't want it called a "Risky" test. + $this->assertTrue(true); return; } } @@ -125,5 +132,109 @@ public function testUpdateBatch() //-------------------------------------------------------------------- + public function testUpdateWithWhereSameColumn() + { + $this->db->table('user') + ->update(['country' => 'CA'], ['country' => 'US']); + + $result = $this->db->table('user')->get()->getResultArray(); + + $rows = []; + + foreach ($result as $row) + { + if ($row['country'] == 'CA') + { + $rows[] = $row; + } + } + + $this->assertCount(2, $rows); + } + + //-------------------------------------------------------------------- + + public function testUpdateWithWhereSameColumn2() + { + // calling order: set() -> where() + $this->db->table('user') + ->set('country', 'CA') + ->where('country', 'US') + ->update(); + + $result = $this->db->table('user')->get()->getResultArray(); + + $rows = []; + + foreach ($result as $row) + { + if ($row['country'] == 'CA') + { + $rows[] = $row; + } + } + + $this->assertCount(2, $rows); + } + + //-------------------------------------------------------------------- + + public function testUpdateWithWhereSameColumn3() + { + // calling order: where() -> set() in update() + $this->db->table('user') + ->where('country', 'US') + ->update(['country' => 'CA']); + + $result = $this->db->table('user')->get()->getResultArray(); + + $rows = []; + + foreach ($result as $row) + { + if ($row['country'] == 'CA') + { + $rows[] = $row; + } + } + + $this->assertCount(2, $rows); + } + + //-------------------------------------------------------------------- + + /** + * @group single + * @see https://github.com/bcit-ci/CodeIgniter4/issues/324 + */ + public function testUpdatePeriods() + { + $this->db->table('misc') + ->where('key', 'spaces and tabs') + ->update([ + 'value' => '30.192' + ]); + + $this->seeInDatabase('misc', [ + 'value' => '30.192' + ]); + } + + //-------------------------------------------------------------------- + + // @see https://bcit-ci.github.io/CodeIgniter4/database/query_builder.html#updating-data + public function testSetWithoutEscape() + { + $this->db->table('job') + ->set('description', 'name', false) + ->update(); + + $result = $this->db->table('user')->get()->getResultArray(); + + $this->seeInDatabase('job', [ + 'name' => 'Developer', + 'description' => 'Developer', + ]); + } -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/WhereTest.php b/tests/system/Database/Live/WhereTest.php index 10ce7e683cca..aeea97b39b02 100644 --- a/tests/system/Database/Live/WhereTest.php +++ b/tests/system/Database/Live/WhereTest.php @@ -1,13 +1,15 @@ db->table('job')->where('id !=', 1)->get()->getResult(); - $this->assertEquals(3, count($jobs)); + $this->assertCount(3, $jobs); } //-------------------------------------------------------------------- @@ -35,7 +37,7 @@ public function testWhereArray() 'name !=' => 'Accountant' ])->get()->getResult(); - $this->assertEquals(1, count($jobs)); + $this->assertCount(1, $jobs); $job = current($jobs); $this->assertEquals('Musician', $job->name); @@ -49,7 +51,7 @@ public function testWhereCustomString() ->get() ->getResult(); - $this->assertEquals(1, count($jobs)); + $this->assertCount(1, $jobs); $job = current($jobs); $this->assertEquals('Musician', $job->name); @@ -65,7 +67,7 @@ public function testOrWhere() ->get() ->getResult(); - $this->assertEquals(3, count($jobs)); + $this->assertCount(3, $jobs); $this->assertEquals('Developer', $jobs[0]->name); $this->assertEquals('Politician', $jobs[1]->name); $this->assertEquals('Musician', $jobs[2]->name); @@ -73,6 +75,21 @@ public function testOrWhere() //-------------------------------------------------------------------- + public function testOrWhereSameColumn() + { + $jobs = $this->db->table('job') + ->where('name', 'Developer') + ->orWhere('name', 'Politician') + ->get() + ->getResult(); + + $this->assertCount(2, $jobs); + $this->assertEquals('Developer', $jobs[0]->name); + $this->assertEquals('Politician', $jobs[1]->name); + } + + //-------------------------------------------------------------------- + public function testWhereIn() { $jobs = $this->db->table('job') @@ -80,13 +97,16 @@ public function testWhereIn() ->get() ->getResult(); - $this->assertEquals(2, count($jobs)); + $this->assertCount(2, $jobs); $this->assertEquals('Politician', $jobs[0]->name); $this->assertEquals('Accountant', $jobs[1]->name); } //-------------------------------------------------------------------- + /** + * @group single + */ public function testWhereNotIn() { $jobs = $this->db->table('job') @@ -94,11 +114,11 @@ public function testWhereNotIn() ->get() ->getResult(); - $this->assertEquals(2, count($jobs)); + $this->assertCount(2, $jobs); $this->assertEquals('Developer', $jobs[0]->name); $this->assertEquals('Musician', $jobs[1]->name); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 550c4492b443..7a354872c5c4 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -4,7 +4,7 @@ class ExceptionsTest extends \CIUnitTestCase { public function testNew() { - $actual = new Exceptions(new \Config\App()); + $actual = new Exceptions(new \Config\Exceptions(), \Config\Services::request(), \Config\Services::response()); $this->assertInstanceOf(Exceptions::class, $actual); } } diff --git a/tests/system/Debug/TimerTest.php b/tests/system/Debug/TimerTest.php index 28852debf0c0..961d7999bcbe 100644 --- a/tests/system/Debug/TimerTest.php +++ b/tests/system/Debug/TimerTest.php @@ -26,7 +26,7 @@ public function testStoresTimers() $timers = $timer->getTimers(); - $this->assertTrue(count($timers) === 1, "No timers were stored."); + $this->assertCount(1, $timers, "No timers were stored."); $this->assertArrayHasKey('test1', $timers, 'No "test1" array found.'); $this->assertArrayHasKey('start', $timers['test1'], 'No "start" value found.'); $this->assertArrayHasKey('end', $timers['test1'], 'No "end" value found.'); @@ -83,5 +83,45 @@ public function testThrowsExceptionStoppingNonTimer() //-------------------------------------------------------------------- + public function testLongExecutionTime() + { + $timer = new Timer(); + $timer->start('longjohn', strtotime('-11 minutes')); + + // Use floor here to account for fractional differences in seconds. + $this->assertEquals(11 * 60, (int)$timer->getElapsedTime('longjohn')); + } + + //-------------------------------------------------------------------- + + public function testLongExecutionTimeThroughCommonFunc() + { + timer()->start('longjohn', strtotime('-11 minutes')); + + // Use floor here to account for fractional differences in seconds. + $this->assertEquals(11 * 60, (int)timer()->getElapsedTime('longjohn')); + } + + //-------------------------------------------------------------------- + + public function testCommonStartStop() + { + timer('test1'); + sleep(1); + timer('test1'); + + $this->assertGreaterThanOrEqual(1.0, timer()->getElapsedTime('test1')); + } + + //-------------------------------------------------------------------- + + public function testReturnsNullGettingElapsedTimeOfNonTimer() + { + $timer = new Timer(); + + $this->assertNull($timer->getElapsedTime('test1')); + } + + //-------------------------------------------------------------------- } diff --git a/tests/system/EntityTest.php b/tests/system/EntityTest.php new file mode 100644 index 000000000000..7333c1f26141 --- /dev/null +++ b/tests/system/EntityTest.php @@ -0,0 +1,474 @@ +getEntity(); + + $entity->foo = 'to wong'; + + $this->assertEquals('to wong', $entity->foo); + } + + public function testGetterSetters() + { + $entity = $this->getEntity(); + + $entity->bar = 'thanks'; + + $this->assertEquals('bar:thanks:bar', $entity->bar); + } + + public function testUnsetResetsToDefaultValue() + { + $entity = $this->getEntity(); + + $this->assertEquals('sumfin', $entity->default); + + $entity->default = 'else'; + + $this->assertEquals('else', $entity->default); + + unset($entity->default); + + $this->assertEquals('sumfin', $entity->default); + } + + public function testIssetWorksLikeTraditionalIsset() + { + $entity = $this->getEntity(); + + $this->assertFalse(isset($entity->foo)); + $this->assertObjectHasAttribute('default', $entity); + } + + public function testFill() + { + $entity = $this->getEntity(); + + $entity->fill([ + 'foo' => 123, + 'bar' => 234, + 'baz' => 4556 + ]); + + $this->assertEquals(123, $entity->foo); + $this->assertEquals('bar:234:bar', $entity->bar); + $this->assertObjectNotHasAttribute('baz', $entity); + } + + public function testDataMappingConvertsOriginalName() + { + $entity = $this->getMappedEntity(); + + $entity->bar = 'made it'; + + // Check mapped field + $this->assertEquals('made it', $entity->foo); + + // Should also get from original name + // since Model's would be looking for the original name + $this->assertEquals('made it', $entity->bar); + + // But it shouldn't actually set a class property for the original name... + $this->expectException(\ReflectionException::class); + $this->getPrivateProperty($entity, 'bar'); + } + + public function testDataMappingWorksWithCustomSettersAndGetters() + { + $entity = $this->getMappedEntity(); + + // Will map to "simple" + $entity->orig = 'first'; + + $this->assertEquals('oo:first:oo', $entity->simple); + + $entity->simple = 'second'; + + $this->assertEquals('oo:second:oo', $entity->simple); + } + + public function testIssetWorksWithMapping() + { + $entity = $this->getMappedEntity(); + + // maps to 'foo' + $entity->bar = 'here'; + + $this->assertObjectHasAttribute('foo', $entity); + $this->assertObjectNotHasAttribute('bar', $entity); + } + + public function testUnsetWorksWithMapping() + { + $entity = $this->getMappedEntity(); + + // maps to 'foo' + $entity->bar = 'here'; + + // doesn't work on original name + unset($entity->bar); + $this->assertEquals('here', $entity->bar); + $this->assertEquals('here', $entity->foo); + + // does work on mapped field + unset($entity->foo); + $this->assertNull($entity->foo); + $this->assertNull($entity->bar); + } + + public function testDateMutationFromString() + { + $entity = $this->getEntity(); + $this->setPrivateProperty($entity, 'created_at', '2017-07-15 13:23:34'); + + $time = $entity->created_at; + + $this->assertInstanceOf(Time::class, $time); + $this->assertEquals('2017-07-15 13:23:34', $time->format('Y-m-d H:i:s')); + } + + public function testDateMutationFromTimestamp() + { + $stamp = time(); + + $entity = $this->getEntity(); + $this->setPrivateProperty($entity, 'created_at', $stamp); + + $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')); + } + + public function testDateMutationFromDatetime() + { + $dt = new \DateTime('now'); + $entity = $this->getEntity(); + $this->setPrivateProperty($entity, 'created_at', $dt); + + $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')); + } + + public function testDateMutationFromTime() + { + $dt = Time::now(); + $entity = $this->getEntity(); + $this->setPrivateProperty($entity, 'created_at', $dt); + + $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')); + } + + public function testDateMutationStringToTime() + { + $entity = $this->getEntity(); + + $entity->created_at = '2017-07-15 13:23:34'; + + $time = $this->getPrivateProperty($entity, 'created_at'); + + $this->assertInstanceOf(Time::class, $time); + $this->assertEquals('2017-07-15 13:23:34', $time->format('Y-m-d H:i:s')); + } + + public function testDateMutationTimestampToTime() + { + $stamp = time(); + $entity = $this->getEntity(); + + $entity->created_at = $stamp; + + $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')); + } + + public function testDateMutationDatetimeToTime() + { + $dt = new \DateTime('now'); + $entity = $this->getEntity(); + + $entity->created_at = $dt; + + $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')); + } + + public function testDateMutationTimeToTime() + { + $dt = Time::now(); + $entity = $this->getEntity(); + + $entity->created_at = $dt; + + $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')); + } + + public function testCastInteger() + { + $entity = $this->getCastEntity(); + + $entity->first = 3.1; + $this->assertInternalType('integer', $entity->first); + $this->assertEquals(3, $entity->first); + + $entity->first = 3.6; + $this->assertEquals(3, $entity->first); + } + + public function testCastFloat() + { + $entity = $this->getCastEntity(); + + $entity->second = 3; + $this->assertInternalType('float', $entity->second); + $this->assertEquals(3.0, $entity->second); + + $entity->second = '3.6'; + $this->assertInternalType('float', $entity->second); + $this->assertEquals(3.6, $entity->second); + } + + public function testCastDouble() + { + $entity = $this->getCastEntity(); + + $entity->third = 3; + $this->assertInternalType('double', $entity->third); + $this->assertSame(3.0, $entity->third); + + $entity->third = '3.6'; + $this->assertInternalType('double', $entity->third); + $this->assertSame(3.6, $entity->third); + } + + public function testCastString() + { + $entity = $this->getCastEntity(); + + $entity->fourth = 3.1415; + $this->assertInternalType('string', $entity->fourth); + $this->assertSame('3.1415', $entity->fourth); + } + + public function testCastBoolean() + { + $entity = $this->getCastEntity(); + + $entity->fifth = 1; + $this->assertInternalType('bool', $entity->fifth); + $this->assertTrue($entity->fifth); + + $entity->fifth = 0; + $this->assertInternalType('bool', $entity->fifth); + $this->assertFalse($entity->fifth); + } + + public function testCastObject() + { + $entity = $this->getCastEntity(); + + $data = ['foo' => 'bar']; + + $entity->sixth = $data; + $this->assertInternalType('object', $entity->sixth); + $this->assertEquals((object)$data, $entity->sixth); + } + + public function testCastDateTime() + { + $entity = $this->getCastEntity(); + + $entity->eighth = 'March 12, 2017'; + $this->assertInstanceOf('DateTime', $entity->eighth); + $this->assertEquals('2017-03-12', $entity->eighth->format('Y-m-d')); + } + + public function testCastTimestamp() + { + $entity = $this->getCastEntity(); + + $date = 'March 12, 2017'; + + $entity->ninth = $date; + $this->assertInternalType('integer', $entity->ninth); + $this->assertEquals(strtotime($date), $entity->ninth); + } + + public function testCastArray() + { + $entity = $this->getCastEntity(); + + $entity->setSeventh(['foo' => 'bar']); + + $check = $this->getPrivateProperty($entity, 'seventh'); + $this->assertEquals(['foo' => 'bar'], $check); + + $this->assertEquals(['foo' => 'bar'], $entity->seventh); + } + + public function testCastArrayByStringSerialize() + { + $entity = $this->getCastEntity(); + + $entity->seventh = 'foobar'; + + // Should be a serialized string now... + $check = $this->getPrivateProperty($entity, 'seventh'); + $this->assertEquals(serialize('foobar'), $check); + + $this->assertEquals(['foobar'], $entity->seventh); + } + + public function testCastArrayByArraySerialize() + { + $entity = $this->getCastEntity(); + + $entity->seventh = ['foo' => 'bar']; + + // Should be a serialized string now... + $check = $this->getPrivateProperty($entity, 'seventh'); + $this->assertEquals(serialize(['foo' => 'bar']), $check); + + $this->assertEquals(['foo' => 'bar'], $entity->seventh); + } + + public function testAsArray() + { + $entity = $this->getEntity(); + + $result = $entity->toArray(); + + $this->assertEquals($result, [ + 'foo' => null, + 'bar' => ':bar', + 'default' => 'sumfin', + 'created_at' => null + ]); + } + + public function testAsArrayMapped() + { + $entity = $this->getMappedEntity(); + + $result = $entity->toArray(); + + $this->assertEquals($result, [ + 'foo' => null, + 'simple' => ':oo', + 'bar' => null, + 'orig' => ':oo' + ]); + } + + + protected function getEntity() + { + return new class extends Entity + { + protected $foo; + protected $bar; + protected $default = 'sumfin'; + protected $created_at; + + public function setBar($value) + { + $this->bar = "bar:{$value}"; + + return $this; + } + + public function getBar() + { + return "{$this->bar}:bar"; + } + + }; + } + + protected function getMappedEntity() + { + return new class extends Entity + { + protected $foo; + protected $simple; + + // 'bar' is db column, 'foo' is internal representation + protected $_options = [ + 'dates' => [], + 'casts' => [], + 'datamap' => [ + 'bar' => 'foo', + 'orig' => 'simple' + ] + ]; + + protected function setSimple(string $val) + { + $this->simple = 'oo:'.$val; + } + + protected function getSimple() + { + return $this->simple.':oo'; + } + }; + } + + protected function getCastEntity() + { + return new class extends Entity + { + protected $first; + protected $second; + protected $third; + protected $fourth; + protected $fifth; + protected $sixth; + protected $seventh; + protected $eighth; + protected $ninth; + + // 'bar' is db column, 'foo' is internal representation + protected $_options = [ + 'casts' => [ + 'first' => 'integer', + 'second' => 'float', + 'third' => 'double', + 'fourth' => 'string', + 'fifth' => 'boolean', + 'sixth' => 'object', + 'seventh' => 'array', + 'eighth' => 'datetime', + 'ninth' => 'timestamp' + ], + 'dates' => [], + 'datamap' => [] + ]; + + public function setSeventh($seventh) { + $this->seventh = $seventh; + } + + }; + } +} diff --git a/tests/system/Events/EventsTest.php b/tests/system/Events/EventsTest.php new file mode 100644 index 000000000000..11c3bb40cf63 --- /dev/null +++ b/tests/system/Events/EventsTest.php @@ -0,0 +1,280 @@ +manager = new MockEvents(); + + $config = config('Modules'); + $config->activeExplorers = []; + Config::injectMock('Modules', $config); + + Events::removeAllListeners(); + } + + //-------------------------------------------------------------------- + + public function testInitialize() + { + // it should start out empty + $default = [APPPATH . 'Config/Events.php']; + $this->manager->setFiles([]); + $this->assertEmpty($this->manager->getFiles()); + + // make sure we have a default events file + $this->manager->unInitialize(); + $this->manager::initialize(); + $this->assertEquals($default, $this->manager->getFiles()); + + // but we should be able to change it through the backdoor + $this->manager::setFiles(['/peanuts']); + $this->assertEquals(['/peanuts'], $this->manager->getFiles()); + } + + //-------------------------------------------------------------------- + + // Not working currently - might want to revisit at some point. +// public function testPerformance() +// { +// $logged = Events::getPerformanceLogs(); +// // there should be a few event activities logged +// $this->assertGreaterThan(0,count($logged)); +// +// // might want additional tests after some activity, or to inspect what has happened so far +// } + + //-------------------------------------------------------------------- + + public function testListeners() + { + $callback1 = function() { + + }; + $callback2 = function() { + + }; + + Events::on('foo', $callback1, EVENT_PRIORITY_HIGH); + Events::on('foo', $callback2, EVENT_PRIORITY_NORMAL); + + $this->assertEquals([$callback2, $callback1], Events::listeners('foo')); + } + + //-------------------------------------------------------------------- + + public function testHandleEvent() + { + $result = null; + + Events::on('foo', function($arg) use(&$result) { + $result = $arg; + }); + + $this->assertTrue(Events::trigger('foo', 'bar')); + + $this->assertEquals('bar', $result); + } + + //-------------------------------------------------------------------- + + public function testCancelEvent() + { + $result = 0; + + // This should cancel the flow of events, and leave + // $result = 1. + Events::on('foo', function($arg) use (&$result) { + $result = 1; + return false; + }); + Events::on('foo', function($arg) use (&$result) { + $result = 2; + }); + + $this->assertFalse(Events::trigger('foo', 'bar')); + $this->assertEquals(1, $result); + } + + //-------------------------------------------------------------------- + + public function testPriority() + { + $result = 0; + + Events::on('foo', function() use (&$result) { + $result = 1; + return false; + }, EVENT_PRIORITY_NORMAL); + // Since this has a higher priority, it will + // run first. + Events::on('foo', function() use (&$result) { + $result = 2; + return false; + }, EVENT_PRIORITY_HIGH); + + $this->assertFalse(Events::trigger('foo', 'bar')); + $this->assertEquals(2, $result); + } + + //-------------------------------------------------------------------- + + public function testPriorityWithMultiple() + { + $result = []; + + Events::on('foo', function() use (&$result) { + $result[] = 'a'; + }, EVENT_PRIORITY_NORMAL); + + Events::on('foo', function() use (&$result) { + $result[] = 'b'; + }, EVENT_PRIORITY_LOW); + + Events::on('foo', function() use (&$result) { + $result[] = 'c'; + }, EVENT_PRIORITY_HIGH); + + Events::on('foo', function() use (&$result) { + $result[] = 'd'; + }, 75); + + Events::trigger('foo'); + $this->assertEquals(['c', 'd', 'a', 'b'], $result); + } + + //-------------------------------------------------------------------- + + public function testRemoveListener() + { + $result = false; + + $callback = function() use (&$result) { + $result = true; + }; + + Events::on('foo', $callback); + + Events::trigger('foo'); + $this->assertTrue($result); + + $result = false; + $this->assertTrue(Events::removeListener('foo', $callback)); + + Events::trigger('foo'); + $this->assertFalse($result); + } + + //-------------------------------------------------------------------- + + public function testRemoveListenerTwice() + { + $result = false; + + $callback = function() use (&$result) { + $result = true; + }; + + Events::on('foo', $callback); + + Events::trigger('foo'); + $this->assertTrue($result); + + $result = false; + $this->assertTrue(Events::removeListener('foo', $callback)); + $this->assertFalse(Events::removeListener('foo', $callback)); + + Events::trigger('foo'); + $this->assertFalse($result); + } + + //-------------------------------------------------------------------- + + public function testRemoveUnknownListener() + { + $result = false; + + $callback = function() use (&$result) { + $result = true; + }; + + Events::on('foo', $callback); + + Events::trigger('foo'); + $this->assertTrue($result); + + $result = false; + $this->assertFalse(Events::removeListener('bar', $callback)); + + Events::trigger('foo'); + $this->assertTrue($result); + } + + //-------------------------------------------------------------------- + + public function testRemoveAllListenersWithSingleEvent() + { + $result = false; + + $callback = function() use (&$result) { + $result = true; + }; + + Events::on('foo', $callback); + + Events::removeAllListeners('foo'); + + $listeners = Events::listeners('foo'); + + $this->assertEquals([], $listeners); + } + + //-------------------------------------------------------------------- + + + public function testRemoveAllListenersWithMultipleEvents() + { + $result = false; + + $callback = function() use (&$result) { + $result = true; + }; + + Events::on('foo', $callback); + Events::on('bar', $callback); + + Events::removeAllListeners(); + + $this->assertEquals([], Events::listeners('foo')); + $this->assertEquals([], Events::listeners('bar')); + } + + //-------------------------------------------------------------------- + + public function testSimulate() + { + $result = 0; + + $callback = function() use (&$result) { + $result += 2; + }; + + Events::on('foo', $callback); + + Events::simulate(true); + Events::trigger('foo'); + + $this->assertEquals(0, $result); + } + +} diff --git a/tests/system/Files/FileTest.php b/tests/system/Files/FileTest.php new file mode 100644 index 000000000000..138a1fae9950 --- /dev/null +++ b/tests/system/Files/FileTest.php @@ -0,0 +1,72 @@ +assertEquals($path, $file->getRealPath()); + } + + public function testNewGoodUnchecked() + { + $path = BASEPATH . 'Common.php'; + $file = new File($path, false); + $this->assertEquals($path, $file->getRealPath()); + } + + public function testNewBadUnchecked() + { + $path = BASEPATH . 'bogus'; + $file = new File($path, false); + $this->assertFalse($file->getRealPath()); + } + + public function testGuessExtension() + { + $file = new File(BASEPATH . 'Common.php'); + $this->assertEquals('php', $file->guessExtension()); + $file = new File(BASEPATH . 'index.html'); + $this->assertEquals('html', $file->guessExtension()); + $file = new File(ROOTPATH . 'phpunit.xml.dist'); + $this->assertEquals('xml', $file->guessExtension()); + } + + public function testRandomName() + { + $file = new File(BASEPATH . 'Common.php'); + $result1 = $file->getRandomName(); + $this->assertNotEquals($result1, $file->getRandomName()); + } + + public function testCanAccessSplFileInfoMethods() + { + $file = new File(BASEPATH . 'Common.php'); + $this->assertEquals('file', $file->getType()); + } + + public function testGetSizeReturnsKB() + { + $file = new File(BASEPATH . 'Common.php'); + $size = number_format(filesize(BASEPATH . 'Common.php') / 1024, 3); + $this->assertEquals($size, $file->getSize('kb')); + } + + public function testGetSizeReturnsMB() + { + $file = new File(BASEPATH . 'Common.php'); + $size = number_format(filesize(BASEPATH . 'Common.php') / 1024 / 1024, 3); + $this->assertEquals($size, $file->getSize('mb')); + } + + /** + * @expectedException \CodeIgniter\Files\Exceptions\FileNotFoundException + */ + public function testThrowsExceptionIfNotAFile() + { + $file = new File(BASEPATH . 'Commoner.php', true); + } + +} diff --git a/tests/system/Files/FileWithVfsTest.php b/tests/system/Files/FileWithVfsTest.php new file mode 100644 index 000000000000..80d339f96d0d --- /dev/null +++ b/tests/system/Files/FileWithVfsTest.php @@ -0,0 +1,121 @@ +root = vfsStream::setup(); + $this->path = '_support/Files/'; + vfsStream::copyFromFileSystem(TESTPATH . $this->path, $this->root); + $this->start = $this->root->url() . '/'; + $this->file = new File($this->start . 'able/apple.php'); + } + + public function tearDown() + { + parent::tearDown(); + + $this->root = null; + } + + public function testDestinationUnknown() + { + $destination = $this->start . 'charlie/cherry.php'; + $this->assertEquals($destination, $this->file->getDestination($destination)); + } + + public function testDestinationSameFileSameFolder() + { + $destination = $this->start . 'able/apple.php'; + $this->assertEquals($this->start . 'able/apple_1.php', $this->file->getDestination($destination)); + } + + public function testDestinationSameFileDifferentFolder() + { + $destination = $this->start . 'baker/apple.php'; + $this->assertEquals($destination, $this->file->getDestination($destination)); + } + + public function testDestinationDifferentFileSameFolder() + { + $destination = $this->start . 'able/date.php'; + $this->assertEquals($destination, $this->file->getDestination($destination)); + } + + public function testDestinationDifferentFileDifferentFolder() + { + $destination = $this->start . 'baker/date.php'; + $this->assertEquals($destination, $this->file->getDestination($destination)); + } + + public function testDestinationExistingFileDifferentFolder() + { + $destination = $this->start . 'baker/banana.php'; + $this->assertEquals($this->start . 'baker/banana_1.php', $this->file->getDestination($destination)); + } + + public function testDestinationDelimited() + { + $destination = $this->start . 'able/fig_3.php'; + $this->assertEquals($this->start . 'able/fig_4.php', $this->file->getDestination($destination)); + } + + public function testDestinationDelimitedAlpha() + { + $destination = $this->start . 'able/prune_ripe.php'; + $this->assertEquals($this->start . 'able/prune_ripe_1.php', $this->file->getDestination($destination)); + } + + public function testMoveNormal() + { + $destination = $this->start . 'baker'; + $this->file->move($destination); + $this->assertTrue($this->root->hasChild('baker/apple.php')); + $this->assertFalse($this->root->hasChild('able/apple.php')); + } + + public function testMoveRename() + { + $destination = $this->start . 'baker'; + $this->file->move($destination, 'popcorn.php'); + $this->assertTrue($this->root->hasChild('baker/popcorn.php')); + $this->assertFalse($this->root->hasChild('able/apple.php')); + } + + public function testMoveOverwrite() + { + $destination = $this->start . 'baker'; + $this->file->move($destination, 'banana.php', true); + $this->assertTrue($this->root->hasChild('baker/banana.php')); + $this->assertFalse($this->root->hasChild('able/apple.php')); + } + + public function testMoveDontOverwrite() + { + $destination = $this->start . 'baker'; + $this->file->move($destination, 'banana.php'); + $this->assertTrue($this->root->hasChild('baker/banana_1.php')); + $this->assertFalse($this->root->hasChild('able/apple.php')); + } + + /** + * @expectedException \Exception + */ + public function testMoveFailure() + { + $here = $this->root->url(); + + chmod($here,400); // make a read-only folder + $destination = $here . '/charlie'; + $this->file->move($destination); // try to move our file there + } + +} diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php new file mode 100644 index 000000000000..1a3bdc029cb7 --- /dev/null +++ b/tests/system/Filters/FiltersTest.php @@ -0,0 +1,547 @@ +request = Services::request(); + $this->response = Services::response(); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodDetectsCLI() + { + $config = [ + 'methods' => [ + 'cli' => ['foo'] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + + $expected = [ + 'before' => ['foo'], + 'after' => [] + ]; + + $this->assertEquals($expected, $filters->initialize()->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodDetectsGetRequests() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'methods' => [ + 'get' => ['foo'] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + + $expected = [ + 'before' => ['foo'], + 'after' => [] + ]; + + $this->assertEquals($expected, $filters->initialize()->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodRespectsMethod() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'methods' => [ + 'post' => ['foo'], + 'get' => ['bar'] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + + $expected = [ + 'before' => ['bar'], + 'after' => [] + ]; + + $this->assertEquals($expected, $filters->initialize()->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodProcessGlobals() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'foo' => ['bar'], + 'bar' + ], + 'after' => [ + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + + $expected = [ + 'before' => [ + 'foo' => ['bar'], + 'bar' + ], + 'after' => ['baz'] + ]; + + $this->assertEquals($expected, $filters->initialize()->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodProcessGlobalsWithExcept() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'foo' => ['except' => ['admin/*']], + 'bar' + ], + 'after' => [ + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => [ + 'bar' + ], + 'after' => ['baz'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodProcessesFiltersBefore() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'filters' => [ + 'foo' => ['before' => ['admin/*'], 'after' => ['/users/*']] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => ['foo'], + 'after' => [] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodProcessesFiltersAfter() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'filters' => [ + 'foo' => ['before' => ['admin/*'], 'after' => ['/users/*']] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'users/foo/bar'; + + $expected = [ + 'before' => [], + 'after' => ['foo'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testProcessMethodProcessesCombined() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'foog' => ['except' => ['admin/*']], + 'barg' + ], + 'after' => [ + 'bazg' + ] + ], + 'methods' => [ + 'post' => ['foo'], + 'get' => ['bar'] + ], + 'filters' => [ + 'foof' => ['before' => ['admin/*'], 'after' => ['/users/*']] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => ['barg', 'bar', 'foof'], + 'after' => ['bazg'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + //-------------------------------------------------------------------- + + public function testRunThrowsWithInvalidAlias() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => [], + 'globals' => [ + 'before' => ['invalid'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + + $this->expectException(FilterException::class); + $uri = 'admin/foo/bar'; + + $filters->run($uri); + } + + //-------------------------------------------------------------------- + + public function testRunThrowsWithInvalidClassType() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['invalid' => 'CodeIgniter\Filters\fixtures\InvalidClass'], + 'globals' => [ + 'before' => ['invalid'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + + $this->expectException(FilterException::class); + $uri = 'admin/foo/bar'; + + $filters->run($uri); + } + + //-------------------------------------------------------------------- + + public function testRunDoesBefore() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => 'CodeIgniter\Filters\fixtures\GoogleMe'], + 'globals' => [ + 'before' => ['google'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $request = $filters->run($uri, 'before'); + + $this->assertEquals('http://google.com', $request->url); + } + + //-------------------------------------------------------------------- + + public function testRunDoesAfter() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => 'CodeIgniter\Filters\fixtures\GoogleMe'], + 'globals' => [ + 'before' => [], + 'after' => ['google'] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $response = $filters->run($uri, 'after'); + + $this->assertEquals('http://google.com', $response->csp); + } + + //-------------------------------------------------------------------- + + public function testShortCircuit() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['banana' => 'CodeIgniter\Filters\fixtures\GoogleYou'], + 'globals' => [ + 'before' => ['banana'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $response = $filters->run($uri, 'before'); + $this->assertTrue($response instanceof ResponseInterface); + $this->assertEquals('http://google.com', $response->csp); + } + + public function testOtherResult() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['nowhere' => 'CodeIgniter\Filters\fixtures\GoogleEmpty', + 'banana' => 'CodeIgniter\Filters\fixtures\GoogleCurious'], + 'globals' => [ + 'before' => ['nowhere', 'banana'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $response = $filters->run($uri, 'before'); + + $this->assertEquals('This is curious', $response); + } + + //-------------------------------------------------------------------- + + public function testBeforeExceptString() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'foo' => ['except' => 'admin/*'], + 'bar' + ], + 'after' => [ + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => [ + 'bar' + ], + 'after' => ['baz'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + public function testBeforeExceptInapplicable() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'foo' => ['except' => 'george/*'], + 'bar' + ], + 'after' => [ + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => [ + 'bar', 'foo' => ['except' => 'george/*'] + ], + 'after' => ['baz'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + public function testAfterExceptString() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'bar' + ], + 'after' => [ + 'foo' => ['except' => 'admin/*'], + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => [ + 'bar' + ], + 'after' => ['baz'] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + public function testAfterExceptInapplicable() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'globals' => [ + 'before' => [ + 'bar' + ], + 'after' => [ + 'foo' => ['except' => 'george/*'], + 'baz' + ] + ] + ]; + $filters = new Filters((object) $config, $this->request, $this->response); + $uri = 'admin/foo/bar'; + + $expected = [ + 'before' => [ + 'bar' + ], + 'after' => ['baz', 'foo' => ['except' => 'george/*'] + ] + ]; + + $this->assertEquals($expected, $filters->initialize($uri)->getFilters()); + } + + public function testAddFilter() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => 'CodeIgniter\Filters\fixtures\GoogleMe'], + 'globals' => [ + 'before' => ['google'], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + + $filters = $filters->addFilter('Some\Class', 'some_alias'); + + $filters = $filters->initialize('admin/foo/bar'); + + $filters = $filters->getFilters(); + + $this->assertTrue(in_array('some_alias', $filters['before'])); + } + + public function testEnableFilter() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['google' => 'CodeIgniter\Filters\fixtures\GoogleMe'], + 'globals' => [ + 'before' => [], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + + $filters = $filters->initialize('admin/foo/bar'); + + $filters->enableFilter('google', 'before'); + + $filters = $filters->getFilters(); + + $this->assertTrue(in_array('google', $filters['before'])); + } + + public function testEnableFilterWithArguments() + { + $_SERVER['REQUEST_METHOD'] = 'GET'; + + $config = [ + 'aliases' => ['role' => 'CodeIgniter\Filters\fixtures\Role'], + 'globals' => [ + 'before' => [], + 'after' => [] + ] + ]; + + $filters = new Filters((object) $config, $this->request, $this->response); + + $filters = $filters->initialize('admin/foo/bar'); + + $filters->enableFilter('role:admin , super', 'before'); + + $found = $filters->getFilters(); + + $this->assertTrue(in_array('role', $found['before'])); + $this->assertEquals(['admin', 'super'], $filters->getArguments('role')); + } +} diff --git a/tests/system/Filters/fixtures/GoogleCurious.php b/tests/system/Filters/fixtures/GoogleCurious.php new file mode 100644 index 000000000000..732326e9bd16 --- /dev/null +++ b/tests/system/Filters/fixtures/GoogleCurious.php @@ -0,0 +1,20 @@ +url = 'http://google.com'; + + return $request; + } + + //-------------------------------------------------------------------- + + public function after(RequestInterface $request, ResponseInterface $response) + { + $response->csp = 'http://google.com'; + + return $response; + } + + //-------------------------------------------------------------------- +} diff --git a/tests/system/Filters/fixtures/GoogleYou.php b/tests/system/Filters/fixtures/GoogleYou.php new file mode 100644 index 000000000000..cafb7c77c6a9 --- /dev/null +++ b/tests/system/Filters/fixtures/GoogleYou.php @@ -0,0 +1,22 @@ +csp = 'http://google.com'; + return $response; + } + + public function after(RequestInterface $request, ResponseInterface $response) + { + } + +} diff --git a/tests/system/Filters/fixtures/InvalidClass.php b/tests/system/Filters/fixtures/InvalidClass.php new file mode 100644 index 000000000000..e627fd38b29f --- /dev/null +++ b/tests/system/Filters/fixtures/InvalidClass.php @@ -0,0 +1,12 @@ +jsonFormatter = new JSONFormatter(); + } + + public function testBasicJSON() + { + $data = [ + 'foo' => 'bar' + ]; + + $expected = '{ + "foo": "bar" +}'; + + $this->assertEquals($expected, $this->jsonFormatter->format($data)); + } + + public function testUnicodeOutput() + { + $data = [ + 'foo' => 'База данни грешка' + ]; + + $expected = '{ + "foo": "База данни грешка" +}'; + + $this->assertEquals($expected, $this->jsonFormatter->format($data)); + } + + public function testKeepsURLs() + { + $data = [ + 'foo' => 'https://www.example.com/foo/bar' + ]; + + $expected = '{ + "foo": "https://www.example.com/foo/bar" +}'; + + $this->assertEquals($expected, $this->jsonFormatter->format($data)); + } + + + /** + * @expectedException RuntimeException + */ + public function testJSONError() + { + $data = ["\xB1\x31"]; + $expected = "Boom"; + $this->assertEquals($expected, $this->jsonFormatter->format($data)); + } + +} diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php new file mode 100644 index 000000000000..5b97c084bcfe --- /dev/null +++ b/tests/system/Format/XMLFormatterTest.php @@ -0,0 +1,68 @@ +xmlFormatter = new XMLFormatter(); + } + + public function testBasicXML() + { + $data = [ + 'foo' => 'bar' + ]; + + $expected = << +bar + +EOH; + + $this->assertEquals($expected, $this->xmlFormatter->format($data)); + } + + public function testFormatXMLWithMultilevelArray() + { + $data = [ + 'foo' => ['bar'] + ]; + + $expected = << +<0>bar + +EOH; + + $this->assertEquals($expected, $this->xmlFormatter->format($data)); + } + + public function testFormatXMLWithMultilevelArrayAndNumericKey() + { + $data = [ + ['foo'] + ]; + + $expected = << +<0>foo + +EOH; + + $this->assertEquals($expected, $this->xmlFormatter->format($data)); + } + + public function testStringFormatting() { + $data = ['Something']; + $expected = << +<0>Something + +EOH; + + $this->assertEquals($expected, $this->xmlFormatter->format($data)); + } +} diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 408768675c91..102c1e276093 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1,6 +1,8 @@ request = new MockCURLRequest(new App(), new URI(), new Response(new \Config\App())); + parent::setUp(); + + Services::reset(); + $this->request = $this->getRequest(); + } + + protected function getRequest(array $options = []) + { + $uri = isset($options['base_uri']) + ? new URI($options['base_uri']) + : new URI(); + + return new MockCURLRequest(new App(), $uri, new Response(new \Config\App()), $options); + } + + //-------------------------------------------------------------------- + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/1029 + */ + public function testGetRemembersBaseURI() + { + $request = $this->getRequest([ + 'base_uri' => 'http://www.foo.com/api/v1/' + ]); + + $response = $request->get('products'); + + $options = $request->curl_options; + + $this->assertEquals('http://www.foo.com/api/v1/products', $options[CURLOPT_URL]); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/1029 + */ + public function testGetRemembersBaseURIWithHelperMethod() + { + $request = Services::curlrequest([ + 'base_uri' => 'http://www.foo.com/api/v1/' + ]); + + $uri = $this->getPrivateProperty($request, 'baseURI'); + $this->assertEquals('www.foo.com', $uri->getHost()); + $this->assertEquals('/api/v1/', $uri->getPath()); } //-------------------------------------------------------------------- @@ -34,7 +80,7 @@ public function testGetSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('GET', $options[CURLOPT_CUSTOMREQUEST]); } @@ -48,7 +94,7 @@ public function testDeleteSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('DELETE', $options[CURLOPT_CUSTOMREQUEST]); } @@ -62,7 +108,7 @@ public function testHeadSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('HEAD', $options[CURLOPT_CUSTOMREQUEST]); } @@ -76,7 +122,7 @@ public function testOptionsSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('OPTIONS', $options[CURLOPT_CUSTOMREQUEST]); } @@ -90,7 +136,7 @@ public function testPatchSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('PATCH', $options[CURLOPT_CUSTOMREQUEST]); } @@ -104,7 +150,7 @@ public function testPostSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('POST', $options[CURLOPT_CUSTOMREQUEST]); } @@ -118,7 +164,7 @@ public function testPutSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('PUT', $options[CURLOPT_CUSTOMREQUEST]); } @@ -132,7 +178,7 @@ public function testCustomMethodSetsCorrectMethod() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('CUSTOM', $options[CURLOPT_CUSTOMREQUEST]); } @@ -146,7 +192,7 @@ public function testRequestMethodGetsSanitized() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_CUSTOMREQUEST])); + $this->assertArrayHasKey(CURLOPT_CUSTOMREQUEST, $options); $this->assertEquals('CUSTOM', $options[CURLOPT_CUSTOMREQUEST]); } @@ -158,22 +204,22 @@ public function testRequestSetsBasicCurlOptions() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_URL])); + $this->assertArrayHasKey(CURLOPT_URL, $options); $this->assertEquals('http://example.com', $options[CURLOPT_URL]); - $this->assertTrue(isset($options[CURLOPT_RETURNTRANSFER])); - $this->assertEquals(true, $options[CURLOPT_RETURNTRANSFER]); + $this->assertArrayHasKey(CURLOPT_RETURNTRANSFER, $options); + $this->assertTrue($options[CURLOPT_RETURNTRANSFER]); - $this->assertTrue(isset($options[CURLOPT_HEADER])); - $this->assertEquals(true, $options[CURLOPT_HEADER]); + $this->assertArrayHasKey(CURLOPT_HEADER, $options); + $this->assertTrue($options[CURLOPT_HEADER]); - $this->assertTrue(isset($options[CURLOPT_FRESH_CONNECT])); - $this->assertEquals(true, $options[CURLOPT_FRESH_CONNECT]); + $this->assertArrayHasKey(CURLOPT_FRESH_CONNECT, $options); + $this->assertTrue($options[CURLOPT_FRESH_CONNECT]); - $this->assertTrue(isset($options[CURLOPT_TIMEOUT_MS])); + $this->assertArrayHasKey(CURLOPT_TIMEOUT_MS, $options); $this->assertEquals(0.0, $options[CURLOPT_TIMEOUT_MS]); - $this->assertTrue(isset($options[CURLOPT_CONNECTTIMEOUT_MS])); + $this->assertArrayHasKey(CURLOPT_CONNECTTIMEOUT_MS, $options); $this->assertEquals(150 * 1000, $options[CURLOPT_CONNECTTIMEOUT_MS]); } @@ -187,10 +233,10 @@ public function testAuthBasicOption() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_USERPWD])); + $this->assertArrayHasKey(CURLOPT_USERPWD, $options); $this->assertEquals('username:password', $options[CURLOPT_USERPWD]); - $this->assertTrue(isset($options[CURLOPT_HTTPAUTH])); + $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options); $this->assertEquals(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]); } @@ -204,10 +250,10 @@ public function testAuthBasicOptionExplicit() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_USERPWD])); + $this->assertArrayHasKey(CURLOPT_USERPWD, $options); $this->assertEquals('username:password', $options[CURLOPT_USERPWD]); - $this->assertTrue(isset($options[CURLOPT_HTTPAUTH])); + $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options); $this->assertEquals(CURLAUTH_BASIC, $options[CURLOPT_HTTPAUTH]); } @@ -221,10 +267,10 @@ public function testAuthDigestOption() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_USERPWD])); + $this->assertArrayHasKey(CURLOPT_USERPWD, $options); $this->assertEquals('username:password', $options[CURLOPT_USERPWD]); - $this->assertTrue(isset($options[CURLOPT_HTTPAUTH])); + $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options); $this->assertEquals(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]); } @@ -240,7 +286,7 @@ public function testCertOption() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_SSLCERT])); + $this->assertArrayHasKey(CURLOPT_SSLCERT, $options); $this->assertEquals($file, $options[CURLOPT_SSLCERT]); } @@ -256,10 +302,10 @@ public function testCertOptionWithPassword() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_SSLCERT])); + $this->assertArrayHasKey(CURLOPT_SSLCERT, $options); $this->assertEquals($file, $options[CURLOPT_SSLCERT]); - $this->assertTrue(isset($options[CURLOPT_SSLCERTPASSWD])); + $this->assertArrayHasKey(CURLOPT_SSLCERTPASSWD, $options); $this->assertEquals('password', $options[CURLOPT_SSLCERTPASSWD]); } @@ -273,10 +319,10 @@ public function testDebugOption() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_VERBOSE])); + $this->assertArrayHasKey(CURLOPT_VERBOSE, $options); $this->assertEquals(1, $options[CURLOPT_VERBOSE]); - $this->assertTrue(isset($options[CURLOPT_STDERR])); + $this->assertArrayHasKey(CURLOPT_STDERR, $options); } //-------------------------------------------------------------------- @@ -289,11 +335,11 @@ public function testAllowRedirectsOptionFalse() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_FOLLOWLOCATION])); + $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options); $this->assertEquals(0, $options[CURLOPT_FOLLOWLOCATION]); - $this->assertFalse(isset($options[CURLOPT_MAXREDIRS])); - $this->assertFalse(isset($options[CURLOPT_REDIR_PROTOCOLS])); + $this->assertArrayNotHasKey(CURLOPT_MAXREDIRS, $options); + $this->assertArrayNotHasKey(CURLOPT_REDIR_PROTOCOLS, $options); } //-------------------------------------------------------------------- @@ -306,12 +352,12 @@ public function testAllowRedirectsOptionTrue() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_FOLLOWLOCATION])); + $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options); $this->assertEquals(1, $options[CURLOPT_FOLLOWLOCATION]); - $this->assertTrue(isset($options[CURLOPT_MAXREDIRS])); + $this->assertArrayHasKey(CURLOPT_MAXREDIRS, $options); $this->assertEquals(5, $options[CURLOPT_MAXREDIRS]); - $this->assertTrue(isset($options[CURLOPT_REDIR_PROTOCOLS])); + $this->assertArrayHasKey(CURLOPT_REDIR_PROTOCOLS, $options); $this->assertEquals(CURLPROTO_HTTP|CURLPROTO_HTTPS, $options[CURLOPT_REDIR_PROTOCOLS]); } @@ -325,12 +371,12 @@ public function testAllowRedirectsOptionDefaults() $options = $this->request->curl_options; - $this->assertTrue(isset($options[CURLOPT_FOLLOWLOCATION])); + $this->assertArrayHasKey(CURLOPT_FOLLOWLOCATION, $options); $this->assertEquals(1, $options[CURLOPT_FOLLOWLOCATION]); - $this->assertTrue(isset($options[CURLOPT_MAXREDIRS])); - $this->assertTrue(isset($options[CURLOPT_REDIR_PROTOCOLS])); + $this->assertArrayHasKey(CURLOPT_MAXREDIRS, $options); + $this->assertArrayHasKey(CURLOPT_REDIR_PROTOCOLS, $options); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index b62f0c19f9ac..26ab526316ea 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -1,19 +1,22 @@ -assertNull($files->all()); + $this->assertEquals([], $files->all()); } //-------------------------------------------------------------------- @@ -22,20 +25,20 @@ public function testAllReturnsValidSingleFile() { $_FILES = [ 'userfile' => [ - 'name' => 'someFile.txt', - 'type' => 'text/plain', - 'size' => '124', - 'tmp_name' => '/tmp/myTempFile.txt', - 'error' => 0 + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0 ] ]; - $collection = new FileCollection(); - $files = $collection->all(); - $this->assertEquals(1, count($files)); + $collection = new FileCollection(); + $files = $collection->all(); + $this->assertCount(1, $files); $file = array_shift($files); - $this->assertTrue($file instanceof UploadedFile); + $this->assertInstanceOf(UploadedFile::class, $file); $this->assertEquals('someFile.txt', $file->getName()); $this->assertEquals(124, $file->getSize()); @@ -47,29 +50,29 @@ public function testAllReturnsValidMultipleFilesSameName() { $_FILES = [ 'userfile' => [ - 'name' => ['fileA.txt', 'fileB.txt'], - 'type' => ['text/plain', 'text/csv'], - 'size' => ['124', '248'], - 'tmp_name' => ['/tmp/fileA.txt', '/tmp/fileB.txt'], - 'error' => 0 + 'name' => ['fileA.txt', 'fileB.txt'], + 'type' => ['text/plain', 'text/csv'], + 'size' => ['124', '248'], + 'tmp_name' => ['/tmp/fileA.txt', '/tmp/fileB.txt'], + 'error' => 0 ] ]; - $collection = new FileCollection(); - $files = $collection->all(); - $this->assertEquals(1, count($files)); + $collection = new FileCollection(); + $files = $collection->all(); + $this->assertCount(1, $files); $this->assertEquals('userfile', key($files)); $files = array_shift($files); - $this->assertEquals(2, count($files)); + $this->assertCount(2, $files); $file = $files[0]; - $this->assertTrue($file instanceof UploadedFile); + $this->assertInstanceOf(UploadedFile::class, $file); $this->assertEquals('fileA.txt', $file->getName()); $this->assertEquals('/tmp/fileA.txt', $file->getTempName()); $this->assertEquals('txt', $file->getClientExtension()); - $this->assertEquals('text/plain', $file->getClientType()); + $this->assertEquals('text/plain', $file->getClientMimeType()); $this->assertEquals(124, $file->getSize()); } @@ -79,43 +82,45 @@ public function testAllReturnsValidMultipleFilesSameName() public function testAllReturnsValidMultipleFilesDifferentName() { $_FILES = [ - 'userfile1' => [ - 'name' => 'fileA.txt', - 'type' => 'text/plain', - 'size' => 124, - 'tmp_name' => '/tmp/fileA.txt', - 'error' => 0 + 'userfile1' => [ + 'name' => 'fileA.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 ], - 'userfile2' => [ - 'name' => 'fileB.txt', - 'type' => 'text/csv', - 'size' => 248, - 'tmp_name' => '/tmp/fileB.txt', - 'error' => 0 + 'userfile2' => [ + 'name' => 'fileB.txt', + 'type' => 'text/csv', + 'size' => 248, + 'tmp_name' => '/tmp/fileB.txt', + 'error' => 0 ], ]; - $collection = new FileCollection(); - $files = $collection->all(); - $this->assertEquals(2, count($files)); + $collection = new FileCollection(); + $files = $collection->all(); + $this->assertCount(2, $files); $this->assertEquals('userfile1', key($files)); $file = array_shift($files); - $this->assertTrue($file instanceof UploadedFile); + $this->assertInstanceOf(UploadedFile::class, $file); $this->assertEquals('fileA.txt', $file->getName()); + $this->assertEquals('fileA.txt', $file->getClientName()); $this->assertEquals('/tmp/fileA.txt', $file->getTempName()); $this->assertEquals('txt', $file->getClientExtension()); - $this->assertEquals('text/plain', $file->getClientType()); + $this->assertEquals('text/plain', $file->getClientMimeType()); $this->assertEquals(124, $file->getSize()); $file = array_pop($files); - $this->assertTrue($file instanceof UploadedFile); + $this->assertInstanceOf(UploadedFile::class, $file); $this->assertEquals('fileB.txt', $file->getName()); + $this->assertEquals('fileB.txt', $file->getClientName()); $this->assertEquals('/tmp/fileB.txt', $file->getTempName()); $this->assertEquals('txt', $file->getClientExtension()); - $this->assertEquals('text/csv', $file->getClientType()); + $this->assertEquals('text/csv', $file->getClientMimeType()); $this->assertEquals(248, $file->getSize()); } @@ -128,44 +133,44 @@ public function testAllReturnsValidSingleFileNestedName() { $_FILES = [ 'userfile' => [ - 'name' => [ + 'name' => [ 'foo' => [ 'bar' => 'fileA.txt' ] ], - 'type' => [ + 'type' => [ 'foo' => [ 'bar' => 'text/plain' ] ], - 'size' => [ + 'size' => [ 'foo' => [ 'bar' => 124 ] ], - 'tmp_name' => [ + 'tmp_name' => [ 'foo' => [ 'bar' => '/tmp/fileA.txt' ] ], - 'error' => 0 + 'error' => 0 ] ]; - $collection = new FileCollection(); - $files = $collection->all(); - $this->assertEquals(1, count($files)); + $collection = new FileCollection(); + $files = $collection->all(); + $this->assertCount(1, $files); $this->assertEquals('userfile', key($files)); - $this->assertTrue(isset($files['userfile']['foo']['bar'])); + $this->assertArrayHasKey('bar', $files['userfile']['foo']); $file = $files['userfile']['foo']['bar']; - $this->assertTrue($file instanceof UploadedFile); + $this->assertInstanceOf(UploadedFile::class, $file); $this->assertEquals('fileA.txt', $file->getName()); $this->assertEquals('/tmp/fileA.txt', $file->getTempName()); $this->assertEquals('txt', $file->getClientExtension()); - $this->assertEquals('text/plain', $file->getClientType()); + $this->assertEquals('text/plain', $file->getClientMimeType()); $this->assertEquals(124, $file->getSize()); } @@ -175,11 +180,11 @@ public function testHasFileWithSingleFile() { $_FILES = [ 'userfile' => [ - 'name' => 'someFile.txt', - 'type' => 'text/plain', - 'size' => '124', - 'tmp_name' => '/tmp/myTempFile.txt', - 'error' => 0 + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0 ] ]; @@ -194,19 +199,19 @@ public function testHasFileWithSingleFile() public function testHasFileWithMultipleFilesWithDifferentNames() { $_FILES = [ - 'userfile1' => [ - 'name' => 'fileA.txt', - 'type' => 'text/plain', - 'size' => 124, - 'tmp_name' => '/tmp/fileA.txt', - 'error' => 0 + 'userfile1' => [ + 'name' => 'fileA.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 ], - 'userfile2' => [ - 'name' => 'fileB.txt', - 'type' => 'text/csv', - 'size' => 248, - 'tmp_name' => '/tmp/fileB.txt', - 'error' => 0 + 'userfile2' => [ + 'name' => 'fileB.txt', + 'type' => 'text/csv', + 'size' => 248, + 'tmp_name' => '/tmp/fileB.txt', + 'error' => 0 ], ]; @@ -225,27 +230,27 @@ public function testHasFileWithSingleFileNestedName() { $_FILES = [ 'userfile' => [ - 'name' => [ + 'name' => [ 'foo' => [ 'bar' => 'fileA.txt' ] ], - 'type' => [ + 'type' => [ 'foo' => [ 'bar' => 'text/plain' ] ], - 'size' => [ + 'size' => [ 'foo' => [ 'bar' => 124 ] ], - 'tmp_name' => [ + 'tmp_name' => [ 'foo' => [ 'bar' => '/tmp/fileA.txt' ] ], - 'error' => 0 + 'error' => 0 ] ]; @@ -262,21 +267,282 @@ public function testErrorString() { $_FILES = [ 'userfile' => [ - 'name' => 'someFile.txt', - 'type' => 'text/plain', - 'size' => '124', - 'tmp_name' => '/tmp/myTempFile.txt', - 'error' => UPLOAD_ERR_INI_SIZE + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => UPLOAD_ERR_INI_SIZE ] ]; $expected = 'The file "someFile.txt" exceeds your upload_max_filesize ini directive.'; - $collection = new FileCollection(); - $file = $collection->getFile('userfile'); + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertEquals($expected, $file->getErrorString()); + } + + public function testErrorStringWithUnknownError() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 123 + ] + ]; + + $expected = 'The file "someFile.txt" was not uploaded due to an unknown error.'; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); $this->assertEquals($expected, $file->getErrorString()); } + public function testErrorStringWithNoError() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + ] + ]; + + $expected = 'The file uploaded with success.'; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertEquals($expected, $file->getErrorString()); + } + + //-------------------------------------------------------------------- + + public function testError() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => UPLOAD_ERR_INI_SIZE + ] + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError()); + } + + public function testErrorWithUnknownError() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + ] + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertEquals(0, $file->getError()); + } + + public function testErrorWithNoError() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0 + ] + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->assertEquals(UPLOAD_ERR_OK, $file->getError()); + } + + //-------------------------------------------------------------------- + + public function testFileReturnsValidSingleFile() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0 + ] + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + $this->assertInstanceOf(UploadedFile::class, $file); + + $this->assertEquals('someFile.txt', $file->getName()); + $this->assertEquals(124, $file->getSize()); + } + + //-------------------------------------------------------------------- + + public function testFileNoExistSingleFile() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0 + ] + ]; + + $collection = new FileCollection(); + $file = $collection->getFile('fileuser'); + $this->AssertNull($file); + } + + //-------------------------------------------------------------------- + + public function testFileReturnValidMultipleFiles() + { + $_FILES = [ + 'userfile' => [ + 'name' => ['fileA.txt', 'fileB.txt'], + 'type' => ['text/plain', 'text/csv'], + 'size' => ['124', '248'], + 'tmp_name' => ['/tmp/fileA.txt', '/tmp/fileB.txt'], + 'error' => 0 + ] + ]; + + $collection = new FileCollection(); + + $file_1 = $collection->getFile('userfile.0'); + $this->assertInstanceOf(UploadedFile::class, $file_1); + $this->assertEquals('fileA.txt', $file_1->getName()); + $this->assertEquals('/tmp/fileA.txt', $file_1->getTempName()); + $this->assertEquals('txt', $file_1->getClientExtension()); + $this->assertEquals('text/plain', $file_1->getClientMimeType()); + $this->assertEquals(124, $file_1->getSize()); + + $file_2 = $collection->getFile('userfile.1'); + $this->assertInstanceOf(UploadedFile::class, $file_2); + $this->assertEquals('fileB.txt', $file_2->getName()); + $this->assertEquals('/tmp/fileB.txt', $file_2->getTempName()); + $this->assertEquals('txt', $file_2->getClientExtension()); + $this->assertEquals('text/csv', $file_2->getClientMimeType()); + $this->assertEquals(248, $file_2->getSize()); + } + + //-------------------------------------------------------------------- + + public function testFileWithMultipleFilesNestedName() + { + $_FILES = [ + 'my-form' => [ + 'name' => [ + 'details' => [ + 'avatars' => ['fileA.txt', 'fileB.txt'] + ] + ], + 'type' => [ + 'details' => [ + 'avatars' => ['text/plain', 'text/plain'] + ] + ], + 'size' => [ + 'details' => [ + 'avatars' => [125, 243] + ] + ], + 'tmp_name' => [ + 'details' => [ + 'avatars' => ['/tmp/fileA.txt', '/tmp/fileB.txt'] + ] + ], + 'error' => [ + 'details' => [ + 'avatars' => [0, 0] + ] + ], + ] + ]; + + $collection = new FileCollection(); + + $file_1 = $collection->getFile('my-form.details.avatars.0'); + $this->assertInstanceOf(UploadedFile::class, $file_1); + $this->assertEquals('fileA.txt', $file_1->getName()); + $this->assertEquals('/tmp/fileA.txt', $file_1->getTempName()); + $this->assertEquals('txt', $file_1->getClientExtension()); + $this->assertEquals('text/plain', $file_1->getClientMimeType()); + $this->assertEquals(125, $file_1->getSize()); + + $file_2 = $collection->getFile('my-form.details.avatars.1'); + $this->assertInstanceOf(UploadedFile::class, $file_2); + $this->assertEquals('fileB.txt', $file_2->getName()); + $this->assertEquals('/tmp/fileB.txt', $file_2->getTempName()); + $this->assertEquals('txt', $file_2->getClientExtension()); + $this->assertEquals('text/plain', $file_2->getClientMimeType()); + $this->assertEquals(243, $file_2->getSize()); + } + + //-------------------------------------------------------------------- + + public function testDoesntHaveFile() + { + $_FILES = [ + 'my-form' => [ + 'name' => [ + 'details' => [ + 'avatars' => ['fileA.txt', 'fileB.txt'] + ] + ], + 'type' => [ + 'details' => [ + 'avatars' => ['text/plain', 'text/plain'] + ] + ], + 'size' => [ + 'details' => [ + 'avatars' => [125, 243] + ] + ], + 'tmp_name' => [ + 'details' => [ + 'avatars' => ['/tmp/fileA.txt', '/tmp/fileB.txt'] + ] + ], + 'error' => [ + 'details' => [ + 'avatars' => [0, 0] + ] + ], + ] + ]; + + $collection = new FileCollection(); + + $this->assertFalse($collection->hasFile('my-form.detailz.avatars.0')); + $this->assertNull($collection->getFile('my-form.detailz.avatars.0')); + } + //-------------------------------------------------------------------- } diff --git a/tests/system/HTTP/Files/FileMovingTest.php b/tests/system/HTTP/Files/FileMovingTest.php new file mode 100644 index 000000000000..be9cb89dcfa9 --- /dev/null +++ b/tests/system/HTTP/Files/FileMovingTest.php @@ -0,0 +1,272 @@ +root = vfsStream::setup(); + $this->path = '_support/Files/'; + vfsStream::copyFromFileSystem(TESTPATH . $this->path, $this->root); + $this->start = $this->root->url() . '/'; + + $_FILES = []; + } + + public function tearDown() + { + parent::tearDown(); + + $this->root = null; + if (is_dir('/tmp/destination')) + rmdir('/tmp/destination'); + } + + //-------------------------------------------------------------------- + + public function testMove() + { + $finalFilename = 'fileA'; + + $_FILES = [ + 'userfile1' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 + ], + 'userfile2' => [ + 'name' => 'fileA.txt', + 'type' => 'text/csv', + 'size' => 248, + 'tmp_name' => '/tmp/fileB.txt', + 'error' => 0 + ], + ]; + + $collection = new FileCollection(); + + $this->assertTrue($collection->hasFile('userfile1')); + $this->assertTrue($collection->hasFile('userfile2')); + + $destination = $this->root->url() . '/destination'; + + // Create the destination if not exists + is_dir($destination) || mkdir($destination, 0777, true); + + foreach ($collection->all() as $file) + { + $this->assertInstanceOf(UploadedFile::class, $file); + $file->move($destination, $file->getName(), false); + } + + $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '.txt')); + $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '_1.txt')); + } + + //-------------------------------------------------------------------- + + public function testMoveOverwriting() + { + $finalFilename = 'file_with_delimiters_underscore'; + + $_FILES = [ + 'userfile1' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 + ], + 'userfile2' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/csv', + 'size' => 248, + 'tmp_name' => '/tmp/fileB.txt', + 'error' => 0 + ], + 'userfile3' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/csv', + 'size' => 248, + 'tmp_name' => '/tmp/fileC.txt', + 'error' => 0 + ], + ]; + + $collection = new FileCollection(); + + $this->assertTrue($collection->hasFile('userfile1')); + $this->assertTrue($collection->hasFile('userfile2')); + $this->assertTrue($collection->hasFile('userfile3')); + + $destination = $this->root->url() . '/destination'; + + // Create the destination if not exists + is_dir($destination) || mkdir($destination, 0777, true); + + foreach ($collection->all() as $file) + { + $this->assertInstanceOf(UploadedFile::class, $file); + $file->move($destination, $file->getName(), true); + } + + $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '.txt')); + $this->assertFalse($this->root->hasChild('destination/' . $finalFilename . '_1.txt')); + $this->assertFalse($this->root->hasChild('destination/' . $finalFilename . '_2.txt')); + $this->assertFileExists($destination . '/' . $finalFilename . '.txt'); + } + + //-------------------------------------------------------------------- + + public function testMoved() + { + $finalFilename = 'fileA'; + + $_FILES = [ + 'userfile1' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 + ], + ]; + + $collection = new FileCollection(); + + $this->assertTrue($collection->hasFile('userfile1')); + + $destination = $this->root->url() . '/destination'; + + // Create the destination if not exists + is_dir($destination) || mkdir($destination, 0777, true); + + $file = $collection->getFile('userfile1'); + + $this->assertInstanceOf(UploadedFile::class, $file); + $this->assertFalse($file->hasMoved()); + $file->move($destination, $file->getName(), false); + $this->assertTrue($file->hasMoved()); + } + + //-------------------------------------------------------------------- + + public function testAlreadyMoved() + { + $finalFilename = 'fileA'; + + $_FILES = [ + 'userfile1' => [ + 'name' => $finalFilename . '.txt', + 'type' => 'text/plain', + 'size' => 124, + 'tmp_name' => '/tmp/fileA.txt', + 'error' => 0 + ], + ]; + + $collection = new FileCollection(); + + $this->assertTrue($collection->hasFile('userfile1')); + + $destination = $this->root->url() . '/destination'; + + // Create the destination if not exists + is_dir($destination) || mkdir($destination, 0777, true); + + $this->expectException(HTTPException::class); + + foreach ($collection->all() as $file) + { + $file->move($destination, $file->getName(), false); + $file->move($destination, $file->getName(), false); + } + } + + //-------------------------------------------------------------------- + + public function testInvalidFile() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => UPLOAD_ERR_INI_SIZE + ] + ]; + + $destination = $this->root->url() . '/destination'; + // don't create the folder, so setPath() is invoked. + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->expectException(HTTPException::class); + $file->move($destination, $file->getName(), false); + } + + //-------------------------------------------------------------------- + + public function testFailedMove() + { + $_FILES = [ + 'userfile' => [ + 'name' => 'someFile.txt', + 'type' => 'text/plain', + 'size' => '124', + 'tmp_name' => '/tmp/myTempFile.txt', + 'error' => 0, + ] + ]; + + $destination = '/tmp/destination'; + // Create the destination and make it read only + is_dir($destination) || mkdir($destination, 0400, true); + + $collection = new FileCollection(); + $file = $collection->getFile('userfile'); + + $this->expectException(HTTPException::class); + $file->move($destination, $file->getName(), false); + } + + //-------------------------------------------------------------------- +} + +/* + * Overwrite the function so that it will only check whether the file exists or not. + * Original function also checks if the file was uploaded with a POST request. + * + * This overwrite is for testing the move operation. + */ + +function is_uploaded_file($filename) +{ + if ( ! file_exists($filename)) + { + file_put_contents($filename, 'data'); + } + return file_exists($filename); +} + +/* + * Overwrite the function so that it just copy without checking the file is an uploaded file. + * + * This overwrite is for testing the move operation. + */ + +function move_uploaded_file($filename, $destination) +{ + return copy($filename, $destination); +} diff --git a/tests/system/HTTP/HeaderTest.php b/tests/system/HTTP/HeaderTest.php index cdc5fcd7652d..d500dd016ef8 100644 --- a/tests/system/HTTP/HeaderTest.php +++ b/tests/system/HTTP/HeaderTest.php @@ -33,12 +33,16 @@ public function testHeaderSetters() $name = 'foo'; $value = ['bar', 'baz']; - $header = new \CodeIgniter\HTTP\Header(); + $header = new \CodeIgniter\HTTP\Header($name); + $this->assertEquals($name, $header->getName()); + $this->assertEquals(null, $header->getValue()); + $this->assertEquals($name . ': ', (string) $header); + $name = 'foo2'; $header->setName($name)->setValue($value); - $this->assertEquals($name, $header->getName()); $this->assertEquals($value, $header->getValue()); + $this->assertEquals($name. ': bar, baz', (string) $header); } //-------------------------------------------------------------------- @@ -122,6 +126,4 @@ public function testHeaderToStringShowsEntireHeader() $this->assertEquals($expected, (string)$header); } - - //-------------------------------------------------------------------- } diff --git a/tests/system/HTTP/IncomingRequestDetectingTest.php b/tests/system/HTTP/IncomingRequestDetectingTest.php new file mode 100644 index 000000000000..0fdb65b59076 --- /dev/null +++ b/tests/system/HTTP/IncomingRequestDetectingTest.php @@ -0,0 +1,152 @@ +request = new IncomingRequest(new App(), new URI($origin), null, new UserAgent()); + } + + //-------------------------------------------------------------------- + + public function testPathDefault() + { + $this->request->uri = '/index.php/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath()); + } + + public function testPathRequestURI() + { + $this->request->uri = '/index.php/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURINested() + { + $this->request->uri = '/ci/index.php/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURISubfolder() + { + $this->request->uri = '/ci/index.php/popcorn/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/ci/index.php/popcorn/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'popcorn/woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURINoIndex() + { + $this->request->uri = '/sub/example'; + $_SERVER['REQUEST_URI'] = '/sub/example'; + $_SERVER['SCRIPT_NAME'] = '/sub/index.php'; + $expected = 'example'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURINginx() + { + $this->request->uri = '/ci/index.php/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/index.php/woot?code=good'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURINginxRedirecting() + { + $this->request->uri = '/?/ci/index.php/woot'; + $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'ci/woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + public function testPathRequestURISuppressed() + { + $this->request->uri = '/woot?code=good#pos'; + $_SERVER['REQUEST_URI'] = '/woot'; + $_SERVER['SCRIPT_NAME'] = '/'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath('REQUEST_URI')); + } + + //-------------------------------------------------------------------- + + public function testPathQueryString() + { + $this->request->uri = '/?/ci/index.php/woot'; + $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + $_SERVER['QUERY_STRING'] = '/ci/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'ci/woot'; + $this->assertEquals($expected, $this->request->detectPath('QUERY_STRING')); + } + + public function testPathQueryStringEmpty() + { + $this->request->uri = '/?/ci/index.php/woot'; + $_SERVER['REQUEST_URI'] = '/?/ci/woot'; + $_SERVER['QUERY_STRING'] = ''; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = ''; + $this->assertEquals($expected, $this->request->detectPath('QUERY_STRING')); + } + + //-------------------------------------------------------------------- + + public function testPathPathInfo() + { + $this->request->uri = '/index.php/woot?code=good#pos'; + $this->request->setGlobal('server', [ + 'PATH_INFO' => null, + ]); + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $expected = 'woot'; + $this->assertEquals($expected, $this->request->detectPath('PATH_INFO')); + } + + public function testPathPathInfoGlobal() + { + $this->request->uri = '/index.php/woot?code=good#pos'; + $this->request->uri = '/index.php/woot?code=good#pos'; + $this->request->setGlobal('server', [ + 'PATH_INFO' => 'silliness', + ]); + $_SERVER['REQUEST_URI'] = '/index.php/woot'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + $expected = 'silliness'; + $this->assertEquals($expected, $this->request->detectPath('PATH_INFO')); + } + +} diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 138ba07139ca..36cf7425a1b8 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -1,9 +1,16 @@ -request = new IncomingRequest(new App(), new URI()); + parent::setUp(); + + $this->request = new IncomingRequest(new App(), new URI(), null, new UserAgent()); $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; } @@ -23,31 +32,25 @@ public function testCanGrabRequestVars() $_REQUEST['TEST'] = 5; $this->assertEquals(5, $this->request->getVar('TEST')); - $this->assertEquals(null, $this->request->getVar('TESTY')); + $this->assertNull($this->request->getVar('TESTY')); } - //-------------------------------------------------------------------- - public function testCanGrabGetVars() { $_GET['TEST'] = 5; $this->assertEquals(5, $this->request->getGet('TEST')); - $this->assertEquals(null, $this->request->getGEt('TESTY')); + $this->assertNull($this->request->getGEt('TESTY')); } - //-------------------------------------------------------------------- - public function testCanGrabPostVars() { $_POST['TEST'] = 5; $this->assertEquals(5, $this->request->getPost('TEST')); - $this->assertEquals(null, $this->request->getPost('TESTY')); + $this->assertNull($this->request->getPost('TESTY')); } - //-------------------------------------------------------------------- - public function testCanGrabPostBeforeGet() { $_POST['TEST'] = 5; @@ -59,132 +62,254 @@ public function testCanGrabPostBeforeGet() //-------------------------------------------------------------------- - public function testCanGrabServerVars() + public function testCanGetOldInput() { - $_SERVER['TEST'] = 5; + $_SESSION['_ci_old_input'] = [ + 'get' => ['one' => 'two'], + 'post' => ['name' => 'foo'] + ]; - $this->assertEquals(5, $this->request->getServer('TEST')); - $this->assertEquals(null, $this->request->getServer('TESTY')); + $this->assertEquals('foo', $this->request->getOldInput('name')); + $this->assertEquals('two', $this->request->getOldInput('one')); + } + + public function testCanGetOldInputDotted() + { + $_SESSION['_ci_old_input'] = [ + 'get' => ['apple' => ['name' => 'two']], + 'post' => ['banana' => ['name' => 'foo']], + ]; + + $this->assertEquals('foo', $this->request->getOldInput('banana.name')); + $this->assertEquals('two', $this->request->getOldInput('apple.name')); } //-------------------------------------------------------------------- + public function testCanGrabServerVars() + { + $server = $this->getPrivateProperty($this->request, 'globals'); + $server['server']['TEST'] = 5; + $this->setPrivateProperty($this->request, 'globals', $server); + + $this->assertEquals(5, $this->request->getServer('TEST')); + $this->assertNull($this->request->getServer('TESTY')); + } + public function testCanGrabEnvVars() { - $_ENV['TEST'] = 5; + $server = $this->getPrivateProperty($this->request, 'globals'); + $server['env']['TEST'] = 5; + $this->setPrivateProperty($this->request, 'globals', $server); $this->assertEquals(5, $this->request->getEnv('TEST')); - $this->assertEquals(null, $this->request->getEnv('TESTY')); + $this->assertNull($this->request->getEnv('TESTY')); } - //-------------------------------------------------------------------- - public function testCanGrabCookieVars() { $_COOKIE['TEST'] = 5; $this->assertEquals(5, $this->request->getCookie('TEST')); - $this->assertEquals(null, $this->request->getCookie('TESTY')); + $this->assertNull($this->request->getCookie('TESTY')); } //-------------------------------------------------------------------- - public function testFetchGlobalReturnsSingleValue() + public function testStoresDefaultLocale() { - $_POST = [ - 'foo' => 'bar', - 'bar' => 'baz', - 'xxx' => 'yyy', - 'yyy' => 'zzz' - ]; + $config = new App(); - $this->assertEquals('baz', $this->request->getPost('bar')); + $this->assertEquals($config->defaultLocale, $this->request->getDefaultLocale()); + $this->assertEquals($config->defaultLocale, $this->request->getLocale()); } - //-------------------------------------------------------------------- + public function testSetLocaleSaves() + { + $config = new App(); + $config->supportedLocales = ['en', 'es']; + $config->defaultLocale = 'es'; + $config->baseURL = 'http://example.com'; + + $request = new IncomingRequest($config, new URI(), null, new UserAgent()); - public function testFetchGlobalFiltersValue() + $request->setLocale('en'); + $this->assertEquals('en', $request->getLocale()); + } + + public function testSetBadLocale() { - $_POST = [ - 'foo' => 'bar $pst"; + $sec = "$pre [removed]alert('Hack');[removed] $pst"; + $unsecured = 'unsecured'; + $secured = 'secured'; + + set_cookie($unsecured, $unsec, $this->expire); + set_cookie($secured, $sec, $this->expire); + + $this->assertTrue($this->response->hasCookie($unsecured, $unsec)); + $this->assertTrue($this->response->hasCookie($secured, $sec)); + + delete_cookie($unsecured); + delete_cookie($secured); + } + + //-------------------------------------------------------------------- + + public function testDeleteCookie() + { + set_cookie($this->name, $this->value, $this->expire); + //$this->response->setCookie($this->name, $this->value, $this->expire); + + delete_cookie($this->name); + + $this->assertEmpty($this->response->getCookie($this->name)); + } + + //-------------------------------------------------------------------- + + public function testGetCookie() + { + $_COOKIE['TEST'] = 5; + + $this->assertEquals(5, get_cookie('TEST')); + } + +} diff --git a/tests/system/Helpers/DateHelperTest.php b/tests/system/Helpers/DateHelperTest.php new file mode 100644 index 000000000000..3dde3beb1b53 --- /dev/null +++ b/tests/system/Helpers/DateHelperTest.php @@ -0,0 +1,38 @@ +assertLessThan(1, abs(now() - time())); // close enough + } + + //-------------------------------------------------------------------- + + public function testNowSpecific() + { + // Chicago should be two hours ahead of Vancouver + $this->assertEquals(7200, now('America/Chicago') - now('America/Vancouver')); + } + +} diff --git a/tests/system/Helpers/FilesystemHelperTest.php b/tests/system/Helpers/FilesystemHelperTest.php new file mode 100644 index 000000000000..40cc791958da --- /dev/null +++ b/tests/system/Helpers/FilesystemHelperTest.php @@ -0,0 +1,350 @@ +structure = [ + 'foo' => [ + 'bar' => 'Once upon a midnight dreary', + 'baz' => 'While I pondered weak and weary' + ], + 'boo' => [ + 'far' => 'Upon a tome of long-forgotten lore', + 'faz' => 'There came a tapping up on the door' + ], + 'AnEmptyFolder' => [], + 'simpleFile' => 'A tap-tap-tapping upon my door', + '.hidden' => 'There is no spoon' + ]; + } + + //-------------------------------------------------------------------- + + public function testDirectoryMapDefaults() + { + helper('filesystem'); + $this->assertTrue(function_exists('directory_map')); + + $expected = [ + 'foo' . DIRECTORY_SEPARATOR => [ + 'bar', + 'baz' + ], + 'boo' . DIRECTORY_SEPARATOR => [ + 'far', + 'faz' + ], + 'AnEmptyFolder' . DIRECTORY_SEPARATOR => [], + 'simpleFile' + ]; + + $root = vfsStream::setup('root', null, $this->structure); + $this->assertTrue($root->hasChild('foo')); + + $this->assertEquals($expected, directory_map(vfsStream::url('root'))); + } + + public function testDirectoryMapShowsHiddenFiles() + { + helper('filesystem'); + $this->assertTrue(function_exists('directory_map')); + + $expected = [ + 'foo' . DIRECTORY_SEPARATOR => [ + 'bar', + 'baz' + ], + 'boo' . DIRECTORY_SEPARATOR => [ + 'far', + 'faz' + ], + 'AnEmptyFolder' . DIRECTORY_SEPARATOR => [], + 'simpleFile', + '.hidden' + ]; + + $root = vfsStream::setup('root', null, $this->structure); + $this->assertTrue($root->hasChild('foo')); + + $this->assertEquals($expected, directory_map(vfsStream::url('root'), false, true)); + } + + public function testDirectoryMapLimitsRecursion() + { + $this->assertTrue(function_exists('directory_map')); + + $expected = [ + 'foo' . DIRECTORY_SEPARATOR, + 'boo' . DIRECTORY_SEPARATOR, + 'AnEmptyFolder' . DIRECTORY_SEPARATOR, + 'simpleFile', + '.hidden' + ]; + + $root = vfsStream::setup('root', null, $this->structure); + $this->assertTrue($root->hasChild('foo')); + + $this->assertEquals($expected, directory_map(vfsStream::url('root'), 1, true)); + } + + public function testDirectoryMapHandlesNotfound() + { + $this->assertEquals([], directory_map(SUPPORTPATH . 'Files/shaker/')); + } + + //-------------------------------------------------------------------- + + public function testWriteFileSuccess() + { + $vfs = vfsStream::setup('root'); + + $this->assertTrue(write_file(vfsStream::url('root/test.php'), 'Simple')); + $this->assertFileExists($vfs->getChild('test.php')->url()); + } + + public function testWriteFileFailure() + { + $vfs = vfsStream::setup('root'); + + $this->assertFalse(write_file(vfsStream::url('apple#test.php'), 'Simple')); + } + + //-------------------------------------------------------------------- + + public function testDeleteFilesDefaultsToOneLevelDeep() + { + $this->assertTrue(function_exists('delete_files')); + + $vfs = vfsStream::setup('root', null, $this->structure); + + delete_files(vfsStream::url('root')); + + $this->assertFalse($vfs->hasChild('simpleFile')); + $this->assertFalse($vfs->hasChild('.hidden')); + $this->assertTrue($vfs->hasChild('foo')); + $this->assertTrue($vfs->hasChild('boo')); + $this->assertTrue($vfs->hasChild('AnEmptyFolder')); + } + + public function testDeleteFilesHandlesRecursion() + { + $this->assertTrue(function_exists('delete_files')); + + $vfs = vfsStream::setup('root', null, $this->structure); + + delete_files(vfsStream::url('root'), true); + + $this->assertFalse($vfs->hasChild('simpleFile')); + $this->assertFalse($vfs->hasChild('.hidden')); + $this->assertFalse($vfs->hasChild('foo')); + $this->assertFalse($vfs->hasChild('boo')); + $this->assertFalse($vfs->hasChild('AnEmptyFolder')); + } + + public function testDeleteFilesLeavesHTFiles() + { + $structure = array_merge($this->structure, [ + '.htaccess' => 'Deny All', + 'index.html' => 'foo', + 'index.php' => 'blah' + ]); + + $vfs = vfsStream::setup('root', null, $structure); + + delete_files(vfsStream::url('root'), true, true); + + $this->assertFalse($vfs->hasChild('simpleFile')); + $this->assertFalse($vfs->hasChild('foo')); + $this->assertFalse($vfs->hasChild('boo')); + $this->assertFalse($vfs->hasChild('AnEmptyFolder')); + $this->assertTrue($vfs->hasChild('.htaccess')); + $this->assertTrue($vfs->hasChild('index.html')); + $this->assertTrue($vfs->hasChild('index.php')); + } + + public function testDeleteFilesFailure() + { + $this->assertFalse(delete_files(SUPPORTPATH . 'Files/shaker/')); + } + + //-------------------------------------------------------------------- + + public function testGetFilenames() + { + $this->assertTrue(function_exists('delete_files')); + + // Not sure the directory names should actually show up + // here but this matches v3.x results. + $expected = [ + 'foo', + 'boo', + 'AnEmptyFolder', + 'simpleFile' + ]; + + $vfs = vfsStream::setup('root', null, $this->structure); + + $this->assertEquals($expected, get_filenames($vfs->url(), false)); + } + + public function testGetFilenamesWithSource() + { + $this->assertTrue(function_exists('delete_files')); + + // Not sure the directory names should actually show up + // here but this matches v3.x results. + $expected = [ + DIRECTORY_SEPARATOR . 'foo', + DIRECTORY_SEPARATOR . 'boo', + DIRECTORY_SEPARATOR . 'AnEmptyFolder', + DIRECTORY_SEPARATOR . 'simpleFile' + ]; + + $vfs = vfsStream::setup('root', null, $this->structure); + + $this->assertEquals($expected, get_filenames($vfs->url(), true)); + } + + public function testGetFilenamesFailure() + { + $this->assertEquals([], get_filenames(SUPPORTPATH . 'Files/shaker/')); + } + + //-------------------------------------------------------------------- + + public function testGetDirFileInfo() + { + $file = SUPPORTPATH.'Files/baker/banana.php'; + $info = get_file_info($file); + + $expected = [ + 'banana.php' => [ + 'name' => 'banana.php', + 'server_path' => $file, + 'size' => $info['size'], + 'date' => $info['date'], + 'relative_path' => realpath(__DIR__ .'/../../_support/Files/baker'), + ] + ]; + + + $this->assertEquals($expected, get_dir_file_info(SUPPORTPATH . 'Files/baker')); + } + + public function testGetDirFileInfoNested() + { + $expected = ['banana.php', 'prune_ripe.php', 'fig_3.php', 'apple.php']; + + $results = get_dir_file_info(SUPPORTPATH . 'Files', false); + $this->assertEmpty(array_diff($expected, array_keys($results))); + } + + public function testGetDirFileInfoFailure() + { + $expected = []; + + $this->assertEquals($expected, get_dir_file_info(SUPPORTPATH . 'Files#baker')); + } + + //-------------------------------------------------------------------- + + public function testGetFileInfo() + { + $file = SUPPORTPATH.'Files/baker/banana.php'; + $info = get_file_info($file); + + $expected = [ + 'name' => 'banana.php', + 'server_path' => $file, + 'size' => $info['size'], + 'date' => $info['date'], + ]; + + + $this->assertEquals($expected, get_file_info(SUPPORTPATH . 'Files/baker/banana.php')); + } + + public function testGetFileInfoCustom() + { + $expected = [ + 'readable' => true, + 'writable' => true, + 'executable' => false, + ]; + + $this->assertEquals($expected, get_file_info(SUPPORTPATH . 'Files/baker/banana.php', 'readable,writable,executable')); + } + + public function testGetFileInfoPerms() + { + $file = SUPPORTPATH.'Files/baker/banana.php'; + $expected = 0664; + chmod($file, $expected); + + $stuff = get_file_info($file, 'fileperms'); + + $this->assertEquals($expected, $stuff['fileperms'] & 0777); + } + + public function testGetFileNotThereInfo() + { + $expected = null; + + $this->assertEquals($expected, get_file_info(SUPPORTPATH . 'Files/icer')); + } + + //-------------------------------------------------------------------- + public function testOctalPermissions() + { + $this->assertEquals('777', octal_permissions(0777)); + $this->assertEquals('655', octal_permissions(0655)); + $this->assertEquals('123', octal_permissions(0123)); + } + + public function testSymbolicPermissions() + { + $expected = [ + 0777 => 'urwxrwxrwx', + 0655 => 'urw-r-xr-x', + 0123 => 'u--x-w--wx', + 010655 => 'prw-r-xr-x', + 020655 => 'crw-r-xr-x', + 040655 => 'drw-r-xr-x', + 060655 => 'brw-r-xr-x', + 0100655 => '-rw-r-xr-x', + 0120655 => 'lrw-r-xr-x', + 0140655 => 'srw-r-xr-x', + ]; + + foreach ($expected as $perm => $value) + $this->assertEquals($value, symbolic_permissions($perm)); + } + + //-------------------------------------------------------------------- + + public function testRealPathURL() + { + $this->expectException(\InvalidArgumentException::class); + set_realpath('http://somewhere.com/overtherainbow'); + } + + public function testRealPathInvalid() + { + $this->expectException(\InvalidArgumentException::class); + set_realpath(SUPPORTPATH . 'root/../', true); + } + + public function testRealPathResolved() + { + $this->assertEquals(SUPPORTPATH . 'Helpers/', set_realpath(SUPPORTPATH . 'Files/../Helpers', true)); + } + +} diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php new file mode 100644 index 000000000000..867f969be897 --- /dev/null +++ b/tests/system/Helpers/FormHelperTest.php @@ -0,0 +1,808 @@ +baseURL = ''; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $before = (new Filters())->globals['before']; + if (in_array('csrf', $before) || array_key_exists('csrf', $before)) + { + $Value = csrf_hash(); + $Name = csrf_token(); + $expected = << + + +EOH; + } + else + { + $expected = << + +EOH; + } + + $attributes = [ + 'name' => 'form', + 'id' => 'form', + 'method' => 'POST' + ]; + $this->assertEquals($expected, form_open('foo/bar', $attributes)); + } + + // ------------------------------------------------------------------------ + public function testFormOpenWithoutAction() + { + $config = new App(); + $config->baseURL = ''; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $before = (new Filters())->globals['before']; + if (in_array('csrf', $before) || array_key_exists('csrf', $before)) + { + $Value = csrf_hash(); + $Name = csrf_token(); + $expected = << + + +EOH; + } + else + { + $expected = << + +EOH; + } + $attributes = [ + 'name' => 'form', + 'id' => 'form', + 'method' => 'POST' + ]; + $this->assertEquals($expected, form_open('', $attributes)); + } + + // ------------------------------------------------------------------------ + public function testFormOpenWithoutMethod() + { + $config = new App(); + $config->baseURL = ''; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $before = (new Filters())->globals['before']; + if (in_array('csrf', $before) || array_key_exists('csrf', $before)) + { + $Value = csrf_hash(); + $Name = csrf_token(); + $expected = << + + +EOH; + } + else + { + $expected = << + +EOH; + } + + $attributes = [ + 'name' => 'form', + 'id' => 'form' + ]; + $this->assertEquals($expected, form_open('foo/bar', $attributes)); + } + + // ------------------------------------------------------------------------ + public function testFormOpenWithHidden() + { + $config = new App(); + $config->baseURL = ''; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $before = (new Filters())->globals['before']; + if (in_array('csrf', $before) || array_key_exists('csrf', $before)) + { + $Value = csrf_hash(); + $Name = csrf_token(); + $expected = << + + + +EOH; + } + else + { + $expected = << + + +EOH; + } + + $attributes = [ + 'name' => 'form', + 'id' => 'form', + 'method' => 'POST' + ]; + $hidden = [ + 'foo' => 'bar' + ]; + $this->assertEquals($expected, form_open('foo/bar', $attributes, $hidden)); + } + +// ------------------------------------------------------------------------ +//FIXME This needs dynamic filters to complete +// public function testFormOpenWithCSRF() +// { +// $config = new App(); +// $config->baseURL = ''; +// $config->indexPage = 'index.php'; +// $request = Services::request($config); +// $request->uri = new URI('http://example.com/'); +// +// Services::injectMock('request', $request); +// +// $filters = Services::filters(); +// $filters->globals['before'][] = 'csrf'; // force CSRF +// $before = $filters->globals['before']; +// +// $Value = csrf_hash(); +// $Name = csrf_token(); +// $expected = << +// +// +// +//EOH; +// +// $attributes = [ +// 'name' => 'form', +// 'id' => 'form', +// 'method' => 'POST' +// ]; +// $hidden = [ +// 'foo' => 'bar' +// ]; +// $this->assertEquals($expected, form_open('foo/bar', $attributes, $hidden)); +// } + // ------------------------------------------------------------------------ + public function testFormOpenMultipart() + { + $config = new App(); + $config->baseURL = ''; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $before = (new Filters())->globals['before']; + if (in_array('csrf', $before) || array_key_exists('csrf', $before)) + { + $Value = csrf_hash(); + $Name = csrf_token(); + $expected = << + + +EOH; + } + else + { + $expected = << + +EOH; + } + $attributes = [ + 'name' => 'form', + 'id' => 'form', + 'method' => 'POST' + ]; + $this->assertEquals($expected, form_open_multipart('foo/bar', $attributes)); + + // make sure it works with attributes as a string too + $attributesString = 'name="form" id="form" method="POST"'; + $this->assertEquals($expected, form_open_multipart('foo/bar', $attributesString)); + } + + // ------------------------------------------------------------------------ + public function testFormHidden() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_hidden('username', 'johndoe')); + } + + // ------------------------------------------------------------------------ + public function testFormHiddenArrayInput() + { + $data = [ + 'foo' => 'bar' + ]; + $expected = << + +EOH; + $this->assertEquals($expected, form_hidden($data, null)); + } + + // ------------------------------------------------------------------------ + public function testFormHiddenArrayValues() + { + $data = [ + 'foo' => 'bar' + ]; + $expected = << + +EOH; + $this->assertEquals($expected, form_hidden('name', $data)); + } + + // ------------------------------------------------------------------------ + public function testFormInput() + { + $expected = <<\n +EOH; + $data = [ + 'name' => 'username', + 'id' => 'username', + 'value' => 'johndoe', + 'maxlength' => '100', + 'size' => '50', + 'style' => 'width:50%', + ]; + $this->assertEquals($expected, form_input($data)); + } + + // ------------------------------------------------------------------------ + public function testFormPassword() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_password('password')); + } + + // ------------------------------------------------------------------------ + public function testFormUpload() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_upload('attachment')); + } + + // ------------------------------------------------------------------------ + public function testFormTextarea() + { + $expected = <<Notes\n +EOH; + $this->assertEquals($expected, form_textarea('notes', 'Notes')); + } + + // ------------------------------------------------------------------------ + public function testFormTextareaWithValueAttribute() + { + $data = [ + 'name' => 'foo', + 'value' => 'bar' + ]; + $expected = <<bar + +EOH; + $this->assertEquals($expected, form_textarea($data)); + } + + // ------------------------------------------------------------------------ + public function testFormDropdown() + { + $expected = << + + + + +\n +EOH; + $options = [ + 'small' => 'Small Shirt', + 'med' => 'Medium Shirt', + 'large' => 'Large Shirt', + 'xlarge' => 'Extra Large Shirt', + ]; + $this->assertEquals($expected, form_dropdown('shirts', $options, 'large')); + $expected = << + + + + +\n +EOH; + $shirts_on_sale = ['small', 'large']; + $this->assertEquals($expected, form_dropdown('shirts', $options, $shirts_on_sale)); + $options = [ + 'Swedish Cars' => [ + 'volvo' => 'Volvo', + 'saab' => 'Saab' + ], + 'German Cars' => [ + 'mercedes' => 'Mercedes', + 'audi' => 'Audi' + ] + ]; + $expected = << + + + + + + + + +\n +EOH; + $this->assertEquals($expected, form_dropdown('cars', $options, ['volvo', 'audi'])); + } + + public function testFormDropdownUnselected() + { + $options = [ + 'Swedish Cars' => [ + 'volvo' => 'Volvo', + 'saab' => 'Saab' + ], + 'German Cars' => [ + 'mercedes' => 'Mercedes', + 'audi' => 'Audi' + ] + ]; + $expected = << + + + + + + + + +\n +EOH; + $this->assertEquals($expected, form_dropdown('cars', $options, [])); + } + + public function testFormDropdownInferred() + { + $options = [ + 'Swedish Cars' => [ + 'volvo' => 'Volvo', + 'saab' => 'Saab' + ], + 'German Cars' => [ + 'mercedes' => 'Mercedes', + 'audi' => 'Audi' + ] + ]; + $expected = << + + + + + + + + +\n +EOH; + $_POST['cars'] = 'audi'; + $this->assertEquals($expected, form_dropdown('cars', $options, [])); + unset($_POST['cars']); + } + + // ------------------------------------------------------------------------ + public function testFormDropdownWithSelectedAttribute() + { + $expected = << + + + +EOH; + $data = [ + 'name' => 'foo', + 'selected' => 'bar' + ]; + $options = [ + 'bar' => 'Bar' + ]; + $this->assertEquals($expected, form_dropdown($data, $options)); + } + + // ------------------------------------------------------------------------ + public function testFormDropdownWithOptionsAttribute() + { + $expected = << + + + +EOH; + $data = [ + 'name' => 'foo', + 'options' => [ + 'bar' => 'Bar' + ] + ]; + $this->assertEquals($expected, form_dropdown($data)); + } + + // ------------------------------------------------------------------------ + public function testFormDropdownWithEmptyArrayOptionValue() + { + $expected = << + + +EOH; + $options = [ + 'bar' => [] + ]; + $this->assertEquals($expected, form_dropdown('foo', $options)); + } + + // ------------------------------------------------------------------------ + public function testFormMultiselect() + { + $expected = << + + + + +\n +EOH; + $options = [ + 'small' => 'Small Shirt', + 'med' => 'Medium Shirt', + 'large' => 'Large Shirt', + 'xlarge' => 'Extra Large Shirt', + ]; + $this->assertEquals($expected, form_multiselect('shirts[]', $options, ['med', 'large'])); + } + + // ------------------------------------------------------------------------ + public function testFormFieldset() + { + $expected = << +Address Information\n +EOH; + $this->assertEquals($expected, form_fieldset('Address Information')); + } + + // ------------------------------------------------------------------------ + public function testFormFieldsetWithNoLegent() + { + $expected = << + +EOH; + $this->assertEquals($expected, form_fieldset()); + } + + // ------------------------------------------------------------------------ + public function testFormFieldsetWithAttributes() + { + $attributes = [ + 'name' => 'bar' + ]; + $expected = << +Foo + +EOH; + $this->assertEquals($expected, form_fieldset('Foo', $attributes)); + } + + // ------------------------------------------------------------------------ + public function testFormFieldsetClose() + { + $expected = <<
+EOH; + $this->assertEquals($expected, form_fieldset_close('')); + } + + // ------------------------------------------------------------------------ + public function testFormCheckbox() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_checkbox('newsletter', 'accept', TRUE)); + } + + // ------------------------------------------------------------------------ + public function testFormCheckboxArrayData() + { + $data = [ + 'name' => 'foo', + 'value' => 'bar', + 'checked' => true + ]; + $expected = << + +EOH; + $this->assertEquals($expected, form_checkbox($data)); + } + + // ------------------------------------------------------------------------ + public function testFormCheckboxArrayDataWithCheckedFalse() + { + $data = [ + 'name' => 'foo', + 'value' => 'bar', + 'checked' => false + ]; + $expected = << + +EOH; + $this->assertEquals($expected, form_checkbox($data)); + } + + // ------------------------------------------------------------------------ + public function testFormRadio() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_radio('newsletter', 'accept', TRUE)); + } + + // ------------------------------------------------------------------------ + public function testFormSubmit() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_submit('mysubmit', 'Submit Post!')); + } + + // ------------------------------------------------------------------------ + public function testFormLabel() + { + $expected = <<What is your Name +EOH; + $this->assertEquals($expected, form_label('What is your Name', 'username')); + } + + // ------------------------------------------------------------------------ + public function testFormLabelWithAttributes() + { + $attributes = [ + 'id' => 'label1' + ]; + $expected = <<bar +EOH; + $this->assertEquals($expected, form_label('bar', 'foo', $attributes)); + } + + // ------------------------------------------------------------------------ + public function testFormReset() + { + $expected = <<\n +EOH; + $this->assertEquals($expected, form_reset('myreset', 'Reset')); + } + + // ------------------------------------------------------------------------ + public function testFormButton() + { + $expected = <<content\n +EOH; + $this->assertEquals($expected, form_button('name', 'content')); + } + + // ------------------------------------------------------------------------ + public function testFormButtonWithDataArray() + { + $data = [ + 'name' => 'foo', + 'content' => 'bar' + ]; + $expected = <<bar + +EOH; + $this->assertEquals($expected, form_button($data)); + } + + // ------------------------------------------------------------------------ + public function testFormClose() + { + $expected = << +EOH; + $this->assertEquals($expected, form_close('')); + } + + // ------------------------------------------------------------------------ + public function testFormDatalist() + { + $options = [ + 'foo1', + 'bar1' + ]; + $expected = << + + + +EOH; + $this->assertEquals($expected, form_datalist('foo', 'bar', $options)); + } + + // ------------------------------------------------------------------------ + public function testSetValue() + { + $_SESSION['_ci_old_input']['post']['foo'] = 'assertEquals('<bar', set_value('foo')); + + unset($_SESSION['_ci_old_input']['post']['foo']); + $this->assertEquals('baz', set_value('foo', 'baz')); + } + + // ------------------------------------------------------------------------ + public function testSetSelect() + { + $_SESSION['_ci_old_input']['post']['foo'] = 'bar'; + $this->assertEquals(' selected="selected"', set_select('foo', 'bar')); + + $_SESSION['_ci_old_input']['post']['foo'] = ['foo' => 'bar']; + $this->assertEquals(' selected="selected"', set_select('foo', 'bar')); + $this->assertEquals('', set_select('foo', 'baz')); + + unset($_SESSION['_ci_old_input']['post']['foo']); + $this->assertEquals(' selected="selected"', set_select('foo', 'baz', true)); + } + + // ------------------------------------------------------------------------ + public function testSetCheckbox() + { + $_SESSION = [ + '_ci_old_input' => [ + 'post' => [ + 'foo' => 'bar' + ] + ] + ]; + + $this->assertEquals(' checked="checked"', set_checkbox('foo', 'bar')); + + $_SESSION = [ + '_ci_old_input' => [ + 'post' => [ + 'foo' => ['foo' => 'bar'] + ] + ] + ]; + $this->assertEquals(' checked="checked"', set_checkbox('foo', 'bar')); + $this->assertEquals('', set_checkbox('foo', 'baz')); + + $_SESSION = []; + $this->assertEquals('', set_checkbox('foo', 'bar')); + } + + // ------------------------------------------------------------------------ + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSetRadio() + { + $_SESSION = [ + '_ci_old_input' => [ + 'post' => [ + 'foo' => 'bar' + ] + ] + ]; + + $this->assertEquals(' checked="checked"', set_radio('foo', 'bar')); + $this->assertEquals('', set_radio('foo', 'baz')); + unset($_SESSION['_ci_old_input']); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSetRadioFromPost() + { + $_POST['bar'] = 'baz'; + $this->assertEquals(' checked="checked"', set_radio('bar', 'baz')); + $this->assertEquals('', set_radio('bar', 'boop')); + } + + public function testSetRadioFromPostArray() + { + $_SESSION = [ + '_ci_old_input' => [ + 'post' => [ + 'bar' => ['boop', 'fuzzy'] + ] + ] + ]; + $this->assertEquals(' checked="checked"', set_radio('bar', 'boop')); + $this->assertEquals('', set_radio('bar', 'baz')); + } + + public function testSetRadioDefault() + { + $this->assertEquals(' checked="checked"', set_radio('code', 'alpha', true)); + $this->assertEquals('', set_radio('code', 'beta', false)); + } + +} diff --git a/tests/system/Helpers/HTMLHelperTest.php b/tests/system/Helpers/HTMLHelperTest.php new file mode 100755 index 000000000000..d07b075e7fa3 --- /dev/null +++ b/tests/system/Helpers/HTMLHelperTest.php @@ -0,0 +1,504 @@ +tracks = [ + track('subtitles_no.vtt', 'subtitles', 'no', 'Norwegian No'), + track('subtitles_yes.vtt', 'subtitles', 'yes', 'Norwegian Yes') + ]; + } + + //-------------------------------------------------------------------- + + public function testBasicUL() + { + $expected = << +
  • foo
  • +
  • bar
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar']; + + $this->assertEquals(ltrim($expected), ul($list)); + } + + public function testULWithClass() + { + $expected = << +
  • foo
  • +
  • bar
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar']; + + $this->assertEquals($expected, ul($list, 'class="test"')); + } + + public function testMultiLevelUL() + { + $expected = << +
  • foo
  • +
  • bar
  • +
  • 2 +
      +
    • foo
    • +
    • bar
    • +
    +
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar', ['foo', 'bar']]; + + $this->assertEquals(ltrim($expected), ul($list)); + } + + //-------------------------------------------------------------------- + + public function testBasicOL() + { + $expected = << +
  • foo
  • +
  • bar
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar']; + + $this->assertEquals(ltrim($expected), ol($list)); + } + + public function testOLWithClass() + { + $expected = << +
  • foo
  • +
  • bar
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar']; + + $this->assertEquals($expected, ol($list, 'class="test"')); + } + + public function testMultiLevelOL() + { + $expected = << +
  • foo
  • +
  • bar
  • +
  • 2 +
      +
    1. foo
    2. +
    3. bar
    4. +
    +
  • + + +EOH; + + $expected = ltrim($expected); + $list = ['foo', 'bar', ['foo', 'bar']]; + + $this->assertEquals(ltrim($expected), ol($list)); + } + + // ------------------------------------------------------------------------ + + public function testIMG() + { + $target = 'http://site.com/images/picture.jpg'; + $expected = ''; + $this->assertEquals($expected, img($target)); + } + + public function testIMGWithoutProtocol() + { + $target = 'assets/mugshot.jpg'; + $expected = ''; + $this->assertEquals($expected, img($target)); + } + + public function testIMGWithIndexpage() + { + $target = 'assets/mugshot.jpg'; + $expected = ''; + $this->assertEquals($expected, img($target, true)); + } + + // ------------------------------------------------------------------------ + + public function testScriptTag() + { + $target = 'http://site.com/js/mystyles.js'; + $expected = ''; + $this->assertEquals($expected, script_tag($target)); + } + + public function testScriptTagWithoutProtocol() + { + $target = 'js/mystyles.js'; + $expected = ''; + $this->assertEquals($expected, script_tag($target)); + } + + public function testScriptTagWithIndexpage() + { + $target = 'js/mystyles.js'; + $expected = ''; + $this->assertEquals($expected, script_tag($target, true)); + } + + // ------------------------------------------------------------------------ + + public function testLinkTag() + { + $target = 'css/mystyles.css'; + $expected = ''; + $this->assertEquals($expected, link_tag($target)); + } + + public function testLinkTagComplete() + { + $target = 'https://styles.com/css/mystyles.css'; + $expected = ''; + $this->assertEquals($expected, link_tag($target, 'banana', 'fruit', 'Go away', 'VHS')); + } + + public function testLinkTagArray() + { + $parms = [ + 'href' => 'css/mystyles.css', + 'indexPage' => true, + ]; + $expected = ''; + $this->assertEquals($expected, link_tag($parms)); + } + + // ------------------------------------------------------------------------ + + public function testDocType() + { + $target = 'html4-strict'; + $expected = ''; + $this->assertEquals($expected, doctype($target)); + } + + public function testDocTypeDefault() + { + $expected = ''; + $this->assertEquals($expected, doctype()); + } + + public function testDocTypeInvalid() + { + $target = 'good-guess'; + $this->assertEquals(false, doctype($target)); + } + + // ------------------------------------------------------------------------ + + public function testVideo() + { + $expected = << + Your browser does not support the video tag. + + +EOH; + + $target = 'http://www.codeigniter.com/test.mp4'; + $message = 'Your browser does not support the video tag.'; + $video = video($target, $message, 'controls'); + $this->assertEquals($expected, $video); + } + + public function testVideoWithTracks() + { + $expected = << + + + Your browser does not support the video tag. + + +EOH; + + $target = 'test.mp4'; + $message = 'Your browser does not support the video tag.'; + $video = video($target, $message, 'controls', $this->tracks); + $this->assertEquals($expected, $video); + } + + public function testVideoWithTracksAndIndex() + { + $expected = << + + + Your browser does not support the video tag. + + +EOH; + + $target = 'test.mp4'; + $message = 'Your browser does not support the video tag.'; + $video = video($target, $message, 'controls', $this->tracks, true); + $this->assertEquals($expected, $video); + } + + public function testVideoMultipleSources() + { + $expected = << + + + + + + + Your browser does not support the video tag. + + +EOH; + + $sources = [ + source('movie.mp4', 'video/mp4', 'class="test"'), + source('movie.ogg', 'video/ogg'), + source('movie.mov', 'video/quicktime'), + source('movie.ogv', 'video/ogv; codecs=dirac, speex') + ]; + $message = 'Your browser does not support the video tag.'; + $video = video($sources, $message, 'class="test" controls', $this->tracks); + + $this->assertEquals($expected, $video); + } + + // ------------------------------------------------------------------------ + + public function testAudio() + { + $expected = << + + + + + Your browser does not support the audio tag. + + +EOH; + + $sources = [ + source('sound.ogg', 'audio/ogg'), + source('sound.mpeg', 'audio/mpeg') + ]; + $message = 'Your browser does not support the audio tag.'; + $audio = audio($sources, $message, 'id="test" controls', $this->tracks); + + $this->assertEquals($expected, $audio); + } + + public function testAudioSimple() + { + $expected = << + Your browser does not support the audio tag. + + +EOH; + + $source = 'sound.mpeg'; + $message = 'Your browser does not support the audio tag.'; + $audio = audio($source, $message, 'type="audio/mpeg" id="test" controls'); + + $this->assertEquals($expected, $audio); + } + + public function testAudioWithSource() + { + $expected = << + Your browser does not support the audio tag. + + +EOH; + + $source = 'http://codeigniter.com/sound.mpeg'; + $message = 'Your browser does not support the audio tag.'; + $audio = audio($source, $message, 'type="audio/mpeg" id="test" controls'); + + $this->assertEquals($expected, $audio); + } + + public function testAudioWithIndex() + { + $expected = << + Your browser does not support the audio tag. + + +EOH; + + $source = 'sound.mpeg'; + $message = 'Your browser does not support the audio tag.'; + $audio = audio($source, $message, 'type="audio/mpeg" id="test" controls', [], true); + + $this->assertEquals($expected, $audio); + } + + public function testAudioWithTracks() + { + $expected = << + + + Your browser does not support the audio tag. + + +EOH; + + $source = 'sound.mpeg'; + $message = 'Your browser does not support the audio tag.'; + $audio = audio($source, $message, 'type="audio/mpeg" id="test" controls', $this->tracks); + + $this->assertEquals($expected, $audio); + } + + // ------------------------------------------------------------------------ + + public function testMediaNameOnly() + { + $expected = << + + +EOH; + $this->assertEquals($expected, _media('av')); + } + + public function testMediaWithSources() + { + $expected = << + + + + +EOH; + $sources = [ + source('sound.ogg', 'audio/ogg'), + source('sound.mpeg', 'audio/mpeg') + ]; + $this->assertEquals($expected, _media('av', $sources)); + } + + public function testSource() + { + $expected = ''; + $this->assertEquals($expected, source('sound.mpeg', 'audio/mpeg', '', true)); + } + + // ------------------------------------------------------------------------ + + public function testEmbed() + { + $expected = << + +EOH; + + $type = 'video/quicktime'; + $embed = embed('movie.mov', $type, 'class="test"'); + $this->assertEquals($expected, $embed); + } + + public function testEmbedIndexed() + { + $expected = << + +EOH; + + $type = 'video/quicktime'; + $embed = embed('movie.mov', $type, 'class="test"', true); + $this->assertEquals($expected, $embed, ''); + } + + public function testObject() + { + $expected = << + +EOH; + + $type = 'application/x-shockwave-flash'; + $object = object('movie.swf', $type, 'class="test"'); + + $this->assertEquals($expected, $object); + } + + public function testObjectWithParams() + { + + $expected = << + + + + +EOH; + + $type = 'application/x-shockwave-flash'; + $parms = [ + param('foo', 'bar', 'ref', 'class="test"'), + param('hello', 'world', 'ref', 'class="test"') + ]; + $object = object('movie.swf', $type, 'class="test"', $parms); + $this->assertEquals($expected, $object); + } + + public function testObjectIndexed() + { + $expected = << + +EOH; + + $type = 'application/x-shockwave-flash'; + $object = object('movie.swf', $type, 'class="test"', [], true); + + $this->assertEquals($expected, $object); + } + + // ------------------------------------------------------------------------ +} diff --git a/tests/system/Helpers/InflectorHelperTest.php b/tests/system/Helpers/InflectorHelperTest.php new file mode 100755 index 000000000000..740e6daa92e5 --- /dev/null +++ b/tests/system/Helpers/InflectorHelperTest.php @@ -0,0 +1,231 @@ + 'matrix', + 'oxen' => 'ox', + 'aliases' => 'alias', + 'octupus' => 'octupus', + 'shoes' => 'shoe', + 'buses' => 'bus', + 'campus' => 'campus', + 'campuses' => 'campus', + 'mice' => 'mouse', + 'movies' => 'movie', + 'series' => 'series', + 'hives' => 'hive', + 'lives' => 'life', + 'analyses' => 'analysis', + 'men' => 'man', + 'people' => 'person', + 'children' => 'child', + 'statuses' => 'status', + 'news' => 'news', + 'us' => 'us', + 'tests' => 'test', + 'queries' => 'query', + 'dogs' => 'dog', + 'cats' => 'cat', + 'families' => 'family', + 'countries' => 'country' + ]; + + foreach ($strings as $pluralizedString => $singularizedString) + { + $singular = singular($pluralizedString); + $this->assertEquals($singular, $singularizedString); + } + } + + //-------------------------------------------------------------------- + + public function testPlural() + { + $strings = + [ + 'searches' => 'search', + 'matrices' => 'matrix', + 'oxen' => 'ox', + 'aliases' => 'alias', + 'octupus' => 'octupus', + 'shoes' => 'shoe', + 'buses' => 'bus', + 'mice' => 'mouse', + 'movies' => 'movie', + 'series' => 'series', + 'hives' => 'hive', + 'lives' => 'life', + 'analyses' => 'analysis', + 'men' => 'man', + 'people' => 'person', + 'children' => 'child', + 'statuses' => 'status', + 'news' => 'news', + 'us' => 'us', + 'tests' => 'test', + 'queries' => 'query', + 'dogs' => 'dog', + 'cats' => 'cat', + 'families' => 'family', + 'countries' => 'country' + ]; + + foreach ($strings as $pluralizedString => $singularizedString) + { + $plural = plural($singularizedString); + $this->assertEquals($plural, $pluralizedString); + } + } + + //-------------------------------------------------------------------- + + public function testCamelize() + { + $strings = + [ + 'hello from codeIgniter 4' => 'helloFromCodeIgniter4', + 'hello_world' => 'helloWorld' + ]; + + foreach ($strings as $lowerCasedString => $camelizedString) + { + $camelized = camelize($lowerCasedString); + $this->assertEquals($camelized, $camelizedString); + } + } + + //-------------------------------------------------------------------- + + public function testUnderscore() + { + $strings = + [ + 'Hello From CodeIgniter 4' => 'Hello_From_CodeIgniter_4', + 'hello world' => 'hello_world' + ]; + + foreach ($strings as $spaced => $underscore) + { + $underscored = underscore($spaced); + $this->assertEquals($underscored, $underscore); + } + } + + //-------------------------------------------------------------------- + + public function testHumanize() + { + $underscored = ['Hello_From_CodeIgniter_4', 'Hello From CodeIgniter 4']; + $dashed = ['hello-world' , 'Hello World']; + + $humanizedUnderscore = humanize($underscored[0]); + $humanizedDash = humanize($dashed[0], '-'); + + $this->assertEquals($humanizedUnderscore, $underscored[1]); + $this->assertEquals($humanizedDash, $dashed[1]); + } + + //-------------------------------------------------------------------- + + public function testIsCountable() + { + $words = + [ + 'tip' => 'advice', + 'fight' => 'bravery', + 'thing' => 'equipment', + 'deocration' => 'jewelry', + 'line' => 'series', + 'letter' => 'spelling' + ]; + + foreach ($words as $countable => $unCountable) + { + $this->assertTrue(is_pluralizable($countable)); + $this->assertFalse(is_pluralizable($unCountable)); + } + } + + //-------------------------------------------------------------------- + + public function testDasherize() + { + $strings = + [ + 'hello_world' => 'hello-world', + 'Hello_From_CodeIgniter_4' => 'Hello-From-CodeIgniter-4' + ]; + + foreach ($strings as $underscored => $dashed) + { + $dasherized = dasherize($underscored); + $this->assertEquals($dasherized, $dashed); + } + } + + //-------------------------------------------------------------------- + + public function testOrdinal() + { + $suffixes = + [ + 'st' => 1, + 'nd' => 2, + 'rd' => 3, + 'th' => 4, + 'th' => 11, + 'th' => 20, + 'st' => 21, + 'nd' => 22, + 'rd' => 23, + 'th' => 24 + ]; + + foreach ($suffixes as $suffix => $number) + { + $ordinal = ordinal($number); + $this->assertEquals($suffix, $ordinal); + } + } + + //-------------------------------------------------------------------- + + public function testOrdinalize() + { + $suffixedNumbers = + [ + '1st' => 1, + '2nd' => 2, + '3rd' => 3, + '4th' => 4, + '11th' => 11, + '20th' => 20, + '21st' => 21, + '22nd' => 22, + '23rd' => 23, + '24th' => 24 + ]; + + foreach ($suffixedNumbers as $suffixed => $number) + { + $ordinalized = ordinalize($number); + $this->assertEquals($suffixed, $ordinalized); + } + } + +} diff --git a/tests/system/Helpers/NumberHelperTest.php b/tests/system/Helpers/NumberHelperTest.php new file mode 100755 index 000000000000..7f76859786f5 --- /dev/null +++ b/tests/system/Helpers/NumberHelperTest.php @@ -0,0 +1,125 @@ +assertEquals('XCVI', number_to_roman(96)); + $this->assertEquals('MMDCCCXCV', number_to_roman(2895)); + $this->assertEquals('CCCXXIX', number_to_roman(329)); + $this->assertEquals('IV', number_to_roman(4)); + $this->assertEquals('X', number_to_roman(10)); + } + + public function testRomanNumberRange() + { + $this->assertEquals(null, number_to_roman(-1)); + $this->assertEquals(null, number_to_roman(0)); + $this->assertEquals(null, number_to_roman(4000)); + } + + public function test_format_number() + { + $this->assertEquals('123,456', format_number(123456, 0, 'en_US')); + } + + public function test_format_number_with_precision() + { + $this->assertEquals('123,456.8', format_number(123456.789, 1, 'en_US')); + $this->assertEquals('123,456.79', format_number(123456.789, 2, 'en_US')); + } + + public function testFormattingOptions() + { + $options = [ + 'before' => '<<', + 'after' => '>>', + ]; + $this->assertEquals('<<123,456.79>>', format_number(123456.789, 2, 'en_US', $options)); + } + + public function test_number_to_size() + { + $this->assertEquals('456 Bytes', number_to_size(456, 1, 'en_US')); + } + + public function test_kb_format() + { + $this->assertEquals('4.5 KB', number_to_size(4567, 1, 'en_US')); + } + + public function test_kb_format_medium() + { + $this->assertEquals('44.6 KB', number_to_size(45678, 1, 'en_US')); + } + + public function test_kb_format_large() + { + $this->assertEquals('446.1 KB', number_to_size(456789, 1, 'en_US')); + } + + public function test_mb_format() + { + $this->assertEquals('3.3 MB', number_to_size(3456789, 1, 'en_US')); + } + + public function test_gb_format() + { + $this->assertEquals('1.8 GB', number_to_size(1932735283.2, 1, 'en_US')); + } + + public function test_tb_format() + { + $this->assertEquals('112,283.3 TB', number_to_size(123456789123456789, 1, 'en_US')); + } + + public function test_thousands() + { + $this->assertEquals('123 thousand', number_to_amount('123,000', 0, 'en_US')); + } + + public function test_millions() + { + $this->assertEquals('123.4 million', number_to_amount('123,400,000', 1, 'en_US')); + } + + public function test_billions() + { + $this->assertEquals('123.46 billion', number_to_amount('123,456,000,000', 2, 'en_US')); + } + + public function test_trillions() + { + $this->assertEquals('123.457 trillion', number_to_amount('123,456,700,000,000', 3, 'en_US')); + } + + public function test_quadrillions() + { + $this->assertEquals('123.5 quadrillion', number_to_amount('123,456,700,000,000,000', 1, 'en_US')); + } + + /** + * @group single + */ + public function test_currency_current_locale() + { + $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')); + } + + public function testNumbersThatArent() + { + $this->assertFalse(number_to_size('1232x')); + $this->assertFalse(number_to_amount('1232x')); + } + +} diff --git a/tests/system/Helpers/SecurityHelperTest.php b/tests/system/Helpers/SecurityHelperTest.php new file mode 100644 index 000000000000..8ecfd21990ee --- /dev/null +++ b/tests/system/Helpers/SecurityHelperTest.php @@ -0,0 +1,35 @@ +assertEquals('hello.doc', sanitize_filename('hello.doc')); + } + + public function testSanitizeFilenameStripsExtras() + { + $filename = './'; + $this->assertEquals('foo ', sanitize_filename($filename)); + } + + public function testStripImageTags() + { + $this->assertEquals('http://example.com/spacer.gif', strip_image_tags('http://example.com/spacer.gif')); + + $this->assertEquals('http://example.com/spacer.gif', strip_image_tags('Who needs CSS when you have a spacer.gif?')); + } + + function test_encode_php_tags() + { + $this->assertEquals('<? echo $foo; ?>', encode_php_tags('')); + } + +} diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php new file mode 100755 index 000000000000..e1f0d3c04354 --- /dev/null +++ b/tests/system/Helpers/TextHelperTest.php @@ -0,0 +1,357 @@ +assertEquals($expected, strip_slashes($str)); + } + + // -------------------------------------------------------------------- + public function test_strip_quotes() + { + $strs = [ + '"me oh my!"' => 'me oh my!', + "it's a winner!" => 'its a winner!', + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, strip_quotes($str)); + } + } + + // -------------------------------------------------------------------- + public function test_quotes_to_entities() + { + $strs = [ + '"me oh my!"' => '"me oh my!"', + "it's a winner!" => 'it's a winner!', + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, quotes_to_entities($str)); + } + } + + // -------------------------------------------------------------------- + public function test_reduce_double_slashes() + { + $strs = [ + 'http://codeigniter.com' => 'http://codeigniter.com', + '//var/www/html/example.com/' => '/var/www/html/example.com/', + '/var/www/html//index.php' => '/var/www/html/index.php' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, reduce_double_slashes($str)); + } + } + + // -------------------------------------------------------------------- + public function test_reduce_multiples() + { + $strs = [ + 'Fred, Bill,, Joe, Jimmy' => 'Fred, Bill, Joe, Jimmy', + 'Ringo, John, Paul,,' => 'Ringo, John, Paul,' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, reduce_multiples($str)); + } + $strs = [ + 'Fred, Bill,, Joe, Jimmy' => 'Fred, Bill, Joe, Jimmy', + 'Ringo, John, Paul,,' => 'Ringo, John, Paul' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, reduce_multiples($str, ',', TRUE)); + } + } + + // -------------------------------------------------------------------- + public function test_random_string() + { + $this->assertEquals(16, strlen(random_string('alnum', 16))); + $this->assertEquals(16, strlen(random_string('alpha', 16))); + $this->assertEquals(16, strlen(random_string('nozero', 16))); + $this->assertEquals(16, strlen(random_string('numeric', 16))); + $this->assertEquals(8, strlen(random_string('numeric'))); + + $this->assertInternalType('string', random_string('basic')); + $this->assertEquals(16, strlen($random = random_string('crypto', 16))); + $this->assertInternalType('string', $random); + + $this->assertEquals(32, strlen($random = random_string('md5'))); + $this->assertEquals(40, strlen($random = random_string('sha1'))); + } + + // -------------------------------------------------------------------- + public function test_increment_string() + { + $this->assertEquals('my-test_1', increment_string('my-test')); + $this->assertEquals('my-test-1', increment_string('my-test', '-')); + $this->assertEquals('file_5', increment_string('file_4')); + $this->assertEquals('file-5', increment_string('file-4', '-')); + $this->assertEquals('file-5', increment_string('file-4', '-')); + $this->assertEquals('file-1', increment_string('file', '-', '1')); + $this->assertEquals(124, increment_string('123', '')); + } + + // ------------------------------------------------------------------- + // Functions from text_helper_test.php + // ------------------------------------------------------------------- + + public function test_word_limiter() + { + $this->assertEquals('Once upon a time,…', word_limiter($this->_long_string, 4)); + $this->assertEquals('Once upon a time,…', word_limiter($this->_long_string, 4, '…')); + $this->assertEquals('', word_limiter('', 4)); + $this->assertEquals('Once upon a…', word_limiter($this->_long_string, 3, '…')); + $this->assertEquals('Once upon a time', word_limiter('Once upon a time', 4, '…')); + } + + // ------------------------------------------------------------------------ + public function test_character_limiter() + { + $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, '…')); + $this->assertEquals('Short', character_limiter('Short', 20)); + $this->assertEquals('Short', character_limiter('Short', 5)); + } + + // ------------------------------------------------------------------------ + public function test_ascii_to_entities() + { + $strs = [ + '“‘ “test” ' => '“‘ “test” ', + '†¥¨ˆøåß∂ƒ©˙∆˚¬' => '†¥¨ˆøåß∂ƒ©˙∆˚¬' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, ascii_to_entities($str)); + } + } + + // ------------------------------------------------------------------------ + public function test_entities_to_ascii() + { + $strs = [ + '“‘ “test” ' => '“‘ “test” ', + '†¥¨ˆøåß∂ƒ©˙∆˚¬' => '†¥¨ˆøåß∂ƒ©˙∆˚¬' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, entities_to_ascii($str)); + } + } + + public function testEntitiesToAsciiUnsafe() + { + $str = '<>'; + $this->assertEquals('<>', entities_to_ascii($str, true)); + $this->assertEquals('<>', entities_to_ascii($str, false)); + } + + public function testEntitiesToAsciiSmallOrdinals() + { + $str = ''; + $this->assertEquals(pack('c', 7), entities_to_ascii($str)); + } + + // ------------------------------------------------------------------------ + public function test_convert_accented_characters() + { + //$this->ci_vfs_clone('application/Config/ForeignChars.php'); + $this->assertEquals('AAAeEEEIIOOEUUUeY', convert_accented_characters('ÀÂÄÈÊËÎÏÔŒÙÛÜŸ')); + $this->assertEquals('a e i o u n ue', convert_accented_characters('á é í ó ú ñ ü')); + } + + // ------------------------------------------------------------------------ + public function test_censored_words() + { + $censored = ['boob', 'nerd', 'ass', 'fart']; + $strs = [ + 'Ted bobbled the ball' => 'Ted bobbled the ball', + 'Jake is a nerdo' => 'Jake is a nerdo', + 'The borg will assimilate you' => 'The borg will assimilate you', + 'Did Mary Fart?' => 'Did Mary $*#?', + 'Jake is really a boob' => 'Jake is really a $*#', + 'Jake is really a (boob)' => 'Jake is really a ($*#)' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, word_censor($str, $censored, '$*#')); + } + // test censored words being sent as a string + $this->assertEquals('this is a test', word_censor('this is a test', 'test')); + } + + // ------------------------------------------------------------------------ + public function test_highlight_code() + { + $expect = "\n<?php var_dump(\$this); ?> \n\n"; + $this->assertEquals($expect, highlight_code('')); + } + + // ------------------------------------------------------------------------ + public function test_highlight_phrase() + { + $strs = [ + 'this is a phrase' => 'this is a phrase', + 'this is another' => 'this is another', + 'Gimme a test, Sally' => 'Gimme a test, Sally', + 'Or tell me what this is' => 'Or tell me what this is', + '' => '' + ]; + foreach ($strs as $str => $expect) + { + $this->assertEquals($expect, highlight_phrase($str, 'this is')); + } + $this->assertEquals('this is a strong test', highlight_phrase('this is a strong test', 'this is', '', '')); + } + + // ------------------------------------------------------------------------ + public function test_ellipsize() + { + $strs = [ + '0' => [ + 'this is my string' => '… my string', + "here's another one" => '…nother one', + 'this one is just a bit longer' => '…bit longer', + 'short' => 'short' + ], + '.5' => [ + 'this is my string' => 'this …tring', + "here's another one" => "here'…r one", + 'this one is just a bit longer' => 'this …onger', + 'short' => 'short' + ], + '1' => [ + 'this is my string' => 'this is my…', + "here's another one" => "here's ano…", + 'this one is just a bit longer' => 'this one i…', + 'short' => 'short' + ], + ]; + foreach ($strs as $pos => $s) + { + foreach ($s as $str => $expect) + { + $this->assertEquals($expect, ellipsize($str, 10, $pos)); + } + } + } + + // ------------------------------------------------------------------------ + public function testWordWrap() + { + $string = 'Here is a simple string of text that will help us demonstrate this function.'; + $expected = "Here is a simple string\nof text that will help us\ndemonstrate this\nfunction."; + $this->assertEquals(substr_count(word_wrap($string, 25), "\n"), 3); + $this->assertEquals($expected, word_wrap($string, 25)); + + $string2 = "Here is a\nbroken up sentence\rspanning lines\r\nwoohoo!"; + $expected2 = "Here is a\nbroken up sentence\nspanning lines\nwoohoo!"; + $this->assertEquals(substr_count(word_wrap($string2, 25), "\n"), 3); + $this->assertEquals($expected2, word_wrap($string2, 25)); + + $string3 = "Here is another slightly longer\nbroken up sentence\rspanning lines\r\nwoohoo!"; + $expected3 = "Here is another slightly\nlonger\nbroken up sentence\nspanning lines\nwoohoo!"; + $this->assertEquals(substr_count(word_wrap($string3, 25), "\n"), 4); + $this->assertEquals($expected3, word_wrap($string3, 25)); + } + + public function testWordWrapUnwrap() + { + $string = 'Here is a {unwrap}simple string of text{/unwrap} that will help us demonstrate this function.'; + $expected = "Here is a simple string of text\nthat will help us\ndemonstrate this\nfunction."; + $this->assertEquals(substr_count(word_wrap($string, 25), "\n"), 3); + $this->assertEquals($expected, word_wrap($string, 25)); + } + + public function testWordWrapLongWords() + { + // the really really long word will be split + $string = 'Here is an unbelievable super-complicated and reallyreallyquiteextraordinarily sophisticated sentence.'; + $expected = "Here is an unbelievable\nsuper-complicated and\nreallyreallyquiteextraor\ndinarily\nsophisticated sentence."; + $this->assertEquals($expected, word_wrap($string, 25)); + } + + public function testWordWrapURL() + { + // the really really long word will be split + $string = 'Here is an unbelievable super-complicated and http://www.reallyreallyquiteextraordinarily.com sophisticated sentence.'; + $expected = "Here is an unbelievable\nsuper-complicated and\nhttp://www.reallyreallyquiteextraordinarily.com\nsophisticated sentence."; + $this->assertEquals($expected, word_wrap($string, 25)); + } + + // ------------------------------------------------------------------------ + public function test_default_word_wrap_charlim() + { + $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); + } + + // ----------------------------------------------------------------------- + + public function test_excerpt() + { + $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. ...'; + $this->assertEquals(excerpt($string), $result); + } + + // ----------------------------------------------------------------------- + + public function test_excerpt_radius() + { + $string = $this->_long_string; + $phrase = 'began'; + $result = '... people began to ...'; + $this->assertEquals(excerpt($string, $phrase, 10), $result); + } + + // ----------------------------------------------------------------------- + + public function test_alternator() + { + $phrase = ' scream! '; + $result = ''; + alternator(); + for ($i = 0; $i < 4; $i ++ ) + $result .= alternator('I', 'you', 'we') . $phrase; + $this->assertEquals('I scream! you scream! we scream! I scream! ', $result); + } + + public function test_empty_alternator() + { + $phrase = ' scream! '; + $result = ''; + for ($i = 0; $i < 4; $i ++ ) + $result .= alternator() . $phrase; + $this->assertEquals(' scream! scream! scream! scream! ', $result); + } + +} diff --git a/tests/system/Helpers/URLHelperTest.php b/tests/system/Helpers/URLHelperTest.php new file mode 100644 index 000000000000..bd678a6df8d4 --- /dev/null +++ b/tests/system/Helpers/URLHelperTest.php @@ -0,0 +1,884 @@ +baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/', site_url('', null, $config)); + } + + public function testSiteURLHTTPS() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTPS'] = 'on'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('https://example.com/index.php/', site_url('', null, $config)); + } + + public function testSiteURLNoIndex() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = ''; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/', site_url('', null, $config)); + } + + public function testSiteURLDifferentIndex() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'banana.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/banana.php/', site_url('', null, $config)); + } + + public function testSiteURLNoIndexButPath() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = ''; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/abc', site_url('abc', null, $config)); + } + + public function testSiteURLAttachesPath() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/foo', site_url('foo', null, $config)); + } + + public function testSiteURLAttachesScheme() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('ftp://example.com/index.php/foo', site_url('foo', 'ftp', $config)); + } + + public function testSiteURLExample() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/news/local/123', site_url('news/local/123', null, $config)); + } + + public function testSiteURLSegments() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/news/local/123', site_url(['news', 'local', '123'], null, $config)); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/240 + */ + public function testSiteURLWithSegments() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/test'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/'; + $request = Services::request($config, false); + $request->uri = new URI('http://example.com/test'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/', site_url()); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/240 + */ + public function testSiteURLWithSegmentsAgain() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/test/page'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com'; + $request = Services::request($config, false); + $request->uri = new URI('http://example.com/test/page'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/index.php/', site_url()); + $this->assertEquals('http://example.com/index.php/profile', site_url('profile')); + } + + //-------------------------------------------------------------------- + // Test base_url + + public function testBaseURLBasics() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $this->assertEquals('http://example.com', base_url()); + } + + public function testBaseURLAttachesPath() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $this->assertEquals('http://example.com/foo', base_url('foo')); + } + + public function testBaseURLAttachesPathArray() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $this->assertEquals('http://example.com/foo/bar', base_url(['foo', 'bar'])); + } + + public function testBaseURLAttachesScheme() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $this->assertEquals('https://example.com/foo', base_url('foo', 'https')); + } + + public function testBaseURLHeedsBaseURL() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/public'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/public', base_url()); + } + + public function testBaseURLExample() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $this->assertEquals('http://example.com/blog/post/123', base_url('blog/post/123')); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/240 + */ + public function testBaseURLWithSegments() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/test'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/'; + $request = Services::request($config, false); + $request->uri = new URI('http://example.com/test'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/', base_url()); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/867 + */ + public function testBaseURLHTTPS() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTPS'] = 'on'; + + $this->assertEquals('https://example.com/blog/post/123', base_url('blog/post/123')); + } + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/240 + */ + public function testBaseURLWithSegmentsAgain() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/test/page'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com'; + $request = Services::request($config, false); + $request->uri = new URI('http://example.com/test/page'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com', base_url()); + $this->assertEquals('http://example.com/profile', base_url('profile')); + } + + //-------------------------------------------------------------------- + // Test current_url + + public function testCurrentURLReturnsBasicURL() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/public'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $this->assertEquals('http://example.com/public', current_url()); + } + + public function testCurrentURLReturnsObject() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/public'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $url = current_url(true); + + $this->assertInstanceOf(URI::class, $url); + $this->assertEquals('http://example.com/public', (string) $url); + } + + public function testCurrentURLEquivalence() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $this->assertEquals(base_url(uri_string()), current_url()); + } + + //-------------------------------------------------------------------- + // Test previous_url + + public function testPreviousURLUsesSessionFirst() + { + $uri1 = 'http://example.com/one?two'; + $uri2 = 'http://example.com/two?foo'; + + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = $uri1; + $_SESSION['_ci_previous_url'] = $uri2; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/public'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $this->assertEquals($uri2, previous_url()); + } + + //-------------------------------------------------------------------- + + public function testPreviousURLUsesRefererIfNeeded() + { + $uri1 = 'http://example.com/one?two'; + $uri2 = 'http://example.com/two?foo'; + + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + $_SERVER['HTTP_REFERER'] = $uri1; + + // Since we're on a CLI, we must provide our own URI + $config = new App(); + $config->baseURL = 'http://example.com/public'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/public'); + + Services::injectMock('request', $request); + + $this->assertEquals($uri1, previous_url()); + } + + //-------------------------------------------------------------------- + // Test uri_string + + public function testUriString() + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $url = current_url(); + $this->assertEquals('/', uri_string()); + } + + public function testUriStringExample() + { + + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/assets/image.jpg'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/assets/image.jpg'); + + Services::injectMock('request', $request); + + $url = current_url(); + $this->assertEquals('/assets/image.jpg', uri_string()); + } + + //-------------------------------------------------------------------- + // Test index_page + + public function testIndexPage() + { + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('index.php', index_page()); + } + + public function testIndexPageAlt() + { + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'banana.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals('banana.php', index_page($config)); + } + + //-------------------------------------------------------------------- + // Test anchor + + public function anchorNormalPatterns() + { + return [ + 'normal01' => ['http://example.com/index.php', ''], + 'normal02' => ['Bananas', '/', 'Bananas'], + 'normal03' => ['http://example.com/index.php', '/', '', 'fruit="peach"'], + 'normal04' => ['Bananas', '/', 'Bananas', 'fruit=peach'], + 'normal05' => ['http://example.com/index.php', '/', '', ['fruit' => 'peach']], + 'normal06' => ['Bananas', '/', 'Bananas', ['fruit' => 'peach']], + 'normal07' => ['http://example.com/index.php', '/'], + ]; + } + + /** + * @dataProvider anchorNormalPatterns + */ + public function testAnchor($expected = '', $uri = '', $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + $this->assertEquals($expected, anchor($uri, $title, $attributes, $config)); + } + + public function anchorNoindexPatterns() + { + return [ + 'noindex01' => ['http://example.com', ''], + 'noindex02' => ['Bananas', '', 'Bananas'], + 'noindex03' => ['http://example.com', '', '', 'fruit="peach"'], + 'noindex04' => ['Bananas', '', 'Bananas', 'fruit=peach'], + 'noindex05' => ['http://example.com', '', '', ['fruit' => 'peach']], + 'noindex06' => ['Bananas', '', 'Bananas', ['fruit' => 'peach']], + 'noindex07' => ['http://example.com', '/'], + ]; + } + + /** + * @dataProvider anchorNoindexPatterns + */ + public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = ''; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + $this->assertEquals($expected, anchor($uri, $title, $attributes, $config)); + } + + public function anchorSubpagePatterns() + { + return [ + 'subpage01' => ['http://example.com/mush', '/mush'], + 'subpage02' => ['Bananas', '/mush', 'Bananas'], + 'subpage03' => ['http://example.com/mush', '/mush', '', 'fruit="peach"'], + 'subpage04' => ['Bananas', '/mush', 'Bananas', 'fruit=peach'], + 'subpage05' => ['http://example.com/mush', '/mush', '', ['fruit' => 'peach']], + 'subpage06' => ['Bananas', '/mush', 'Bananas', ['fruit' => 'peach']], + ]; + } + + /** + * @dataProvider anchorSubpagePatterns + */ + public function testAnchorTargetted($expected = '', $uri = '', $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = ''; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + $this->assertEquals($expected, anchor($uri, $title, $attributes, $config)); + } + + public function anchorExamplePatterns() + { + return [ + 'egpage01' => ['My News', 'news/local/123', 'My News', 'title="News title"'], + 'egpage02' => ['My News', 'news/local/123', 'My News', ['title' => 'The best news!']], + 'egpage03' => ['Click here', '', 'Click here'], + 'egpage04' => ['Click here', '/', 'Click here'], + ]; + } + + /** + * @dataProvider anchorExamplePatterns + */ + public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + $this->assertEquals($expected, anchor($uri, $title, $attributes, $config)); + } + + //-------------------------------------------------------------------- + // Test anchor_popup + + public function anchorPopupPatterns() + { + return [ + 'normal01' => ['http://example.com/index.php', ''], + 'normal02' => ['Bananas', '/', 'Bananas'], + 'normal07' => ['http://example.com/index.php', '/'], + 'normal08' => ['Click Me!', + 'news/local/123', 'Click Me!', [ + 'width' => 800, + 'height' => 600, + 'scrollbars' => 'yes', + 'status' => 'yes', + 'resizable' => 'yes', + 'screenx' => 0, + 'screeny' => 0, + 'window_name' => '_blank' + ]], + 'normal09' => [ + 'Click Me!', + 'news/local/123', + 'Click Me!', + []], + ]; + } + + /** + * @dataProvider anchorPopupPatterns + */ + public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false) + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + $this->assertEquals($expected, anchor_popup($uri, $title, $attributes, $config)); + } + + //-------------------------------------------------------------------- + // Test mailto + + public function mailtoPatterns() + { + return [ + 'page01' => ['Click Here to Contact Me', 'me@my-site.com', 'Click Here to Contact Me'], + 'page02' => ['Contact Me', 'me@my-site.com', 'Contact Me', ['title' => 'Mail me']], + 'page03' => ['me@my-site.com', 'me@my-site.com'], + ]; + } + + /** + * @dataProvider mailtoPatterns + */ + public function testMailto($expected = '', $email, $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals($expected, mailto($email, $title, $attributes)); + } + + //-------------------------------------------------------------------- + // Test safe_mailto + + public function safeMailtoPatterns() + { + return [ + 'page01' => ["", + 'me@my-site.com', 'Click Here to Contact Me'], + 'page02' => ["", + 'me@my-site.com', 'Contact Me', ['title' => 'Mail me']], + 'page03' => ["", + 'me@my-site.com'], + ]; + } + + /** + * @dataProvider safeMailtoPatterns + */ + public function testSafeMailto($expected = '', $email, $title = '', $attributes = '') + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/'; + + $config = new App(); + $config->baseURL = 'http://example.com'; + $config->indexPage = 'index.php'; + $request = Services::request($config); + $request->uri = new URI('http://example.com/'); + + Services::injectMock('request', $request); + + $this->assertEquals($expected, safe_mailto($email, $title, $attributes)); + } + + //-------------------------------------------------------------------- + // Test auto_link + + public function autolinkUrls() + { + return [ + 'test01' => ['www.codeigniter.com test', + 'www.codeigniter.com test'], + 'test02' => ['This is my noreply@codeigniter.com test', + 'This is my noreply@codeigniter.com test'], + 'test03' => ['
    www.google.com', + '
    www.google.com'], + 'test04' => ['Download CodeIgniter at www.codeigniter.com. Period test.', + 'Download CodeIgniter at www.codeigniter.com. Period test.'], + 'test05' => ['Download CodeIgniter at www.codeigniter.com, comma test', + 'Download CodeIgniter at www.codeigniter.com, comma test'], + 'test06' => ['This one: ://codeigniter.com must not break this one: http://codeigniter.com', + 'This one: ://codeigniter.com must not break this one: http://codeigniter.com'], + 'test07' => ['Visit example.com or email foo@bar.com', + 'Visit example.com or email foo@bar.com'], + 'test08' => ['Visit www.example.com or email foo@bar.com', + 'Visit www.example.com or email foo@bar.com'], + ]; + } + + /** + * @dataProvider autolinkUrls + */ + public function testAutoLinkUrl($in, $out) + { + $this->assertEquals($out, auto_link($in, 'url')); + } + + public function autolinkEmails() + { + return [ + 'test01' => ['www.codeigniter.com test', + 'www.codeigniter.com test'], + 'test02' => ['This is my noreply@codeigniter.com test', + "This is my test"], + 'test03' => ['
    www.google.com', + '
    www.google.com'], + 'test04' => ['Download CodeIgniter at www.codeigniter.com. Period test.', + 'Download CodeIgniter at www.codeigniter.com. Period test.'], + 'test05' => ['Download CodeIgniter at www.codeigniter.com, comma test', + 'Download CodeIgniter at www.codeigniter.com, comma test'], + 'test06' => ['This one: ://codeigniter.com must not break this one: http://codeigniter.com', + 'This one: ://codeigniter.com must not break this one: http://codeigniter.com'], + 'test07' => ['Visit example.com or email foo@bar.com', + "Visit example.com or email "], + 'test08' => ['Visit www.example.com or email foo@bar.com', + "Visit www.example.com or email "], + ]; + } + + /** + * @dataProvider autolinkEmails + */ + public function testAutoLinkEmail($in, $out) + { + $this->assertEquals($out, auto_link($in, 'email')); + } + + public function autolinkBoth() + { + return [ + 'test01' => ['www.codeigniter.com test', + 'www.codeigniter.com test'], + 'test02' => ['This is my noreply@codeigniter.com test', + "This is my test"], + 'test03' => ['
    www.google.com', + '
    www.google.com'], + 'test04' => ['Download CodeIgniter at www.codeigniter.com. Period test.', + 'Download CodeIgniter at www.codeigniter.com. Period test.'], + 'test05' => ['Download CodeIgniter at www.codeigniter.com, comma test', + 'Download CodeIgniter at www.codeigniter.com, comma test'], + 'test06' => ['This one: ://codeigniter.com must not break this one: http://codeigniter.com', + 'This one: ://codeigniter.com must not break this one: http://codeigniter.com'], + 'test07' => ['Visit example.com or email foo@bar.com', + "Visit example.com or email "], + 'test08' => ['Visit www.example.com or email foo@bar.com', + "Visit www.example.com or email "], + ]; + } + + /** + * @dataProvider autolinkBoth + */ + public function testAutolinkBoth($in, $out) + { + $this->assertEquals($out, auto_link($in)); + } + + public function autolinkPopup() + { + return [ + 'test01' => ['www.codeigniter.com test', + 'www.codeigniter.com test'], + 'test02' => ['This is my noreply@codeigniter.com test', + 'This is my noreply@codeigniter.com test'], + 'test03' => ['
    www.google.com', + '
    www.google.com'], + 'test04' => ['Download CodeIgniter at www.codeigniter.com. Period test.', + 'Download CodeIgniter at www.codeigniter.com. Period test.'], + 'test05' => ['Download CodeIgniter at www.codeigniter.com, comma test', + 'Download CodeIgniter at www.codeigniter.com, comma test'], + 'test06' => ['This one: ://codeigniter.com must not break this one: http://codeigniter.com', + 'This one: ://codeigniter.com must not break this one: http://codeigniter.com'], + 'test07' => ['Visit example.com or email foo@bar.com', + 'Visit example.com or email foo@bar.com'], + 'test08' => ['Visit www.example.com or email foo@bar.com', + 'Visit www.example.com or email foo@bar.com'], + ]; + } + + /** + * @dataProvider autolinkPopup + */ + public function testAutoLinkPopup($in, $out) + { + $this->assertEquals($out, auto_link($in, 'url', true)); + } + + //-------------------------------------------------------------------- + // Test prep_url + + public function testPrepUrl() + { + $this->assertEquals('http://codeigniter.com', prep_url('codeigniter.com')); + $this->assertEquals('http://www.codeigniter.com', prep_url('www.codeigniter.com')); + $this->assertEquals('', prep_url()); + $this->assertEquals('http://www.codeigniter.com', prep_url('http://www.codeigniter.com')); + } + + //-------------------------------------------------------------------- + // Test url_title + + public function testUrlTitle() + { + $words = [ + 'foo bar /' => 'foo-bar', + '\ testing 12' => 'testing-12' + ]; + + foreach ($words as $in => $out) + { + $this->assertEquals($out, url_title($in, '-', TRUE)); + } + } + + public function testUrlTitleExtraDashes() + { + $words = [ + '_foo bar_' => 'foo_bar', + '_What\'s wrong with CSS?_' => 'Whats_wrong_with_CSS' + ]; + + foreach ($words as $in => $out) + { + $this->assertEquals($out, url_title($in, '_')); + } + } + +} diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php new file mode 100644 index 000000000000..df134c2da04e --- /dev/null +++ b/tests/system/Honeypot/HoneypotTest.php @@ -0,0 +1,44 @@ +request = Services::request(); + $this->response = Services::response(); + $config = new \Config\Honeypot(); + $this->honeypot = new Honeypot($config); + + } + + public function testAttachHoneypot() + { + + $this->response->setBody('
    '); + $this->honeypot->attachHoneypot($this->response); + $this->assertContains('honeypot', $this->response->getBody()); + $this->response->setBody('
    '); + $this->assertNotContains('honeypot', $this->response->getBody()); + } + + public function testHasHoneypot() + { + + $_REQUEST['honeypot'] = 'hey'; + $this->assertEquals(true, $this->honeypot->hasContent($this->request)); + $_POST['honeypot'] = 'hey'; + $this->assertEquals(true, $this->honeypot->hasContent($this->request)); + $_GET['honeypot'] = 'hey'; + $this->assertEquals(true, $this->honeypot->hasContent($this->request)); + } +} \ No newline at end of file diff --git a/tests/system/Hooks/HooksTest.php b/tests/system/Hooks/HooksTest.php deleted file mode 100644 index 2c66001a11b8..000000000000 --- a/tests/system/Hooks/HooksTest.php +++ /dev/null @@ -1,223 +0,0 @@ -assertEquals([$callback2, $callback1], Hooks::listeners('foo')); - } - - //-------------------------------------------------------------------- - - public function testHandleEvent() - { - $result = null; - - Hooks::on('foo', function($arg) use(&$result) { - $result = $arg; - }); - - $this->assertTrue(Hooks::trigger('foo', 'bar') ); - - $this->assertEquals('bar', $result); - } - - //-------------------------------------------------------------------- - - public function testCancelEvent() - { - $result = 0; - - // This should cancel the flow of events, and leave - // $result = 1. - Hooks::on('foo', function($arg) use (&$result) { - $result = 1; - return false; - }); - Hooks::on('foo', function($arg) use (&$result) { - $result = 2; - }); - - $this->assertFalse(Hooks::trigger('foo', 'bar')); - $this->assertEquals(1, $result); - } - - //-------------------------------------------------------------------- - - public function testPriority() - { - $result = 0; - - Hooks::on('foo', function() use (&$result) { - $result = 1; - return false; - }, HOOKS_PRIORITY_NORMAL); - // Since this has a higher priority, it will - // run first. - Hooks::on('foo', function() use (&$result) { - $result = 2; - return false; - }, HOOKS_PRIORITY_HIGH); - - $this->assertFalse(Hooks::trigger('foo', 'bar')); - $this->assertEquals(2, $result); - } - - //-------------------------------------------------------------------- - - public function testPriorityWithMultiple() - { - $result = []; - - Hooks::on('foo', function() use (&$result) { - $result[] = 'a'; - }, HOOKS_PRIORITY_NORMAL); - - Hooks::on('foo', function() use (&$result) { - $result[] = 'b'; - }, HOOKS_PRIORITY_LOW); - - Hooks::on('foo', function() use (&$result) { - $result[] = 'c'; - }, HOOKS_PRIORITY_HIGH); - - Hooks::on('foo', function() use (&$result) { - $result[] = 'd'; - }, 75); - - Hooks::trigger('foo'); - $this->assertEquals(['c', 'd', 'a', 'b'], $result); - } - - //-------------------------------------------------------------------- - - public function testRemoveListener() - { - $result = false; - - $callback = function() use (&$result) - { - $result = true; - }; - - Hooks::on('foo', $callback); - - Hooks::trigger('foo'); - $this->assertTrue($result); - - $result = false; - $this->assertTrue( Hooks::removeListener('foo', $callback) ); - - Hooks::trigger('foo'); - $this->assertFalse($result); - } - - //-------------------------------------------------------------------- - - public function testRemoveListenerTwice() - { - $result = false; - - $callback = function() use (&$result) - { - $result = true; - }; - - Hooks::on('foo', $callback); - - Hooks::trigger('foo'); - $this->assertTrue($result); - - $result = false; - $this->assertTrue( Hooks::removeListener('foo', $callback) ); - $this->assertFalse( Hooks::removeListener('foo', $callback) ); - - Hooks::trigger('foo'); - $this->assertFalse($result); - } - - //-------------------------------------------------------------------- - - public function testRemoveUnknownListener() - { - $result = false; - - $callback = function() use (&$result) - { - $result = true; - }; - - Hooks::on('foo', $callback); - - Hooks::trigger('foo'); - $this->assertTrue($result); - - $result = false; - $this->assertFalse( Hooks::removeListener('bar', $callback) ); - - Hooks::trigger('foo'); - $this->assertTrue($result); - } - - //-------------------------------------------------------------------- - - public function testRemoveAllListenersWithSingleEvent() - { - $result = false; - - $callback = function() use (&$result) - { - $result = true; - }; - - Hooks::on('foo', $callback); - - Hooks::removeAllListeners('foo'); - - $listeners = Hooks::listeners('foo'); - - $this->assertEquals([], $listeners); - } - - //-------------------------------------------------------------------- - - - public function testRemoveAllListenersWithMultipleEvents() - { - $result = false; - - $callback = function() use (&$result) - { - $result = true; - }; - - Hooks::on('foo', $callback); - Hooks::on('bar', $callback); - - Hooks::removeAllListeners(); - - $this->assertEquals([], Hooks::listeners('foo')); - $this->assertEquals([], Hooks::listeners('bar')); - } - - //-------------------------------------------------------------------- - -} diff --git a/tests/system/I18n/TimeDifferenceTest.php b/tests/system/I18n/TimeDifferenceTest.php new file mode 100644 index 000000000000..d66aff1d018c --- /dev/null +++ b/tests/system/I18n/TimeDifferenceTest.php @@ -0,0 +1,183 @@ +getTimestamp() - $test->getTimestamp(); + + $obj = $current->difference($test); + + $this->assertEquals(-7, $obj->getYears()); + $this->assertEquals(-84, $obj->getMonths()); + $this->assertEquals(-365, $obj->getWeeks()); + $this->assertEquals(-2557, $obj->getDays()); + $this->assertEquals(-61368, $obj->getHours()); + $this->assertEquals(-3682080, $obj->getMinutes()); + $this->assertEquals(-220924800, $obj->getSeconds()); + + $this->assertEquals($diff / YEAR, $obj->getYears(true)); + $this->assertEquals($diff / MONTH, $obj->getMonths(true)); + $this->assertEquals($diff / WEEK, $obj->getWeeks(true)); + $this->assertEquals($diff / DAY, $obj->getDays(true)); + $this->assertEquals($diff / HOUR, $obj->getHours(true)); + $this->assertEquals($diff / MINUTE, $obj->getMinutes(true)); + $this->assertEquals($diff / SECOND, $obj->getSeconds(true)); + } + + public function testHumanizeYearsSingle() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + + $diff = $current->difference('March 9, 2016 12:00:00', 'America/Chicago'); + + $this->assertEquals('1 year ago', $diff->humanize('en')); + } + + public function testHumanizeYearsPlural() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 9, 2014 12:00:00', 'America/Chicago'); + + $this->assertEquals('3 years ago', $diff->humanize('en')); + } + + public function testHumanizeYearsForward() + { + $current = Time::parse('January 1, 2017', 'America/Chicago'); + $diff = $current->difference('January 1, 2018 12:00:00', 'America/Chicago'); + + $this->assertEquals('in 1 year', $diff->humanize('en')); + } + + public function testHumanizeMonthsSingle() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('February 9, 2017', 'America/Chicago'); + + $this->assertEquals('1 month ago', $diff->humanize('en')); + } + + public function testHumanizeMonthsPlural() + { + $current = Time::parse('March 1, 2017', 'America/Chicago'); + $diff = $current->difference('January 1, 2017', 'America/Chicago'); + + $this->assertEquals('2 months ago', $diff->humanize('en')); + } + + public function testHumanizeMonthsForward() + { + $current = Time::parse('March 1, 2017', 'America/Chicago'); + $diff = $current->difference('May 1, 2017', 'America/Chicago'); + + $this->assertEquals('in 1 month', $diff->humanize('en')); + } + + public function testHumanizeDaysSingle() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 9, 2017', 'America/Chicago'); + + $this->assertEquals('1 day ago', $diff->humanize('en')); + } + + public function testHumanizeDaysPlural() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 8, 2017', 'America/Chicago'); + + $this->assertEquals('2 days ago', $diff->humanize('en')); + } + + public function testHumanizeDaysForward() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 11, 2017', 'America/Chicago'); + + $this->assertEquals('in 1 day', $diff->humanize('en')); + } + + public function testHumanizeHoursSingle() + { + $current = Time::parse('March 10, 2017 12:00', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 11:00', 'America/Chicago'); + + $this->assertEquals('1 hour ago', $diff->humanize('en')); + } + + public function testHumanizeHoursPlural() + { + $current = Time::parse('March 10, 2017 12:00', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 10:00', 'America/Chicago'); + + $this->assertEquals('2 hours ago', $diff->humanize('en')); + } + + public function testHumanizeHoursForward() + { + $current = Time::parse('March 10, 2017 12:00', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 13:00', 'America/Chicago'); + + $this->assertEquals('in 1 hour', $diff->humanize('en')); + } + + public function testHumanizeMinutesSingle() + { + $current = Time::parse('March 10, 2017 12:30', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 12:29', 'America/Chicago'); + + $this->assertEquals('1 minute ago', $diff->humanize('en')); + } + + public function testHumanizeMinutesPlural() + { + $current = Time::parse('March 10, 2017 12:30', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 12:28', 'America/Chicago'); + + $this->assertEquals('2 minutes ago', $diff->humanize('en')); + } + + public function testHumanizeMinutesForward() + { + $current = Time::parse('March 10, 2017 12:30', 'America/Chicago'); + $diff = $current->difference('March 10, 2017 12:31', 'America/Chicago'); + + $this->assertEquals('in 1 minute', $diff->humanize('en')); + } + + public function testHumanizeWeeksSingle() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 2, 2017', 'America/Chicago'); + + $this->assertEquals('1 week ago', $diff->humanize('en')); + } + + public function testHumanizeWeeksPlural() + { + $current = Time::parse('March 30, 2017', 'America/Chicago'); + $diff = $current->difference('March 15, 2017', 'America/Chicago'); + + $this->assertEquals('2 weeks ago', $diff->humanize('en')); + } + + public function testHumanizeWeeksForward() + { + $current = Time::parse('March 10, 2017', 'America/Chicago'); + $diff = $current->difference('March 18, 2017', 'America/Chicago'); + + $this->assertEquals('in 1 week', $diff->humanize('en')); + } +} diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php new file mode 100644 index 000000000000..b243577c1c1d --- /dev/null +++ b/tests/system/I18n/TimeTest.php @@ -0,0 +1,945 @@ +assertEquals($formatter->format(strtotime('now')), (string)$time); + } + + public function testTimeWithTimezone() + { + $time = new Time('now', 'Europe/London'); + + $formatter = new IntlDateFormatter( + 'en_US', + IntlDateFormatter::SHORT, + IntlDateFormatter::SHORT, + 'Europe/London', // Default for CodeIgniter + IntlDateFormatter::GREGORIAN, + 'yyyy-MM-dd HH:mm:ss' + ); + + $this->assertEquals($formatter->format(strtotime('now')), (string)$time); + } + + public function testTimeWithTimezoneAndLocale() + { + $time = new Time('now', 'Europe/London', 'fr_FR'); + + $formatter = new IntlDateFormatter( + 'fr_FR', + IntlDateFormatter::SHORT, + IntlDateFormatter::SHORT, + 'Europe/London', // Default for CodeIgniter + IntlDateFormatter::GREGORIAN, + 'yyyy-MM-dd HH:mm:ss' + ); + + $this->assertEquals($formatter->format(strtotime('now')), (string)$time); + } + + public function testTimeWithDateTimeZone() + { + $time = new Time('now', new \DateTimeZone('Europe/London'), 'fr_FR'); + + $formatter = new IntlDateFormatter( + 'fr_FR', + IntlDateFormatter::SHORT, + IntlDateFormatter::SHORT, + 'Europe/London', + IntlDateFormatter::GREGORIAN, + 'yyyy-MM-dd HH:mm:ss' + ); + + $this->assertEquals($formatter->format(strtotime('now')), (string)$time); + } + + public function testToDateTime() + { + $time = new Time(); + + $obj = $time->toDateTime(); + + $this->assertInstanceOf(\DateTime::class, $obj); + } + + public function testNow() + { + $time = Time::now(); + $time1 = new \DateTime(); + + $this->assertInstanceOf(Time::class, $time); + $this->assertEquals($time->getTimestamp(), $time1->getTimestamp()); + } + + public function testParse() + { + $time = Time::parse('next Tuesday', 'America/Chicago'); + $time1 = new \DateTime('now', new \DateTimeZone('America/Chicago')); + $time1->modify('next Tuesday'); + + $this->assertEquals($time->getTimestamp(), $time1->getTimestamp()); + } + + public function testToDateTimeString() + { + $time = Time::parse('2017-01-12 00:00', 'America/Chicago'); + + $this->assertEquals('2017-01-12 00:00:00', (string)$time); + $this->assertEquals('2017-01-12 00:00:00', $time->toDateTimeString()); + } + + public function testToDateTimeStringWithTimeZone() + { + $time = Time::parse('2017-01-12 00:00', 'Europe/London'); + + $expects = new \DateTime('2017-01-12', new \DateTimeZone('Europe/London')); + + $this->assertEquals($expects->format('Y-m-d H:i:s'), $time->toDateTimeString()); + } + + public function testToday() + { + $time = Time::today(); + + $this->assertEquals(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + } + + public function testTodayLocalized() + { + $time = Time::today('Europe/London'); + + $this->assertEquals(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + } + + public function testYesterday() + { + $time = Time::yesterday(); + + $this->assertEquals(date('Y-m-d 00:00:00', strtotime('-1 day')), $time->toDateTimeString()); + } + + public function testTomorrow() + { + $time = Time::tomorrow(); + + $this->assertEquals(date('Y-m-d 00:00:00', strtotime('+1 day')), $time->toDateTimeString()); + } + + public function testCreateFromDate() + { + $time = Time::createFromDate(2017, 03, 05, 'America/Chicago'); + + $this->assertEquals(date('Y-m-d 00:00:00', strtotime('2017-03-05 00:00:00')), $time->toDateTimeString()); + } + + public function testCreateFromDateLocalized() + { + $time = Time::createFromDate(2017, 03, 05, 'Europe/London'); + + $this->assertEquals(date('Y-m-d 00:00:00', strtotime('2017-03-05 00:00:00')), $time->toDateTimeString()); + } + + public function testCreateFromTime() + { + $time = Time::createFromTime(10, 03, 05, 'America/Chicago'); + + $this->assertEquals(date('Y-m-d 10:03:05'), $time->toDateTimeString()); + } + + public function testCreateFromTimeEvening() + { + $time = Time::createFromTime(20, 03, 05, 'America/Chicago'); + + $this->assertEquals(date('Y-m-d 20:03:05'), $time->toDateTimeString()); + } + + public function testCreateFromTimeLocalized() + { + $time = Time::createFromTime(10, 03, 05, 'Europe/London'); + + $this->assertEquals(date('Y-m-d 10:03:05'), $time->toDateTimeString()); + } + + public function testCreateFromFormat() + { + $now = new \DateTime('now'); + + 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()); + Time::setTestNow(); + } + + public function testCreateFromFormatWithTimezoneString() + { + $time = Time::createFromFormat('F j, Y', 'January 15, 2017', 'Europe/London'); + + $this->assertEquals(date('2017-01-15 H:i:s'), $time->toDateTimeString()); + } + + public function testCreateFromFormatWithTimezoneObject() + { + $tz = new \DateTimeZone('Europe/London'); + + $time = Time::createFromFormat('F j, Y', 'January 15, 2017', $tz); + + $this->assertEquals(date('2017-01-15 H:i:s'), $time->toDateTimeString()); + } + + public function testCreateFromTimestamp() + { + $time = Time::createFromTimestamp(strtotime('2017-03-18 midnight')); + + $this->assertEquals(date('2017-03-18 00:00:00'), $time->toDateTimeString()); + } + + public function testTestNow() + { + $this->assertFalse(Time::hasTestNow()); + $this->assertEquals(date('Y-m-d H:i:s', time()), Time::now()->toDateTimeString()); + + $t = new Time('2000-01-02'); + Time::setTestNow($t); + + $this->assertTrue(Time::hasTestNow()); + $this->assertEquals('2000-01-02 00:00:00', Time::now()->toDateTimeString()); + + Time::setTestNow(); + $this->assertEquals(date('Y-m-d H:i:s', time()), Time::now()->toDateTimeString()); + } + + //-------------------------------------------------------------------- + + public function testGetYear() + { + $time = Time::parse('January 1, 2016'); + + $this->assertEquals(2016, $time->year); + } + + public function testGetMonth() + { + $time = Time::parse('August 1, 2016'); + + $this->assertEquals(8, $time->month); + } + + public function testGetDay() + { + $time = Time::parse('August 12, 2016'); + + $this->assertEquals(12, $time->day); + } + + public function testGetHour() + { + $time = Time::parse('August 12, 2016 4:15pm'); + + $this->assertEquals(16, $time->hour); + } + + public function testGetMinute() + { + $time = Time::parse('August 12, 2016 4:15pm'); + + $this->assertEquals(15, $time->minute); + } + + public function testGetSecond() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + + $this->assertEquals(23, $time->second); + } + + public function testGetDayOfWeek() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + + $this->assertEquals(6, $time->dayOfWeek); + } + + public function testGetDayOfYear() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + + $this->assertEquals(225, $time->dayOfYear); + } + + public function testGetWeekOfMonth() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + + $this->assertEquals(2, $time->weekOfMonth); + } + + public function testGetWeekOfYear() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + + $this->assertEquals(33, $time->weekOfYear); + } + + public function testGetTimestamp() + { + $time = Time::parse('August 12, 2016 4:15:23pm'); + $expected = strtotime('August 12, 2016 4:15:23pm'); + + $this->assertEquals($expected, $time->timestamp); + } + + public function testGetAge() + { + $time = Time::parse('5 years ago'); + + $this->assertEquals(5, $time->age); + } + + public function testGetQuarter() + { + $time = Time::parse('April 15, 2015'); + + $this->assertEquals(2, $time->quarter); + } + + public function testGetDST() + { + $this->assertFalse(Time::createFromDate(2012, 1, 1)->dst); + $this->assertTrue(Time::createFromDate(2012, 9, 1)->dst); + } + + public function testGetLocal() + { + $this->assertTrue(Time::now()->local); + $this->assertFalse(Time::now('Europe/London')->local); + } + + public function testGetUtc() + { + $this->assertFalse(Time::now('America/Chicago')->utc); + $this->assertTrue(Time::now('UTC')->utc); + } + + public function testGetTimezone() + { + $instance = Time::now()->getTimezone(); + + $this->assertInstanceOf(\DateTimeZone::class, $instance); + } + + public function testGetTimezonename() + { + $this->assertEquals('America/Chicago', Time::now('America/Chicago')->getTimezoneName()); + $this->assertEquals('Europe/London', Time::now('Europe/London')->timezoneName); + } + + public function testSetYear() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setYear(2015); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2015-05-10 00:00:00', $time2->toDateTimeString()); + } + + public function testSetMonthNumber() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setMonth(4); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-04-10 00:00:00', $time2->toDateTimeString()); + } + + public function testSetMonthLongName() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setMonth('April'); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-04-10 00:00:00', $time2->toDateTimeString()); + } + + public function testSetMonthShortName() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setMonth('Feb'); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-02-10 00:00:00', $time2->toDateTimeString()); + } + + public function testSetDay() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setDay(15); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-05-15 00:00:00', $time2->toDateTimeString()); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetDayOverMaxInCurrentMonth() + { + $time = Time::parse('Feb 02, 2009'); + $time->setDay(29); + } + + public function testSetDayNotOverMaxInCurrentMonth() + { + $time = Time::parse('Feb 02, 2012'); + $time2 = $time->setDay(29); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2012-02-29 00:00:00', $time2->toDateTimeString()); + } + + public function testSetHour() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setHour(15); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-05-10 15:00:00', $time2->toDateTimeString()); + } + + public function testSetMinute() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setMinute(30); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-05-10 00:30:00', $time2->toDateTimeString()); + } + + public function testSetSecond() + { + $time = Time::parse('May 10, 2017'); + $time2 = $time->setSecond(20); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-05-10 00:00:20', $time2->toDateTimeString()); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetMonthTooSmall() + { + $time = Time::parse('May 10, 2017'); + $time->setMonth(-5); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetMonthTooBig() + { + $time = Time::parse('May 10, 2017'); + $time->setMonth(30); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetDayTooSmall() + { + $time = Time::parse('May 10, 2017'); + $time->setDay(-5); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetDayTooBig() + { + $time = Time::parse('May 10, 2017'); + $time->setDay(80); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetHourTooSmall() + { + $time = Time::parse('May 10, 2017'); + $time->setHour(-5); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetHourTooBig() + { + $time = Time::parse('May 10, 2017'); + $time->setHour(80); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetMinuteTooSmall() + { + $time = Time::parse('May 10, 2017'); + $time->setMinute(-5); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetMinuteTooBig() + { + $time = Time::parse('May 10, 2017'); + $time->setMinute(80); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetSecondTooSmall() + { + $time = Time::parse('May 10, 2017'); + $time->setSecond(-5); + } + + /** + * @expectedException \CodeIgniter\I18n\Exceptions\I18nException + */ + public function testSetSecondTooBig() + { + $time = Time::parse('May 10, 2017'); + $time->setSecond(80); + } + + public function testSetTimezone() + { + $time = Time::parse('May 10, 2017', 'America/Chicago'); + $time2 = $time->setTimezone('Europe/London'); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('America/Chicago', $time->getTimezoneName()); + $this->assertEquals('Europe/London', $time2->getTimezoneName()); + } + + public function testSetTimestamp() + { + $time = Time::parse('May 10, 2017', 'America/Chicago'); + $stamp = strtotime('April 1, 2017'); + $time2 = $time->setTimestamp($stamp); + + $this->assertInstanceOf(Time::class, $time2); + $this->assertNotSame($time, $time2); + $this->assertEquals('2017-04-01 00:00:00', $time2->toDateTimeString()); + } + + public function testToDateString() + { + $time = Time::parse('May 10, 2017', 'America/Chicago'); + $this->assertEquals('2017-05-10', $time->toDateString()); + } + + /** + * Unfortunately, ubuntu 14.04 (on TravisCI) fails this test and + * shows a numeric version of the month instead of the textual version. + * Not sure what the fix is just yet.... + */ +// public function testToFormattedDateString() +// { +// $time = Time::parse('February 10, 2017', 'America/Chicago'); +// $this->assertEquals('Feb 10, 2017', $time->toFormattedDateString()); +// } + + public function testToTimeString() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $this->assertEquals('13:20:33', $time->toTimeString()); + } + + //-------------------------------------------------------------------- + // Add/Subtract + //-------------------------------------------------------------------- + + public function testCanAddSeconds() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addSeconds(10); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 13:20:43', $newTime->toDateTimeString()); + } + + public function testCanAddMinutes() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addMinutes(10); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 13:30:33', $newTime->toDateTimeString()); + } + + public function testCanAddHours() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addHours(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 16:20:33', $newTime->toDateTimeString()); + } + + public function testCanAddDays() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addDays(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-13 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanAddMonths() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addMonths(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-04-10 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanAddMonthsOverYearBoundary() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addMonths(13); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2018-02-10 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanAddYears() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addYears(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2020-01-10 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractSeconds() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subSeconds(10); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 13:20:23', $newTime->toDateTimeString()); + } + + public function testCanSubtractMinutes() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subMinutes(10); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 13:10:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractHours() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subHours(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-10 10:20:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractDays() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subDays(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2017-01-07 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractMonths() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subMonths(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2016-10-10 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractYears() + { + $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subYears(3); + $this->assertEquals('2017-01-10 13:20:33', $time->toDateTimeString()); + $this->assertEquals('2014-01-10 13:20:33', $newTime->toDateTimeString()); + } + + //-------------------------------------------------------------------- + // Comparison + //-------------------------------------------------------------------- + + public function testEqualWithDifferent() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 11, 2017 03:50:00', 'Europe/London'); + + $this->assertTrue($time1->equals($time2)); + } + + public function testEqualWithSame() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertTrue($time1->equals($time2)); + } + + public function testEqualWithDateTime() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = new \DateTime('January 11, 2017 03:50:00', new \DateTimeZone('Europe/London')); + + $this->assertTrue($time1->equals($time2)); + } + + public function testEqualWithSameDateTime() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = new \DateTime('January 10, 2017 21:50:00', new \DateTimeZone('America/Chicago')); + + $this->assertTrue($time1->equals($time2)); + } + + public function testEqualWithString() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertTrue($time1->equals('January 11, 2017 03:50:00', 'Europe/London')); + } + + public function testEqualWithStringAndNotimezone() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertTrue($time1->equals('January 10, 2017 21:50:00')); + } + + public function testSameSuccess() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertTrue($time1->sameAs($time2)); + } + + public function testSameFailure() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 11, 2017 03:50:00', 'Europe/London'); + + $this->assertFalse($time1->sameAs($time2)); + } + + public function testSameSuccessAsString() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertTrue($time1->sameAs('January 10, 2017 21:50:00', 'America/Chicago')); + } + + public function testSameFailAsString() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + + $this->assertFalse($time1->sameAs('January 11, 2017 03:50:00', 'Europe/London')); + } + + public function testBefore() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 11, 2017 03:50:00', 'America/Chicago'); + + $this->assertTrue($time1->isBefore($time2)); + $this->assertFalse($time2->isBefore($time1)); + } + + public function testAfter() + { + $time1 = Time::parse('January 10, 2017 21:50:00', 'America/Chicago'); + $time2 = Time::parse('January 11, 2017 03:50:00', 'America/Chicago'); + + $this->assertFalse($time1->isAfter($time2)); + $this->assertTrue($time2->isAfter($time1)); + } + + public function testHumanizeYearsSingle() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 9, 2016 12:00:00', 'America/Chicago'); + + $this->assertEquals('1 year ago', $time->humanize()); + } + + public function testHumanizeYearsPlural() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 9, 2014 12:00:00', 'America/Chicago'); + + $this->assertEquals('3 years ago', $time->humanize()); + } + + public function testHumanizeYearsForward() + { + Time::setTestNow('January 1, 2017', 'America/Chicago'); + $time = Time::parse('January 1, 2018 12:00:00', 'America/Chicago'); + + $this->assertEquals('in 1 year', $time->humanize()); + } + + public function testHumanizeMonthsSingle() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('February 9, 2017', 'America/Chicago'); + + $this->assertEquals('1 month ago', $time->humanize()); + } + + public function testHumanizeMonthsPlural() + { + Time::setTestNow('March 1, 2017', 'America/Chicago'); + $time = Time::parse('January 1, 2017', 'America/Chicago'); + + $this->assertEquals('2 months ago', $time->humanize()); + } + + public function testHumanizeMonthsForward() + { + Time::setTestNow('March 1, 2017', 'America/Chicago'); + $time = Time::parse('April 1, 2017', 'America/Chicago'); + + $this->assertEquals('in 1 month', $time->humanize()); + } + + public function testHumanizeDaysSingle() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 8, 2017', 'America/Chicago'); + + $this->assertEquals('2 days ago', $time->humanize()); + } + + public function testHumanizeDaysPlural() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 8, 2017', 'America/Chicago'); + + $this->assertEquals('2 days ago', $time->humanize()); + } + + public function testHumanizeDaysForward() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 12, 2017', 'America/Chicago'); + + $this->assertEquals('in 2 days', $time->humanize()); + } + + public function testHumanizeDaysTomorrow() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 11, 2017', 'America/Chicago'); + + $this->assertEquals('Tomorrow', $time->humanize()); + } + + public function testHumanizeDaysYesterday() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 9, 2017', 'America/Chicago'); + + $this->assertEquals('Yesterday', $time->humanize()); + } + + public function testHumanizeHoursAsTime() + { + Time::setTestNow('March 10, 2017 12:00', 'America/Chicago'); + $time = Time::parse('March 10, 2017 14:00', 'America/Chicago'); + + $this->assertEquals('2:00 pm', $time->humanize()); + } + + public function testHumanizeMinutesSingle() + { + Time::setTestNow('March 10, 2017 12:30', 'America/Chicago'); + $time = Time::parse('March 10, 2017 12:29', 'America/Chicago'); + + $this->assertEquals('1 minute ago', $time->humanize()); + } + + public function testHumanizeMinutesPlural() + { + Time::setTestNow('March 10, 2017 12:30', 'America/Chicago'); + $time = Time::parse('March 10, 2017 12:28', 'America/Chicago'); + + $this->assertEquals('2 minutes ago', $time->humanize()); + } + + public function testHumanizeMinutesForward() + { + Time::setTestNow('March 10, 2017 12:30', 'America/Chicago'); + $time = Time::parse('March 10, 2017 12:31', 'America/Chicago'); + + $this->assertEquals('in 1 minute', $time->humanize()); + } + + public function testHumanizeWeeksSingle() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 2, 2017', 'America/Chicago'); + + $this->assertEquals('1 week ago', $time->humanize()); + } + + public function testHumanizeWeeksPlural() + { + Time::setTestNow('March 30, 2017', 'America/Chicago'); + $time = Time::parse('March 15, 2017', 'America/Chicago'); + + $this->assertEquals('2 weeks ago', $time->humanize()); + } + + public function testHumanizeWeeksForward() + { + Time::setTestNow('March 10, 2017', 'America/Chicago'); + $time = Time::parse('March 18, 2017', 'America/Chicago'); + + $this->assertEquals('in 2 weeks', $time->humanize()); + } + + +} diff --git a/tests/system/Images/BaseHandlerTest.php b/tests/system/Images/BaseHandlerTest.php new file mode 100644 index 000000000000..a58003dd937b --- /dev/null +++ b/tests/system/Images/BaseHandlerTest.php @@ -0,0 +1,96 @@ +markTestSkipped('The GD extension is not available.'); + return; + } + + // create virtual file system + $this->root = vfsStream::setup(); + // copy our support files + $this->origin = SUPPORTPATH . 'Images/'; + vfsStream::copyFromFileSystem($this->origin, $this->root); + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + // for VFS tests + $this->start = $this->root->url() . '/'; + $this->path = $this->start . 'ci-logo.png'; + } + + //-------------------------------------------------------------------- + + public function testNew() + { + $handler = Services::image('gd', null, false); + $this->assertTrue($handler instanceof Handlers\BaseHandler); + } + + public function testWithFile() + { + $path = $this->origin . 'ci-logo.png'; + $handler = Services::image('gd', null, false); + $handler->withFile($path); + + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + $this->assertEquals(155, $image->origWidth); + $this->assertEquals($path, $image->getPathname()); + } + + public function testMissingFile() + { + $this->expectException(\CodeIgniter\Files\Exceptions\FileNotFoundException::class); + $handler = Services::image('gd', null, false); + $handler->withFile($this->start . 'No_such_file.jpg'); + } + + public function testFileTypes() + { + $handler = Services::image('gd', null, false); + $handler->withFile($this->start . 'ci-logo.png'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + + $handler->withFile($this->start . 'ci-logo.jpeg'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + + $handler->withFile($this->start . 'ci-logo.gif'); + $image = $handler->getFile(); + $this->assertTrue($image instanceof Image); + } + + //-------------------------------------------------------------------- + // Something handled by our Image + public function testImageHandled() + { + $handler = Services::image('gd', null, false); + $handler->withFile($this->path); + $this->assertEquals($this->path, $handler->getPathname()); + } + +} diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php new file mode 100644 index 000000000000..20ad4b484e48 --- /dev/null +++ b/tests/system/Images/GDHandlerTest.php @@ -0,0 +1,326 @@ +markTestSkipped('The GD extension is not available.'); + return; + } + + // create virtual file system + $this->root = vfsStream::setup(); + // copy our support files + $this->origin = SUPPORTPATH . 'Images/'; + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + $this->start = $this->root->url() . '/'; + + $this->path = $this->origin . 'ci-logo.png'; + $this->handler = Services::image('gd', null, false); + } + + public function testGetVersion() + { + $version = $this->handler->getVersion(); + // make sure that the call worked + $this->assertNotFalse($version); + // we should have a numeric version, with 3 digits + $this->assertGreaterThan(100, $version); + $this->assertLessThan(999, $version); + } + + public function testImageProperties() + { + $this->handler->withFile($this->path); + $file = $this->handler->getFile(); + $props = $file->getProperties(true); + + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(155, $props['width']); + $this->assertEquals(155, $file->origWidth); + + $this->assertEquals(200, $this->handler->getHeight()); + $this->assertEquals(200, $props['height']); + $this->assertEquals(200, $file->origHeight); + + $this->assertEquals('width="155" height="200"', $props['size_str']); + } + + public function testImageTypeProperties() + { + $this->handler->withFile($this->path); + $file = $this->handler->getFile(); + $props = $file->getProperties(true); + + $this->assertEquals(IMAGETYPE_PNG, $props['image_type']); + $this->assertEquals('image/png', $props['mime_type']); + } + +//-------------------------------------------------------------------- + + public function testResizeIgnored() + { + $this->handler->withFile($this->path); + $this->handler->resize(155, 200); // 155x200 result + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testResizeAbsolute() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 456, false); // 123x456 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(456, $this->handler->getHeight()); + } + + public function testResizeAspect() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 456, true); // 123x159 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(159, $this->handler->getHeight()); + } + + public function testResizeAspectWidth() + { + $this->handler->withFile($this->path); + $this->handler->resize(123, 0, true); // 123x159 result + $this->assertEquals(123, $this->handler->getWidth()); + $this->assertEquals(159, $this->handler->getHeight()); + } + + public function testResizeAspectHeight() + { + $this->handler->withFile($this->path); + $this->handler->resize(0, 456, true); // 354x456 result + $this->assertEquals(354, $this->handler->getWidth()); + $this->assertEquals(456, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testCropTopLeft() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropMiddle() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 50, 50, false); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropMiddlePreserved() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 50, 50, true); // 78x100 result + $this->assertEquals(78, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropTopLeftPreserveAspect() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100); // 100x100 result + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testCropNothing() + { + $this->handler->withFile($this->path); + $this->handler->crop(155, 200); // 155x200 result + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testCropOutOfBounds() + { + $this->handler->withFile($this->path); + $this->handler->crop(100, 100, 100); // 55x100 result in 100x100 + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testRotate() + { + $this->handler->withFile($this->path); // 155x200 + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + + // first rotation + $this->handler->rotate(90); // 200x155 + $this->assertEquals(200, $this->handler->getWidth()); + + // check image size again after another rotation + $this->handler->rotate(180); // 200x155 + $this->assertEquals(200, $this->handler->getWidth()); + } + + public function testRotateBadAngle() + { + $this->handler->withFile($this->path); + $this->expectException(ImageException::class); + $this->handler->rotate(77); + } + +//-------------------------------------------------------------------- + + public function testFlatten() + { + $this->handler->withFile($this->path); + $this->handler->flatten(); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testFlip() + { + $this->handler->withFile($this->path); + $this->handler->flip(); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testHorizontal() + { + $this->handler->withFile($this->path); + $this->handler->flip('horizontal'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testFlipVertical() + { + $this->handler->withFile($this->path); + $this->handler->flip('vertical'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + + public function testFlipUnknown() + { + $this->handler->withFile($this->path); + $this->expectException(ImageException::class); + $this->handler->flip('bogus'); + } + +//-------------------------------------------------------------------- + public function testFit() + { + $this->handler->withFile($this->path); + $this->handler->fit(100, 100); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(100, $this->handler->getHeight()); + } + + public function testFitTaller() + { + $this->handler->withFile($this->path); + $this->handler->fit(100, 400); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(400, $this->handler->getHeight()); + } + + public function testFitAutoHeight() + { + $this->handler->withFile($this->path); + $this->handler->fit(100); + $this->assertEquals(100, $this->handler->getWidth()); + $this->assertEquals(129, $this->handler->getHeight()); + } + + public function testFitPositions() + { + $choices = ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right']; + $this->handler->withFile($this->path); + foreach ($choices as $position) + { + $this->handler->fit(100, 100, $position); + $this->assertEquals(100, $this->handler->getWidth(), 'Position ' . $position . ' failed'); + $this->assertEquals(100, $this->handler->getHeight(), 'Position ' . $position . ' failed'); + } + } + +//-------------------------------------------------------------------- + + public function testText() + { + $this->handler->withFile($this->path); + $this->handler->text('vertical', ['hAlign' => 'right', 'vAlign' => 'bottom']); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testMoreText() + { + $this->handler->withFile($this->path); + $this->handler->text('vertical', ['vAlign' => 'middle', 'withShadow' => 'sure', 'shadowOffset' => 3]); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + +//-------------------------------------------------------------------- + + public function testImageCreation() + { + foreach (['gif', 'jpeg', 'png'] as $type) + { + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->handler->text('vertical'); + $this->assertEquals(155, $this->handler->getWidth()); + $this->assertEquals(200, $this->handler->getHeight()); + } + } + +//-------------------------------------------------------------------- + + public function testImageSave() + { + foreach (['gif', 'jpeg', 'png'] as $type) + { + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->handler->getResource(); // make sure resource is loaded + $this->handler->save($this->start . 'work/ci-logo.' . $type); + $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + } + } + +} diff --git a/tests/system/Images/ImageTest.php b/tests/system/Images/ImageTest.php new file mode 100644 index 000000000000..5005718af9d6 --- /dev/null +++ b/tests/system/Images/ImageTest.php @@ -0,0 +1,84 @@ +root = vfsStream::setup(); + // copy our support files + $this->origin = '_support/Images/'; + vfsStream::copyFromFileSystem(TESTPATH . $this->origin, $this->root); + // make subfolders + $structure = ['work' => [], 'wontwork' => []]; + vfsStream::create($structure); + // with one of them read only + $wont = $this->root->getChild('wontwork')->chmod(0400); + + $this->start = $this->root->url() . '/'; + + $this->image = new Image($this->start . 'ci-logo.png'); + } + + public function testBasicPropertiesInherited() + { + $this->assertEquals('ci-logo.png', $this->image->getFilename()); + $this->assertEquals($this->start . 'ci-logo.png', $this->image->getPathname()); + $this->assertEquals($this->root->url(), $this->image->getPath()); + $this->assertEquals('ci-logo.png', $this->image->getBasename()); + } + + public function testGetProperties() + { + $expected = [ + 'width' => 155, + 'height' => 200, + 'image_type' => IMAGETYPE_PNG, + 'size_str' => 'width="155" height="200"', + 'mime_type' => "image/png", + ]; + + $this->assertEquals($expected, $this->image->getProperties(true)); + } + + public function testExtractProperties() + { + // extract properties from the image + $this->assertTrue($this->image->getProperties(false)); + + $this->assertEquals(155, $this->image->origWidth); + $this->assertEquals(200, $this->image->origHeight); + $this->assertEquals(IMAGETYPE_PNG, $this->image->imageType); + $this->assertEquals('width="155" height="200"', $this->image->sizeStr); + $this->assertEquals("image/png", $this->image->mime); + } + + public function testCopyDefaultName() + { + $targetPath = $this->start . 'work'; + $this->image->copy($targetPath); + $this->assertTrue($this->root->hasChild('work/ci-logo.png')); + } + + public function testCopyNewName() + { + $this->image->copy($this->root->url(), 'new-logo.png'); + $this->assertTrue($this->root->hasChild('new-logo.png')); + } + + public function testCopyNowhere() + { + $this->expectException(ImageException::class); + $targetPath = $this->start . 'work'; + $this->image->copy($targetPath, ''); + } + +} diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php new file mode 100644 index 000000000000..152e61bdef40 --- /dev/null +++ b/tests/system/Language/LanguageTest.php @@ -0,0 +1,154 @@ +assertEquals('something', $lang->getLine('something')); + } + + //-------------------------------------------------------------------- + + public function testGetLineReturnsLine() + { + $lang = new MockLanguage('en'); + + $lang->setData([ + 'bookSaved' => 'We kept the book free from the boogeyman', + 'booksSaved' => 'We saved some more' + ]); + + $this->assertEquals('We saved some more', $lang->getLine('books.booksSaved')); + } + + //-------------------------------------------------------------------- + + public function testGetLineArrayReturnsLineArray() + { + $lang = new MockLanguage('en'); + + $lang->setData([ + 'booksList' => [ + 'The Boogeyman', + 'We Saved' + ] + ]); + + $this->assertEquals([ + 'The Boogeyman', + 'We Saved' + ], $lang->getLine('books.booksList')); + } + + //-------------------------------------------------------------------- + + public function testGetLineFormatsMessage() + { + // No intl extension? then we can't test this - go away.... + if ( ! class_exists('\MessageFormatter')) + return; + + $lang = new MockLanguage('en'); + + $lang->setData([ + 'bookCount' => '{0, number, integer} books have been saved.' + ]); + + $this->assertEquals('45 books have been saved.', $lang->getLine('books.bookCount', [91 / 2])); + } + + //-------------------------------------------------------------------- + + public function testGetLineArrayFormatsMessages() + { + // No intl extension? Then we can't test this - go away... + if ( ! class_exists('\MessageFormatter')) + { + return; + } + + $lang = new MockLanguage('en'); + + $lang->setData([ + 'bookList' => [ + '{0, number, integer} related books.' + ] + ]); + + $this->assertEquals(['45 related books.'], $lang->getLine('books.bookList', [91 / 2])); + } + + //-------------------------------------------------------------------- + + /** + * @see https://github.com/bcit-ci/CodeIgniter4/issues/891 + */ + public function testLangAllowsOtherLocales() + { + $str1 = lang('Language.languageGetLineInvalidArgumentException', [], 'en'); + $str2 = lang('Language.languageGetLineInvalidArgumentException', [], 'ru'); + + $this->assertEquals('Get line must be a string or array of strings.', $str1); + $this->assertEquals('Language.languageGetLineInvalidArgumentException', $str2); + } + + //-------------------------------------------------------------------- + + public function testLangDoesntFormat() + { + $lang = new MockLanguage('en'); + $lang->disableIntlSupport(); + + $lang->setData([ + 'bookList' => [ + '{0, number, integer} related books.' + ] + ]); + + $this->assertEquals(['{0, number, integer} related books.'], $lang->getLine('books.bookList', [15])); + } + + //-------------------------------------------------------------------- + + public function testLanguageDuplicateKey() + { + $lang = new Language('en'); + $this->assertEquals('These are not the droids you are looking for', $lang->getLine('More.strongForce', [])); + $this->assertEquals('I have a very bad feeling about this', $lang->getLine('More.cannotMove', [])); + $this->assertEquals('Could not move file {0} to {1} ({2})', $lang->getLine('Files.cannotMove', [])); + $this->assertEquals('I have a very bad feeling about this', $lang->getLine('More.cannotMove', [])); + } + + //-------------------------------------------------------------------- + + public function testLanguageFileLoading() + { + $lang = new SecondMockLanguage('en'); + + $result = $lang->loadem('More', 'en'); + $this->assertTrue(in_array('More', $lang->loaded())); + $result = $lang->loadem('More', 'en'); + $this->assertEquals(1, count($lang->loaded())); // should only be there once + } + + //-------------------------------------------------------------------- + + public function testLanguageFileLoadingReturns() + { + $lang = new SecondMockLanguage('en'); + + $result = $lang->loadem('More', 'en', true); + $this->assertFalse(in_array('More', $lang->loaded())); + $this->assertEquals(3, count($result)); + $result = $lang->loadem('More', 'en'); + $this->assertTrue(in_array('More', $lang->loaded())); + $this->assertEquals(1, count($lang->loaded())); + } + +} diff --git a/tests/system/Log/ChromeLoggerHandlerTest.php b/tests/system/Log/ChromeLoggerHandlerTest.php new file mode 100644 index 000000000000..e48e286ec32f --- /dev/null +++ b/tests/system/Log/ChromeLoggerHandlerTest.php @@ -0,0 +1,78 @@ +handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new ChromeLoggerHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']); + $this->assertFalse($logger->canHandle('foo')); + } + + //-------------------------------------------------------------------- + + public function testHandle() + { + $config = new LoggerConfig(); + $config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new ChromeLoggerHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']); + $this->assertTrue($logger->handle("warning", "This a log test")); + } + + //-------------------------------------------------------------------- + + public function testSendLogs() + { + $config = new LoggerConfig(); + $config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new ChromeLoggerHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']); + $logger->sendLogs(); + + $response = Services::response(null, true); + + $this->assertTrue($response->hasHeader('X-ChromeLogger-Data')); + } + + //-------------------------------------------------------------------- + + public function testSetDateFormat() + { + $config = new LoggerConfig(); + $config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new ChromeLoggerHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']); + $result = $logger->setDateFormat('F j, Y'); + + $this->assertObjectHasAttribute('dateFormat', $result); + $this->assertObjectHasAttribute('dateFormat', $logger); + } + + //-------------------------------------------------------------------- + + public function testObjectMessage() + { + $config = new LoggerConfig(); + $config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new MockChromeHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']); + $data = new \stdClass(); + $data->code = 123; + $data->explanation = "That's no moon, it's a pumpkin"; + $result = $logger->setDateFormat('F j, Y'); + + $logger->handle('debug', $data); + $peek = $logger->peekaboo(); + + $this->assertEquals($data->explanation, $peek[0]['explanation']); + } + +} diff --git a/tests/system/Log/FileHandlerTest.php b/tests/system/Log/FileHandlerTest.php new file mode 100644 index 000000000000..f93c289170ac --- /dev/null +++ b/tests/system/Log/FileHandlerTest.php @@ -0,0 +1,81 @@ +root = vfsStream::setup('root'); + $this->start = $this->root->url() . '/'; + } + + public function testHandle() + { + $config = new LoggerConfig(); + $config->handlers['Tests\Support\Log\Handlers\TestHandler']['handles'] = ['critical']; + + $logger = new FileHandler($config->handlers['Tests\Support\Log\Handlers\TestHandler']); + $logger->setDateFormat("Y-m-d H:i:s:u"); + $this->assertTrue($logger->handle("warning", "This is a test log")); + } + + //-------------------------------------------------------------------- + + public function testBasicHandle() + { + $config = new LoggerConfig(); + $config->path = $this->start . 'charlie/'; + $config->handlers['Tests\Support\Log\Handlers\TestHandler']['handles'] = ['critical']; + $logger = new MockFileHandler($config->handlers['Tests\Support\Log\Handlers\TestHandler']); + $logger->setDateFormat("Y-m-d H:i:s:u"); + $this->assertTrue($logger->handle("warning", "This is a test log")); + } + + public function testHandleCreateFile() + { + $config = new LoggerConfig(); + $config->path = $this->start; + $logger = new MockFileHandler((array) $config); + + $logger->setDateFormat("Y-m-d H:i:s:u"); + $logger->handle("warning", "This is a test log"); + + $expected = 'log-' . date('Y-m-d') . '.php'; + $fp = fopen($config->path . $expected, 'r'); + $line = fgets($fp); + fclose($fp); + + // did the log file get created? + $expectedResult = "\n"; + $this->assertEquals($expectedResult, $line); + } + + public function testHandleDateTimeCorrectly() + { + $config = new LoggerConfig(); + $config->path = $this->start; + $logger = new MockFileHandler((array) $config); + + $logger->setDateFormat('Y-m-d'); + $expected = 'log-' . date('Y-m-d') . '.php'; + + $logger->handle('debug', 'Test message'); + + $fp = fopen($config->path . $expected, 'r'); + $line = fgets($fp); // skip opening PHP tag + $line = fgets($fp); // skip blank line + $line = fgets($fp); // and get the second line + fclose($fp); + + $expectedResult = 'DEBUG - ' . date('Y-m-d') . ' --> Test message'; + $this->assertEquals($expectedResult, substr($line, 0, strlen($expectedResult))); + } + +} diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 52f78d531ad2..fb54ac548aef 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -1,23 +1,21 @@ -handlers = null; - $this->setExpectedException('RuntimeException', 'LoggerConfig must provide at least one Handler.'); + $this->expectException(FrameworkException::class); + $this->expectExceptionMessage(lang('Core.noHandlers', ['LoggerConfig'])); $logger = new Logger($config); } @@ -28,7 +26,8 @@ public function testLogThrowsExceptionOnInvalidLevel() { $config = new LoggerConfig(); - $this->setExpectedException('InvalidArgumentException', 'foo is an invalid log level.'); + $this->expectException(LogException::class); + $this->expectExceptionMessage(lang('Log.invalidLogLevel', ['foo'])); $logger = new Logger($config); @@ -56,13 +55,13 @@ public function testLogActuallyLogs() $logger = new Logger($config); - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message'; + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message'; $logger->log('debug', 'Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -71,7 +70,7 @@ public function testLogActuallyLogs() public function testLogDoesnotLogUnhandledLevels() { $config = new LoggerConfig(); - $config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['critical']; + $config->handlers['Tests\Support\Log\Handlers\TestHandler']['handles'] = ['critical']; $logger = new Logger($config); @@ -79,7 +78,7 @@ public function testLogDoesnotLogUnhandledLevels() $logs = TestHandler::getLogs(); - $this->assertEquals(0, count($logs)); + $this->assertCount(0, $logs); } //-------------------------------------------------------------------- @@ -90,13 +89,13 @@ public function testLogInterpolatesMessage() $logger = new Logger($config); - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message bar baz'; + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message bar baz'; $logger->log('debug', 'Test message {foo} {bar}', ['foo' => 'bar', 'bar' => 'baz']); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -109,13 +108,13 @@ public function testLogInterpolatesPost() $logger = new Logger($config); $_POST = ['foo' => 'bar']; - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message $_POST: '. print_r($_POST, true); + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message $_POST: ' . print_r($_POST, true); $logger->log('debug', 'Test message {post_vars}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -128,13 +127,13 @@ public function testLogInterpolatesGet() $logger = new Logger($config); $_GET = ['bar' => 'baz']; - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message $_GET: '. print_r($_GET, true); + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message $_GET: ' . print_r($_GET, true); $logger->log('debug', 'Test message {get_vars}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -147,13 +146,13 @@ public function testLogInterpolatesSession() $logger = new Logger($config); $_SESSION = ['xxx' => 'yyy']; - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message $_SESSION: '. print_r($_SESSION, true); + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message $_SESSION: ' . print_r($_SESSION, true); $logger->log('debug', 'Test message {session_vars}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -165,13 +164,13 @@ public function testLogInterpolatesCurrentEnvironment() $logger = new Logger($config); - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message '. ENVIRONMENT; + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message ' . ENVIRONMENT; $logger->log('debug', 'Test message {env}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -185,13 +184,13 @@ public function testLogInterpolatesEnvironmentVars() $_ENV['foo'] = 'bar'; - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message bar'; + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message bar'; $logger->log('debug', 'Test message {env:foo}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -206,14 +205,38 @@ public function testLogInterpolatesFileAndLine() $_ENV['foo'] = 'bar'; // For whatever reason, this will often be the class/function instead of file and line. - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message CodeIgniter\Log\LoggerTest testLogInterpolatesFileAndLine'; + // Other times it actually returns the line number, so don't look for either + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message LoggerTest'; $logger->log('debug', 'Test message {file} {line}'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); - $this->assertEquals($expected, $logs[0]); + $this->assertCount(1, $logs); + $this->assertTrue(strpos($logs[0], $expected) === 0); + } + + //-------------------------------------------------------------------- + + public function testLogInterpolatesExceptions() + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + $expected = 'ERROR - ' . date('Y-m-d') . ' --> [ERROR] These are not the droids you are looking for'; + + try + { + throw new Exception('These are not the droids you are looking for'); + } catch (\Exception $e) + { + $logger->log('error', '[ERROR] {exception}', ['exception' => $e]); + } + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertTrue(strpos($logs[0], $expected) === 0); } //-------------------------------------------------------------------- @@ -223,13 +246,13 @@ public function testEmergencyLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'EMERGENCY - '.date('Y-m-d').' --> Test message'; + $expected = 'EMERGENCY - ' . date('Y-m-d') . ' --> Test message'; $logger->emergency('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -240,13 +263,13 @@ public function testAlertLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'ALERT - '.date('Y-m-d').' --> Test message'; + $expected = 'ALERT - ' . date('Y-m-d') . ' --> Test message'; $logger->alert('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -257,13 +280,13 @@ public function testCriticalLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'CRITICAL - '.date('Y-m-d').' --> Test message'; + $expected = 'CRITICAL - ' . date('Y-m-d') . ' --> Test message'; $logger->critical('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -274,13 +297,13 @@ public function testErrorLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'ERROR - '.date('Y-m-d').' --> Test message'; + $expected = 'ERROR - ' . date('Y-m-d') . ' --> Test message'; $logger->error('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -291,13 +314,13 @@ public function testWarningLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'WARNING - '.date('Y-m-d').' --> Test message'; + $expected = 'WARNING - ' . date('Y-m-d') . ' --> Test message'; $logger->warning('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -308,13 +331,13 @@ public function testNoticeLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'NOTICE - '.date('Y-m-d').' --> Test message'; + $expected = 'NOTICE - ' . date('Y-m-d') . ' --> Test message'; $logger->notice('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -325,13 +348,13 @@ public function testInfoLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'INFO - '.date('Y-m-d').' --> Test message'; + $expected = 'INFO - ' . date('Y-m-d') . ' --> Test message'; $logger->info('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } @@ -342,16 +365,60 @@ public function testDebugLogsCorrectly() $config = new LoggerConfig(); $logger = new Logger($config); - $expected = 'DEBUG - '.date('Y-m-d').' --> Test message'; + $expected = 'DEBUG - ' . date('Y-m-d') . ' --> Test message'; $logger->debug('Test message'); $logs = TestHandler::getLogs(); - $this->assertEquals(1, count($logs)); + $this->assertCount(1, $logs); $this->assertEquals($expected, $logs[0]); } //-------------------------------------------------------------------- + public function testLogLevels() + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + $expected = 'WARNING - ' . date('Y-m-d') . ' --> Test message'; + + $logger->log(5, 'Test message'); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertEquals($expected, $logs[0]); + } + + //-------------------------------------------------------------------- + + public function testNonStringMessage() + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + $expected = '[Tests\Support\Log\Handlers\TestHandler]'; + $logger->log(5, $config); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertContains($expected, $logs[0]); + } + + //-------------------------------------------------------------------- + + public function testFilenameCleaning() + { + $config = new LoggerConfig(); + $logger = new \Tests\Support\Log\TestLogger($config); + + $ohoh = APPPATH . 'LoggerTest'; + $expected = 'APPPATH/LoggerTest'; + + $this->assertEquals($expected, $logger->cleanup($ohoh)); + } + } diff --git a/tests/system/Pager/PagerRendererTest.php b/tests/system/Pager/PagerRendererTest.php new file mode 100644 index 000000000000..efc9c4ea40c0 --- /dev/null +++ b/tests/system/Pager/PagerRendererTest.php @@ -0,0 +1,230 @@ +uri = new URI('http://example.com/foo'); + $this->expect = 'http://example.com/foo?page='; + } + + //-------------------------------------------------------------------- + + public function testHasPreviousReturnsFalseWhenFirstIsOne() + { + $details = [ + 'uri' => $this->uri, + 'pageCount' => 5, + 'currentPage' => 1, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + + $this->assertFalse($pager->hasPrevious()); + } + + //-------------------------------------------------------------------- + + public function testHasPreviousReturnsTrueWhenFirstIsMoreThanOne() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 10, + 'currentPage' => 5, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(2); + + $this->assertTrue($pager->hasPrevious()); + $this->assertEquals('http://example.com/foo?foo=bar&page=2', $pager->getPrevious()); + } + + //-------------------------------------------------------------------- + + public function testGetPreviousWhenSurroundCountIsZero() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 50, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(0); + + $this->assertTrue($pager->hasPrevious()); + $this->assertEquals('http://example.com/foo?foo=bar&page=3', $pager->getPrevious()); + } + + //-------------------------------------------------------------------- + + public function testHasNextReturnsFalseWhenLastIsTotal() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 5, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(2); + + $this->assertFalse($pager->hasNext()); + } + + //-------------------------------------------------------------------- + + public function testHasNextReturnsTrueWhenLastIsSmallerThanTotal() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 50, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(2); + + $this->assertTrue($pager->hasNext()); + $this->assertEquals('http://example.com/foo?foo=bar&page=7', $pager->getNext()); + } + + //-------------------------------------------------------------------- + + public function testGetNextWhenSurroundCountIsZero() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 50, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(0); + + $this->assertTrue($pager->hasNext()); + $this->assertEquals('http://example.com/foo?foo=bar&page=5', $pager->getNext()); + } + + //-------------------------------------------------------------------- + + public function testLinksBasics() + { + $details = [ + 'uri' => $this->uri, + 'pageCount' => 50, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + $pager->setSurroundCount(1); + + $expected = [ + [ + 'uri' => 'http://example.com/foo?page=3', + 'title' => 3, + 'active' => false + ], + [ + 'uri' => 'http://example.com/foo?page=4', + 'title' => 4, + 'active' => true + ], + [ + 'uri' => 'http://example.com/foo?page=5', + 'title' => 5, + 'active' => false + ], + ]; + + $this->assertEquals($expected, $pager->links()); + } + + //-------------------------------------------------------------------- + + public function testGetFirstAndGetLast() + { + $uri = $this->uri; + $uri->addQuery('foo', 'bar'); + + $details = [ + 'uri' => $uri, + 'pageCount' => 50, + 'currentPage' => 4, + 'total' => 100 + ]; + + $pager = new PagerRenderer($details); + + $this->assertEquals('http://example.com/foo?foo=bar&page=1', $pager->getFirst()); + $this->assertEquals('http://example.com/foo?foo=bar&page=50', $pager->getLast()); + } + + //-------------------------------------------------------------------- + + public function testSurroundCount() + { + $uri = $this->uri; + + $details = [ + 'uri' => $uri, + 'pageCount' => 10, // 10 pages + 'currentPage' => 4, + 'total' => 100 // 100 records, so 10 per page + ]; + + $pager = new PagerRenderer($details); + + // without any surround count + $this->assertEquals(null, $pager->getPrevious()); + $this->assertEquals(null, $pager->getNext()); + + // with surropund count of 2 + $pager->setSurroundCount(2); + $this->assertEquals($this->expect . '1', $pager->getPrevious()); + $this->assertEquals($this->expect . '7', $pager->getNext()); + + // with unchanged surround count + $pager->setSurroundCount(); + $this->assertEquals($this->expect . '1', $pager->getPrevious()); + $this->assertEquals($this->expect . '7', $pager->getNext()); + + // and with huge surround count + $pager->setSurroundCount(100); + $this->assertEquals(null, $pager->getPrevious()); + $this->assertEquals(null, $pager->getNext()); + } + +} diff --git a/tests/system/Pager/PagerTest.php b/tests/system/Pager/PagerTest.php new file mode 100644 index 000000000000..4c9a505fbd5d --- /dev/null +++ b/tests/system/Pager/PagerTest.php @@ -0,0 +1,293 @@ +config = new Pager(); + $this->pager = new \CodeIgniter\Pager\Pager($this->config, Services::renderer()); + } + + public function testSetPathRemembersPath() + { + $this->pager->setPath('foo/bar'); + + $details = $this->pager->getDetails(); + + $this->assertEquals('foo/bar', $details['uri']->getPath()); + } + + public function testGetDetailsRecognizesPageQueryVar() + { + $_GET['page'] = 2; + + // Need this to create the group. + $this->pager->setPath('foo/bar'); + + $details = $this->pager->getDetails(); + + $this->assertEquals(2, $details['currentPage']); + } + + public function testGetDetailsRecognizesGroupedPageQueryVar() + { + $_GET['page_foo'] = 2; + + // Need this to create the group. + $this->pager->setPath('foo/bar', 'foo'); + + $details = $this->pager->getDetails('foo'); + + $this->assertEquals(2, $details['currentPage']); + } + + public function testGetDetailsThrowExceptionIfGroupNotFound() + { + $this->expectException(PagerException::class); + + $this->pager->getDetails('foo'); + } + + public function testDetailsHasConfiguredPerPageValue() + { + // Need this to create the group. + $this->pager->setPath('foo/bar', 'foo'); + + $details = $this->pager->getDetails('foo'); + + $this->assertEquals($this->config->perPage, $details['perPage']); + } + + public function testStoreDoesBasicCalcs() + { + $this->pager->store('foo', 3, 25, 100); + + $details = $this->pager->getDetails('foo'); + + $this->assertEquals($details['total'], 100); + $this->assertEquals($details['perPage'], 25); + $this->assertEquals($details['currentPage'], 3); + } + + public function testStoreAndHasMore() + { + $this->pager->store('foo', 3, 25, 100); + + $this->assertTrue($this->pager->hasMore('foo')); + } + + public function testStoreAndHasMoreCanBeFalse() + { + $this->pager->store('foo', 3, 25, 70); + + $this->assertFalse($this->pager->hasMore('foo')); + } + + public function testHasMoreDefaultsToFalse() + { + $this->assertFalse($this->pager->hasMore('foo')); + } + + public function testPerPageHasDefaultValue() + { + $this->assertEquals($this->config->perPage, $this->pager->getPerPage()); + } + + public function testPerPageKeepsStoredValue() + { + $this->pager->store('foo', 3, 13, 70); + + $this->assertEquals(13, $this->pager->getPerPage('foo')); + } + + public function testGetCurrentPageDefaultsToOne() + { + $this->assertEquals(1, $this->pager->getCurrentPage()); + } + + public function testGetCurrentPageRemembersStoredPage() + { + $this->pager->store('foo', 3, 13, 70); + + $this->assertEquals(3, $this->pager->getCurrentPage('foo')); + } + + public function testGetCurrentPageDetectsURI() + { + $_GET['page'] = 2; + + $this->assertEquals(2, $this->pager->getCurrentPage()); + } + + public function testGetCurrentPageDetectsGroupedURI() + { + $_GET['page_foo'] = 2; + + $this->assertEquals(2, $this->pager->getCurrentPage('foo')); + } + + public function testGetTotalPagesDefaultsToOne() + { + $this->assertEquals(1, $this->pager->getPageCount()); + } + + public function testGetTotalPagesCalcsCorrectValue() + { + $this->pager->store('foo', 3, 12, 70); + + $this->assertEquals(6, $this->pager->getPageCount('foo')); + } + + public function testGetNextURIUsesCurrentURI() + { + $_GET['page'] = 2; + + $this->pager->store('foo', 2, 12, 70); + + $expected = current_url(true); + $expected = (string)$expected->setQuery('page=3'); + + $this->assertEquals((string)$expected, $this->pager->getNextPageURI('foo')); + } + + public function testGetNextURIReturnsNullOnLastPage() + { + $this->pager->store('foo', 6, 12, 70); + + $this->assertNull($this->pager->getNextPageURI('foo')); + } + + public function testGetNextURICorrectOnFirstPage() + { + $this->pager->store('foo', 1, 12, 70); + + $expected = current_url(true); + $expected = (string)$expected->setQuery('page=2'); + + $this->assertEquals($expected, $this->pager->getNextPageURI('foo')); + } + + public function testGetPreviousURIUsesCurrentURI() + { + $_GET['page'] = 2; + + $this->pager->store('foo', 2, 12, 70); + + $expected = current_url(true); + $expected = (string)$expected->setQuery('page=1'); + + $this->assertEquals((string)$expected, $this->pager->getPreviousPageURI('foo')); + } + + public function testGetNextURIReturnsNullOnFirstPage() + { + $this->pager->store('foo', 1, 12, 70); + + $this->assertNull($this->pager->getPreviousPageURI('foo')); + } + + public function testGetNextURIWithQueryStringUsesCurrentURI() + { + $_GET = [ + 'page' => 3, + 'status' => 1, + ]; + + $expected = current_url(true); + $expected = (string)$expected->setQueryArray($_GET); + + $this->pager->store('foo', $_GET['page']-1, 12, 70); + + $this->assertEquals((string)$expected, $this->pager->getNextPageURI('foo')); + } + + public function testGetPreviousURIWithQueryStringUsesCurrentURI() + { + $_GET = [ + 'page' => 1, + 'status' => 1, + ]; + $expected = current_url(true); + $expected = (string)$expected->setQueryArray($_GET); + + $this->pager->store('foo', $_GET['page']+1, 12, 70); + + $this->assertEquals((string)$expected, $this->pager->getPreviousPageURI('foo')); + } + + public function testGetOnlyQueries() + { + $_GET = [ + 'page' => 2, + 'search' => 'foo', + 'order' => 'asc', + 'hello' => 'xxx', + 'category' => 'baz', + ]; + $onlyQueries = ['search', 'order']; + + $this->pager->store('default', $_GET['page'], 10, 100); + + $uri = current_url(true); + + $this->assertEquals( + $this->pager->only($onlyQueries) + ->getPreviousPageURI(), (string)$uri->setQuery('search=foo&order=asc&page=1') + ); + $this->assertEquals( + $this->pager->only($onlyQueries) + ->getNextPageURI(), (string)$uri->setQuery('search=foo&order=asc&page=3') + ); + $this->assertEquals( + $this->pager->only($onlyQueries) + ->getPageURI(4), (string)$uri->setQuery('search=foo&order=asc&page=4') + ); + } + + public function testBadTemplate() + { + $this->expectException(PagerException::class); + $this->pager->links('default', 'bogus'); + } + + + // the tests below are looking for specific
      elements. + // not the most rigorous, but a start :-/ + + public function testLinks() + { + $this->assertTrue(strpos($this->pager->links(), '
        ') > 0); + } + + public function testSimpleLinks() + { + $this->assertTrue(strpos($this->pager->simpleLinks(), '
          ') > 0); + } + + public function testMakeLinks() + { + $actual = $this->pager->makeLinks(4, 10, 50); + $this->assertTrue(strpos($this->pager->makeLinks(4, 10, 50), '
            ') > 0); + } + +} diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 714798d6d7a2..f82824276cfb 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1,23 +1,45 @@ APPPATH.'Config', + 'App' => APPPATH, + ]; + $config = array_merge($config, $defaults); - //-------------------------------------------------------------------- + $autoload = new \Config\Autoload(); + $autoload->psr4 = $config; + + $loader = new MockFileLocator($autoload); + $loader->setFiles($files); + + if ($moduleConfig === null) + { + $moduleConfig = new \Config\Modules(); + $moduleConfig->enabled = false; + } + + return new RouteCollection($loader, $moduleConfig); + } public function testBasicAdd() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->add('home', '\my\controller'); @@ -34,7 +56,7 @@ public function testBasicAdd() public function testAddPrefixesDefaultNamespaceWhenNoneExist() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->add('home', 'controller'); @@ -51,7 +73,7 @@ public function testAddPrefixesDefaultNamespaceWhenNoneExist() public function testAddIgnoresDefaultNamespaceWhenExists() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->add('home', 'my\controller'); @@ -70,7 +92,7 @@ public function testAddWorksWithCurrentHTTPMethods() { $_SERVER['REQUEST_METHOD'] = 'GET'; - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->match(['get'], 'home', 'controller'); @@ -85,11 +107,28 @@ public function testAddWorksWithCurrentHTTPMethods() //-------------------------------------------------------------------- + public function testAddWithLeadingSlash() + { + $routes = $this->getCollector(); + + $routes->add('/home', 'controller'); + + $expects = [ + 'home' => '\controller', + ]; + + $routes = $routes->getRoutes(); + + $this->assertEquals($expects, $routes); + } + + //-------------------------------------------------------------------- + public function testMatchIgnoresInvalidHTTPMethods() { $_SERVER['REQUEST_METHOD'] = 'GET'; - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->match(['put'], 'home', 'controller'); @@ -104,7 +143,7 @@ public function testAddWorksWithArrayOFHTTPMethods() { $_SERVER['REQUEST_METHOD'] = 'POST'; - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->add('home', 'controller', ['get', 'post']); @@ -121,7 +160,7 @@ public function testAddWorksWithArrayOFHTTPMethods() public function testAddReplacesDefaultPlaceholders() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->add('home/(:any)', 'controller'); @@ -138,7 +177,7 @@ public function testAddReplacesDefaultPlaceholders() public function testAddReplacesCustomPlaceholders() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->addPlaceholder('smiley', ':-)'); $routes->add('home/(:smiley)', 'controller'); @@ -156,7 +195,7 @@ public function testAddReplacesCustomPlaceholders() public function testAddRecognizesCustomNamespaces() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->setDefaultNamespace('\CodeIgniter'); $routes->add('home', 'controller'); @@ -174,7 +213,7 @@ public function testAddRecognizesCustomNamespaces() public function testSetDefaultControllerStoresIt() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->setDefaultController('godzilla'); $this->assertEquals('godzilla', $routes->getDefaultController()); @@ -184,7 +223,7 @@ public function testSetDefaultControllerStoresIt() public function testSetDefaultMethodStoresIt() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->setDefaultMethod('biggerBox'); $this->assertEquals('biggerBox', $routes->getDefaultMethod()); @@ -194,27 +233,27 @@ public function testSetDefaultMethodStoresIt() public function testTranslateURIDashesWorks() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->setTranslateURIDashes(true); - $this->assertEquals(true, $routes->shouldTranslateURIDashes()); + $this->assertTrue($routes->shouldTranslateURIDashes()); } //-------------------------------------------------------------------- public function testAutoRouteStoresIt() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->setAutoRoute(true); - $this->assertEquals(true, $routes->shouldAutoRoute()); + $this->assertTrue($routes->shouldAutoRoute()); } //-------------------------------------------------------------------- public function testGroupingWorks() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->group('admin', function($routes) { @@ -232,7 +271,7 @@ public function testGroupingWorks() public function testGroupGetsSanitized() { - $routes = new RouteCollection(); + $routes = $this->getCollector(); $routes->group(' + - {% include "pulldown.html" %} - + {% block extrabody %} {% endblock %}
            {# SIDE NAV, TOGGLES ON MOBILE #}
            {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} @@ -117,8 +158,13 @@
            {% include "breadcrumbs.html" %} -
            +
            +
            {% block body %}{% endblock %} +
            +
            + {% block comments %}{% endblock %} +
            {% include "footer.html" %}
            @@ -137,7 +183,8 @@ VERSION:'{{ release|e }}', COLLAPSE_INDEX:false, FILE_SUFFIX:'{{ '' if no_search_suffix else file_suffix }}', - HAS_SOURCE: {{ has_source|lower }} + HAS_SOURCE: {{ has_source|lower }}, + SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}' }; {%- for scriptfile in script_files %} diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/layout_old.html b/user_guide_src/source/_themes/sphinx_rtd_theme/layout_old.html index deb8df2a1a74..9f2d1999b6d7 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/layout_old.html +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/layout_old.html @@ -91,7 +91,8 @@

            {{ _('Navigation') }}

            VERSION: '{{ release|e }}', COLLAPSE_INDEX: false, FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}', - HAS_SOURCE: {{ has_source|lower }} + HAS_SOURCE: {{ has_source|lower }}, + SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}' }; {%- for scriptfile in script_files %} @@ -125,6 +126,9 @@

            {{ _('Navigation') }}

            {%- if favicon %} {%- endif %} + {%- if theme_canonical_url %} + + {%- endif %} {%- endif %} {%- block linktags %} {%- if hasdoc('about') %} diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/pulldown.html b/user_guide_src/source/_themes/sphinx_rtd_theme/pulldown.html deleted file mode 100644 index 7877346d8d5d..000000000000 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/pulldown.html +++ /dev/null @@ -1,17 +0,0 @@ - - diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/searchbox.html b/user_guide_src/source/_themes/sphinx_rtd_theme/searchbox.html index 35ad52c5f63c..606f5c8c9d76 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/searchbox.html +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/searchbox.html @@ -1,7 +1,7 @@ {%- if builder != 'singlehtml' %}
            - +
            diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css index 7e17fb148c63..19fd5e8a3a84 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css @@ -1,2 +1,2 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} /*# sourceMappingURL=badge_only.css.map */ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css.map b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css.map new file mode 100644 index 000000000000..431ac41f6b41 --- /dev/null +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/badge_only.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "CAyDA,SAAY,EACV,qBAAsB,EAAE,UAAW,EAqDrC,QAAS,EARP,IAAK,EAAE,AAAC,EACR,+BAAS,EAEP,MAAO,EAAE,IAAK,EACd,MAAO,EAAE,CAAE,EACb,cAAO,EACL,IAAK,EAAE,GAAI,EC1Gb,SAkBC,EAjBC,UAAW,ECFJ,UAAW,EDGlB,UAAW,EAHqC,KAAM,EAItD,SAAU,EAJsD,KAAM,EAapE,EAAG,EAAE,qCAAwB,EAC7B,EAAG,EAAE,0PAG2D,ECftE,SAAU,EACR,MAAO,EAAE,WAAY,EACrB,UAAW,EAAE,UAAW,EACxB,SAAU,EAAE,KAAM,EAClB,UAAW,EAAE,KAAM,EACnB,UAAW,EAAE,AAAC,EACd,cAAe,EAAE,MAAO,EAG1B,IAAK,EACH,MAAO,EAAE,WAAY,EACrB,cAAe,EAAE,MAAO,EAIxB,KAAG,EACD,MAAO,EAAE,WAAY,EACvB,sCAAiB,EAGf,IAAK,EAAE,MAAY,EAEvB,KAAM,EACJ,cAAe,EAAE,GAAI,EACrB,UAAW,EAAE,EAAG,EAChB,UAAW,EAAE,KAAM,EAEjB,YAAG,EACD,IAAK,EAAE,IAAI,EACb,oDAAiB,EAGf,aAAc,EAAE,OAAQ,EAG9B,cAAe,EACb,MAAO,EAAE,EAAO,EAElB,gBAAiB,EACf,MAAO,EAAE,EAAO,EAElB,oBAAqB,EACnB,MAAO,EAAE,EAAO,EAElB,sBAAuB,EACrB,MAAO,EAAE,EAAO,EAElB,kBAAmB,EACjB,MAAO,EAAE,EAAO,EAElB,oBAAqB,EACnB,MAAO,EAAE,EAAO,EAElB,oBAAqB,EACnB,MAAO,EAAE,EAAO,EAElB,sBAAuB,EACrB,MAAO,EAAE,EAAO,EAElB,qBAAsB,EACpB,MAAO,EAAE,EAAO,EAElB,uBAAwB,EACtB,MAAO,EAAE,EAAO,ECnElB,YAAa,EACX,OAAQ,EAAE,IAAK,EACf,KAAM,EAAE,AAAC,EACT,GAAI,EAAE,AAAC,EACP,IAAK,EC6E+B,IAAK,ED5EzC,IAAK,EEuC+B,MAAyB,EFtC7D,SAAU,EAAE,MAAkC,EAC9C,SAAU,EAAE,iBAAiC,EAC7C,UAAW,EEkDyB,sDAA2D,EFjD/F,MAAO,EC+E6B,EAAG,ED9EvC,cAAC,EACC,IAAK,EEkC6B,MAAK,EFjCvC,cAAe,EAAE,GAAI,EACvB,6BAAgB,EACd,MAAO,EAAE,GAAI,EACf,iCAAoB,EAClB,MAAO,EAAE,GAAqB,EAC9B,eAAgB,EAAE,MAAkC,EACpD,MAAO,EAAE,IAAK,EACd,SAAU,EAAE,IAAK,EACjB,QAAS,EAAE,EAAG,EACd,KAAM,EAAE,MAAO,EACf,IAAK,EEX6B,MAAM,EL4F1C,IAAK,EAAE,AAAC,EACR,iFAAS,EAEP,MAAO,EAAE,IAAK,EACd,MAAO,EAAE,CAAE,EACb,uCAAO,EACL,IAAK,EAAE,GAAI,EGrFX,qCAAG,EACD,IAAK,EEmB2B,MAAyB,EFlB3D,0CAAQ,EACN,IAAK,EAAE,GAAI,EACb,4CAAU,EACR,IAAK,EAAE,GAAI,EACb,iDAAiB,EACf,eAAgB,ECQgB,MAAI,EDPpC,IAAK,EEO2B,GAAM,EFNxC,wDAAwB,EACtB,eAAgB,EEsBgB,MAAO,EFrBvC,IAAK,ECzB2B,GAAI,ED0BxC,yCAA8B,EAC5B,MAAO,EAAE,IAAK,EAChB,gCAAmB,EACjB,QAAS,EAAE,EAAG,EACd,MAAO,EAAE,GAAqB,EAC9B,IAAK,EEJ6B,GAAY,EFK9C,MAAO,EAAE,GAAI,EACb,mCAAE,EACA,MAAO,EAAE,IAAK,EACd,KAAM,EAAE,EAAG,EACX,KAAM,EAAE,AAAC,EACT,KAAM,EAAE,KAAM,EACd,MAAO,EAAE,AAAC,EACV,SAAU,EAAE,gBAA6C,EAC3D,mCAAE,EACA,MAAO,EAAE,WAAY,EACrB,KAAM,EAAE,AAAC,EACT,qCAAC,EACC,MAAO,EAAE,WAAY,EACrB,MAAO,EAAE,EAAqB,EAC9B,IAAK,EEZyB,MAAyB,EFa7D,sBAAW,EACT,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,IAAK,EAAE,GAAI,EACX,GAAI,EAAE,GAAI,EACV,KAAM,EAAE,GAAI,EACZ,QAAS,ECkByB,IAAK,EDjBvC,iCAAU,EACR,IAAK,EAAE,GAAI,EACb,+BAAQ,EACN,IAAK,EAAE,GAAI,EACb,oDAA+B,EAC7B,SAAU,EAAE,IAAK,EACjB,6DAAQ,EACN,IAAK,EAAE,GAAI,EACb,+DAAU,EACR,IAAK,EAAE,GAAI,EACf,2CAAoB,EAClB,IAAK,EAAE,GAAI,EACX,KAAM,EAAE,GAAI,EACZ,UAAW,EAAE,GAAI,EACjB,MAAO,EAAE,IAAuB,EAChC,MAAO,EAAE,IAAK,EACd,SAAU,EAAE,KAAM,EGhDpB,mCAAsB,EHmDxB,YAAa,EACX,IAAK,EAAE,EAAG,EACV,MAAO,EAAE,GAAI,EACb,kBAAO,EACL,MAAO,EAAE,IAAK", +"sources": ["../../../bower_components/wyrm/sass/wyrm_core/_mixin.sass","../../../bower_components/bourbon/dist/css3/_font-face.scss","../../../sass/_theme_badge_fa.sass","../../../sass/_theme_badge.sass","../../../bower_components/wyrm/sass/wyrm_core/_wy_variables.sass","../../../sass/_theme_variables.sass","../../../bower_components/neat/app/assets/stylesheets/grid/_media.scss"], +"names": [], +"file": "badge_only.css" +} diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css index c1842a4f6fe6..2115c8467996 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css @@ -1,2105 +1,6033 @@ -* { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box } - -article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block } +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} -audio, canvas, video { display: inline-block; *display: inline; *zoom: 1 } +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { + display: block +} -audio:not([controls]) { display: none } +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1 +} -[hidden] { display: none } +audio:not([controls]) { + display: none +} -* { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box } +[hidden] { + display: none +} -html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100% } +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} -body { margin: 0 } +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100% +} -a:hover, a:active { outline: 0 } +body { + margin: 0 +} -abbr[title] { border-bottom: 1px dotted } +a:hover, a:active { + outline: 0 +} -b, strong { font-weight: bold } +abbr[title] { + border-bottom: 1px dotted +} -blockquote { margin: 0 } +b, strong { + font-weight: bold +} -dfn { font-style: italic } +blockquote { + margin: 0 +} -ins { background: #ffff99; color: #000000; text-decoration: none } +dfn { + font-style: italic +} -mark { background: #ffff00; color: #000000; font-style: italic; font-weight: bold } +ins { + background: #ff9; + color: #000; + text-decoration: none +} -pre, code, .rst-content tt, kbd, samp { font-family: monospace, serif; _font-family: "courier new", monospace; font-size: 1em } +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold +} -.rst-content { max-width: 65em; } +pre, code, .rst-content tt, .rst-content code, kbd, samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em +} -pre { white-space: pre } +pre { + white-space: pre +} -q { quotes: none } +q { + quotes: none +} -q:before, q:after { content: ""; content: none } +q:before, q:after { + content: ""; + content: none +} -small { font-size: 85% } +small { + font-size: 85% +} -sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline } +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} -sup { top: -0.5em } +sup { + top: -0.5em +} -sub { bottom: -0.25em } +sub { + bottom: -0.25em +} -ul, ol, dl { margin: 0; padding: 0; list-style: none; list-style-image: none } +ul, ol, dl { + margin: 0; + padding: 0; + list-style: none; + list-style-image: none +} -li { list-style: none } +li { + list-style: none +} -dd { margin: 0 } +dd { + margin: 0 +} -img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; max-width: 100% } +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; + max-width: 100% +} -svg:not(:root) { overflow: hidden } +svg:not(:root) { + overflow: hidden +} -figure { margin: 0 } +figure { + margin: 0 +} -form { margin: 0 } +form { + margin: 0 +} -fieldset { border: 0; margin: 0; padding: 0 } +fieldset { + border: 0; + margin: 0; + padding: 0 +} -label { cursor: pointer } +label { + cursor: pointer +} -legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal } +legend { + border: 0; + *margin-left: -7px; + padding: 0; + white-space: normal +} -button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle } +button, input, select, textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} -button, input { line-height: normal } +button, input { + line-height: normal +} -button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible } +button, input[type="button"], input[type="reset"], input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + *overflow: visible +} -button[disabled], input[disabled] { cursor: default } +button[disabled], input[disabled] { + cursor: default +} -input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px } +input[type="checkbox"], input[type="radio"] { + box-sizing: border-box; + padding: 0; + *width: 13px; + *height: 13px +} -input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box } +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} -input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none } +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none +} -button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0 } +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0 +} -textarea { overflow: auto; vertical-align: top; resize: vertical } +textarea { + overflow: auto; + vertical-align: top; + resize: vertical +} -table { border-collapse: collapse; border-spacing: 0 } +table { + border-collapse: collapse; + border-spacing: 0 +} -td { vertical-align: top } +td { + vertical-align: top +} -.chromeframe { margin: 0.2em 0; background: #cccccc; color: #000000; padding: 0.2em 0 } +.chromeframe { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0 +} -.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0 } +.ir { + display: block; + border: 0; + text-indent: -999em; + overflow: hidden; + background-color: transparent; + background-repeat: no-repeat; + text-align: left; + direction: ltr; + *line-height: 0 +} -.ir br { display: none } +.ir br { + display: none +} -.hidden { display: none !important; visibility: hidden } +.hidden { + display: none !important; + visibility: hidden +} -.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px } +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px +} -.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto } +.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto +} -.invisible { visibility: hidden } +.invisible { + visibility: hidden +} -.relative { position: relative } +.relative { + position: relative +} -big, small { font-size: 100% } +big, small { + font-size: 100% +} @media print { - html, body, section { background: none !important } + html, body, section { + background: none !important + } - * { box-shadow: none !important; text-shadow: none !important; filter: none !important; -ms-filter: none !important } + * { + box-shadow: none !important; + text-shadow: none !important; + filter: none !important; + -ms-filter: none !important + } - a, a:visited { text-decoration: underline } + a, a:visited { + text-decoration: underline + } - .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: "" } + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { + content: "" + } - pre, blockquote { page-break-inside: avoid } + pre, blockquote { + page-break-inside: avoid + } - thead { display: table-header-group } + thead { + display: table-header-group + } - tr, img { page-break-inside: avoid } + tr, img { + page-break-inside: avoid + } - img { max-width: 100% !important } + img { + max-width: 100% !important + } @page { - margin: 1.5cm 0.5cm 2.5cm + margin: 0.5cm } - p, h2, h3 { orphans: 3; widows: 3 } + p, h2, .rst-content .toctree-wrapper p.caption, h3 { + orphans: 3; + widows: 3 + } - h2, h3 { page-break-after: avoid } + h2, .rst-content .toctree-wrapper p.caption, h3 { + page-break-after: avoid + } } -.fa:before, .rst-content .admonition-title:before, .rst-content h1 .headerlink:before, .rst-content h2 .headerlink:before, .rst-content h3 .headerlink:before, .rst-content h4 .headerlink:before, .rst-content h5 .headerlink:before, .rst-content h6 .headerlink:before, .rst-content dl dt .headerlink:before, .icon:before, .wy-dropdown .caret:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before, .wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo, .btn, input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"], select, textarea, .wy-menu-vertical li.on a, .wy-menu-vertical li.current > a, .wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a, .wy-nav-top a { -webkit-font-smoothing: antialiased } +.fa:before, .wy-menu-vertical li span.toctree-expand:before, .wy-menu-vertical li.on a span.toctree-expand:before, .wy-menu-vertical li.current > a span.toctree-expand:before, .rst-content .admonition-title:before, .rst-content h1 .headerlink:before, .rst-content h2 .headerlink:before, .rst-content h3 .headerlink:before, .rst-content h4 .headerlink:before, .rst-content h5 .headerlink:before, .rst-content h6 .headerlink:before, .rst-content dl dt .headerlink:before, .rst-content p.caption .headerlink:before, .rst-content tt.download span:first-child:before, .rst-content code.download span:first-child:before, .icon:before, .wy-dropdown .caret:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before, .wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo, .btn, input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"], select, textarea, .wy-menu-vertical li.on a, .wy-menu-vertical li.current > a, .wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a, .wy-nav-top a { + -webkit-font-smoothing: antialiased +} -.clearfix { *zoom: 1 } +.clearfix { + *zoom: 1 +} -.clearfix:before, .clearfix:after { display: table; content: "" } +.clearfix:before, .clearfix:after { + display: table; + content: "" +} -.clearfix:after { clear: both } +.clearfix:after { + clear: both +} /*! - * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ @font-face { font-family: 'FontAwesome'; - src: url("../fonts/fontawesome-webfont.eot?v=4.1.0"); - src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff?v=4.1.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.1.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular") format("svg"); + src: url("../fonts/fontawesome-webfont.eot?v=4.6.3"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.6.3") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.6.3") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.6.3") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular") format("svg"); font-weight: normal; font-style: normal } -.fa, .rst-content .admonition-title, .rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink, .icon { display: inline-block; font-family: FontAwesome; font-style: normal; font-weight: normal; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale } +.fa, .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.current > a span.toctree-expand, .rst-content .admonition-title, .rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink, .rst-content p.caption .headerlink, .rst-content tt.download span:first-child, .rst-content code.download span:first-child, .icon { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} -.fa-lg { font-size: 1.33333em; line-height: 0.75em; vertical-align: -15% } +.fa-lg { + font-size: 1.33333em; + line-height: .75em; + vertical-align: -15% +} -.fa-2x { font-size: 2em } +.fa-2x { + font-size: 2em +} -.fa-3x { font-size: 3em } +.fa-3x { + font-size: 3em +} -.fa-4x { font-size: 4em } +.fa-4x { + font-size: 4em +} -.fa-5x { font-size: 5em } +.fa-5x { + font-size: 5em +} -.fa-fw { width: 1.28571em; text-align: center } +.fa-fw { + width: 1.28571em; + text-align: center +} -.fa-ul { padding-left: 0; margin-left: 2.14286em; list-style-type: none } +.fa-ul { + padding-left: 0; + margin-left: 2.14286em; + list-style-type: none +} -.fa-ul > li { position: relative } +.fa-ul > li { + position: relative +} -.fa-li { position: absolute; left: -2.14286em; width: 2.14286em; top: 0.14286em; text-align: center } +.fa-li { + position: absolute; + left: -2.14286em; + width: 2.14286em; + top: .14286em; + text-align: center +} -.fa-li.fa-lg { left: -1.85714em } +.fa-li.fa-lg { + left: -1.85714em +} -.fa-border { padding: .2em .25em .15em; border: solid 0.08em #eeeeee; border-radius: .1em } +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eee; + border-radius: .1em +} -.pull-right { float: right } +.fa-pull-left { + float: left +} -.pull-left { float: left } +.fa-pull-right { + float: right +} -.fa.pull-left, .rst-content .pull-left.admonition-title, .rst-content h1 .pull-left.headerlink, .rst-content h2 .pull-left.headerlink, .rst-content h3 .pull-left.headerlink, .rst-content h4 .pull-left.headerlink, .rst-content h5 .pull-left.headerlink, .rst-content h6 .pull-left.headerlink, .rst-content dl dt .pull-left.headerlink, .pull-left.icon { margin-right: .3em } +.fa.fa-pull-left, .wy-menu-vertical li span.fa-pull-left.toctree-expand, .wy-menu-vertical li.on a span.fa-pull-left.toctree-expand, .wy-menu-vertical li.current > a span.fa-pull-left.toctree-expand, .rst-content .fa-pull-left.admonition-title, .rst-content h1 .fa-pull-left.headerlink, .rst-content h2 .fa-pull-left.headerlink, .rst-content h3 .fa-pull-left.headerlink, .rst-content h4 .fa-pull-left.headerlink, .rst-content h5 .fa-pull-left.headerlink, .rst-content h6 .fa-pull-left.headerlink, .rst-content dl dt .fa-pull-left.headerlink, .rst-content p.caption .fa-pull-left.headerlink, .rst-content tt.download span.fa-pull-left:first-child, .rst-content code.download span.fa-pull-left:first-child, .fa-pull-left.icon { + margin-right: .3em +} -.fa.pull-right, .rst-content .pull-right.admonition-title, .rst-content h1 .pull-right.headerlink, .rst-content h2 .pull-right.headerlink, .rst-content h3 .pull-right.headerlink, .rst-content h4 .pull-right.headerlink, .rst-content h5 .pull-right.headerlink, .rst-content h6 .pull-right.headerlink, .rst-content dl dt .pull-right.headerlink, .pull-right.icon { margin-left: .3em } +.fa.fa-pull-right, .wy-menu-vertical li span.fa-pull-right.toctree-expand, .wy-menu-vertical li.on a span.fa-pull-right.toctree-expand, .wy-menu-vertical li.current > a span.fa-pull-right.toctree-expand, .rst-content .fa-pull-right.admonition-title, .rst-content h1 .fa-pull-right.headerlink, .rst-content h2 .fa-pull-right.headerlink, .rst-content h3 .fa-pull-right.headerlink, .rst-content h4 .fa-pull-right.headerlink, .rst-content h5 .fa-pull-right.headerlink, .rst-content h6 .fa-pull-right.headerlink, .rst-content dl dt .fa-pull-right.headerlink, .rst-content p.caption .fa-pull-right.headerlink, .rst-content tt.download span.fa-pull-right:first-child, .rst-content code.download span.fa-pull-right:first-child, .fa-pull-right.icon { + margin-left: .3em +} -.fa-spin { -webkit-animation: spin 2s infinite linear; -moz-animation: spin 2s infinite linear; -o-animation: spin 2s infinite linear; animation: spin 2s infinite linear } +.pull-right { + float: right +} -@-moz-keyframes spin { - 0% { -moz-transform: rotate(0deg) } - 100% { -moz-transform: rotate(359deg) } +.pull-left { + float: left } -@-webkit-keyframes spin { - 0% { -webkit-transform: rotate(0deg) } - 100% { -webkit-transform: rotate(359deg) } +.fa.pull-left, .wy-menu-vertical li span.pull-left.toctree-expand, .wy-menu-vertical li.on a span.pull-left.toctree-expand, .wy-menu-vertical li.current > a span.pull-left.toctree-expand, .rst-content .pull-left.admonition-title, .rst-content h1 .pull-left.headerlink, .rst-content h2 .pull-left.headerlink, .rst-content h3 .pull-left.headerlink, .rst-content h4 .pull-left.headerlink, .rst-content h5 .pull-left.headerlink, .rst-content h6 .pull-left.headerlink, .rst-content dl dt .pull-left.headerlink, .rst-content p.caption .pull-left.headerlink, .rst-content tt.download span.pull-left:first-child, .rst-content code.download span.pull-left:first-child, .pull-left.icon { + margin-right: .3em } -@-o-keyframes spin { - 0% { -o-transform: rotate(0deg) } - 100% { -o-transform: rotate(359deg) } +.fa.pull-right, .wy-menu-vertical li span.pull-right.toctree-expand, .wy-menu-vertical li.on a span.pull-right.toctree-expand, .wy-menu-vertical li.current > a span.pull-right.toctree-expand, .rst-content .pull-right.admonition-title, .rst-content h1 .pull-right.headerlink, .rst-content h2 .pull-right.headerlink, .rst-content h3 .pull-right.headerlink, .rst-content h4 .pull-right.headerlink, .rst-content h5 .pull-right.headerlink, .rst-content h6 .pull-right.headerlink, .rst-content dl dt .pull-right.headerlink, .rst-content p.caption .pull-right.headerlink, .rst-content tt.download span.pull-right:first-child, .rst-content code.download span.pull-right:first-child, .pull-right.icon { + margin-left: .3em } -@keyframes spin { - 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg) } - 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg) } +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear } -.fa-rotate-90 { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg) } +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8) +} -.fa-rotate-180 { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); -webkit-transform: rotate(180deg); -moz-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg) } +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} -.fa-rotate-270 { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); -webkit-transform: rotate(270deg); -moz-transform: rotate(270deg); -ms-transform: rotate(270deg); -o-transform: rotate(270deg); transform: rotate(270deg) } +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} -.fa-flip-horizontal { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0); -webkit-transform: scale(-1, 1); -moz-transform: scale(-1, 1); -ms-transform: scale(-1, 1); -o-transform: scale(-1, 1); transform: scale(-1, 1) } +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg) +} -.fa-flip-vertical { filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); -webkit-transform: scale(1, -1); -moz-transform: scale(1, -1); -ms-transform: scale(1, -1); -o-transform: scale(1, -1); transform: scale(1, -1) } +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg) +} -.fa-stack { position: relative; display: inline-block; width: 2em; height: 2em; line-height: 2em; vertical-align: middle } +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg) +} -.fa-stack-1x, .fa-stack-2x { position: absolute; left: 0; width: 100%; text-align: center } +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1) +} -.fa-stack-1x { line-height: inherit } +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1) +} -.fa-stack-2x { font-size: 2em } +:root .fa-rotate-90, :root .fa-rotate-180, :root .fa-rotate-270, :root .fa-flip-horizontal, :root .fa-flip-vertical { + filter: none +} -.fa-inverse { color: #ffffff } +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle +} -.fa-glass:before { content: "" } +.fa-stack-1x, .fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center +} -.fa-music:before { content: "" } +.fa-stack-1x { + line-height: inherit +} -.fa-search:before, .icon-search:before { content: "" } +.fa-stack-2x { + font-size: 2em +} -.fa-envelope-o:before { content: "" } +.fa-inverse { + color: #fff +} -.fa-heart:before { content: "" } +.fa-glass:before { + content: "" +} -.fa-star:before { content: "" } +.fa-music:before { + content: "" +} -.fa-star-o:before { content: "" } +.fa-search:before, .icon-search:before { + content: "" +} -.fa-user:before { content: "" } +.fa-envelope-o:before { + content: "" +} -.fa-film:before { content: "" } +.fa-heart:before { + content: "" +} -.fa-th-large:before { content: "" } +.fa-star:before { + content: "" +} -.fa-th:before { content: "" } +.fa-star-o:before { + content: "" +} -.fa-th-list:before { content: "" } +.fa-user:before { + content: "" +} -.fa-check:before { content: "" } +.fa-film:before { + content: "" +} -.fa-times:before { content: "" } +.fa-th-large:before { + content: "" +} -.fa-search-plus:before { content: "" } +.fa-th:before { + content: "" +} -.fa-search-minus:before { content: "" } +.fa-th-list:before { + content: "" +} -.fa-power-off:before { content: "" } +.fa-check:before { + content: "" +} -.fa-signal:before { content: "" } +.fa-remove:before, .fa-close:before, .fa-times:before { + content: "" +} -.fa-gear:before, .fa-cog:before { content: "" } +.fa-search-plus:before { + content: "" +} -.fa-trash-o:before { content: "" } +.fa-search-minus:before { + content: "" +} -.fa-home:before, .icon-home:before { content: "" } +.fa-power-off:before { + content: "" +} -.fa-file-o:before { content: "" } +.fa-signal:before { + content: "" +} -.fa-clock-o:before { content: "" } +.fa-gear:before, .fa-cog:before { + content: "" +} -.fa-road:before { content: "" } +.fa-trash-o:before { + content: "" +} -.fa-download:before { content: "" } +.fa-home:before, .icon-home:before { + content: "" +} -.fa-arrow-circle-o-down:before { content: "" } +.fa-file-o:before { + content: "" +} -.fa-arrow-circle-o-up:before { content: "" } +.fa-clock-o:before { + content: "" +} -.fa-inbox:before { content: "" } +.fa-road:before { + content: "" +} -.fa-play-circle-o:before { content: "" } +.fa-download:before, .rst-content tt.download span:first-child:before, .rst-content code.download span:first-child:before { + content: "" +} -.fa-rotate-right:before, .fa-repeat:before { content: "" } +.fa-arrow-circle-o-down:before { + content: "" +} -.fa-refresh:before { content: "" } +.fa-arrow-circle-o-up:before { + content: "" +} -.fa-list-alt:before { content: "" } +.fa-inbox:before { + content: "" +} -.fa-lock:before { content: "" } +.fa-play-circle-o:before { + content: "" +} -.fa-flag:before { content: "" } +.fa-rotate-right:before, .fa-repeat:before { + content: "" +} -.fa-headphones:before { content: "" } +.fa-refresh:before { + content: "" +} -.fa-volume-off:before { content: "" } +.fa-list-alt:before { + content: "" +} -.fa-volume-down:before { content: "" } +.fa-lock:before { + content: "" +} -.fa-volume-up:before { content: "" } +.fa-flag:before { + content: "" +} -.fa-qrcode:before { content: "" } +.fa-headphones:before { + content: "" +} -.fa-barcode:before { content: "" } +.fa-volume-off:before { + content: "" +} -.fa-tag:before { content: "" } +.fa-volume-down:before { + content: "" +} -.fa-tags:before { content: "" } +.fa-volume-up:before { + content: "" +} -.fa-book:before, .icon-book:before { content: "" } +.fa-qrcode:before { + content: "" +} -.fa-bookmark:before { content: "" } +.fa-barcode:before { + content: "" +} -.fa-print:before { content: "" } +.fa-tag:before { + content: "" +} -.fa-camera:before { content: "" } +.fa-tags:before { + content: "" +} -.fa-font:before { content: "" } +.fa-book:before, .icon-book:before { + content: "" +} -.fa-bold:before { content: "" } +.fa-bookmark:before { + content: "" +} -.fa-italic:before { content: "" } +.fa-print:before { + content: "" +} -.fa-text-height:before { content: "" } +.fa-camera:before { + content: "" +} -.fa-text-width:before { content: "" } +.fa-font:before { + content: "" +} -.fa-align-left:before { content: "" } +.fa-bold:before { + content: "" +} -.fa-align-center:before { content: "" } +.fa-italic:before { + content: "" +} -.fa-align-right:before { content: "" } +.fa-text-height:before { + content: "" +} -.fa-align-justify:before { content: "" } +.fa-text-width:before { + content: "" +} -.fa-list:before { content: "" } +.fa-align-left:before { + content: "" +} -.fa-dedent:before, .fa-outdent:before { content: "" } +.fa-align-center:before { + content: "" +} -.fa-indent:before { content: "" } +.fa-align-right:before { + content: "" +} -.fa-video-camera:before { content: "" } +.fa-align-justify:before { + content: "" +} -.fa-photo:before, .fa-image:before, .fa-picture-o:before { content: "" } +.fa-list:before { + content: "" +} -.fa-pencil:before { content: "" } +.fa-dedent:before, .fa-outdent:before { + content: "" +} -.fa-map-marker:before { content: "" } +.fa-indent:before { + content: "" +} -.fa-adjust:before { content: "" } +.fa-video-camera:before { + content: "" +} -.fa-tint:before { content: "" } +.fa-photo:before, .fa-image:before, .fa-picture-o:before { + content: "" +} -.fa-edit:before, .fa-pencil-square-o:before { content: "" } +.fa-pencil:before { + content: "" +} -.fa-share-square-o:before { content: "" } +.fa-map-marker:before { + content: "" +} -.fa-check-square-o:before { content: "" } +.fa-adjust:before { + content: "" +} -.fa-arrows:before { content: "" } +.fa-tint:before { + content: "" +} -.fa-step-backward:before { content: "" } +.fa-edit:before, .fa-pencil-square-o:before { + content: "" +} -.fa-fast-backward:before { content: "" } +.fa-share-square-o:before { + content: "" +} -.fa-backward:before { content: "" } +.fa-check-square-o:before { + content: "" +} -.fa-play:before { content: "" } +.fa-arrows:before { + content: "" +} -.fa-pause:before { content: "" } +.fa-step-backward:before { + content: "" +} -.fa-stop:before { content: "" } +.fa-fast-backward:before { + content: "" +} -.fa-forward:before { content: "" } +.fa-backward:before { + content: "" +} -.fa-fast-forward:before { content: "" } +.fa-play:before { + content: "" +} -.fa-step-forward:before { content: "" } +.fa-pause:before { + content: "" +} -.fa-eject:before { content: "" } +.fa-stop:before { + content: "" +} -.fa-chevron-left:before { content: "" } +.fa-forward:before { + content: "" +} -.fa-chevron-right:before { content: "" } +.fa-fast-forward:before { + content: "" +} -.fa-plus-circle:before { content: "" } +.fa-step-forward:before { + content: "" +} -.fa-minus-circle:before { content: "" } +.fa-eject:before { + content: "" +} -.fa-times-circle:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before { content: "" } +.fa-chevron-left:before { + content: "" +} -.fa-check-circle:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before { content: "" } +.fa-chevron-right:before { + content: "" +} -.fa-question-circle:before { content: "" } +.fa-plus-circle:before { + content: "" +} -.fa-info-circle:before { content: "" } +.fa-minus-circle:before { + content: "" +} -.fa-crosshairs:before { content: "" } +.fa-times-circle:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before { + content: "" +} -.fa-times-circle-o:before { content: "" } +.fa-check-circle:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before { + content: "" +} -.fa-check-circle-o:before { content: "" } +.fa-question-circle:before { + content: "" +} -.fa-ban:before { content: "" } +.fa-info-circle:before { + content: "" +} -.fa-arrow-left:before { content: "" } +.fa-crosshairs:before { + content: "" +} -.fa-arrow-right:before { content: "" } +.fa-times-circle-o:before { + content: "" +} -.fa-arrow-up:before { content: "" } +.fa-check-circle-o:before { + content: "" +} -.fa-arrow-down:before { content: "" } +.fa-ban:before { + content: "" +} -.fa-mail-forward:before, .fa-share:before { content: "" } +.fa-arrow-left:before { + content: "" +} -.fa-expand:before { content: "" } +.fa-arrow-right:before { + content: "" +} -.fa-compress:before { content: "" } +.fa-arrow-up:before { + content: "" +} -.fa-plus:before { content: "" } +.fa-arrow-down:before { + content: "" +} -.fa-minus:before { content: "" } +.fa-mail-forward:before, .fa-share:before { + content: "" +} -.fa-asterisk:before { content: "" } +.fa-expand:before { + content: "" +} -.fa-exclamation-circle:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before, .rst-content .admonition-title:before { content: "" } +.fa-compress:before { + content: "" +} -.fa-gift:before { content: "" } +.fa-plus:before { + content: "" +} -.fa-leaf:before { content: "" } +.fa-minus:before { + content: "" +} -.fa-fire:before, .icon-fire:before { content: "" } +.fa-asterisk:before { + content: "" +} -.fa-eye:before { content: "" } +.fa-exclamation-circle:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before, .rst-content .admonition-title:before { + content: "" +} -.fa-eye-slash:before { content: "" } +.fa-gift:before { + content: "" +} -.fa-warning:before, .fa-exclamation-triangle:before { content: "" } +.fa-leaf:before { + content: "" +} -.fa-plane:before { content: "" } +.fa-fire:before, .icon-fire:before { + content: "" +} -.fa-calendar:before { content: "" } +.fa-eye:before { + content: "" +} -.fa-random:before { content: "" } +.fa-eye-slash:before { + content: "" +} -.fa-comment:before { content: "" } +.fa-warning:before, .fa-exclamation-triangle:before { + content: "" +} -.fa-magnet:before { content: "" } +.fa-plane:before { + content: "" +} -.fa-chevron-up:before { content: "" } +.fa-calendar:before { + content: "" +} -.fa-chevron-down:before { content: "" } +.fa-random:before { + content: "" +} -.fa-retweet:before { content: "" } +.fa-comment:before { + content: "" +} -.fa-shopping-cart:before { content: "" } +.fa-magnet:before { + content: "" +} -.fa-folder:before { content: "" } +.fa-chevron-up:before { + content: "" +} -.fa-folder-open:before { content: "" } +.fa-chevron-down:before { + content: "" +} -.fa-arrows-v:before { content: "" } +.fa-retweet:before { + content: "" +} -.fa-arrows-h:before { content: "" } +.fa-shopping-cart:before { + content: "" +} -.fa-bar-chart-o:before { content: "" } +.fa-folder:before { + content: "" +} -.fa-twitter-square:before { content: "" } +.fa-folder-open:before { + content: "" +} -.fa-facebook-square:before { content: "" } +.fa-arrows-v:before { + content: "" +} -.fa-camera-retro:before { content: "" } +.fa-arrows-h:before { + content: "" +} -.fa-key:before { content: "" } +.fa-bar-chart-o:before, .fa-bar-chart:before { + content: "" +} -.fa-gears:before, .fa-cogs:before { content: "" } +.fa-twitter-square:before { + content: "" +} -.fa-comments:before { content: "" } +.fa-facebook-square:before { + content: "" +} -.fa-thumbs-o-up:before { content: "" } +.fa-camera-retro:before { + content: "" +} -.fa-thumbs-o-down:before { content: "" } +.fa-key:before { + content: "" +} -.fa-star-half:before { content: "" } +.fa-gears:before, .fa-cogs:before { + content: "" +} -.fa-heart-o:before { content: "" } +.fa-comments:before { + content: "" +} -.fa-sign-out:before { content: "" } +.fa-thumbs-o-up:before { + content: "" +} -.fa-linkedin-square:before { content: "" } +.fa-thumbs-o-down:before { + content: "" +} -.fa-thumb-tack:before { content: "" } +.fa-star-half:before { + content: "" +} -.fa-external-link:before { content: "" } +.fa-heart-o:before { + content: "" +} -.fa-sign-in:before { content: "" } +.fa-sign-out:before { + content: "" +} -.fa-trophy:before { content: "" } +.fa-linkedin-square:before { + content: "" +} -.fa-github-square:before { content: "" } +.fa-thumb-tack:before { + content: "" +} -.fa-upload:before { content: "" } +.fa-external-link:before { + content: "" +} -.fa-lemon-o:before { content: "" } +.fa-sign-in:before { + content: "" +} -.fa-phone:before { content: "" } +.fa-trophy:before { + content: "" +} -.fa-square-o:before { content: "" } +.fa-github-square:before { + content: "" +} -.fa-bookmark-o:before { content: "" } +.fa-upload:before { + content: "" +} -.fa-phone-square:before { content: "" } +.fa-lemon-o:before { + content: "" +} -.fa-twitter:before { content: "" } +.fa-phone:before { + content: "" +} -.fa-facebook:before { content: "" } +.fa-square-o:before { + content: "" +} -.fa-github:before, .icon-github:before { content: "" } +.fa-bookmark-o:before { + content: "" +} -.fa-unlock:before { content: "" } +.fa-phone-square:before { + content: "" +} -.fa-credit-card:before { content: "" } +.fa-twitter:before { + content: "" +} -.fa-rss:before { content: "" } +.fa-facebook-f:before, .fa-facebook:before { + content: "" +} -.fa-hdd-o:before { content: "" } +.fa-github:before, .icon-github:before { + content: "" +} -.fa-bullhorn:before { content: "" } +.fa-unlock:before { + content: "" +} -.fa-bell:before { content: "" } +.fa-credit-card:before { + content: "" +} -.fa-certificate:before { content: "" } +.fa-feed:before, .fa-rss:before { + content: "" +} -.fa-hand-o-right:before { content: "" } +.fa-hdd-o:before { + content: "" +} -.fa-hand-o-left:before { content: "" } +.fa-bullhorn:before { + content: "" +} -.fa-hand-o-up:before { content: "" } +.fa-bell:before { + content: "" +} -.fa-hand-o-down:before { content: "" } +.fa-certificate:before { + content: "" +} -.fa-arrow-circle-left:before, .icon-circle-arrow-left:before { content: "" } +.fa-hand-o-right:before { + content: "" +} -.fa-arrow-circle-right:before, .icon-circle-arrow-right:before { content: "" } +.fa-hand-o-left:before { + content: "" +} -.fa-arrow-circle-up:before { content: "" } +.fa-hand-o-up:before { + content: "" +} -.fa-arrow-circle-down:before { content: "" } +.fa-hand-o-down:before { + content: "" +} -.fa-globe:before { content: "" } +.fa-arrow-circle-left:before, .icon-circle-arrow-left:before { + content: "" +} -.fa-wrench:before { content: "" } +.fa-arrow-circle-right:before, .icon-circle-arrow-right:before { + content: "" +} -.fa-tasks:before { content: "" } +.fa-arrow-circle-up:before { + content: "" +} -.fa-filter:before { content: "" } +.fa-arrow-circle-down:before { + content: "" +} -.fa-briefcase:before { content: "" } +.fa-globe:before { + content: "" +} -.fa-arrows-alt:before { content: "" } +.fa-wrench:before { + content: "" +} -.fa-group:before, .fa-users:before { content: "" } +.fa-tasks:before { + content: "" +} -.fa-chain:before, .fa-link:before, .icon-link:before { content: "" } +.fa-filter:before { + content: "" +} -.fa-cloud:before { content: "" } +.fa-briefcase:before { + content: "" +} -.fa-flask:before { content: "" } +.fa-arrows-alt:before { + content: "" +} -.fa-cut:before, .fa-scissors:before { content: "" } +.fa-group:before, .fa-users:before { + content: "" +} -.fa-copy:before, .fa-files-o:before { content: "" } +.fa-chain:before, .fa-link:before, .icon-link:before { + content: "" +} -.fa-paperclip:before { content: "" } +.fa-cloud:before { + content: "" +} -.fa-save:before, .fa-floppy-o:before { content: "" } +.fa-flask:before { + content: "" +} -.fa-square:before { content: "" } +.fa-cut:before, .fa-scissors:before { + content: "" +} -.fa-navicon:before, .fa-reorder:before, .fa-bars:before { content: "" } +.fa-copy:before, .fa-files-o:before { + content: "" +} -.fa-list-ul:before { content: "" } +.fa-paperclip:before { + content: "" +} -.fa-list-ol:before { content: "" } +.fa-save:before, .fa-floppy-o:before { + content: "" +} -.fa-strikethrough:before { content: "" } +.fa-square:before { + content: "" +} -.fa-underline:before { content: "" } +.fa-navicon:before, .fa-reorder:before, .fa-bars:before { + content: "" +} -.fa-table:before { content: "" } +.fa-list-ul:before { + content: "" +} -.fa-magic:before { content: "" } +.fa-list-ol:before { + content: "" +} -.fa-truck:before { content: "" } +.fa-strikethrough:before { + content: "" +} -.fa-pinterest:before { content: "" } +.fa-underline:before { + content: "" +} -.fa-pinterest-square:before { content: "" } +.fa-table:before { + content: "" +} -.fa-google-plus-square:before { content: "" } +.fa-magic:before { + content: "" +} -.fa-google-plus:before { content: "" } +.fa-truck:before { + content: "" +} -.fa-money:before { content: "" } +.fa-pinterest:before { + content: "" +} -.fa-caret-down:before, .wy-dropdown .caret:before, .icon-caret-down:before { content: "" } +.fa-pinterest-square:before { + content: "" +} -.fa-caret-up:before { content: "" } +.fa-google-plus-square:before { + content: "" +} -.fa-caret-left:before { content: "" } +.fa-google-plus:before { + content: "" +} -.fa-caret-right:before { content: "" } +.fa-money:before { + content: "" +} -.fa-columns:before { content: "" } +.fa-caret-down:before, .wy-dropdown .caret:before, .icon-caret-down:before { + content: "" +} -.fa-unsorted:before, .fa-sort:before { content: "" } +.fa-caret-up:before { + content: "" +} -.fa-sort-down:before, .fa-sort-desc:before { content: "" } +.fa-caret-left:before { + content: "" +} -.fa-sort-up:before, .fa-sort-asc:before { content: "" } +.fa-caret-right:before { + content: "" +} -.fa-envelope:before { content: "" } +.fa-columns:before { + content: "" +} -.fa-linkedin:before { content: "" } +.fa-unsorted:before, .fa-sort:before { + content: "" +} -.fa-rotate-left:before, .fa-undo:before { content: "" } +.fa-sort-down:before, .fa-sort-desc:before { + content: "" +} -.fa-legal:before, .fa-gavel:before { content: "" } +.fa-sort-up:before, .fa-sort-asc:before { + content: "" +} -.fa-dashboard:before, .fa-tachometer:before { content: "" } +.fa-envelope:before { + content: "" +} -.fa-comment-o:before { content: "" } +.fa-linkedin:before { + content: "" +} -.fa-comments-o:before { content: "" } +.fa-rotate-left:before, .fa-undo:before { + content: "" +} -.fa-flash:before, .fa-bolt:before { content: "" } +.fa-legal:before, .fa-gavel:before { + content: "" +} -.fa-sitemap:before { content: "" } +.fa-dashboard:before, .fa-tachometer:before { + content: "" +} -.fa-umbrella:before { content: "" } +.fa-comment-o:before { + content: "" +} -.fa-paste:before, .fa-clipboard:before { content: "" } +.fa-comments-o:before { + content: "" +} -.fa-lightbulb-o:before { content: "" } +.fa-flash:before, .fa-bolt:before { + content: "" +} -.fa-exchange:before { content: "" } +.fa-sitemap:before { + content: "" +} -.fa-cloud-download:before { content: "" } +.fa-umbrella:before { + content: "" +} -.fa-cloud-upload:before { content: "" } +.fa-paste:before, .fa-clipboard:before { + content: "" +} -.fa-user-md:before { content: "" } +.fa-lightbulb-o:before { + content: "" +} -.fa-stethoscope:before { content: "" } +.fa-exchange:before { + content: "" +} -.fa-suitcase:before { content: "" } +.fa-cloud-download:before { + content: "" +} -.fa-bell-o:before { content: "" } +.fa-cloud-upload:before { + content: "" +} -.fa-coffee:before { content: "" } +.fa-user-md:before { + content: "" +} -.fa-cutlery:before { content: "" } +.fa-stethoscope:before { + content: "" +} -.fa-file-text-o:before { content: "" } +.fa-suitcase:before { + content: "" +} -.fa-building-o:before { content: "" } +.fa-bell-o:before { + content: "" +} -.fa-hospital-o:before { content: "" } +.fa-coffee:before { + content: "" +} -.fa-ambulance:before { content: "" } +.fa-cutlery:before { + content: "" +} -.fa-medkit:before { content: "" } +.fa-file-text-o:before { + content: "" +} -.fa-fighter-jet:before { content: "" } +.fa-building-o:before { + content: "" +} -.fa-beer:before { content: "" } +.fa-hospital-o:before { + content: "" +} -.fa-h-square:before { content: "" } +.fa-ambulance:before { + content: "" +} -.fa-plus-square:before { content: "" } +.fa-medkit:before { + content: "" +} -.fa-angle-double-left:before { content: "" } +.fa-fighter-jet:before { + content: "" +} -.fa-angle-double-right:before { content: "" } +.fa-beer:before { + content: "" +} -.fa-angle-double-up:before { content: "" } +.fa-h-square:before { + content: "" +} -.fa-angle-double-down:before { content: "" } +.fa-plus-square:before { + content: "" +} -.fa-angle-left:before { content: "" } +.fa-angle-double-left:before { + content: "" +} -.fa-angle-right:before { content: "" } +.fa-angle-double-right:before { + content: "" +} -.fa-angle-up:before { content: "" } +.fa-angle-double-up:before { + content: "" +} -.fa-angle-down:before { content: "" } +.fa-angle-double-down:before { + content: "" +} -.fa-desktop:before { content: "" } +.fa-angle-left:before { + content: "" +} -.fa-laptop:before { content: "" } +.fa-angle-right:before { + content: "" +} -.fa-tablet:before { content: "" } +.fa-angle-up:before { + content: "" +} -.fa-mobile-phone:before, .fa-mobile:before { content: "" } +.fa-angle-down:before { + content: "" +} -.fa-circle-o:before { content: "" } +.fa-desktop:before { + content: "" +} -.fa-quote-left:before { content: "" } +.fa-laptop:before { + content: "" +} -.fa-quote-right:before { content: "" } +.fa-tablet:before { + content: "" +} -.fa-spinner:before { content: "" } +.fa-mobile-phone:before, .fa-mobile:before { + content: "" +} -.fa-circle:before { content: "" } +.fa-circle-o:before { + content: "" +} -.fa-mail-reply:before, .fa-reply:before { content: "" } +.fa-quote-left:before { + content: "" +} -.fa-github-alt:before { content: "" } +.fa-quote-right:before { + content: "" +} -.fa-folder-o:before { content: "" } +.fa-spinner:before { + content: "" +} -.fa-folder-open-o:before { content: "" } +.fa-circle:before { + content: "" +} -.fa-smile-o:before { content: "" } +.fa-mail-reply:before, .fa-reply:before { + content: "" +} -.fa-frown-o:before { content: "" } +.fa-github-alt:before { + content: "" +} -.fa-meh-o:before { content: "" } +.fa-folder-o:before { + content: "" +} -.fa-gamepad:before { content: "" } +.fa-folder-open-o:before { + content: "" +} -.fa-keyboard-o:before { content: "" } +.fa-smile-o:before { + content: "" +} -.fa-flag-o:before { content: "" } +.fa-frown-o:before { + content: "" +} -.fa-flag-checkered:before { content: "" } +.fa-meh-o:before { + content: "" +} -.fa-terminal:before { content: "" } +.fa-gamepad:before { + content: "" +} -.fa-code:before { content: "" } +.fa-keyboard-o:before { + content: "" +} -.fa-mail-reply-all:before, .fa-reply-all:before { content: "" } +.fa-flag-o:before { + content: "" +} -.fa-star-half-empty:before, .fa-star-half-full:before, .fa-star-half-o:before { content: "" } +.fa-flag-checkered:before { + content: "" +} -.fa-location-arrow:before { content: "" } +.fa-terminal:before { + content: "" +} -.fa-crop:before { content: "" } +.fa-code:before { + content: "" +} -.fa-code-fork:before { content: "" } +.fa-mail-reply-all:before, .fa-reply-all:before { + content: "" +} -.fa-unlink:before, .fa-chain-broken:before { content: "" } +.fa-star-half-empty:before, .fa-star-half-full:before, .fa-star-half-o:before { + content: "" +} -.fa-question:before { content: "" } +.fa-location-arrow:before { + content: "" +} -.fa-info:before { content: "" } +.fa-crop:before { + content: "" +} -.fa-exclamation:before { content: "" } +.fa-code-fork:before { + content: "" +} -.fa-superscript:before { content: "" } +.fa-unlink:before, .fa-chain-broken:before { + content: "" +} -.fa-subscript:before { content: "" } +.fa-question:before { + content: "" +} -.fa-eraser:before { content: "" } +.fa-info:before { + content: "" +} -.fa-puzzle-piece:before { content: "" } +.fa-exclamation:before { + content: "" +} -.fa-microphone:before { content: "" } +.fa-superscript:before { + content: "" +} -.fa-microphone-slash:before { content: "" } +.fa-subscript:before { + content: "" +} -.fa-shield:before { content: "" } +.fa-eraser:before { + content: "" +} -.fa-calendar-o:before { content: "" } +.fa-puzzle-piece:before { + content: "" +} -.fa-fire-extinguisher:before { content: "" } +.fa-microphone:before { + content: "" +} -.fa-rocket:before { content: "" } +.fa-microphone-slash:before { + content: "" +} -.fa-maxcdn:before { content: "" } +.fa-shield:before { + content: "" +} -.fa-chevron-circle-left:before { content: "" } +.fa-calendar-o:before { + content: "" +} -.fa-chevron-circle-right:before { content: "" } +.fa-fire-extinguisher:before { + content: "" +} -.fa-chevron-circle-up:before { content: "" } +.fa-rocket:before { + content: "" +} -.fa-chevron-circle-down:before { content: "" } +.fa-maxcdn:before { + content: "" +} -.fa-html5:before { content: "" } +.fa-chevron-circle-left:before { + content: "" +} -.fa-css3:before { content: "" } +.fa-chevron-circle-right:before { + content: "" +} -.fa-anchor:before { content: "" } +.fa-chevron-circle-up:before { + content: "" +} -.fa-unlock-alt:before { content: "" } +.fa-chevron-circle-down:before { + content: "" +} -.fa-bullseye:before { content: "" } +.fa-html5:before { + content: "" +} -.fa-ellipsis-h:before { content: "" } +.fa-css3:before { + content: "" +} -.fa-ellipsis-v:before { content: "" } +.fa-anchor:before { + content: "" +} -.fa-rss-square:before { content: "" } +.fa-unlock-alt:before { + content: "" +} -.fa-play-circle:before { content: "" } +.fa-bullseye:before { + content: "" +} -.fa-ticket:before { content: "" } +.fa-ellipsis-h:before { + content: "" +} -.fa-minus-square:before { content: "" } +.fa-ellipsis-v:before { + content: "" +} -.fa-minus-square-o:before { content: "" } +.fa-rss-square:before { + content: "" +} -.fa-level-up:before { content: "" } +.fa-play-circle:before { + content: "" +} -.fa-level-down:before { content: "" } +.fa-ticket:before { + content: "" +} -.fa-check-square:before { content: "" } +.fa-minus-square:before { + content: "" +} -.fa-pencil-square:before { content: "" } +.fa-minus-square-o:before, .wy-menu-vertical li.on a span.toctree-expand:before, .wy-menu-vertical li.current > a span.toctree-expand:before { + content: "" +} -.fa-external-link-square:before { content: "" } +.fa-level-up:before { + content: "" +} -.fa-share-square:before { content: "" } +.fa-level-down:before { + content: "" +} -.fa-compass:before { content: "" } +.fa-check-square:before { + content: "" +} -.fa-toggle-down:before, .fa-caret-square-o-down:before { content: "" } +.fa-pencil-square:before { + content: "" +} -.fa-toggle-up:before, .fa-caret-square-o-up:before { content: "" } +.fa-external-link-square:before { + content: "" +} -.fa-toggle-right:before, .fa-caret-square-o-right:before { content: "" } +.fa-share-square:before { + content: "" +} -.fa-euro:before, .fa-eur:before { content: "" } +.fa-compass:before { + content: "" +} -.fa-gbp:before { content: "" } +.fa-toggle-down:before, .fa-caret-square-o-down:before { + content: "" +} -.fa-dollar:before, .fa-usd:before { content: "" } +.fa-toggle-up:before, .fa-caret-square-o-up:before { + content: "" +} -.fa-rupee:before, .fa-inr:before { content: "" } +.fa-toggle-right:before, .fa-caret-square-o-right:before { + content: "" +} -.fa-cny:before, .fa-rmb:before, .fa-yen:before, .fa-jpy:before { content: "" } +.fa-euro:before, .fa-eur:before { + content: "" +} -.fa-ruble:before, .fa-rouble:before, .fa-rub:before { content: "" } +.fa-gbp:before { + content: "" +} -.fa-won:before, .fa-krw:before { content: "" } +.fa-dollar:before, .fa-usd:before { + content: "" +} -.fa-bitcoin:before, .fa-btc:before { content: "" } +.fa-rupee:before, .fa-inr:before { + content: "" +} -.fa-file:before { content: "" } +.fa-cny:before, .fa-rmb:before, .fa-yen:before, .fa-jpy:before { + content: "" +} -.fa-file-text:before { content: "" } +.fa-ruble:before, .fa-rouble:before, .fa-rub:before { + content: "" +} -.fa-sort-alpha-asc:before { content: "" } +.fa-won:before, .fa-krw:before { + content: "" +} -.fa-sort-alpha-desc:before { content: "" } +.fa-bitcoin:before, .fa-btc:before { + content: "" +} -.fa-sort-amount-asc:before { content: "" } +.fa-file:before { + content: "" +} -.fa-sort-amount-desc:before { content: "" } +.fa-file-text:before { + content: "" +} -.fa-sort-numeric-asc:before { content: "" } +.fa-sort-alpha-asc:before { + content: "" +} -.fa-sort-numeric-desc:before { content: "" } +.fa-sort-alpha-desc:before { + content: "" +} -.fa-thumbs-up:before { content: "" } +.fa-sort-amount-asc:before { + content: "" +} -.fa-thumbs-down:before { content: "" } +.fa-sort-amount-desc:before { + content: "" +} -.fa-youtube-square:before { content: "" } +.fa-sort-numeric-asc:before { + content: "" +} -.fa-youtube:before { content: "" } +.fa-sort-numeric-desc:before { + content: "" +} -.fa-xing:before { content: "" } +.fa-thumbs-up:before { + content: "" +} -.fa-xing-square:before { content: "" } +.fa-thumbs-down:before { + content: "" +} -.fa-youtube-play:before { content: "" } +.fa-youtube-square:before { + content: "" +} -.fa-dropbox:before { content: "" } +.fa-youtube:before { + content: "" +} -.fa-stack-overflow:before { content: "" } +.fa-xing:before { + content: "" +} -.fa-instagram:before { content: "" } +.fa-xing-square:before { + content: "" +} -.fa-flickr:before { content: "" } +.fa-youtube-play:before { + content: "" +} -.fa-adn:before { content: "" } +.fa-dropbox:before { + content: "" +} -.fa-bitbucket:before, .icon-bitbucket:before { content: "" } +.fa-stack-overflow:before { + content: "" +} -.fa-bitbucket-square:before { content: "" } +.fa-instagram:before { + content: "" +} -.fa-tumblr:before { content: "" } +.fa-flickr:before { + content: "" +} -.fa-tumblr-square:before { content: "" } +.fa-adn:before { + content: "" +} -.fa-long-arrow-down:before { content: "" } +.fa-bitbucket:before, .icon-bitbucket:before { + content: "" +} -.fa-long-arrow-up:before { content: "" } +.fa-bitbucket-square:before { + content: "" +} -.fa-long-arrow-left:before { content: "" } +.fa-tumblr:before { + content: "" +} -.fa-long-arrow-right:before { content: "" } +.fa-tumblr-square:before { + content: "" +} -.fa-apple:before { content: "" } +.fa-long-arrow-down:before { + content: "" +} -.fa-windows:before { content: "" } +.fa-long-arrow-up:before { + content: "" +} -.fa-android:before { content: "" } +.fa-long-arrow-left:before { + content: "" +} -.fa-linux:before { content: "" } +.fa-long-arrow-right:before { + content: "" +} -.fa-dribbble:before { content: "" } +.fa-apple:before { + content: "" +} -.fa-skype:before { content: "" } +.fa-windows:before { + content: "" +} -.fa-foursquare:before { content: "" } +.fa-android:before { + content: "" +} -.fa-trello:before { content: "" } +.fa-linux:before { + content: "" +} -.fa-female:before { content: "" } +.fa-dribbble:before { + content: "" +} -.fa-male:before { content: "" } +.fa-skype:before { + content: "" +} -.fa-gittip:before { content: "" } +.fa-foursquare:before { + content: "" +} -.fa-sun-o:before { content: "" } +.fa-trello:before { + content: "" +} -.fa-moon-o:before { content: "" } +.fa-female:before { + content: "" +} -.fa-archive:before { content: "" } +.fa-male:before { + content: "" +} -.fa-bug:before { content: "" } +.fa-gittip:before, .fa-gratipay:before { + content: "" +} -.fa-vk:before { content: "" } +.fa-sun-o:before { + content: "" +} -.fa-weibo:before { content: "" } +.fa-moon-o:before { + content: "" +} -.fa-renren:before { content: "" } +.fa-archive:before { + content: "" +} -.fa-pagelines:before { content: "" } +.fa-bug:before { + content: "" +} -.fa-stack-exchange:before { content: "" } +.fa-vk:before { + content: "" +} -.fa-arrow-circle-o-right:before { content: "" } +.fa-weibo:before { + content: "" +} -.fa-arrow-circle-o-left:before { content: "" } +.fa-renren:before { + content: "" +} -.fa-toggle-left:before, .fa-caret-square-o-left:before { content: "" } +.fa-pagelines:before { + content: "" +} -.fa-dot-circle-o:before { content: "" } +.fa-stack-exchange:before { + content: "" +} -.fa-wheelchair:before { content: "" } +.fa-arrow-circle-o-right:before { + content: "" +} -.fa-vimeo-square:before { content: "" } +.fa-arrow-circle-o-left:before { + content: "" +} -.fa-turkish-lira:before, .fa-try:before { content: "" } +.fa-toggle-left:before, .fa-caret-square-o-left:before { + content: "" +} -.fa-plus-square-o:before { content: "" } +.fa-dot-circle-o:before { + content: "" +} -.fa-space-shuttle:before { content: "" } +.fa-wheelchair:before { + content: "" +} -.fa-slack:before { content: "" } +.fa-vimeo-square:before { + content: "" +} -.fa-envelope-square:before { content: "" } +.fa-turkish-lira:before, .fa-try:before { + content: "" +} -.fa-wordpress:before { content: "" } +.fa-plus-square-o:before, .wy-menu-vertical li span.toctree-expand:before { + content: "" +} -.fa-openid:before { content: "" } +.fa-space-shuttle:before { + content: "" +} -.fa-institution:before, .fa-bank:before, .fa-university:before { content: "" } +.fa-slack:before { + content: "" +} -.fa-mortar-board:before, .fa-graduation-cap:before { content: "" } +.fa-envelope-square:before { + content: "" +} -.fa-yahoo:before { content: "" } +.fa-wordpress:before { + content: "" +} -.fa-google:before { content: "" } +.fa-openid:before { + content: "" +} -.fa-reddit:before { content: "" } +.fa-institution:before, .fa-bank:before, .fa-university:before { + content: "" +} -.fa-reddit-square:before { content: "" } +.fa-mortar-board:before, .fa-graduation-cap:before { + content: "" +} -.fa-stumbleupon-circle:before { content: "" } +.fa-yahoo:before { + content: "" +} -.fa-stumbleupon:before { content: "" } +.fa-google:before { + content: "" +} -.fa-delicious:before { content: "" } +.fa-reddit:before { + content: "" +} -.fa-digg:before { content: "" } +.fa-reddit-square:before { + content: "" +} -.fa-pied-piper-square:before, .fa-pied-piper:before { content: "" } +.fa-stumbleupon-circle:before { + content: "" +} -.fa-pied-piper-alt:before { content: "" } +.fa-stumbleupon:before { + content: "" +} -.fa-drupal:before { content: "" } +.fa-delicious:before { + content: "" +} -.fa-joomla:before { content: "" } +.fa-digg:before { + content: "" +} -.fa-language:before { content: "" } +.fa-pied-piper-pp:before { + content: "" +} -.fa-fax:before { content: "" } +.fa-pied-piper-alt:before { + content: "" +} -.fa-building:before { content: "" } +.fa-drupal:before { + content: "" +} -.fa-child:before { content: "" } +.fa-joomla:before { + content: "" +} -.fa-paw:before { content: "" } +.fa-language:before { + content: "" +} -.fa-spoon:before { content: "" } +.fa-fax:before { + content: "" +} -.fa-cube:before { content: "" } +.fa-building:before { + content: "" +} -.fa-cubes:before { content: "" } +.fa-child:before { + content: "" +} -.fa-behance:before { content: "" } +.fa-paw:before { + content: "" +} -.fa-behance-square:before { content: "" } +.fa-spoon:before { + content: "" +} -.fa-steam:before { content: "" } +.fa-cube:before { + content: "" +} -.fa-steam-square:before { content: "" } +.fa-cubes:before { + content: "" +} -.fa-recycle:before { content: "" } +.fa-behance:before { + content: "" +} -.fa-automobile:before, .fa-car:before { content: "" } +.fa-behance-square:before { + content: "" +} -.fa-cab:before, .fa-taxi:before { content: "" } +.fa-steam:before { + content: "" +} -.fa-tree:before { content: "" } +.fa-steam-square:before { + content: "" +} -.fa-spotify:before { content: "" } +.fa-recycle:before { + content: "" +} -.fa-deviantart:before { content: "" } +.fa-automobile:before, .fa-car:before { + content: "" +} -.fa-soundcloud:before { content: "" } +.fa-cab:before, .fa-taxi:before { + content: "" +} -.fa-database:before { content: "" } +.fa-tree:before { + content: "" +} -.fa-file-pdf-o:before { content: "" } +.fa-spotify:before { + content: "" +} -.fa-file-word-o:before { content: "" } +.fa-deviantart:before { + content: "" +} -.fa-file-excel-o:before { content: "" } +.fa-soundcloud:before { + content: "" +} -.fa-file-powerpoint-o:before { content: "" } +.fa-database:before { + content: "" +} -.fa-file-photo-o:before, .fa-file-picture-o:before, .fa-file-image-o:before { content: "" } +.fa-file-pdf-o:before { + content: "" +} -.fa-file-zip-o:before, .fa-file-archive-o:before { content: "" } +.fa-file-word-o:before { + content: "" +} -.fa-file-sound-o:before, .fa-file-audio-o:before { content: "" } +.fa-file-excel-o:before { + content: "" +} -.fa-file-movie-o:before, .fa-file-video-o:before { content: "" } +.fa-file-powerpoint-o:before { + content: "" +} -.fa-file-code-o:before { content: "" } +.fa-file-photo-o:before, .fa-file-picture-o:before, .fa-file-image-o:before { + content: "" +} -.fa-vine:before { content: "" } +.fa-file-zip-o:before, .fa-file-archive-o:before { + content: "" +} -.fa-codepen:before { content: "" } +.fa-file-sound-o:before, .fa-file-audio-o:before { + content: "" +} -.fa-jsfiddle:before { content: "" } +.fa-file-movie-o:before, .fa-file-video-o:before { + content: "" +} -.fa-life-bouy:before, .fa-life-saver:before, .fa-support:before, .fa-life-ring:before { content: "" } +.fa-file-code-o:before { + content: "" +} -.fa-circle-o-notch:before { content: "" } +.fa-vine:before { + content: "" +} -.fa-ra:before, .fa-rebel:before { content: "" } +.fa-codepen:before { + content: "" +} -.fa-ge:before, .fa-empire:before { content: "" } +.fa-jsfiddle:before { + content: "" +} -.fa-git-square:before { content: "" } +.fa-life-bouy:before, .fa-life-buoy:before, .fa-life-saver:before, .fa-support:before, .fa-life-ring:before { + content: "" +} -.fa-git:before { content: "" } +.fa-circle-o-notch:before { + content: "" +} -.fa-hacker-news:before { content: "" } +.fa-ra:before, .fa-resistance:before, .fa-rebel:before { + content: "" +} -.fa-tencent-weibo:before { content: "" } +.fa-ge:before, .fa-empire:before { + content: "" +} -.fa-qq:before { content: "" } +.fa-git-square:before { + content: "" +} -.fa-wechat:before, .fa-weixin:before { content: "" } +.fa-git:before { + content: "" +} -.fa-send:before, .fa-paper-plane:before { content: "" } +.fa-y-combinator-square:before, .fa-yc-square:before, .fa-hacker-news:before { + content: "" +} -.fa-send-o:before, .fa-paper-plane-o:before { content: "" } +.fa-tencent-weibo:before { + content: "" +} -.fa-history:before { content: "" } +.fa-qq:before { + content: "" +} -.fa-circle-thin:before { content: "" } +.fa-wechat:before, .fa-weixin:before { + content: "" +} -.fa-header:before { content: "" } +.fa-send:before, .fa-paper-plane:before { + content: "" +} -.fa-paragraph:before { content: "" } +.fa-send-o:before, .fa-paper-plane-o:before { + content: "" +} -.fa-sliders:before { content: "" } +.fa-history:before { + content: "" +} -.fa-share-alt:before { content: "" } +.fa-circle-thin:before { + content: "" +} -.fa-share-alt-square:before { content: "" } +.fa-header:before { + content: "" +} -.fa-bomb:before { content: "" } +.fa-paragraph:before { + content: "" +} -.fa, .rst-content .admonition-title, .rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink, .icon, .wy-dropdown .caret, .wy-inline-validate.wy-inline-validate-success .wy-input-context, .wy-inline-validate.wy-inline-validate-danger .wy-input-context, .wy-inline-validate.wy-inline-validate-warning .wy-input-context, .wy-inline-validate.wy-inline-validate-info .wy-input-context { font-family: inherit } +.fa-sliders:before { + content: "" +} -.fa:before, .rst-content .admonition-title:before, .rst-content h1 .headerlink:before, .rst-content h2 .headerlink:before, .rst-content h3 .headerlink:before, .rst-content h4 .headerlink:before, .rst-content h5 .headerlink:before, .rst-content h6 .headerlink:before, .rst-content dl dt .headerlink:before, .icon:before, .wy-dropdown .caret:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before { font-family: "FontAwesome"; display: inline-block; font-style: normal; font-weight: normal; line-height: 1; text-decoration: inherit } +.fa-share-alt:before { + content: "" +} -a .fa, a .rst-content .admonition-title, .rst-content a .admonition-title, a .rst-content h1 .headerlink, .rst-content h1 a .headerlink, a .rst-content h2 .headerlink, .rst-content h2 a .headerlink, a .rst-content h3 .headerlink, .rst-content h3 a .headerlink, a .rst-content h4 .headerlink, .rst-content h4 a .headerlink, a .rst-content h5 .headerlink, .rst-content h5 a .headerlink, a .rst-content h6 .headerlink, .rst-content h6 a .headerlink, a .rst-content dl dt .headerlink, .rst-content dl dt a .headerlink, a .icon { display: inline-block; text-decoration: inherit } +.fa-share-alt-square:before { + content: "" +} -.btn .fa, .btn .rst-content .admonition-title, .rst-content .btn .admonition-title, .btn .rst-content h1 .headerlink, .rst-content h1 .btn .headerlink, .btn .rst-content h2 .headerlink, .rst-content h2 .btn .headerlink, .btn .rst-content h3 .headerlink, .rst-content h3 .btn .headerlink, .btn .rst-content h4 .headerlink, .rst-content h4 .btn .headerlink, .btn .rst-content h5 .headerlink, .rst-content h5 .btn .headerlink, .btn .rst-content h6 .headerlink, .rst-content h6 .btn .headerlink, .btn .rst-content dl dt .headerlink, .rst-content dl dt .btn .headerlink, .btn .icon, .nav .fa, .nav .rst-content .admonition-title, .rst-content .nav .admonition-title, .nav .rst-content h1 .headerlink, .rst-content h1 .nav .headerlink, .nav .rst-content h2 .headerlink, .rst-content h2 .nav .headerlink, .nav .rst-content h3 .headerlink, .rst-content h3 .nav .headerlink, .nav .rst-content h4 .headerlink, .rst-content h4 .nav .headerlink, .nav .rst-content h5 .headerlink, .rst-content h5 .nav .headerlink, .nav .rst-content h6 .headerlink, .rst-content h6 .nav .headerlink, .nav .rst-content dl dt .headerlink, .rst-content dl dt .nav .headerlink, .nav .icon { display: inline } +.fa-bomb:before { + content: "" +} -.btn .fa.fa-large, .btn .rst-content .fa-large.admonition-title, .rst-content .btn .fa-large.admonition-title, .btn .rst-content h1 .fa-large.headerlink, .rst-content h1 .btn .fa-large.headerlink, .btn .rst-content h2 .fa-large.headerlink, .rst-content h2 .btn .fa-large.headerlink, .btn .rst-content h3 .fa-large.headerlink, .rst-content h3 .btn .fa-large.headerlink, .btn .rst-content h4 .fa-large.headerlink, .rst-content h4 .btn .fa-large.headerlink, .btn .rst-content h5 .fa-large.headerlink, .rst-content h5 .btn .fa-large.headerlink, .btn .rst-content h6 .fa-large.headerlink, .rst-content h6 .btn .fa-large.headerlink, .btn .rst-content dl dt .fa-large.headerlink, .rst-content dl dt .btn .fa-large.headerlink, .btn .fa-large.icon, .nav .fa.fa-large, .nav .rst-content .fa-large.admonition-title, .rst-content .nav .fa-large.admonition-title, .nav .rst-content h1 .fa-large.headerlink, .rst-content h1 .nav .fa-large.headerlink, .nav .rst-content h2 .fa-large.headerlink, .rst-content h2 .nav .fa-large.headerlink, .nav .rst-content h3 .fa-large.headerlink, .rst-content h3 .nav .fa-large.headerlink, .nav .rst-content h4 .fa-large.headerlink, .rst-content h4 .nav .fa-large.headerlink, .nav .rst-content h5 .fa-large.headerlink, .rst-content h5 .nav .fa-large.headerlink, .nav .rst-content h6 .fa-large.headerlink, .rst-content h6 .nav .fa-large.headerlink, .nav .rst-content dl dt .fa-large.headerlink, .rst-content dl dt .nav .fa-large.headerlink, .nav .fa-large.icon { line-height: 0.9em } +.fa-soccer-ball-o:before, .fa-futbol-o:before { + content: "" +} -.btn .fa.fa-spin, .btn .rst-content .fa-spin.admonition-title, .rst-content .btn .fa-spin.admonition-title, .btn .rst-content h1 .fa-spin.headerlink, .rst-content h1 .btn .fa-spin.headerlink, .btn .rst-content h2 .fa-spin.headerlink, .rst-content h2 .btn .fa-spin.headerlink, .btn .rst-content h3 .fa-spin.headerlink, .rst-content h3 .btn .fa-spin.headerlink, .btn .rst-content h4 .fa-spin.headerlink, .rst-content h4 .btn .fa-spin.headerlink, .btn .rst-content h5 .fa-spin.headerlink, .rst-content h5 .btn .fa-spin.headerlink, .btn .rst-content h6 .fa-spin.headerlink, .rst-content h6 .btn .fa-spin.headerlink, .btn .rst-content dl dt .fa-spin.headerlink, .rst-content dl dt .btn .fa-spin.headerlink, .btn .fa-spin.icon, .nav .fa.fa-spin, .nav .rst-content .fa-spin.admonition-title, .rst-content .nav .fa-spin.admonition-title, .nav .rst-content h1 .fa-spin.headerlink, .rst-content h1 .nav .fa-spin.headerlink, .nav .rst-content h2 .fa-spin.headerlink, .rst-content h2 .nav .fa-spin.headerlink, .nav .rst-content h3 .fa-spin.headerlink, .rst-content h3 .nav .fa-spin.headerlink, .nav .rst-content h4 .fa-spin.headerlink, .rst-content h4 .nav .fa-spin.headerlink, .nav .rst-content h5 .fa-spin.headerlink, .rst-content h5 .nav .fa-spin.headerlink, .nav .rst-content h6 .fa-spin.headerlink, .rst-content h6 .nav .fa-spin.headerlink, .nav .rst-content dl dt .fa-spin.headerlink, .rst-content dl dt .nav .fa-spin.headerlink, .nav .fa-spin.icon { display: inline-block } +.fa-tty:before { + content: "" +} -.btn.fa:before, .rst-content .btn.admonition-title:before, .rst-content h1 .btn.headerlink:before, .rst-content h2 .btn.headerlink:before, .rst-content h3 .btn.headerlink:before, .rst-content h4 .btn.headerlink:before, .rst-content h5 .btn.headerlink:before, .rst-content h6 .btn.headerlink:before, .rst-content dl dt .btn.headerlink:before, .btn.icon:before { opacity: 0.5; -webkit-transition: opacity 0.05s ease-in; -moz-transition: opacity 0.05s ease-in; transition: opacity 0.05s ease-in } +.fa-binoculars:before { + content: "" +} -.btn.fa:hover:before, .rst-content .btn.admonition-title:hover:before, .rst-content h1 .btn.headerlink:hover:before, .rst-content h2 .btn.headerlink:hover:before, .rst-content h3 .btn.headerlink:hover:before, .rst-content h4 .btn.headerlink:hover:before, .rst-content h5 .btn.headerlink:hover:before, .rst-content h6 .btn.headerlink:hover:before, .rst-content dl dt .btn.headerlink:hover:before, .btn.icon:hover:before { opacity: 1 } +.fa-plug:before { + content: "" +} -.btn-mini .fa:before, .btn-mini .rst-content .admonition-title:before, .rst-content .btn-mini .admonition-title:before, .btn-mini .rst-content h1 .headerlink:before, .rst-content h1 .btn-mini .headerlink:before, .btn-mini .rst-content h2 .headerlink:before, .rst-content h2 .btn-mini .headerlink:before, .btn-mini .rst-content h3 .headerlink:before, .rst-content h3 .btn-mini .headerlink:before, .btn-mini .rst-content h4 .headerlink:before, .rst-content h4 .btn-mini .headerlink:before, .btn-mini .rst-content h5 .headerlink:before, .rst-content h5 .btn-mini .headerlink:before, .btn-mini .rst-content h6 .headerlink:before, .rst-content h6 .btn-mini .headerlink:before, .btn-mini .rst-content dl dt .headerlink:before, .rst-content dl dt .btn-mini .headerlink:before, .btn-mini .icon:before { font-size: 14px; vertical-align: -15% } +.fa-slideshare:before { + content: "" +} -.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo { padding: 12px; line-height: 24px; margin-bottom: 24px; background: #dedede } +.fa-twitch:before { + content: "" +} -.wy-alert-title, .rst-content .admonition-title { color: #ffffff; font-weight: bold; display: block; color: #ffffff; background: #8ba8af; margin: -12px; padding: 6px 12px; margin-bottom: 12px } +.fa-yelp:before { + content: "" +} -.wy-alert.wy-alert-danger, .rst-content .wy-alert-danger.note, .rst-content .wy-alert-danger.attention, .rst-content .wy-alert-danger.caution, .rst-content .danger, .rst-content .error, .rst-content .wy-alert-danger.hint, .rst-content .wy-alert-danger.important, .rst-content .wy-alert-danger.tip, .rst-content .wy-alert-danger.warning, .rst-content .wy-alert-danger.seealso, .rst-content .wy-alert-danger.admonition-todo { background: #fdf3f2 } +.fa-newspaper-o:before { + content: "" +} -.wy-alert.wy-alert-danger .wy-alert-title, .rst-content .wy-alert-danger.note .wy-alert-title, .rst-content .wy-alert-danger.attention .wy-alert-title, .rst-content .wy-alert-danger.caution .wy-alert-title, .rst-content .danger .wy-alert-title, .rst-content .error .wy-alert-title, .rst-content .wy-alert-danger.hint .wy-alert-title, .rst-content .wy-alert-danger.important .wy-alert-title, .rst-content .wy-alert-danger.tip .wy-alert-title, .rst-content .wy-alert-danger.warning .wy-alert-title, .rst-content .wy-alert-danger.seealso .wy-alert-title, .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, .wy-alert.wy-alert-danger .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-danger .admonition-title, .rst-content .wy-alert-danger.note .admonition-title, .rst-content .wy-alert-danger.attention .admonition-title, .rst-content .wy-alert-danger.caution .admonition-title, .rst-content .danger .admonition-title, .rst-content .error .admonition-title, .rst-content .wy-alert-danger.hint .admonition-title, .rst-content .wy-alert-danger.important .admonition-title, .rst-content .wy-alert-danger.tip .admonition-title, .rst-content .wy-alert-danger.warning .admonition-title, .rst-content .wy-alert-danger.seealso .admonition-title, .rst-content .wy-alert-danger.admonition-todo .admonition-title { background: #f29f97 } +.fa-wifi:before { + content: "" +} -.wy-alert.wy-alert-warning, .rst-content .wy-alert-warning.note, .rst-content .attention, .rst-content .caution, .rst-content .wy-alert-warning.danger, .rst-content .wy-alert-warning.error, .rst-content .wy-alert-warning.hint, .rst-content .wy-alert-warning.important, .rst-content .wy-alert-warning.tip, .rst-content .warning, .rst-content .wy-alert-warning.seealso, .rst-content .admonition-todo { background: #ffedcc } +.fa-calculator:before { + content: "" +} -.wy-alert.wy-alert-warning .wy-alert-title, .rst-content .wy-alert-warning.note .wy-alert-title, .rst-content .attention .wy-alert-title, .rst-content .caution .wy-alert-title, .rst-content .wy-alert-warning.danger .wy-alert-title, .rst-content .wy-alert-warning.error .wy-alert-title, .rst-content .wy-alert-warning.hint .wy-alert-title, .rst-content .wy-alert-warning.important .wy-alert-title, .rst-content .wy-alert-warning.tip .wy-alert-title, .rst-content .warning .wy-alert-title, .rst-content .wy-alert-warning.seealso .wy-alert-title, .rst-content .admonition-todo .wy-alert-title, .wy-alert.wy-alert-warning .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-warning .admonition-title, .rst-content .wy-alert-warning.note .admonition-title, .rst-content .attention .admonition-title, .rst-content .caution .admonition-title, .rst-content .wy-alert-warning.danger .admonition-title, .rst-content .wy-alert-warning.error .admonition-title, .rst-content .wy-alert-warning.hint .admonition-title, .rst-content .wy-alert-warning.important .admonition-title, .rst-content .wy-alert-warning.tip .admonition-title, .rst-content .warning .admonition-title, .rst-content .wy-alert-warning.seealso .admonition-title, .rst-content .admonition-todo .admonition-title { background: #f0b37e } +.fa-paypal:before { + content: "" +} -.wy-alert.wy-alert-info, .rst-content .note, .rst-content .wy-alert-info.attention, .rst-content .wy-alert-info.caution, .rst-content .wy-alert-info.danger, .rst-content .wy-alert-info.error, .rst-content .wy-alert-info.hint, .rst-content .wy-alert-info.important, .rst-content .wy-alert-info.tip, .rst-content .wy-alert-info.warning, .rst-content .seealso, .rst-content .wy-alert-info.admonition-todo { background: #dedede } +.fa-google-wallet:before { + content: "" +} -.wy-alert.wy-alert-info .wy-alert-title, .rst-content .note .wy-alert-title, .rst-content .wy-alert-info.attention .wy-alert-title, .rst-content .wy-alert-info.caution .wy-alert-title, .rst-content .wy-alert-info.danger .wy-alert-title, .rst-content .wy-alert-info.error .wy-alert-title, .rst-content .wy-alert-info.hint .wy-alert-title, .rst-content .wy-alert-info.important .wy-alert-title, .rst-content .wy-alert-info.tip .wy-alert-title, .rst-content .wy-alert-info.warning .wy-alert-title, .rst-content .seealso .wy-alert-title, .rst-content .wy-alert-info.admonition-todo .wy-alert-title, .wy-alert.wy-alert-info .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-info .admonition-title, .rst-content .note .admonition-title, .rst-content .wy-alert-info.attention .admonition-title, .rst-content .wy-alert-info.caution .admonition-title, .rst-content .wy-alert-info.danger .admonition-title, .rst-content .wy-alert-info.error .admonition-title, .rst-content .wy-alert-info.hint .admonition-title, .rst-content .wy-alert-info.important .admonition-title, .rst-content .wy-alert-info.tip .admonition-title, .rst-content .wy-alert-info.warning .admonition-title, .rst-content .seealso .admonition-title, .rst-content .wy-alert-info.admonition-todo .admonition-title { background: #8ba8af } +.fa-cc-visa:before { + content: "" +} -.wy-alert.wy-alert-success, .rst-content .wy-alert-success.note, .rst-content .wy-alert-success.attention, .rst-content .wy-alert-success.caution, .rst-content .wy-alert-success.danger, .rst-content .wy-alert-success.error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .wy-alert-success.warning, .rst-content .wy-alert-success.seealso, .rst-content .wy-alert-success.admonition-todo { background: #dedede } +.fa-cc-mastercard:before { + content: "" +} -.wy-alert.wy-alert-success .wy-alert-title, .rst-content .wy-alert-success.note .wy-alert-title, .rst-content .wy-alert-success.attention .wy-alert-title, .rst-content .wy-alert-success.caution .wy-alert-title, .rst-content .wy-alert-success.danger .wy-alert-title, .rst-content .wy-alert-success.error .wy-alert-title, .rst-content .hint .wy-alert-title, .rst-content .important .wy-alert-title, .rst-content .tip .wy-alert-title, .rst-content .wy-alert-success.warning .wy-alert-title, .rst-content .wy-alert-success.seealso .wy-alert-title, .rst-content .wy-alert-success.admonition-todo .wy-alert-title, .wy-alert.wy-alert-success .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-success .admonition-title, .rst-content .wy-alert-success.note .admonition-title, .rst-content .wy-alert-success.attention .admonition-title, .rst-content .wy-alert-success.caution .admonition-title, .rst-content .wy-alert-success.danger .admonition-title, .rst-content .wy-alert-success.error .admonition-title, .rst-content .hint .admonition-title, .rst-content .important .admonition-title, .rst-content .tip .admonition-title, .rst-content .wy-alert-success.warning .admonition-title, .rst-content .wy-alert-success.seealso .admonition-title, .rst-content .wy-alert-success.admonition-todo .admonition-title { background: #dd4814 } +.fa-cc-discover:before { + content: "" +} -.wy-alert.wy-alert-neutral, .rst-content .wy-alert-neutral.note, .rst-content .wy-alert-neutral.attention, .rst-content .wy-alert-neutral.caution, .rst-content .wy-alert-neutral.danger, .rst-content .wy-alert-neutral.error, .rst-content .wy-alert-neutral.hint, .rst-content .wy-alert-neutral.important, .rst-content .wy-alert-neutral.tip, .rst-content .wy-alert-neutral.warning, .rst-content .wy-alert-neutral.seealso, .rst-content .wy-alert-neutral.admonition-todo { background: #f3f6f6 } +.fa-cc-amex:before { + content: "" +} -.wy-alert.wy-alert-neutral .wy-alert-title, .rst-content .wy-alert-neutral.note .wy-alert-title, .rst-content .wy-alert-neutral.attention .wy-alert-title, .rst-content .wy-alert-neutral.caution .wy-alert-title, .rst-content .wy-alert-neutral.danger .wy-alert-title, .rst-content .wy-alert-neutral.error .wy-alert-title, .rst-content .wy-alert-neutral.hint .wy-alert-title, .rst-content .wy-alert-neutral.important .wy-alert-title, .rst-content .wy-alert-neutral.tip .wy-alert-title, .rst-content .wy-alert-neutral.warning .wy-alert-title, .rst-content .wy-alert-neutral.seealso .wy-alert-title, .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, .wy-alert.wy-alert-neutral .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-neutral .admonition-title, .rst-content .wy-alert-neutral.note .admonition-title, .rst-content .wy-alert-neutral.attention .admonition-title, .rst-content .wy-alert-neutral.caution .admonition-title, .rst-content .wy-alert-neutral.danger .admonition-title, .rst-content .wy-alert-neutral.error .admonition-title, .rst-content .wy-alert-neutral.hint .admonition-title, .rst-content .wy-alert-neutral.important .admonition-title, .rst-content .wy-alert-neutral.tip .admonition-title, .rst-content .wy-alert-neutral.warning .admonition-title, .rst-content .wy-alert-neutral.seealso .admonition-title, .rst-content .wy-alert-neutral.admonition-todo .admonition-title { color: #404040; background: #e1e4e5 } +.fa-cc-paypal:before { + content: "" +} -.wy-alert.wy-alert-neutral a, .rst-content .wy-alert-neutral.note a, .rst-content .wy-alert-neutral.attention a, .rst-content .wy-alert-neutral.caution a, .rst-content .wy-alert-neutral.danger a, .rst-content .wy-alert-neutral.error a, .rst-content .wy-alert-neutral.hint a, .rst-content .wy-alert-neutral.important a, .rst-content .wy-alert-neutral.tip a, .rst-content .wy-alert-neutral.warning a, .rst-content .wy-alert-neutral.seealso a, .rst-content .wy-alert-neutral.admonition-todo a { color: #dd4814 } +.fa-cc-stripe:before { + content: "" +} -.wy-alert p:last-child, .rst-content .note p:last-child, .rst-content .attention p:last-child, .rst-content .caution p:last-child, .rst-content .danger p:last-child, .rst-content .error p:last-child, .rst-content .hint p:last-child, .rst-content .important p:last-child, .rst-content .tip p:last-child, .rst-content .warning p:last-child, .rst-content .seealso p:last-child, .rst-content .admonition-todo p:last-child { margin-bottom: 0 } +.fa-bell-slash:before { + content: "" +} -.wy-tray-container { position: fixed; bottom: 0px; left: 0; z-index: 600 } +.fa-bell-slash-o:before { + content: "" +} -.wy-tray-container li { display: block; width: 300px; background: transparent; color: #ffffff; text-align: center; box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.1); padding: 0 24px; min-width: 20%; opacity: 0; height: 0; line-height: 56px; overflow: hidden; -webkit-transition: all 0.3s ease-in; -moz-transition: all 0.3s ease-in; transition: all 0.3s ease-in } +.fa-trash:before { + content: "" +} -.wy-tray-container li.wy-tray-item-success { background: #27ae60 } +.fa-copyright:before { + content: "" +} -.wy-tray-container li.wy-tray-item-info { background: #dd4814 } +.fa-at:before { + content: "" +} -.wy-tray-container li.wy-tray-item-warning { background: #e67e22 } +.fa-eyedropper:before { + content: "" +} -.wy-tray-container li.wy-tray-item-danger { background: #e74c3c } +.fa-paint-brush:before { + content: "" +} -.wy-tray-container li.on { opacity: 1; height: 56px } +.fa-birthday-cake:before { + content: "" +} -@media screen and (max-width: 768px) { - .wy-tray-container { bottom: auto; top: 0; width: 100% } +.fa-area-chart:before { + content: "" +} - .wy-tray-container li { width: 100% } +.fa-pie-chart:before { + content: "" } -button { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; cursor: pointer; line-height: normal; -webkit-appearance: button; *overflow: visible } +.fa-line-chart:before { + content: "" +} -button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0 } +.fa-lastfm:before { + content: "" +} -button[disabled] { cursor: default } +.fa-lastfm-square:before { + content: "" +} -.btn { display: inline-block; border-radius: 2px; line-height: normal; white-space: nowrap; text-align: center; cursor: pointer; font-size: 100%; padding: 6px 12px 8px 12px; color: #ffffff; border: 1px solid rgba(0, 0, 0, 0.1); background-color: #27ae60; text-decoration: none; font-weight: normal; font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; box-shadow: 0px 1px 2px -1px rgba(255, 255, 255, 0.5) inset, 0px -2px 0px 0px rgba(0, 0, 0, 0.1) inset; outline-none: false; vertical-align: middle; *display: inline; zoom: 1; -webkit-user-drag: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-transition: all 0.1s linear; -moz-transition: all 0.1s linear; transition: all 0.1s linear } +.fa-toggle-off:before { + content: "" +} -.btn-hover { background: #2e8ece; color: #ffffff } +.fa-toggle-on:before { + content: "" +} -.btn:hover { background: #2cc36b; color: #ffffff } +.fa-bicycle:before { + content: "" +} -.btn:focus { background: #2cc36b; outline: 0 } +.fa-bus:before { + content: "" +} -.btn:active { box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.05) inset, 0px 2px 0px 0px rgba(0, 0, 0, 0.1) inset; padding: 8px 12px 6px 12px } +.fa-ioxhost:before { + content: "" +} -.btn:visited { color: #ffffff } +.fa-angellist:before { + content: "" +} -.btn:disabled { background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); filter: alpha(opacity=40); opacity: 0.4; cursor: not-allowed; box-shadow: none } +.fa-cc:before { + content: "" +} -.btn-disabled { background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); filter: alpha(opacity=40); opacity: 0.4; cursor: not-allowed; box-shadow: none } +.fa-shekel:before, .fa-sheqel:before, .fa-ils:before { + content: "" +} -.btn-disabled:hover, .btn-disabled:focus, .btn-disabled:active { background-image: none; filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); filter: alpha(opacity=40); opacity: 0.4; cursor: not-allowed; box-shadow: none } +.fa-meanpath:before { + content: "" +} -.btn::-moz-focus-inner { padding: 0; border: 0 } +.fa-buysellads:before { + content: "" +} -.btn-small { font-size: 80% } +.fa-connectdevelop:before { + content: "" +} -.btn-info { background-color: #dd4814 !important } +.fa-dashcube:before { + content: "" +} -.btn-info:hover { background-color: #2e8ece !important } +.fa-forumbee:before { + content: "" +} -.btn-neutral { background-color: #f3f6f6 !important; color: #404040 !important } +.fa-leanpub:before { + content: "" +} -.btn-neutral:hover { background-color: #e5ebeb !important; color: #404040 } +.fa-sellsy:before { + content: "" +} -.btn-neutral:visited { color: #404040 !important } +.fa-shirtsinbulk:before { + content: "" +} -.btn-success { background-color: #27ae60 !important } +.fa-simplybuilt:before { + content: "" +} -.btn-success:hover { background-color: #229955 !important } +.fa-skyatlas:before { + content: "" +} -.btn-danger { background-color: #e74c3c !important } +.fa-cart-plus:before { + content: "" +} -.btn-danger:hover { background-color: #ea6153 !important } +.fa-cart-arrow-down:before { + content: "" +} -.btn-warning { background-color: #e67e22 !important } +.fa-diamond:before { + content: "" +} -.btn-warning:hover { background-color: #e98b39 !important } +.fa-ship:before { + content: "" +} -.btn-invert { background-color: #222222 } +.fa-user-secret:before { + content: "" +} -.btn-invert:hover { background-color: #2f2f2f !important } +.fa-motorcycle:before { + content: "" +} -.btn-link { background-color: transparent !important; color: #dd4814; box-shadow: none; border-color: transparent !important } +.fa-street-view:before { + content: "" +} -.btn-link:hover { background-color: transparent !important; color: #409ad5 !important; box-shadow: none } +.fa-heartbeat:before { + content: "" +} -.btn-link:active { background-color: transparent !important; color: #409ad5 !important; box-shadow: none } +.fa-venus:before { + content: "" +} -.btn-link:visited { color: #97310e } +.fa-mars:before { + content: "" +} -.wy-btn-group .btn, .wy-control .btn { vertical-align: middle } +.fa-mercury:before { + content: "" +} -.wy-btn-group { margin-bottom: 24px; *zoom: 1 } +.fa-intersex:before, .fa-transgender:before { + content: "" +} -.wy-btn-group:before, .wy-btn-group:after { display: table; content: "" } +.fa-transgender-alt:before { + content: "" +} -.wy-btn-group:after { clear: both } +.fa-venus-double:before { + content: "" +} -.wy-dropdown { position: relative; display: inline-block } +.fa-mars-double:before { + content: "" +} -.wy-dropdown-active .wy-dropdown-menu { display: block } +.fa-venus-mars:before { + content: "" +} -.wy-dropdown-menu { position: absolute; left: 0; display: none; float: left; top: 100%; min-width: 100%; background: #fcfcfc; z-index: 100; border: solid 1px #cfd7dd; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); padding: 12px } +.fa-mars-stroke:before { + content: "" +} -.wy-dropdown-menu > dd > a { display: block; clear: both; color: #404040; white-space: nowrap; font-size: 90%; padding: 0 12px; cursor: pointer } +.fa-mars-stroke-v:before { + content: "" +} -.wy-dropdown-menu > dd > a:hover { background: #dd4814; color: #ffffff } +.fa-mars-stroke-h:before { + content: "" +} -.wy-dropdown-menu > dd.divider { border-top: solid 1px #cfd7dd; margin: 6px 0 } +.fa-neuter:before { + content: "" +} -.wy-dropdown-menu > dd.search { padding-bottom: 12px } +.fa-genderless:before { + content: "" +} -.wy-dropdown-menu > dd.search input[type="search"] { width: 100% } +.fa-facebook-official:before { + content: "" +} -.wy-dropdown-menu > dd.call-to-action { background: #e3e3e3; text-transform: uppercase; font-weight: 500; font-size: 80% } +.fa-pinterest-p:before { + content: "" +} -.wy-dropdown-menu > dd.call-to-action:hover { background: #e3e3e3 } +.fa-whatsapp:before { + content: "" +} -.wy-dropdown-menu > dd.call-to-action .btn { color: #ffffff } +.fa-server:before { + content: "" +} -.wy-dropdown.wy-dropdown-up .wy-dropdown-menu { bottom: 100%; top: auto; left: auto; right: 0 } +.fa-user-plus:before { + content: "" +} -.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { background: #fcfcfc; margin-top: 2px } +.fa-user-times:before { + content: "" +} -.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a { padding: 6px 12px } +.fa-hotel:before, .fa-bed:before { + content: "" +} -.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { background: #dd4814; color: #ffffff } +.fa-viacoin:before { + content: "" +} -.wy-dropdown.wy-dropdown-left .wy-dropdown-menu { right: 0; text-align: right } +.fa-train:before { + content: "" +} -.wy-dropdown-arrow:before { content: " "; border-bottom: 5px solid #f5f5f5; border-left: 5px solid transparent; border-right: 5px solid transparent; position: absolute; display: block; top: -4px; left: 50%; margin-left: -3px } +.fa-subway:before { + content: "" +} -.wy-dropdown-arrow.wy-dropdown-arrow-left:before { left: 11px } +.fa-medium:before { + content: "" +} -.wy-form-stacked select { display: block } +.fa-yc:before, .fa-y-combinator:before { + content: "" +} -.wy-form-aligned input, .wy-form-aligned textarea, .wy-form-aligned select, .wy-form-aligned .wy-help-inline, .wy-form-aligned label { display: inline-block; *display: inline; *zoom: 1; vertical-align: middle } +.fa-optin-monster:before { + content: "" +} -.wy-form-aligned .wy-control-group > label { display: inline-block; vertical-align: middle; width: 10em; margin: 6px 12px 0 0; float: left } +.fa-opencart:before { + content: "" +} -.wy-form-aligned .wy-control { float: left } +.fa-expeditedssl:before { + content: "" +} -.wy-form-aligned .wy-control label { display: block } +.fa-battery-4:before, .fa-battery-full:before { + content: "" +} -.wy-form-aligned .wy-control select { margin-top: 6px } +.fa-battery-3:before, .fa-battery-three-quarters:before { + content: "" +} -fieldset { border: 0; margin: 0; padding: 0 } +.fa-battery-2:before, .fa-battery-half:before { + content: "" +} -legend { display: block; width: 100%; border: 0; padding: 0; white-space: normal; margin-bottom: 24px; font-size: 150%; *margin-left: -7px } +.fa-battery-1:before, .fa-battery-quarter:before { + content: "" +} -label { display: block; margin: 0 0 0.3125em 0; color: #333333; font-size: 90% } +.fa-battery-0:before, .fa-battery-empty:before { + content: "" +} -input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle } +.fa-mouse-pointer:before { + content: "" +} -.wy-control-group { margin-bottom: 24px; *zoom: 1; max-width: 68em; margin-left: auto; margin-right: auto; *zoom: 1 } +.fa-i-cursor:before { + content: "" +} -.wy-control-group:before, .wy-control-group:after { display: table; content: "" } +.fa-object-group:before { + content: "" +} -.wy-control-group:after { clear: both } +.fa-object-ungroup:before { + content: "" +} -.wy-control-group:before, .wy-control-group:after { display: table; content: "" } +.fa-sticky-note:before { + content: "" +} -.wy-control-group:after { clear: both } +.fa-sticky-note-o:before { + content: "" +} -.wy-control-group.wy-control-group-required > label:after { content: " *"; color: #e74c3c } +.fa-cc-jcb:before { + content: "" +} -.wy-control-group .wy-form-full, .wy-control-group .wy-form-halves, .wy-control-group .wy-form-thirds { padding-bottom: 12px } +.fa-cc-diners-club:before { + content: "" +} -.wy-control-group .wy-form-full select, .wy-control-group .wy-form-halves select, .wy-control-group .wy-form-thirds select { width: 100% } +.fa-clone:before { + content: "" +} -.wy-control-group .wy-form-full input[type="text"], .wy-control-group .wy-form-full input[type="password"], .wy-control-group .wy-form-full input[type="email"], .wy-control-group .wy-form-full input[type="url"], .wy-control-group .wy-form-full input[type="date"], .wy-control-group .wy-form-full input[type="month"], .wy-control-group .wy-form-full input[type="time"], .wy-control-group .wy-form-full input[type="datetime"], .wy-control-group .wy-form-full input[type="datetime-local"], .wy-control-group .wy-form-full input[type="week"], .wy-control-group .wy-form-full input[type="number"], .wy-control-group .wy-form-full input[type="search"], .wy-control-group .wy-form-full input[type="tel"], .wy-control-group .wy-form-full input[type="color"], .wy-control-group .wy-form-halves input[type="text"], .wy-control-group .wy-form-halves input[type="password"], .wy-control-group .wy-form-halves input[type="email"], .wy-control-group .wy-form-halves input[type="url"], .wy-control-group .wy-form-halves input[type="date"], .wy-control-group .wy-form-halves input[type="month"], .wy-control-group .wy-form-halves input[type="time"], .wy-control-group .wy-form-halves input[type="datetime"], .wy-control-group .wy-form-halves input[type="datetime-local"], .wy-control-group .wy-form-halves input[type="week"], .wy-control-group .wy-form-halves input[type="number"], .wy-control-group .wy-form-halves input[type="search"], .wy-control-group .wy-form-halves input[type="tel"], .wy-control-group .wy-form-halves input[type="color"], .wy-control-group .wy-form-thirds input[type="text"], .wy-control-group .wy-form-thirds input[type="password"], .wy-control-group .wy-form-thirds input[type="email"], .wy-control-group .wy-form-thirds input[type="url"], .wy-control-group .wy-form-thirds input[type="date"], .wy-control-group .wy-form-thirds input[type="month"], .wy-control-group .wy-form-thirds input[type="time"], .wy-control-group .wy-form-thirds input[type="datetime"], .wy-control-group .wy-form-thirds input[type="datetime-local"], .wy-control-group .wy-form-thirds input[type="week"], .wy-control-group .wy-form-thirds input[type="number"], .wy-control-group .wy-form-thirds input[type="search"], .wy-control-group .wy-form-thirds input[type="tel"], .wy-control-group .wy-form-thirds input[type="color"] { width: 100% } +.fa-balance-scale:before { + content: "" +} -.wy-control-group .wy-form-full { float: left; display: block; margin-right: 2.35765%; width: 100%; margin-right: 0 } +.fa-hourglass-o:before { + content: "" +} -.wy-control-group .wy-form-full:last-child { margin-right: 0 } +.fa-hourglass-1:before, .fa-hourglass-start:before { + content: "" +} -.wy-control-group .wy-form-halves { float: left; display: block; margin-right: 2.35765%; width: 48.82117% } +.fa-hourglass-2:before, .fa-hourglass-half:before { + content: "" +} -.wy-control-group .wy-form-halves:last-child { margin-right: 0 } +.fa-hourglass-3:before, .fa-hourglass-end:before { + content: "" +} -.wy-control-group .wy-form-halves:nth-of-type(2n) { margin-right: 0 } +.fa-hourglass:before { + content: "" +} -.wy-control-group .wy-form-halves:nth-of-type(2n+1) { clear: left } +.fa-hand-grab-o:before, .fa-hand-rock-o:before { + content: "" +} -.wy-control-group .wy-form-thirds { float: left; display: block; margin-right: 2.35765%; width: 31.76157% } +.fa-hand-stop-o:before, .fa-hand-paper-o:before { + content: "" +} -.wy-control-group .wy-form-thirds:last-child { margin-right: 0 } +.fa-hand-scissors-o:before { + content: "" +} -.wy-control-group .wy-form-thirds:nth-of-type(3n) { margin-right: 0 } +.fa-hand-lizard-o:before { + content: "" +} -.wy-control-group .wy-form-thirds:nth-of-type(3n+1) { clear: left } +.fa-hand-spock-o:before { + content: "" +} -.wy-control-group.wy-control-group-no-input .wy-control { margin: 6px 0 0 0; font-size: 90% } +.fa-hand-pointer-o:before { + content: "" +} -.wy-control-no-input { display: inline-block; margin: 6px 0 0 0; font-size: 90% } +.fa-hand-peace-o:before { + content: "" +} -.wy-control-group.fluid-input input[type="text"], .wy-control-group.fluid-input input[type="password"], .wy-control-group.fluid-input input[type="email"], .wy-control-group.fluid-input input[type="url"], .wy-control-group.fluid-input input[type="date"], .wy-control-group.fluid-input input[type="month"], .wy-control-group.fluid-input input[type="time"], .wy-control-group.fluid-input input[type="datetime"], .wy-control-group.fluid-input input[type="datetime-local"], .wy-control-group.fluid-input input[type="week"], .wy-control-group.fluid-input input[type="number"], .wy-control-group.fluid-input input[type="search"], .wy-control-group.fluid-input input[type="tel"], .wy-control-group.fluid-input input[type="color"] { width: 100% } +.fa-trademark:before { + content: "" +} -.wy-form-message-inline { display: inline-block; padding-left: 0.3em; color: #666666; vertical-align: middle; font-size: 90% } +.fa-registered:before { + content: "" +} -.wy-form-message { display: block; color: #999999; font-size: 70%; margin-top: 0.3125em; font-style: italic } +.fa-creative-commons:before { + content: "" +} -input { line-height: normal } +.fa-gg:before { + content: "" +} -input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; cursor: pointer; font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; *overflow: visible } +.fa-gg-circle:before { + content: "" +} -input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"] { -webkit-appearance: none; padding: 6px; display: inline-block; border: 1px solid #cccccc; font-size: 80%; font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; box-shadow: inset 0 1px 3px #dddddd; border-radius: 0; -webkit-transition: border 0.3s linear; -moz-transition: border 0.3s linear; transition: border 0.3s linear } +.fa-tripadvisor:before { + content: "" +} -input[type="datetime-local"] { padding: 0.34375em 0.625em } +.fa-odnoklassniki:before { + content: "" +} -input[disabled] { cursor: default } +.fa-odnoklassniki-square:before { + content: "" +} -input[type="checkbox"], input[type="radio"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 0; margin-right: 0.3125em; *height: 13px; *width: 13px } +.fa-get-pocket:before { + content: "" +} -input[type="search"] { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box } +.fa-wikipedia-w:before { + content: "" +} -input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none } +.fa-safari:before { + content: "" +} -input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus { outline: 0; outline: thin dotted \9; border-color: #333333 } +.fa-chrome:before { + content: "" +} -input.no-focus:focus { border-color: #cccccc !important } +.fa-firefox:before { + content: "" +} -input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { outline: thin dotted #333333; outline: 1px auto #129fea } +.fa-opera:before { + content: "" +} -input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled] { cursor: not-allowed; background-color: #f3f6f6; color: #cad2d3 } +.fa-internet-explorer:before { + content: "" +} -input:focus:invalid, textarea:focus:invalid, select:focus:invalid { color: #e74c3c; border: 1px solid #e74c3c } +.fa-tv:before, .fa-television:before { + content: "" +} -input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:focus { border-color: #e74c3c } +.fa-contao:before { + content: "" +} -input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus { outline-color: #e74c3c } +.fa-500px:before { + content: "" +} -input.wy-input-large { padding: 12px; font-size: 100% } +.fa-amazon:before { + content: "" +} -textarea { overflow: auto; vertical-align: top; width: 100%; font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif } +.fa-calendar-plus-o:before { + content: "" +} -select, textarea { padding: 0.5em 0.625em; display: inline-block; border: 1px solid #cccccc; font-size: 80%; box-shadow: inset 0 1px 3px #dddddd; -webkit-transition: border 0.3s linear; -moz-transition: border 0.3s linear; transition: border 0.3s linear } +.fa-calendar-minus-o:before { + content: "" +} -select { border: 1px solid #cccccc; background-color: #ffffff } +.fa-calendar-times-o:before { + content: "" +} -select[multiple] { height: auto } +.fa-calendar-check-o:before { + content: "" +} -select:focus, textarea:focus { outline: 0 } +.fa-industry:before { + content: "" +} -select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { cursor: not-allowed; background-color: #ffffff; color: #cad2d3; border-color: transparent } +.fa-map-pin:before { + content: "" +} -.wy-checkbox, .wy-radio { margin: 6px 0; color: #404040; display: block } +.fa-map-signs:before { + content: "" +} -.wy-checkbox input, .wy-radio input { vertical-align: baseline } +.fa-map-o:before { + content: "" +} -.wy-form-message-inline { display: inline-block; *display: inline; *zoom: 1; vertical-align: middle } +.fa-map:before { + content: "" +} -.wy-input-prefix, .wy-input-suffix { white-space: nowrap; padding: 6px } +.fa-commenting:before { + content: "" +} -.wy-input-prefix .wy-input-context, .wy-input-suffix .wy-input-context { line-height: 27px; padding: 0 8px; display: inline-block; font-size: 80%; background-color: #f3f6f6; border: solid 1px #cccccc; color: #999999 } +.fa-commenting-o:before { + content: "" +} -.wy-input-suffix .wy-input-context { border-left: 0 } +.fa-houzz:before { + content: "" +} -.wy-input-prefix .wy-input-context { border-right: 0 } +.fa-vimeo:before { + content: "" +} -.wy-control-group.wy-control-group-error .wy-form-message, .wy-control-group.wy-control-group-error > label { color: #e74c3c } +.fa-black-tie:before { + content: "" +} -.wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="color"] { border: solid 1px #e74c3c } +.fa-fonticons:before { + content: "" +} -.wy-control-group.wy-control-group-error textarea { border: solid 1px #e74c3c } +.fa-reddit-alien:before { + content: "" +} -.wy-inline-validate { white-space: nowrap } +.fa-edge:before { + content: "" +} -.wy-inline-validate .wy-input-context { padding: 0.5em 0.625em; display: inline-block; font-size: 80% } +.fa-credit-card-alt:before { + content: "" +} -.wy-inline-validate.wy-inline-validate-success .wy-input-context { color: #27ae60 } +.fa-codiepie:before { + content: "" +} -.wy-inline-validate.wy-inline-validate-danger .wy-input-context { color: #e74c3c } +.fa-modx:before { + content: "" +} -.wy-inline-validate.wy-inline-validate-warning .wy-input-context { color: #e67e22 } +.fa-fort-awesome:before { + content: "" +} -.wy-inline-validate.wy-inline-validate-info .wy-input-context { color: #dd4814 } +.fa-usb:before { + content: "" +} -.rotate-90 { -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg) } +.fa-product-hunt:before { + content: "" +} -.rotate-180 { -webkit-transform: rotate(180deg); -moz-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg) } +.fa-mixcloud:before { + content: "" +} -.rotate-270 { -webkit-transform: rotate(270deg); -moz-transform: rotate(270deg); -ms-transform: rotate(270deg); -o-transform: rotate(270deg); transform: rotate(270deg) } +.fa-scribd:before { + content: "" +} -.mirror { -webkit-transform: scaleX(-1); -moz-transform: scaleX(-1); -ms-transform: scaleX(-1); -o-transform: scaleX(-1); transform: scaleX(-1) } +.fa-pause-circle:before { + content: "" +} -.mirror.rotate-90 { -webkit-transform: scaleX(-1) rotate(90deg); -moz-transform: scaleX(-1) rotate(90deg); -ms-transform: scaleX(-1) rotate(90deg); -o-transform: scaleX(-1) rotate(90deg); transform: scaleX(-1) rotate(90deg) } +.fa-pause-circle-o:before { + content: "" +} -.mirror.rotate-180 { -webkit-transform: scaleX(-1) rotate(180deg); -moz-transform: scaleX(-1) rotate(180deg); -ms-transform: scaleX(-1) rotate(180deg); -o-transform: scaleX(-1) rotate(180deg); transform: scaleX(-1) rotate(180deg) } +.fa-stop-circle:before { + content: "" +} -.mirror.rotate-270 { -webkit-transform: scaleX(-1) rotate(270deg); -moz-transform: scaleX(-1) rotate(270deg); -ms-transform: scaleX(-1) rotate(270deg); -o-transform: scaleX(-1) rotate(270deg); transform: scaleX(-1) rotate(270deg) } +.fa-stop-circle-o:before { + content: "" +} -@media only screen and (max-width: 480px) { - .wy-form button[type="submit"] { margin: 0.7em 0 0 } +.fa-shopping-bag:before { + content: "" +} - .wy-form input[type="text"], .wy-form input[type="password"], .wy-form input[type="email"], .wy-form input[type="url"], .wy-form input[type="date"], .wy-form input[type="month"], .wy-form input[type="time"], .wy-form input[type="datetime"], .wy-form input[type="datetime-local"], .wy-form input[type="week"], .wy-form input[type="number"], .wy-form input[type="search"], .wy-form input[type="tel"], .wy-form input[type="color"] { margin-bottom: 0.3em; display: block } +.fa-shopping-basket:before { + content: "" +} - .wy-form label { margin-bottom: 0.3em; display: block } +.fa-hashtag:before { + content: "" +} - .wy-form input[type="password"], .wy-form input[type="email"], .wy-form input[type="url"], .wy-form input[type="date"], .wy-form input[type="month"], .wy-form input[type="time"], .wy-form input[type="datetime"], .wy-form input[type="datetime-local"], .wy-form input[type="week"], .wy-form input[type="number"], .wy-form input[type="search"], .wy-form input[type="tel"], .wy-form input[type="color"] { margin-bottom: 0 } +.fa-bluetooth:before { + content: "" +} - .wy-form-aligned .wy-control-group label { margin-bottom: 0.3em; text-align: left; display: block; width: 100% } +.fa-bluetooth-b:before { + content: "" +} - .wy-form-aligned .wy-control { margin: 1.5em 0 0 0 } +.fa-percent:before { + content: "" +} - .wy-form .wy-help-inline, .wy-form-message-inline, .wy-form-message { display: block; font-size: 80%; padding: 6px 0 } +.fa-gitlab:before, .icon-gitlab:before { + content: "" } -@media screen and (max-width: 768px) { - .tablet-hide { display: none } +.fa-wpbeginner:before { + content: "" +} - .wy-table-responsive table td, .wy-table-responsive table th { white-space: nowrap } +.fa-wpforms:before { + content: "" } -@media screen and (max-width: 480px) { - .mobile-hide { display: none } +.fa-envira:before { + content: "" } -.float-left { float: left } +.fa-universal-access:before { + content: "" +} -.float-right { float: right } +.fa-wheelchair-alt:before { + content: "" +} -.full-width { width: 100% } +.fa-question-circle-o:before { + content: "" +} -.wy-table, .rst-content table.docutils, .rst-content table.field-list { border-collapse: collapse; border-spacing: 0; empty-cells: show; margin-bottom: 24px } +.fa-blind:before { + content: "" +} -.wy-table caption, .rst-content table.docutils caption, .rst-content table.field-list caption { color: #000000; font: italic 85%/1 arial, sans-serif; padding: 1em 0; text-align: center } +.fa-audio-description:before { + content: "" +} -.wy-table td, .rst-content table.docutils td, .rst-content table.field-list td, .wy-table th, .rst-content table.docutils th, .rst-content table.field-list th { font-size: 90%; margin: 0; overflow: visible; padding: 8px 16px } +.fa-volume-control-phone:before { + content: "" +} -.wy-table td:first-child, .rst-content table.docutils td:first-child, .rst-content table.field-list td:first-child, .wy-table th:first-child, .rst-content table.docutils th:first-child, .rst-content table.field-list th:first-child { border-left-width: 0 } +.fa-braille:before { + content: "" +} -.wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead { color: #000000; text-align: left; vertical-align: bottom; white-space: nowrap } +.fa-assistive-listening-systems:before { + content: "" +} -.wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th { font-weight: bold; border-bottom: solid 2px #e1e4e5 } +.fa-asl-interpreting:before, .fa-american-sign-language-interpreting:before { + content: "" +} -.wy-table td, .rst-content table.docutils td, .rst-content table.field-list td { background-color: transparent; vertical-align: middle } +.fa-deafness:before, .fa-hard-of-hearing:before, .fa-deaf:before { + content: "" +} -.wy-table td p, .rst-content table.docutils td p, .rst-content table.field-list td p { line-height: 18px } +.fa-glide:before { + content: "" +} -.wy-table td p:last-child, .rst-content table.docutils td p:last-child, .rst-content table.field-list td p:last-child { margin-bottom: 0 } +.fa-glide-g:before { + content: "" +} -.wy-table .wy-table-cell-min, .rst-content table.docutils .wy-table-cell-min, .rst-content table.field-list .wy-table-cell-min { width: 1%; padding-right: 0 } +.fa-signing:before, .fa-sign-language:before { + content: "" +} -.wy-table .wy-table-cell-min input[type=checkbox], .rst-content table.docutils .wy-table-cell-min input[type=checkbox], .rst-content table.field-list .wy-table-cell-min input[type=checkbox], .wy-table .wy-table-cell-min input[type=checkbox], .rst-content table.docutils .wy-table-cell-min input[type=checkbox], .rst-content table.field-list .wy-table-cell-min input[type=checkbox] { margin: 0 } +.fa-low-vision:before { + content: "" +} -.wy-table-secondary { color: gray; font-size: 90% } +.fa-viadeo:before { + content: "" +} -.wy-table-tertiary { color: gray; font-size: 80% } +.fa-viadeo-square:before { + content: "" +} -.wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { background-color: #f3f6f6 } +.fa-snapchat:before { + content: "" +} -.wy-table-backed { background-color: #f3f6f6 } +.fa-snapchat-ghost:before { + content: "" +} -.wy-table-bordered-all, .rst-content table.docutils { border: 1px solid #e1e4e5 } +.fa-snapchat-square:before { + content: "" +} -.wy-table-bordered-all td, .rst-content table.docutils td { border-bottom: 1px solid #e1e4e5; border-left: 1px solid #e1e4e5 } +.fa-pied-piper:before { + content: "" +} -.wy-table-bordered-all tbody > tr:last-child td, .rst-content table.docutils tbody > tr:last-child td { border-bottom-width: 0 } +.fa-first-order:before { + content: "" +} -.wy-table-bordered { border: 1px solid #e1e4e5 } +.fa-yoast:before { + content: "" +} -.wy-table-bordered-rows td { border-bottom: 1px solid #e1e4e5 } +.fa-themeisle:before { + content: "" +} -.wy-table-bordered-rows tbody > tr:last-child td { border-bottom-width: 0 } +.fa-google-plus-circle:before, .fa-google-plus-official:before { + content: "" +} -.wy-table-horizontal tbody > tr:last-child td { border-bottom-width: 0 } +.fa-fa:before, .fa-font-awesome:before { + content: "" +} -.wy-table-horizontal td, .wy-table-horizontal th { border-width: 0 0 1px 0; border-bottom: 1px solid #e1e4e5 } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0 +} -.wy-table-horizontal tbody > tr:last-child td { border-bottom-width: 0 } +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto +} -.wy-table-responsive { margin-bottom: 24px; max-width: 100%; overflow: auto } +.fa, .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.current > a span.toctree-expand, .rst-content .admonition-title, .rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink, .rst-content p.caption .headerlink, .rst-content tt.download span:first-child, .rst-content code.download span:first-child, .icon, .wy-dropdown .caret, .wy-inline-validate.wy-inline-validate-success .wy-input-context, .wy-inline-validate.wy-inline-validate-danger .wy-input-context, .wy-inline-validate.wy-inline-validate-warning .wy-input-context, .wy-inline-validate.wy-inline-validate-info .wy-input-context { + font-family: inherit +} -.wy-table-responsive table { margin-bottom: 0 !important } +.fa:before, .wy-menu-vertical li span.toctree-expand:before, .wy-menu-vertical li.on a span.toctree-expand:before, .wy-menu-vertical li.current > a span.toctree-expand:before, .rst-content .admonition-title:before, .rst-content h1 .headerlink:before, .rst-content h2 .headerlink:before, .rst-content h3 .headerlink:before, .rst-content h4 .headerlink:before, .rst-content h5 .headerlink:before, .rst-content h6 .headerlink:before, .rst-content dl dt .headerlink:before, .rst-content p.caption .headerlink:before, .rst-content tt.download span:first-child:before, .rst-content code.download span:first-child:before, .icon:before, .wy-dropdown .caret:before, .wy-inline-validate.wy-inline-validate-success .wy-input-context:before, .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, .wy-inline-validate.wy-inline-validate-info .wy-input-context:before { + font-family: "FontAwesome"; + display: inline-block; + font-style: normal; + font-weight: normal; + line-height: 1; + text-decoration: inherit +} -a, a:visited { color: #dd4814; text-decoration: none; cursor: pointer } +a .fa, a .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li a span.toctree-expand, .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.current > a span.toctree-expand, a .rst-content .admonition-title, .rst-content a .admonition-title, a .rst-content h1 .headerlink, .rst-content h1 a .headerlink, a .rst-content h2 .headerlink, .rst-content h2 a .headerlink, a .rst-content h3 .headerlink, .rst-content h3 a .headerlink, a .rst-content h4 .headerlink, .rst-content h4 a .headerlink, a .rst-content h5 .headerlink, .rst-content h5 a .headerlink, a .rst-content h6 .headerlink, .rst-content h6 a .headerlink, a .rst-content dl dt .headerlink, .rst-content dl dt a .headerlink, a .rst-content p.caption .headerlink, .rst-content p.caption a .headerlink, a .rst-content tt.download span:first-child, .rst-content tt.download a span:first-child, a .rst-content code.download span:first-child, .rst-content code.download a span:first-child, a .icon { + display: inline-block; + text-decoration: inherit +} -a:hover { color: #97310e } +.btn .fa, .btn .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li .btn span.toctree-expand, .btn .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.on a .btn span.toctree-expand, .btn .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.current > a .btn span.toctree-expand, .btn .rst-content .admonition-title, .rst-content .btn .admonition-title, .btn .rst-content h1 .headerlink, .rst-content h1 .btn .headerlink, .btn .rst-content h2 .headerlink, .rst-content h2 .btn .headerlink, .btn .rst-content h3 .headerlink, .rst-content h3 .btn .headerlink, .btn .rst-content h4 .headerlink, .rst-content h4 .btn .headerlink, .btn .rst-content h5 .headerlink, .rst-content h5 .btn .headerlink, .btn .rst-content h6 .headerlink, .rst-content h6 .btn .headerlink, .btn .rst-content dl dt .headerlink, .rst-content dl dt .btn .headerlink, .btn .rst-content p.caption .headerlink, .rst-content p.caption .btn .headerlink, .btn .rst-content tt.download span:first-child, .rst-content tt.download .btn span:first-child, .btn .rst-content code.download span:first-child, .rst-content code.download .btn span:first-child, .btn .icon, .nav .fa, .nav .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li .nav span.toctree-expand, .nav .wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.on a .nav span.toctree-expand, .nav .wy-menu-vertical li.current > a span.toctree-expand, .wy-menu-vertical li.current > a .nav span.toctree-expand, .nav .rst-content .admonition-title, .rst-content .nav .admonition-title, .nav .rst-content h1 .headerlink, .rst-content h1 .nav .headerlink, .nav .rst-content h2 .headerlink, .rst-content h2 .nav .headerlink, .nav .rst-content h3 .headerlink, .rst-content h3 .nav .headerlink, .nav .rst-content h4 .headerlink, .rst-content h4 .nav .headerlink, .nav .rst-content h5 .headerlink, .rst-content h5 .nav .headerlink, .nav .rst-content h6 .headerlink, .rst-content h6 .nav .headerlink, .nav .rst-content dl dt .headerlink, .rst-content dl dt .nav .headerlink, .nav .rst-content p.caption .headerlink, .rst-content p.caption .nav .headerlink, .nav .rst-content tt.download span:first-child, .rst-content tt.download .nav span:first-child, .nav .rst-content code.download span:first-child, .rst-content code.download .nav span:first-child, .nav .icon { + display: inline +} -html { height: 100%; overflow-x: hidden } +.btn .fa.fa-large, .btn .wy-menu-vertical li span.fa-large.toctree-expand, .wy-menu-vertical li .btn span.fa-large.toctree-expand, .btn .rst-content .fa-large.admonition-title, .rst-content .btn .fa-large.admonition-title, .btn .rst-content h1 .fa-large.headerlink, .rst-content h1 .btn .fa-large.headerlink, .btn .rst-content h2 .fa-large.headerlink, .rst-content h2 .btn .fa-large.headerlink, .btn .rst-content h3 .fa-large.headerlink, .rst-content h3 .btn .fa-large.headerlink, .btn .rst-content h4 .fa-large.headerlink, .rst-content h4 .btn .fa-large.headerlink, .btn .rst-content h5 .fa-large.headerlink, .rst-content h5 .btn .fa-large.headerlink, .btn .rst-content h6 .fa-large.headerlink, .rst-content h6 .btn .fa-large.headerlink, .btn .rst-content dl dt .fa-large.headerlink, .rst-content dl dt .btn .fa-large.headerlink, .btn .rst-content p.caption .fa-large.headerlink, .rst-content p.caption .btn .fa-large.headerlink, .btn .rst-content tt.download span.fa-large:first-child, .rst-content tt.download .btn span.fa-large:first-child, .btn .rst-content code.download span.fa-large:first-child, .rst-content code.download .btn span.fa-large:first-child, .btn .fa-large.icon, .nav .fa.fa-large, .nav .wy-menu-vertical li span.fa-large.toctree-expand, .wy-menu-vertical li .nav span.fa-large.toctree-expand, .nav .rst-content .fa-large.admonition-title, .rst-content .nav .fa-large.admonition-title, .nav .rst-content h1 .fa-large.headerlink, .rst-content h1 .nav .fa-large.headerlink, .nav .rst-content h2 .fa-large.headerlink, .rst-content h2 .nav .fa-large.headerlink, .nav .rst-content h3 .fa-large.headerlink, .rst-content h3 .nav .fa-large.headerlink, .nav .rst-content h4 .fa-large.headerlink, .rst-content h4 .nav .fa-large.headerlink, .nav .rst-content h5 .fa-large.headerlink, .rst-content h5 .nav .fa-large.headerlink, .nav .rst-content h6 .fa-large.headerlink, .rst-content h6 .nav .fa-large.headerlink, .nav .rst-content dl dt .fa-large.headerlink, .rst-content dl dt .nav .fa-large.headerlink, .nav .rst-content p.caption .fa-large.headerlink, .rst-content p.caption .nav .fa-large.headerlink, .nav .rst-content tt.download span.fa-large:first-child, .rst-content tt.download .nav span.fa-large:first-child, .nav .rst-content code.download span.fa-large:first-child, .rst-content code.download .nav span.fa-large:first-child, .nav .fa-large.icon { + line-height: 0.9em +} -body { font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; font-weight: normal; color: #404040; min-height: 100%; overflow-x: hidden; background: #edf0f2 } +.btn .fa.fa-spin, .btn .wy-menu-vertical li span.fa-spin.toctree-expand, .wy-menu-vertical li .btn span.fa-spin.toctree-expand, .btn .rst-content .fa-spin.admonition-title, .rst-content .btn .fa-spin.admonition-title, .btn .rst-content h1 .fa-spin.headerlink, .rst-content h1 .btn .fa-spin.headerlink, .btn .rst-content h2 .fa-spin.headerlink, .rst-content h2 .btn .fa-spin.headerlink, .btn .rst-content h3 .fa-spin.headerlink, .rst-content h3 .btn .fa-spin.headerlink, .btn .rst-content h4 .fa-spin.headerlink, .rst-content h4 .btn .fa-spin.headerlink, .btn .rst-content h5 .fa-spin.headerlink, .rst-content h5 .btn .fa-spin.headerlink, .btn .rst-content h6 .fa-spin.headerlink, .rst-content h6 .btn .fa-spin.headerlink, .btn .rst-content dl dt .fa-spin.headerlink, .rst-content dl dt .btn .fa-spin.headerlink, .btn .rst-content p.caption .fa-spin.headerlink, .rst-content p.caption .btn .fa-spin.headerlink, .btn .rst-content tt.download span.fa-spin:first-child, .rst-content tt.download .btn span.fa-spin:first-child, .btn .rst-content code.download span.fa-spin:first-child, .rst-content code.download .btn span.fa-spin:first-child, .btn .fa-spin.icon, .nav .fa.fa-spin, .nav .wy-menu-vertical li span.fa-spin.toctree-expand, .wy-menu-vertical li .nav span.fa-spin.toctree-expand, .nav .rst-content .fa-spin.admonition-title, .rst-content .nav .fa-spin.admonition-title, .nav .rst-content h1 .fa-spin.headerlink, .rst-content h1 .nav .fa-spin.headerlink, .nav .rst-content h2 .fa-spin.headerlink, .rst-content h2 .nav .fa-spin.headerlink, .nav .rst-content h3 .fa-spin.headerlink, .rst-content h3 .nav .fa-spin.headerlink, .nav .rst-content h4 .fa-spin.headerlink, .rst-content h4 .nav .fa-spin.headerlink, .nav .rst-content h5 .fa-spin.headerlink, .rst-content h5 .nav .fa-spin.headerlink, .nav .rst-content h6 .fa-spin.headerlink, .rst-content h6 .nav .fa-spin.headerlink, .nav .rst-content dl dt .fa-spin.headerlink, .rst-content dl dt .nav .fa-spin.headerlink, .nav .rst-content p.caption .fa-spin.headerlink, .rst-content p.caption .nav .fa-spin.headerlink, .nav .rst-content tt.download span.fa-spin:first-child, .rst-content tt.download .nav span.fa-spin:first-child, .nav .rst-content code.download span.fa-spin:first-child, .rst-content code.download .nav span.fa-spin:first-child, .nav .fa-spin.icon { + display: inline-block +} -.wy-text-left { text-align: left } +.btn.fa:before, .wy-menu-vertical li span.btn.toctree-expand:before, .rst-content .btn.admonition-title:before, .rst-content h1 .btn.headerlink:before, .rst-content h2 .btn.headerlink:before, .rst-content h3 .btn.headerlink:before, .rst-content h4 .btn.headerlink:before, .rst-content h5 .btn.headerlink:before, .rst-content h6 .btn.headerlink:before, .rst-content dl dt .btn.headerlink:before, .rst-content p.caption .btn.headerlink:before, .rst-content tt.download span.btn:first-child:before, .rst-content code.download span.btn:first-child:before, .btn.icon:before { + opacity: 0.5; + -webkit-transition: opacity 0.05s ease-in; + -moz-transition: opacity 0.05s ease-in; + transition: opacity 0.05s ease-in +} -.wy-text-center { text-align: center } +.btn.fa:hover:before, .wy-menu-vertical li span.btn.toctree-expand:hover:before, .rst-content .btn.admonition-title:hover:before, .rst-content h1 .btn.headerlink:hover:before, .rst-content h2 .btn.headerlink:hover:before, .rst-content h3 .btn.headerlink:hover:before, .rst-content h4 .btn.headerlink:hover:before, .rst-content h5 .btn.headerlink:hover:before, .rst-content h6 .btn.headerlink:hover:before, .rst-content dl dt .btn.headerlink:hover:before, .rst-content p.caption .btn.headerlink:hover:before, .rst-content tt.download span.btn:first-child:hover:before, .rst-content code.download span.btn:first-child:hover:before, .btn.icon:hover:before { + opacity: 1 +} -.wy-text-right { text-align: right } +.btn-mini .fa:before, .btn-mini .wy-menu-vertical li span.toctree-expand:before, .wy-menu-vertical li .btn-mini span.toctree-expand:before, .btn-mini .rst-content .admonition-title:before, .rst-content .btn-mini .admonition-title:before, .btn-mini .rst-content h1 .headerlink:before, .rst-content h1 .btn-mini .headerlink:before, .btn-mini .rst-content h2 .headerlink:before, .rst-content h2 .btn-mini .headerlink:before, .btn-mini .rst-content h3 .headerlink:before, .rst-content h3 .btn-mini .headerlink:before, .btn-mini .rst-content h4 .headerlink:before, .rst-content h4 .btn-mini .headerlink:before, .btn-mini .rst-content h5 .headerlink:before, .rst-content h5 .btn-mini .headerlink:before, .btn-mini .rst-content h6 .headerlink:before, .rst-content h6 .btn-mini .headerlink:before, .btn-mini .rst-content dl dt .headerlink:before, .rst-content dl dt .btn-mini .headerlink:before, .btn-mini .rst-content p.caption .headerlink:before, .rst-content p.caption .btn-mini .headerlink:before, .btn-mini .rst-content tt.download span:first-child:before, .rst-content tt.download .btn-mini span:first-child:before, .btn-mini .rst-content code.download span:first-child:before, .rst-content code.download .btn-mini span:first-child:before, .btn-mini .icon:before { + font-size: 14px; + vertical-align: -15% +} -.wy-text-large { font-size: 120% } +.wy-alert, .rst-content .note, .rst-content .attention, .rst-content .caution, .rst-content .danger, .rst-content .error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .warning, .rst-content .seealso, .rst-content .admonition-todo { + padding: 12px; + line-height: 24px; + margin-bottom: 24px; + background: #dedede +} -.wy-text-normal { font-size: 100% } +.wy-alert-title, .rst-content .admonition-title { + color: #fff; + font-weight: bold; + display: block; + color: #fff; + background: #8ba8af; + margin: -12px; + padding: 6px 12px; + margin-bottom: 12px +} -.wy-text-small, small { font-size: 80% } +.wy-alert.wy-alert-danger, .rst-content .wy-alert-danger.note, .rst-content .wy-alert-danger.attention, .rst-content .wy-alert-danger.caution, .rst-content .danger, .rst-content .error, .rst-content .wy-alert-danger.hint, .rst-content .wy-alert-danger.important, .rst-content .wy-alert-danger.tip, .rst-content .wy-alert-danger.warning, .rst-content .wy-alert-danger.seealso, .rst-content .wy-alert-danger.admonition-todo { + background: #fdf3f2 +} -.wy-text-strike { text-decoration: line-through } +.wy-alert.wy-alert-danger .wy-alert-title, .rst-content .wy-alert-danger.note .wy-alert-title, .rst-content .wy-alert-danger.attention .wy-alert-title, .rst-content .wy-alert-danger.caution .wy-alert-title, .rst-content .danger .wy-alert-title, .rst-content .error .wy-alert-title, .rst-content .wy-alert-danger.hint .wy-alert-title, .rst-content .wy-alert-danger.important .wy-alert-title, .rst-content .wy-alert-danger.tip .wy-alert-title, .rst-content .wy-alert-danger.warning .wy-alert-title, .rst-content .wy-alert-danger.seealso .wy-alert-title, .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, .wy-alert.wy-alert-danger .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-danger .admonition-title, .rst-content .wy-alert-danger.note .admonition-title, .rst-content .wy-alert-danger.attention .admonition-title, .rst-content .wy-alert-danger.caution .admonition-title, .rst-content .danger .admonition-title, .rst-content .error .admonition-title, .rst-content .wy-alert-danger.hint .admonition-title, .rst-content .wy-alert-danger.important .admonition-title, .rst-content .wy-alert-danger.tip .admonition-title, .rst-content .wy-alert-danger.warning .admonition-title, .rst-content .wy-alert-danger.seealso .admonition-title, .rst-content .wy-alert-danger.admonition-todo .admonition-title { + background: #f29f97 +} -.wy-text-warning { color: #e67e22 !important } +.wy-alert.wy-alert-warning, .rst-content .wy-alert-warning.note, .rst-content .attention, .rst-content .caution, .rst-content .wy-alert-warning.danger, .rst-content .wy-alert-warning.error, .rst-content .wy-alert-warning.hint, .rst-content .wy-alert-warning.important, .rst-content .wy-alert-warning.tip, .rst-content .warning, .rst-content .wy-alert-warning.seealso, .rst-content .admonition-todo { + background: #ffedcc +} -a.wy-text-warning:hover { color: #eb9950 !important } +.wy-alert.wy-alert-warning .wy-alert-title, .rst-content .wy-alert-warning.note .wy-alert-title, .rst-content .attention .wy-alert-title, .rst-content .caution .wy-alert-title, .rst-content .wy-alert-warning.danger .wy-alert-title, .rst-content .wy-alert-warning.error .wy-alert-title, .rst-content .wy-alert-warning.hint .wy-alert-title, .rst-content .wy-alert-warning.important .wy-alert-title, .rst-content .wy-alert-warning.tip .wy-alert-title, .rst-content .warning .wy-alert-title, .rst-content .wy-alert-warning.seealso .wy-alert-title, .rst-content .admonition-todo .wy-alert-title, .wy-alert.wy-alert-warning .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-warning .admonition-title, .rst-content .wy-alert-warning.note .admonition-title, .rst-content .attention .admonition-title, .rst-content .caution .admonition-title, .rst-content .wy-alert-warning.danger .admonition-title, .rst-content .wy-alert-warning.error .admonition-title, .rst-content .wy-alert-warning.hint .admonition-title, .rst-content .wy-alert-warning.important .admonition-title, .rst-content .wy-alert-warning.tip .admonition-title, .rst-content .warning .admonition-title, .rst-content .wy-alert-warning.seealso .admonition-title, .rst-content .admonition-todo .admonition-title { + background: #f0b37e +} -.wy-text-info { color: #dd4814 !important } +.wy-alert.wy-alert-info, .rst-content .note, .rst-content .wy-alert-info.attention, .rst-content .wy-alert-info.caution, .rst-content .wy-alert-info.danger, .rst-content .wy-alert-info.error, .rst-content .wy-alert-info.hint, .rst-content .wy-alert-info.important, .rst-content .wy-alert-info.tip, .rst-content .wy-alert-info.warning, .rst-content .seealso, .rst-content .wy-alert-info.admonition-todo { + background: #dedede +} -a.wy-text-info:hover { color: #409ad5 !important } +.wy-alert.wy-alert-info .wy-alert-title, .rst-content .note .wy-alert-title, .rst-content .wy-alert-info.attention .wy-alert-title, .rst-content .wy-alert-info.caution .wy-alert-title, .rst-content .wy-alert-info.danger .wy-alert-title, .rst-content .wy-alert-info.error .wy-alert-title, .rst-content .wy-alert-info.hint .wy-alert-title, .rst-content .wy-alert-info.important .wy-alert-title, .rst-content .wy-alert-info.tip .wy-alert-title, .rst-content .wy-alert-info.warning .wy-alert-title, .rst-content .seealso .wy-alert-title, .rst-content .wy-alert-info.admonition-todo .wy-alert-title, .wy-alert.wy-alert-info .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-info .admonition-title, .rst-content .note .admonition-title, .rst-content .wy-alert-info.attention .admonition-title, .rst-content .wy-alert-info.caution .admonition-title, .rst-content .wy-alert-info.danger .admonition-title, .rst-content .wy-alert-info.error .admonition-title, .rst-content .wy-alert-info.hint .admonition-title, .rst-content .wy-alert-info.important .admonition-title, .rst-content .wy-alert-info.tip .admonition-title, .rst-content .wy-alert-info.warning .admonition-title, .rst-content .seealso .admonition-title, .rst-content .wy-alert-info.admonition-todo .admonition-title { + background: #8ba8af +} -.wy-text-success { color: #27ae60 !important } +.wy-alert.wy-alert-success, .rst-content .wy-alert-success.note, .rst-content .wy-alert-success.attention, .rst-content .wy-alert-success.caution, .rst-content .wy-alert-success.danger, .rst-content .wy-alert-success.error, .rst-content .hint, .rst-content .important, .rst-content .tip, .rst-content .wy-alert-success.warning, .rst-content .wy-alert-success.seealso, .rst-content .wy-alert-success.admonition-todo { + background: #dedede +} -a.wy-text-success:hover { color: #36d278 !important } +.wy-alert.wy-alert-success .wy-alert-title, .rst-content .wy-alert-success.note .wy-alert-title, .rst-content .wy-alert-success.attention .wy-alert-title, .rst-content .wy-alert-success.caution .wy-alert-title, .rst-content .wy-alert-success.danger .wy-alert-title, .rst-content .wy-alert-success.error .wy-alert-title, .rst-content .hint .wy-alert-title, .rst-content .important .wy-alert-title, .rst-content .tip .wy-alert-title, .rst-content .wy-alert-success.warning .wy-alert-title, .rst-content .wy-alert-success.seealso .wy-alert-title, .rst-content .wy-alert-success.admonition-todo .wy-alert-title, .wy-alert.wy-alert-success .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-success .admonition-title, .rst-content .wy-alert-success.note .admonition-title, .rst-content .wy-alert-success.attention .admonition-title, .rst-content .wy-alert-success.caution .admonition-title, .rst-content .wy-alert-success.danger .admonition-title, .rst-content .wy-alert-success.error .admonition-title, .rst-content .hint .admonition-title, .rst-content .important .admonition-title, .rst-content .tip .admonition-title, .rst-content .wy-alert-success.warning .admonition-title, .rst-content .wy-alert-success.seealso .admonition-title, .rst-content .wy-alert-success.admonition-todo .admonition-title { + background: #dd4814 +} -.wy-text-danger { color: #e74c3c !important } +.wy-alert.wy-alert-neutral, .rst-content .wy-alert-neutral.note, .rst-content .wy-alert-neutral.attention, .rst-content .wy-alert-neutral.caution, .rst-content .wy-alert-neutral.danger, .rst-content .wy-alert-neutral.error, .rst-content .wy-alert-neutral.hint, .rst-content .wy-alert-neutral.important, .rst-content .wy-alert-neutral.tip, .rst-content .wy-alert-neutral.warning, .rst-content .wy-alert-neutral.seealso, .rst-content .wy-alert-neutral.admonition-todo { + background: #f3f6f6 +} -a.wy-text-danger:hover { color: #ed7669 !important } +.wy-alert.wy-alert-neutral .wy-alert-title, .rst-content .wy-alert-neutral.note .wy-alert-title, .rst-content .wy-alert-neutral.attention .wy-alert-title, .rst-content .wy-alert-neutral.caution .wy-alert-title, .rst-content .wy-alert-neutral.danger .wy-alert-title, .rst-content .wy-alert-neutral.error .wy-alert-title, .rst-content .wy-alert-neutral.hint .wy-alert-title, .rst-content .wy-alert-neutral.important .wy-alert-title, .rst-content .wy-alert-neutral.tip .wy-alert-title, .rst-content .wy-alert-neutral.warning .wy-alert-title, .rst-content .wy-alert-neutral.seealso .wy-alert-title, .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, .wy-alert.wy-alert-neutral .rst-content .admonition-title, .rst-content .wy-alert.wy-alert-neutral .admonition-title, .rst-content .wy-alert-neutral.note .admonition-title, .rst-content .wy-alert-neutral.attention .admonition-title, .rst-content .wy-alert-neutral.caution .admonition-title, .rst-content .wy-alert-neutral.danger .admonition-title, .rst-content .wy-alert-neutral.error .admonition-title, .rst-content .wy-alert-neutral.hint .admonition-title, .rst-content .wy-alert-neutral.important .admonition-title, .rst-content .wy-alert-neutral.tip .admonition-title, .rst-content .wy-alert-neutral.warning .admonition-title, .rst-content .wy-alert-neutral.seealso .admonition-title, .rst-content .wy-alert-neutral.admonition-todo .admonition-title { + color: #404040; + background: #e1e4e5 +} -.wy-text-neutral { color: #404040 !important } +.wy-alert.wy-alert-neutral a, .rst-content .wy-alert-neutral.note a, .rst-content .wy-alert-neutral.attention a, .rst-content .wy-alert-neutral.caution a, .rst-content .wy-alert-neutral.danger a, .rst-content .wy-alert-neutral.error a, .rst-content .wy-alert-neutral.hint a, .rst-content .wy-alert-neutral.important a, .rst-content .wy-alert-neutral.tip a, .rst-content .wy-alert-neutral.warning a, .rst-content .wy-alert-neutral.seealso a, .rst-content .wy-alert-neutral.admonition-todo a { + color: #dd4814 +} -a.wy-text-neutral:hover { color: #595959 !important } +.wy-alert p:last-child, .rst-content .note p:last-child, .rst-content .attention p:last-child, .rst-content .caution p:last-child, .rst-content .danger p:last-child, .rst-content .error p:last-child, .rst-content .hint p:last-child, .rst-content .important p:last-child, .rst-content .tip p:last-child, .rst-content .warning p:last-child, .rst-content .seealso p:last-child, .rst-content .admonition-todo p:last-child { + margin-bottom: 0 +} -h1, h2, h3, h4, h5, h6, legend { margin-top: 0; font-weight: 700; font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif } +.wy-tray-container { + position: fixed; + bottom: 0px; + left: 0; + z-index: 600 +} -p { line-height: 24px; margin: 0; font-size: 16px; margin-bottom: 24px } +.wy-tray-container li { + display: block; + width: 300px; + background: transparent; + color: #fff; + text-align: center; + box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.1); + padding: 0 24px; + min-width: 20%; + opacity: 0; + height: 0; + line-height: 56px; + overflow: hidden; + -webkit-transition: all 0.3s ease-in; + -moz-transition: all 0.3s ease-in; + transition: all 0.3s ease-in +} -h1 { font-size: 175% } +.wy-tray-container li.wy-tray-item-success { + background: #27AE60 +} -h2 { font-size: 150% } +.wy-tray-container li.wy-tray-item-info { + background: #dd4814 +} -h3 { font-size: 125% } +.wy-tray-container li.wy-tray-item-warning { + background: #E67E22 +} -h4 { font-size: 115% } +.wy-tray-container li.wy-tray-item-danger { + background: #E74C3C +} -h5 { font-size: 110% } +.wy-tray-container li.on { + opacity: 1; + height: 56px +} -h6 { font-size: 100% } +@media screen and (max-width: 768px) { + .wy-tray-container { + bottom: auto; + top: 0; + width: 100% + } -hr { display: block; height: 1px; border: 0; border-top: 1px solid #e1e4e5; margin: 24px 0; padding: 0 } + .wy-tray-container li { + width: 100% + } +} -code, .rst-content tt { white-space: nowrap; max-width: 100%; padding: 0 5px; font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; color: #e74c3c; overflow-x: auto } +button { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + cursor: pointer; + line-height: normal; + -webkit-appearance: button; + *overflow: visible +} -code.code-large, .rst-content tt.code-large { font-size: 90% } +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0 +} -.wy-plain-list-disc, .rst-content .section ul, .rst-content .toctree-wrapper ul, article ul { list-style: disc; line-height: 24px; margin-bottom: 24px } +button[disabled] { + cursor: default +} -.wy-plain-list-disc li, .rst-content .section ul li, .rst-content .toctree-wrapper ul li, article ul li { list-style: disc; margin-left: 24px } +.btn { + display: inline-block; + border-radius: 2px; + line-height: normal; + white-space: nowrap; + text-align: center; + cursor: pointer; + font-size: 100%; + padding: 6px 12px 8px 12px; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.1); + background-color: #27AE60; + text-decoration: none; + font-weight: normal; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: 0px 1px 2px -1px rgba(255, 255, 255, 0.5) inset, 0px -2px 0px 0px rgba(0, 0, 0, 0.1) inset; + outline-none: false; + vertical-align: middle; + *display: inline; + zoom: 1; + -webkit-user-drag: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-transition: all 0.1s linear; + -moz-transition: all 0.1s linear; + transition: all 0.1s linear +} -.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child { margin-bottom: 0 } +.btn-hover { + background: #2e8ece; + color: #fff +} -.wy-plain-list-disc li ul, .rst-content .section ul li ul, .rst-content .toctree-wrapper ul li ul, article ul li ul { margin-bottom: 0 } +.btn:hover { + background: #2cc36b; + color: #fff +} -.wy-plain-list-disc li li, .rst-content .section ul li li, .rst-content .toctree-wrapper ul li li, article ul li li { list-style: circle } +.btn:focus { + background: #2cc36b; + outline: 0 +} -.wy-plain-list-disc li li li, .rst-content .section ul li li li, .rst-content .toctree-wrapper ul li li li, article ul li li li { list-style: square } +.btn:active { + box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.05) inset, 0px 2px 0px 0px rgba(0, 0, 0, 0.1) inset; + padding: 8px 12px 6px 12px +} -.wy-plain-list-disc li ol li, .rst-content .section ul li ol li, .rst-content .toctree-wrapper ul li ol li, article ul li ol li { list-style: decimal } +.btn:visited { + color: #fff +} -.wy-plain-list-decimal, .rst-content .section ol, .rst-content ol.arabic, article ol { list-style: decimal; line-height: 24px; margin-bottom: 24px } +.btn:disabled { + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} -.wy-plain-list-decimal li, .rst-content .section ol li, .rst-content ol.arabic li, article ol li { list-style: decimal; margin-left: 24px } +.btn-disabled { + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} -.wy-plain-list-decimal li p:last-child, .rst-content .section ol li p:last-child, .rst-content ol.arabic li p:last-child, article ol li p:last-child { margin-bottom: 0 } +.btn-disabled:hover, .btn-disabled:focus, .btn-disabled:active { + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} -.wy-plain-list-decimal li ul, .rst-content .section ol li ul, .rst-content ol.arabic li ul, article ol li ul { margin-bottom: 0 } +.btn::-moz-focus-inner { + padding: 0; + border: 0 +} -.wy-plain-list-decimal li ul li, .rst-content .section ol li ul li, .rst-content ol.arabic li ul li, article ol li ul li { list-style: disc } +.btn-small { + font-size: 80% +} -.codeblock-example { border: 1px solid #e1e4e5; border-bottom: none; padding: 24px; padding-top: 48px; font-weight: 500; background: #ffffff; position: relative } +.btn-info { + background-color: #dd4814 !important +} -.codeblock-example:after { content: "Example"; position: absolute; top: 0px; left: 0px; background: #97310e; color: #ffffff; padding: 6px 12px } +.btn-info:hover { + background-color: #2e8ece !important +} -.codeblock-example.prettyprint-example-only { border: 1px solid #e1e4e5; margin-bottom: 24px } +.btn-neutral { + background-color: #f3f6f6 !important; + color: #404040 !important +} -.codeblock, pre.literal-block, .rst-content .literal-block, .rst-content pre.literal-block, div[class^='highlight'] { border: 1px solid #e1e4e5; padding: 0px; overflow-x: auto; background: #ffffff; margin: 1px 0 24px 0 } +.btn-neutral:hover { + background-color: #e5ebeb !important; + color: #404040 +} -.codeblock div[class^='highlight'], pre.literal-block div[class^='highlight'], .rst-content .literal-block div[class^='highlight'], div[class^='highlight'] div[class^='highlight'] { border: none; background: none; margin: 0 } +.btn-neutral:visited { + color: #404040 !important +} -div[class^='highlight'] td.code { width: 100% } +.btn-success { + background-color: #27AE60 !important +} -.linenodiv pre { border-right: solid 1px #e6e9ea; margin: 0; padding: 12px 12px; font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-size: 12px; line-height: 1.5; color: #d9d9d9 } +.btn-success:hover { + background-color: #295 !important +} -div[class^='highlight'] pre { white-space: pre; margin: 0; padding: 12px 12px; font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-size: 12px; line-height: 1.5; display: block; overflow: auto; color: #404040 } +.btn-danger { + background-color: #E74C3C !important +} -@media print { - .codeblock, pre.literal-block, .rst-content .literal-block, .rst-content pre.literal-block, div[class^='highlight'], div[class^='highlight'] pre { white-space: pre-wrap } +.btn-danger:hover { + background-color: #ea6153 !important } -.hll { background-color: #ffffcc; margin: 0 -12px; padding: 0 12px; display: block } +.btn-warning { + background-color: #E67E22 !important +} -.c { color: #999988; font-style: italic } +.btn-warning:hover { + background-color: #e98b39 !important +} -.err { color: #a61717; background-color: #e3d2d2 } +.btn-invert { + background-color: #222 +} -.k { font-weight: bold } +.btn-invert:hover { + background-color: #2f2f2f !important +} -.o { font-weight: bold } +.btn-link { + background-color: transparent !important; + color: #dd4814; + box-shadow: none; + border-color: transparent !important +} -.cm { color: #999988; font-style: italic } +.btn-link:hover { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} -.cp { color: #999999; font-weight: bold } +.btn-link:active { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} -.c1 { color: #999988; font-style: italic } +.btn-link:visited { + color: #97310e +} -.cs { color: #999999; font-weight: bold; font-style: italic } +.wy-btn-group .btn, .wy-control .btn { + vertical-align: middle +} -.gd { color: #000000; background-color: #ffdddd } +.wy-btn-group { + margin-bottom: 24px; + *zoom: 1 +} -.gd .x { color: #000000; background-color: #ffaaaa } +.wy-btn-group:before, .wy-btn-group:after { + display: table; + content: "" +} -.ge { font-style: italic } +.wy-btn-group:after { + clear: both +} -.gr { color: #aa0000 } +.wy-dropdown { + position: relative; + display: inline-block +} -.gh { color: #999999 } +.wy-dropdown-active .wy-dropdown-menu { + display: block +} -.gi { color: #000000; background-color: #ddffdd } +.wy-dropdown-menu { + position: absolute; + left: 0; + display: none; + float: left; + top: 100%; + min-width: 100%; + background: #fcfcfc; + z-index: 100; + border: solid 1px #cfd7dd; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); + padding: 12px +} -.gi .x { color: #000000; background-color: #aaffaa } +.wy-dropdown-menu > dd > a { + display: block; + clear: both; + color: #404040; + white-space: nowrap; + font-size: 90%; + padding: 0 12px; + cursor: pointer +} -.go { color: #888888 } +.wy-dropdown-menu > dd > a:hover { + background: #dd4814; + color: #fff +} -.gp { color: #555555 } +.wy-dropdown-menu > dd.divider { + border-top: solid 1px #cfd7dd; + margin: 6px 0 +} -.gs { font-weight: bold } +.wy-dropdown-menu > dd.search { + padding-bottom: 12px +} -.gu { color: purple; font-weight: bold } +.wy-dropdown-menu > dd.search input[type="search"] { + width: 100% +} -.gt { color: #aa0000 } +.wy-dropdown-menu > dd.call-to-action { + background: #e3e3e3; + text-transform: uppercase; + font-weight: 500; + font-size: 80% +} -.kc { font-weight: bold } +.wy-dropdown-menu > dd.call-to-action:hover { + background: #e3e3e3 +} -.kd { font-weight: bold } +.wy-dropdown-menu > dd.call-to-action .btn { + color: #fff +} -.kn { font-weight: bold } +.wy-dropdown.wy-dropdown-up .wy-dropdown-menu { + bottom: 100%; + top: auto; + left: auto; + right: 0 +} -.kp { font-weight: bold } +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { + background: #fcfcfc; + margin-top: 2px +} -.kr { font-weight: bold } +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a { + padding: 6px 12px +} -.kt { color: #445588; font-weight: bold } +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { + background: #dd4814; + color: #fff +} -.m { color: #009999 } +.wy-dropdown.wy-dropdown-left .wy-dropdown-menu { + right: 0; + left: auto; + text-align: right +} -.s { color: #dd1144 } +.wy-dropdown-arrow:before { + content: " "; + border-bottom: 5px solid #f5f5f5; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + position: absolute; + display: block; + top: -4px; + left: 50%; + margin-left: -3px +} -.n { color: #333333 } +.wy-dropdown-arrow.wy-dropdown-arrow-left:before { + left: 11px +} -.na { color: teal } +.wy-form-stacked select { + display: block +} -.nb { color: #0086b3 } +.wy-form-aligned input, .wy-form-aligned textarea, .wy-form-aligned select, .wy-form-aligned .wy-help-inline, .wy-form-aligned label { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} -.nc { color: #445588; font-weight: bold } +.wy-form-aligned .wy-control-group > label { + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 6px 12px 0 0; + float: left +} -.no { color: teal } +.wy-form-aligned .wy-control { + float: left +} -.ni { color: purple } +.wy-form-aligned .wy-control label { + display: block +} -.ne { color: #990000; font-weight: bold } +.wy-form-aligned .wy-control select { + margin-top: 6px +} -.nf { color: #990000; font-weight: bold } +fieldset { + border: 0; + margin: 0; + padding: 0 +} -.nn { color: #555555 } +legend { + display: block; + width: 100%; + border: 0; + padding: 0; + white-space: normal; + margin-bottom: 24px; + font-size: 150%; + *margin-left: -7px +} -.nt { color: navy } +label { + display: block; + margin: 0 0 .3125em 0; + color: #333; + font-size: 90% +} -.nv { color: teal } +input, select, textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} -.ow { font-weight: bold } +.wy-control-group { + margin-bottom: 24px; + *zoom: 1; + max-width: 68em; + margin-left: auto; + margin-right: auto; + *zoom: 1 +} -.w { color: #bbbbbb } +.wy-control-group:before, .wy-control-group:after { + display: table; + content: "" +} -.mf { color: #009999 } +.wy-control-group:after { + clear: both +} -.mh { color: #009999 } +.wy-control-group:before, .wy-control-group:after { + display: table; + content: "" +} -.mi { color: #009999 } +.wy-control-group:after { + clear: both +} -.mo { color: #009999 } +.wy-control-group.wy-control-group-required > label:after { + content: " *"; + color: #E74C3C +} -.sb { color: #dd1144 } +.wy-control-group .wy-form-full, .wy-control-group .wy-form-halves, .wy-control-group .wy-form-thirds { + padding-bottom: 12px +} -.sc { color: #dd1144 } +.wy-control-group .wy-form-full select, .wy-control-group .wy-form-halves select, .wy-control-group .wy-form-thirds select { + width: 100% +} -.sd { color: #dd1144 } +.wy-control-group .wy-form-full input[type="text"], .wy-control-group .wy-form-full input[type="password"], .wy-control-group .wy-form-full input[type="email"], .wy-control-group .wy-form-full input[type="url"], .wy-control-group .wy-form-full input[type="date"], .wy-control-group .wy-form-full input[type="month"], .wy-control-group .wy-form-full input[type="time"], .wy-control-group .wy-form-full input[type="datetime"], .wy-control-group .wy-form-full input[type="datetime-local"], .wy-control-group .wy-form-full input[type="week"], .wy-control-group .wy-form-full input[type="number"], .wy-control-group .wy-form-full input[type="search"], .wy-control-group .wy-form-full input[type="tel"], .wy-control-group .wy-form-full input[type="color"], .wy-control-group .wy-form-halves input[type="text"], .wy-control-group .wy-form-halves input[type="password"], .wy-control-group .wy-form-halves input[type="email"], .wy-control-group .wy-form-halves input[type="url"], .wy-control-group .wy-form-halves input[type="date"], .wy-control-group .wy-form-halves input[type="month"], .wy-control-group .wy-form-halves input[type="time"], .wy-control-group .wy-form-halves input[type="datetime"], .wy-control-group .wy-form-halves input[type="datetime-local"], .wy-control-group .wy-form-halves input[type="week"], .wy-control-group .wy-form-halves input[type="number"], .wy-control-group .wy-form-halves input[type="search"], .wy-control-group .wy-form-halves input[type="tel"], .wy-control-group .wy-form-halves input[type="color"], .wy-control-group .wy-form-thirds input[type="text"], .wy-control-group .wy-form-thirds input[type="password"], .wy-control-group .wy-form-thirds input[type="email"], .wy-control-group .wy-form-thirds input[type="url"], .wy-control-group .wy-form-thirds input[type="date"], .wy-control-group .wy-form-thirds input[type="month"], .wy-control-group .wy-form-thirds input[type="time"], .wy-control-group .wy-form-thirds input[type="datetime"], .wy-control-group .wy-form-thirds input[type="datetime-local"], .wy-control-group .wy-form-thirds input[type="week"], .wy-control-group .wy-form-thirds input[type="number"], .wy-control-group .wy-form-thirds input[type="search"], .wy-control-group .wy-form-thirds input[type="tel"], .wy-control-group .wy-form-thirds input[type="color"] { + width: 100% +} -.s2 { color: #dd1144 } +.wy-control-group .wy-form-full { + float: left; + display: block; + margin-right: 2.35765%; + width: 100%; + margin-right: 0 +} -.se { color: #dd1144 } +.wy-control-group .wy-form-full:last-child { + margin-right: 0 +} -.sh { color: #dd1144 } +.wy-control-group .wy-form-halves { + float: left; + display: block; + margin-right: 2.35765%; + width: 48.82117% +} -.si { color: #dd1144 } +.wy-control-group .wy-form-halves:last-child { + margin-right: 0 +} -.sx { color: #dd1144 } +.wy-control-group .wy-form-halves:nth-of-type(2n) { + margin-right: 0 +} -.sr { color: #009926 } +.wy-control-group .wy-form-halves:nth-of-type(2n+1) { + clear: left +} -.s1 { color: #dd1144 } +.wy-control-group .wy-form-thirds { + float: left; + display: block; + margin-right: 2.35765%; + width: 31.76157% +} -.ss { color: #990073 } +.wy-control-group .wy-form-thirds:last-child { + margin-right: 0 +} -.bp { color: #999999 } +.wy-control-group .wy-form-thirds:nth-of-type(3n) { + margin-right: 0 +} -.vc { color: teal } +.wy-control-group .wy-form-thirds:nth-of-type(3n+1) { + clear: left +} -.vg { color: teal } +.wy-control-group.wy-control-group-no-input .wy-control { + margin: 6px 0 0 0; + font-size: 90% +} -.vi { color: teal } +.wy-control-no-input { + display: inline-block; + margin: 6px 0 0 0; + font-size: 90% +} -.il { color: #009999 } +.wy-control-group.fluid-input input[type="text"], .wy-control-group.fluid-input input[type="password"], .wy-control-group.fluid-input input[type="email"], .wy-control-group.fluid-input input[type="url"], .wy-control-group.fluid-input input[type="date"], .wy-control-group.fluid-input input[type="month"], .wy-control-group.fluid-input input[type="time"], .wy-control-group.fluid-input input[type="datetime"], .wy-control-group.fluid-input input[type="datetime-local"], .wy-control-group.fluid-input input[type="week"], .wy-control-group.fluid-input input[type="number"], .wy-control-group.fluid-input input[type="search"], .wy-control-group.fluid-input input[type="tel"], .wy-control-group.fluid-input input[type="color"] { + width: 100% +} -.gc { color: #999999; background-color: #eaf2f5 } +.wy-form-message-inline { + display: inline-block; + padding-left: 0.3em; + color: #666; + vertical-align: middle; + font-size: 90% +} -.wy-breadcrumbs li { display: inline-block } +.wy-form-message { + display: block; + color: #999; + font-size: 70%; + margin-top: .3125em; + font-style: italic +} -.wy-breadcrumbs li.wy-breadcrumbs-aside { float: right } +.wy-form-message p { + font-size: inherit; + font-style: italic; + margin-bottom: 6px +} -.wy-breadcrumbs li a { display: inline-block; padding: 5px } +.wy-form-message p:last-child { + margin-bottom: 0 +} -.wy-breadcrumbs li a:first-child { padding-left: 0 } +input { + line-height: normal +} -.wy-breadcrumbs-extra { margin-bottom: 0; color: #b3b3b3; font-size: 80%; display: inline-block } +input[type="button"], input[type="reset"], input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + *overflow: visible +} -@media screen and (max-width: 480px) { - .wy-breadcrumbs-extra { display: none } +input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"] { + -webkit-appearance: none; + padding: 6px; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 0; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} - .wy-breadcrumbs li.wy-breadcrumbs-aside { display: none } +input[type="datetime-local"] { + padding: .34375em .625em } -@media print { - .wy-breadcrumbs li.wy-breadcrumbs-aside { display: none } +input[disabled] { + cursor: default } -.wy-affix { position: fixed; top: 1.618em } +input[type="checkbox"], input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin-right: .3125em; + *height: 13px; + *width: 13px +} -.wy-menu a:hover { text-decoration: none } +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} -.wy-menu-horiz { *zoom: 1 } +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none +} -.wy-menu-horiz:before, .wy-menu-horiz:after { display: table; content: "" } +input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus { + outline: 0; + outline: thin dotted \9; + border-color: #333 +} -.wy-menu-horiz:after { clear: both } +input.no-focus:focus { + border-color: #ccc !important +} -.wy-menu-horiz ul, .wy-menu-horiz li { display: inline-block } +input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 1px auto #129FEA +} -.wy-menu-horiz li:hover { background: rgba(255, 255, 255, 0.1) } +input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled] { + cursor: not-allowed; + background-color: #fafafa +} -.wy-menu-horiz li.divide-left { border-left: solid 1px #404040 } +input:focus:invalid, textarea:focus:invalid, select:focus:invalid { + color: #E74C3C; + border: 1px solid #E74C3C +} -.wy-menu-horiz li.divide-right { border-right: solid 1px #404040 } +input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:focus { + border-color: #E74C3C +} -.wy-menu-horiz a { height: 32px; display: inline-block; line-height: 32px; padding: 0 16px } +input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus { + outline-color: #E74C3C +} -.wy-menu-vertical header { height: 32px; display: inline-block; line-height: 32px; padding: 0 1.618em; display: block; font-weight: bold; text-transform: uppercase; font-size: 80%; color: #dd4814; white-space: nowrap } +input.wy-input-large { + padding: 12px; + font-size: 100% +} -.wy-menu-vertical ul { margin-bottom: 0 } +textarea { + overflow: auto; + vertical-align: top; + width: 100%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif +} -.wy-menu-vertical li.divide-top { border-top: solid 1px #404040 } +select, textarea { + padding: .5em .625em; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + box-shadow: inset 0 1px 3px #ddd; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} -.wy-menu-vertical li.divide-bottom { border-bottom: solid 1px #404040 } +select { + border: 1px solid #ccc; + background-color: #fff +} -.wy-menu-vertical li.current { background: #e3e3e3 } +select[multiple] { + height: auto +} -.wy-menu-vertical li.current a { color: gray; border-right: solid 1px #c9c9c9; padding: 0.4045em 2.427em } +select:focus, textarea:focus { + outline: 0 +} -.wy-menu-vertical li.current a:hover { background: #d6d6d6 } +select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { + cursor: not-allowed; + background-color: #fafafa +} -.wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { color: #404040; padding: 0.4045em 1.618em; font-weight: bold; position: relative; background: #fcfcfc; border: none; border-bottom: solid 1px #c9c9c9; border-top: solid 1px #c9c9c9; padding-left: 1.618em -4px } +input[type="radio"][disabled], input[type="checkbox"][disabled] { + cursor: not-allowed +} -.wy-menu-vertical li.on a:hover, .wy-menu-vertical li.current > a:hover { background: #fcfcfc } +.wy-checkbox, .wy-radio { + margin: 6px 0; + color: #404040; + display: block +} -.wy-menu-vertical li.toctree-l2.current > a { background: #c9c9c9; padding: 0.4045em 2.427em } +.wy-checkbox input, .wy-radio input { + vertical-align: baseline +} -.wy-menu-vertical li.current ul { display: block } +.wy-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} -.wy-menu-vertical li ul { margin-bottom: 0; display: none } +.wy-input-prefix, .wy-input-suffix { + white-space: nowrap; + padding: 6px +} -.wy-menu-vertical .local-toc li ul { display: block } +.wy-input-prefix .wy-input-context, .wy-input-suffix .wy-input-context { + line-height: 27px; + padding: 0 8px; + display: inline-block; + font-size: 80%; + background-color: #f3f6f6; + border: solid 1px #ccc; + color: #999 +} -.wy-menu-vertical li ul li a { margin-bottom: 0; color: #b3b3b3; font-weight: normal } +.wy-input-suffix .wy-input-context { + border-left: 0 +} -.wy-menu-vertical a { display: inline-block; line-height: 18px; padding: 0.4045em 1.618em; display: block; position: relative; font-size: 90%; color: #b3b3b3 } +.wy-input-prefix .wy-input-context { + border-right: 0 +} -.wy-menu-vertical a:hover { background-color: #4e4a4a; cursor: pointer } +.wy-switch { + width: 36px; + height: 12px; + margin: 12px 0; + position: relative; + border-radius: 4px; + background: #ccc; + cursor: pointer; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} -.wy-menu-vertical a:active { background-color: #dd4814; cursor: pointer; color: #ffffff } +.wy-switch:before { + position: absolute; + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 4px; + background: #999; + left: -3px; + top: -3px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} -.wy-side-nav-search { z-index: 200; background-color: #dd4814; text-align: center; padding: 0.809em; display: block; color: #fcfcfc; margin-bottom: 0.809em } +.wy-switch:after { + content: "false"; + position: absolute; + left: 48px; + display: block; + font-size: 12px; + color: #ccc +} -.wy-side-nav-search input[type=text] { width: 100%; border-radius: 50px; padding: 6px 12px; border-color: #97310e } +.wy-switch.active { + background: #1e8449 +} -.wy-side-nav-search img { display: block; margin: auto auto 0.809em auto; height: 45px; width: 45px; background-color: #dd4814; padding: 5px; border-radius: 100% } +.wy-switch.active:before { + left: 24px; + background: #27AE60 +} -.wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a { color: #fcfcfc; font-size: 100%; font-weight: bold; display: inline-block; padding: 4px 6px; margin-bottom: 0.809em } +.wy-switch.active:after { + content: "true" +} -.wy-side-nav-search > a:hover, .wy-side-nav-search .wy-dropdown > a:hover { background: rgba(255, 255, 255, 0.1) } +.wy-switch.disabled, .wy-switch.active.disabled { + cursor: not-allowed +} -.wy-nav .wy-menu-vertical header { color: #dd4814 } +.wy-control-group.wy-control-group-error .wy-form-message, .wy-control-group.wy-control-group-error > label { + color: #E74C3C +} -.wy-nav .wy-menu-vertical a { color: #b3b3b3 } +.wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="color"] { + border: solid 1px #E74C3C +} -.wy-nav .wy-menu-vertical a:hover { background-color: #dd4814; color: #ffffff } +.wy-control-group.wy-control-group-error textarea { + border: solid 1px #E74C3C +} -[data-menu-wrap] { -webkit-transition: all 0.2s ease-in; -moz-transition: all 0.2s ease-in; transition: all 0.2s ease-in; position: absolute; opacity: 1; width: 100%; opacity: 0 } +.wy-inline-validate { + white-space: nowrap +} -[data-menu-wrap].move-center { left: 0; right: auto; opacity: 1 } +.wy-inline-validate .wy-input-context { + padding: .5em .625em; + display: inline-block; + font-size: 80% +} -[data-menu-wrap].move-left { right: auto; left: -100%; opacity: 0 } +.wy-inline-validate.wy-inline-validate-success .wy-input-context { + color: #27AE60 +} -[data-menu-wrap].move-right { right: -100%; left: auto; opacity: 0 } +.wy-inline-validate.wy-inline-validate-danger .wy-input-context { + color: #E74C3C +} -.wy-body-for-nav { background: left repeat-y #fcfcfc; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); background-size: 300px 1px } +.wy-inline-validate.wy-inline-validate-warning .wy-input-context { + color: #E67E22 +} -.wy-grid-for-nav { position: absolute; width: 100%; height: 100% } +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + color: #dd4814 +} -.wy-nav-side { position: absolute; top: 0; left: 0; width: 300px; overflow: hidden; min-height: 100%; background: #343131; z-index: 200 } +.rotate-90 { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg) +} -.wy-nav-top { display: none; background: #dd4814; color: #ffffff; padding: 0.4045em 0.809em; position: relative; line-height: 50px; text-align: center; font-size: 100%; *zoom: 1 } +.rotate-180 { + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg) +} -.wy-nav-top:before, .wy-nav-top:after { display: table; content: "" } +.rotate-270 { + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg) +} -.wy-nav-top:after { clear: both } +.mirror { + -webkit-transform: scaleX(-1); + -moz-transform: scaleX(-1); + -ms-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1) +} -.wy-nav-top a { color: #ffffff; font-weight: bold } +.mirror.rotate-90 { + -webkit-transform: scaleX(-1) rotate(90deg); + -moz-transform: scaleX(-1) rotate(90deg); + -ms-transform: scaleX(-1) rotate(90deg); + -o-transform: scaleX(-1) rotate(90deg); + transform: scaleX(-1) rotate(90deg) +} -.wy-nav-top img { margin-right: 12px; height: 45px; width: 45px; background-color: #dd4814; padding: 5px; border-radius: 100% } +.mirror.rotate-180 { + -webkit-transform: scaleX(-1) rotate(180deg); + -moz-transform: scaleX(-1) rotate(180deg); + -ms-transform: scaleX(-1) rotate(180deg); + -o-transform: scaleX(-1) rotate(180deg); + transform: scaleX(-1) rotate(180deg) +} -.wy-nav-top i { font-size: 30px; float: left; cursor: pointer } +.mirror.rotate-270 { + -webkit-transform: scaleX(-1) rotate(270deg); + -moz-transform: scaleX(-1) rotate(270deg); + -ms-transform: scaleX(-1) rotate(270deg); + -o-transform: scaleX(-1) rotate(270deg); + transform: scaleX(-1) rotate(270deg) +} -.wy-nav-content-wrap { margin-left: 300px; background: #fcfcfc; min-height: 100% } +@media only screen and (max-width: 480px) { + .wy-form button[type="submit"] { + margin: 0.7em 0 0 + } -.wy-nav-content { padding: 1.618em 3.236em; height: 100x; margin: auto } + .wy-form input[type="text"], .wy-form input[type="password"], .wy-form input[type="email"], .wy-form input[type="url"], .wy-form input[type="date"], .wy-form input[type="month"], .wy-form input[type="time"], .wy-form input[type="datetime"], .wy-form input[type="datetime-local"], .wy-form input[type="week"], .wy-form input[type="number"], .wy-form input[type="search"], .wy-form input[type="tel"], .wy-form input[type="color"] { + margin-bottom: 0.3em; + display: block + } -.wy-body-mask { position: fixed; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.2); display: none; z-index: 499 } + .wy-form label { + margin-bottom: 0.3em; + display: block + } -.wy-body-mask.on { display: block } + .wy-form input[type="password"], .wy-form input[type="email"], .wy-form input[type="url"], .wy-form input[type="date"], .wy-form input[type="month"], .wy-form input[type="time"], .wy-form input[type="datetime"], .wy-form input[type="datetime-local"], .wy-form input[type="week"], .wy-form input[type="number"], .wy-form input[type="search"], .wy-form input[type="tel"], .wy-form input[type="color"] { + margin-bottom: 0 + } -footer { color: #999999 } + .wy-form-aligned .wy-control-group label { + margin-bottom: 0.3em; + text-align: left; + display: block; + width: 100% + } -footer p { margin-bottom: 12px } + .wy-form-aligned .wy-control { + margin: 1.5em 0 0 0 + } -.rst-footer-buttons { *zoom: 1 } + .wy-form .wy-help-inline, .wy-form-message-inline, .wy-form-message { + display: block; + font-size: 80%; + padding: 6px 0 + } +} -.rst-footer-buttons:before, .rst-footer-buttons:after { display: table; content: "" } +@media screen and (max-width: 768px) { + .tablet-hide { + display: none + } +} -.rst-footer-buttons:after { clear: both } +@media screen and (max-width: 480px) { + .mobile-hide { + display: none + } +} -#search-results .search li { margin-bottom: 24px; border-bottom: solid 1px #e1e4e5; padding-bottom: 24px } +.float-left { + float: left +} -#search-results .search li:first-child { border-top: solid 1px #e1e4e5; padding-top: 24px } +.float-right { + float: right +} -#search-results .search li a { font-size: 120%; margin-bottom: 12px; display: inline-block } +.full-width { + width: 100% +} -#search-results .context { color: gray; font-size: 90% } +.wy-table, .rst-content table.docutils, .rst-content table.field-list { + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + margin-bottom: 24px +} -@media screen and (max-width: 768px) { - .wy-body-for-nav { background: #fcfcfc } +.wy-table caption, .rst-content table.docutils caption, .rst-content table.field-list caption { + color: #000; + font: italic 85%/1 arial, sans-serif; + padding: 1em 0; + text-align: center +} - .wy-nav-top { display: block } +.wy-table td, .rst-content table.docutils td, .rst-content table.field-list td, .wy-table th, .rst-content table.docutils th, .rst-content table.field-list th { + font-size: 90%; + margin: 0; + overflow: visible; + padding: 8px 16px +} - .wy-nav-side { left: -300px } +.wy-table td:first-child, .rst-content table.docutils td:first-child, .rst-content table.field-list td:first-child, .wy-table th:first-child, .rst-content table.docutils th:first-child, .rst-content table.field-list th:first-child { + border-left-width: 0 +} - .wy-nav-side.shift { width: 85%; left: 0 } +.wy-table thead, .rst-content table.docutils thead, .rst-content table.field-list thead { + color: #000; + text-align: left; + vertical-align: bottom; + white-space: nowrap +} - .wy-nav-content-wrap { margin-left: 0 } +.wy-table thead th, .rst-content table.docutils thead th, .rst-content table.field-list thead th { + font-weight: bold; + border-bottom: solid 2px #e1e4e5 +} - .wy-nav-content-wrap .wy-nav-content { padding: 1.618em } +.wy-table td, .rst-content table.docutils td, .rst-content table.field-list td { + background-color: transparent; + vertical-align: middle +} - .wy-nav-content-wrap.shift { position: fixed; min-width: 100%; left: 85%; top: 0; height: 100%; overflow: hidden } +.wy-table td p, .rst-content table.docutils td p, .rst-content table.field-list td p { + line-height: 18px } -@media screen and (min-width: 1400px) { - .wy-nav-content-wrap { background: rgba(0, 0, 0, 0.05) } +.wy-table td p:last-child, .rst-content table.docutils td p:last-child, .rst-content table.field-list td p:last-child { + margin-bottom: 0 +} - .wy-nav-content { margin: 0; background: #fcfcfc } +.wy-table .wy-table-cell-min, .rst-content table.docutils .wy-table-cell-min, .rst-content table.field-list .wy-table-cell-min { + width: 1%; + padding-right: 0 } -@media print { - .rst-versions, footer, .wy-nav-side { display: none } +.wy-table .wy-table-cell-min input[type=checkbox], .rst-content table.docutils .wy-table-cell-min input[type=checkbox], .rst-content table.field-list .wy-table-cell-min input[type=checkbox], .wy-table .wy-table-cell-min input[type=checkbox], .rst-content table.docutils .wy-table-cell-min input[type=checkbox], .rst-content table.field-list .wy-table-cell-min input[type=checkbox] { + margin: 0 +} - .wy-nav-content-wrap { margin-left: 0 } +.wy-table-secondary { + color: gray; + font-size: 90% } -nav.stickynav { position: fixed; top: 0 } +.wy-table-tertiary { + color: gray; + font-size: 80% +} -.rst-versions { position: fixed; bottom: 0; left: 0; width: 300px; color: #fcfcfc; background: #1f1d1d; border-top: solid 10px #343131; font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; z-index: 400 } +.wy-table-odd td, .wy-table-striped tr:nth-child(2n-1) td, .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #f3f6f6 +} -.rst-versions a { color: #dd4814; text-decoration: none } +.wy-table-backed { + background-color: #f3f6f6 +} -.rst-versions .rst-badge-small { display: none } +.wy-table-bordered-all, .rst-content table.docutils { + border: 1px solid #e1e4e5 +} -.rst-versions .rst-current-version { padding: 12px; background-color: #272525; display: block; text-align: right; font-size: 90%; cursor: pointer; color: #27ae60; *zoom: 1 } +.wy-table-bordered-all td, .rst-content table.docutils td { + border-bottom: 1px solid #e1e4e5; + border-left: 1px solid #e1e4e5 +} -.rst-versions .rst-current-version:before, .rst-versions .rst-current-version:after { display: table; content: "" } +.wy-table-bordered-all tbody > tr:last-child td, .rst-content table.docutils tbody > tr:last-child td { + border-bottom-width: 0 +} -.rst-versions .rst-current-version:after { clear: both } +.wy-table-bordered { + border: 1px solid #e1e4e5 +} -.rst-versions .rst-current-version .fa, .rst-versions .rst-current-version .rst-content .admonition-title, .rst-content .rst-versions .rst-current-version .admonition-title, .rst-versions .rst-current-version .rst-content h1 .headerlink, .rst-content h1 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h2 .headerlink, .rst-content h2 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h3 .headerlink, .rst-content h3 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h4 .headerlink, .rst-content h4 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h5 .headerlink, .rst-content h5 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h6 .headerlink, .rst-content h6 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content dl dt .headerlink, .rst-content dl dt .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .icon { color: #fcfcfc } +.wy-table-bordered-rows td { + border-bottom: 1px solid #e1e4e5 +} -.rst-versions .rst-current-version .fa-book, .rst-versions .rst-current-version .icon-book { float: left } +.wy-table-bordered-rows tbody > tr:last-child td { + border-bottom-width: 0 +} -.rst-versions .rst-current-version .icon-book { float: left } +.wy-table-horizontal tbody > tr:last-child td { + border-bottom-width: 0 +} -.rst-versions .rst-current-version.rst-out-of-date { background-color: #e74c3c; color: #ffffff } +.wy-table-horizontal td, .wy-table-horizontal th { + border-width: 0 0 1px 0; + border-bottom: 1px solid #e1e4e5 +} -.rst-versions .rst-current-version.rst-active-old-version { background-color: #f1c40f; color: #000000 } +.wy-table-horizontal tbody > tr:last-child td { + border-bottom-width: 0 +} -.rst-versions.shift-up .rst-other-versions { display: block } +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: auto +} -.rst-versions .rst-other-versions { font-size: 90%; padding: 12px; color: gray; display: none } +.wy-table-responsive table { + margin-bottom: 0 !important +} -.rst-versions .rst-other-versions hr { display: block; height: 1px; border: 0; margin: 20px 0; padding: 0; border-top: solid 1px #413d3d } +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: nowrap +} -.rst-versions .rst-other-versions dd { display: inline-block; margin: 0 } +a { + color: #dd4814; + text-decoration: none; + cursor: pointer +} -.rst-versions .rst-other-versions dd a { display: inline-block; padding: 6px; color: #fcfcfc } +a:hover { + color: #97310e +} -.rst-versions.rst-badge { width: auto; bottom: 20px; right: 20px; left: auto; border: none; max-width: 300px } +a:visited { + color: #dd4814 +} -.rst-versions.rst-badge .icon-book { float: none } +html { + height: 100%; + overflow-x: hidden +} -.rst-versions.rst-badge .fa-book, .rst-versions.rst-badge .icon-book { float: none } +body { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-weight: normal; + color: #404040; + min-height: 100%; + overflow-x: hidden; + background: #edf0f2 +} -.rst-versions.rst-badge.shift-up .rst-current-version { text-align: right } +.wy-text-left { + text-align: left +} -.rst-versions.rst-badge.shift-up .rst-current-version .fa-book, .rst-versions.rst-badge.shift-up .rst-current-version .icon-book { float: left } +.wy-text-center { + text-align: center +} -.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { float: left } +.wy-text-right { + text-align: right +} -.rst-versions.rst-badge .rst-current-version { width: auto; height: 30px; line-height: 30px; padding: 0 6px; display: block; text-align: center } +.wy-text-large { + font-size: 120% +} -@media screen and (max-width: 768px) { - .rst-versions { width: 85%; display: none } +.wy-text-normal { + font-size: 100% +} - .rst-versions.shift { display: block } +.wy-text-small, small { + font-size: 80% +} - img { width: 100%; height: auto } +.wy-text-strike { + text-decoration: line-through } -.rst-content img { max-width: 100%; height: auto !important } +.wy-text-warning { + color: #E67E22 !important +} -.rst-content div.figure { margin-bottom: 24px } +a.wy-text-warning:hover { + color: #eb9950 !important +} -.rst-content div.figure.align-center { text-align: center } +.wy-text-info { + color: #dd4814 !important +} -.rst-content .section > img, .rst-content .section > a > img { margin-bottom: 24px } +a.wy-text-info:hover { + color: #409ad5 !important +} -.rst-content blockquote { margin-left: 24px; line-height: 24px; margin-bottom: 24px } +.wy-text-success { + color: #27AE60 !important +} -.rst-content .note .last, .rst-content .attention .last, .rst-content .caution .last, .rst-content .danger .last, .rst-content .error .last, .rst-content .hint .last, .rst-content .important .last, .rst-content .tip .last, .rst-content .warning .last, .rst-content .seealso .last, .rst-content .admonition-todo .last { margin-bottom: 0 } +a.wy-text-success:hover { + color: #36d278 !important +} -.rst-content .admonition-title:before { margin-right: 4px } +.wy-text-danger { + color: #E74C3C !important +} -.rst-content .admonition table { border-color: rgba(0, 0, 0, 0.1) } +a.wy-text-danger:hover { + color: #ed7669 !important +} -.rst-content .admonition table td, .rst-content .admonition table th { background: transparent !important; border-color: rgba(0, 0, 0, 0.1) !important } +.wy-text-neutral { + color: #404040 !important +} -.rst-content .section ol.loweralpha, .rst-content .section ol.loweralpha li { list-style: lower-alpha } +a.wy-text-neutral:hover { + color: #595959 !important +} -.rst-content .section ol.upperalpha, .rst-content .section ol.upperalpha li { list-style: upper-alpha } +h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend { + margin-top: 0; + font-weight: 700; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif +} -.rst-content .section ol p, .rst-content .section ul p { margin-bottom: 12px } +p { + line-height: 24px; + margin: 0; + font-size: 16px; + margin-bottom: 24px +} -.rst-content .line-block { margin-left: 24px } +h1 { + font-size: 175% +} -.rst-content .topic-title { font-weight: bold; margin-bottom: 12px } +h2, .rst-content .toctree-wrapper p.caption { + font-size: 150% +} -.rst-content .toc-backref { color: #404040 } +h3 { + font-size: 125% +} -.rst-content .align-right { float: right; margin: 0px 0px 24px 24px } +h4 { + font-size: 115% +} -.rst-content .align-left { float: left; margin: 0px 24px 24px 0px } +h5 { + font-size: 110% +} -.rst-content .align-center { margin: auto; display: block } +h6 { + font-size: 100% +} -.rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink { display: none; visibility: hidden; font-size: 14px } +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #e1e4e5; + margin: 24px 0; + padding: 0 +} -.rst-content h1 .headerlink:after, .rst-content h2 .headerlink:after, .rst-content h3 .headerlink:after, .rst-content h4 .headerlink:after, .rst-content h5 .headerlink:after, .rst-content h6 .headerlink:after, .rst-content dl dt .headerlink:after { visibility: visible; content: ""; font-family: FontAwesome; display: inline-block } +code, .rst-content tt, .rst-content code { + white-space: nowrap; + max-width: 100%; + background: #fff; + border: solid 1px #e1e4e5; + font-size: 75%; + padding: 0 5px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + color: #E74C3C; + overflow-x: auto +} -.rst-content h1:hover .headerlink, .rst-content h2:hover .headerlink, .rst-content h3:hover .headerlink, .rst-content h4:hover .headerlink, .rst-content h5:hover .headerlink, .rst-content h6:hover .headerlink, .rst-content dl dt:hover .headerlink { display: inline-block } +code.code-large, .rst-content tt.code-large { + font-size: 90% +} -.rst-content .sidebar { float: right; width: 40%; display: block; margin: 0 0 24px 24px; padding: 24px; background: #f3f6f6; border: solid 1px #e1e4e5 } +.wy-plain-list-disc, .rst-content .section ul, .rst-content .toctree-wrapper ul, article ul { + list-style: disc; + line-height: 24px; + margin-bottom: 24px +} -.rst-content .sidebar p, .rst-content .sidebar ul, .rst-content .sidebar dl { font-size: 90% } +.wy-plain-list-disc li, .rst-content .section ul li, .rst-content .toctree-wrapper ul li, article ul li { + list-style: disc; + margin-left: 24px +} -.rst-content .sidebar .last { margin-bottom: 0 } +.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child { + margin-bottom: 0 +} -.rst-content .sidebar .sidebar-title { display: block; font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif; font-weight: bold; background: #e1e4e5; padding: 6px 12px; margin: -24px; margin-bottom: 24px; font-size: 100% } +.wy-plain-list-disc li ul, .rst-content .section ul li ul, .rst-content .toctree-wrapper ul li ul, article ul li ul { + margin-bottom: 0 +} -.rst-content .highlighted { background: #f1c40f; display: inline-block; font-weight: bold; padding: 0 6px } +.wy-plain-list-disc li li, .rst-content .section ul li li, .rst-content .toctree-wrapper ul li li, article ul li li { + list-style: circle +} -.rst-content .footnote-reference, .rst-content .citation-reference { vertical-align: super; font-size: 90% } +.wy-plain-list-disc li li li, .rst-content .section ul li li li, .rst-content .toctree-wrapper ul li li li, article ul li li li { + list-style: square +} -.rst-content table.docutils.citation, .rst-content table.docutils.footnote { background: none; border: none; color: #999999 } +.wy-plain-list-disc li ol li, .rst-content .section ul li ol li, .rst-content .toctree-wrapper ul li ol li, article ul li ol li { + list-style: decimal +} -.rst-content table.docutils.citation td, .rst-content table.docutils.citation tr, .rst-content table.docutils.footnote td, .rst-content table.docutils.footnote tr { border: none; background-color: transparent !important; white-space: normal } +.wy-plain-list-decimal, .rst-content .section ol, .rst-content ol.arabic, article ol { + list-style: decimal; + line-height: 24px; + margin-bottom: 24px +} -.rst-content table.docutils.citation td.label, .rst-content table.docutils.footnote td.label { padding-left: 0; padding-right: 0; vertical-align: top } +.wy-plain-list-decimal li, .rst-content .section ol li, .rst-content ol.arabic li, article ol li { + list-style: decimal; + margin-left: 24px +} -.rst-content table.field-list { border: none } +.wy-plain-list-decimal li p:last-child, .rst-content .section ol li p:last-child, .rst-content ol.arabic li p:last-child, article ol li p:last-child { + margin-bottom: 0 +} -.rst-content table.field-list td { border: none; padding-top: 5px } +.wy-plain-list-decimal li ul, .rst-content .section ol li ul, .rst-content ol.arabic li ul, article ol li ul { + margin-bottom: 0 +} -.rst-content table.field-list td > strong { display: inline-block; margin-top: 3px } +.wy-plain-list-decimal li ul li, .rst-content .section ol li ul li, .rst-content ol.arabic li ul li, article ol li ul li { + list-style: disc +} -.rst-content table.field-list .field-name { padding-right: 10px; text-align: left; white-space: nowrap } +.codeblock-example { + border: 1px solid #e1e4e5; + border-bottom: none; + padding: 24px; + padding-top: 48px; + font-weight: 500; + background: #fff; + position: relative +} -.rst-content table.field-list .field-body { text-align: left; padding-left: 0 } +.codeblock-example:after { + content: "Example"; + position: absolute; + top: 0px; + left: 0px; + background: #97310e; + color: #fff; + padding: 6px 12px +} -.rst-content tt { color: #000000 } +.codeblock-example.prettyprint-example-only { + border: 1px solid #e1e4e5; + margin-bottom: 24px +} -.rst-content tt big, .rst-content tt em { font-size: 100% !important; line-height: normal } +.codeblock, pre.literal-block, .rst-content .literal-block, .rst-content pre.literal-block, div[class^='highlight'] { + border: 1px solid #e1e4e5; + padding: 0px; + overflow-x: auto; + background: #fff; + margin: 1px 0 24px 0 +} -.rst-content tt .xref, a .rst-content tt { font-weight: bold } +.codeblock div[class^='highlight'], pre.literal-block div[class^='highlight'], .rst-content .literal-block div[class^='highlight'], div[class^='highlight'] div[class^='highlight'] { + border: none; + background: none; + margin: 0 +} -.rst-content a tt { color: #dd4814 } +div[class^='highlight'] td.code { + width: 100% +} -.rst-content dl { margin-bottom: 24px } +.linenodiv pre { + border-right: solid 1px #e6e9ea; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + color: #d9d9d9 +} -.rst-content dl dt { font-weight: bold } +div[class^='highlight'] pre { + white-space: pre; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + display: block; + overflow: auto; + color: #404040 +} -.rst-content dl p, .rst-content dl table, .rst-content dl ul, .rst-content dl ol { margin-bottom: 12px !important } +@media print { + .codeblock, pre.literal-block, .rst-content .literal-block, .rst-content pre.literal-block, div[class^='highlight'], div[class^='highlight'] pre { + white-space: pre-wrap + } +} -.rst-content dl dd { margin: 0 0 12px 24px } +.hll { + background-color: #ffc; + margin: 0 -12px; + padding: 0 12px; + display: block +} -.rst-content dl:not(.docutils) { margin-bottom: 24px } +.c { + color: #998; + font-style: italic +} -.rst-content dl:not(.docutils) dt { display: inline-block; margin: 6px 0; line-height: normal; background: #fdc894; color: #444; border-top: solid 3px #dd4814; padding: 6px; position: relative } +.err { + color: #a61717; + background-color: #e3d2d2 +} -.rst-content dl:not(.docutils) dt:before { color: #8ba8af } +.k { + font-weight: bold +} -.rst-content dl:not(.docutils) dt .headerlink { color: #404040; font-size: 100% !important } +.o { + font-weight: bold +} -.rst-content dl:not(.docutils) dl dt { margin-bottom: 6px; border: none; border-left: solid 3px #cccccc; background: #f0f0f0; color: gray } +.cm { + color: #998; + font-style: italic +} + +.cp { + color: #999; + font-weight: bold +} + +.c1 { + color: #998; + font-style: italic +} + +.cs { + color: #999; + font-weight: bold; + font-style: italic +} + +.gd { + color: #000; + background-color: #fdd +} + +.gd .x { + color: #000; + background-color: #faa +} + +.ge { + font-style: italic +} + +.gr { + color: #a00 +} + +.gh { + color: #999 +} + +.gi { + color: #000; + background-color: #dfd +} + +.gi .x { + color: #000; + background-color: #afa +} + +.go { + color: #888 +} + +.gp { + color: #555 +} + +.gs { + font-weight: bold +} + +.gu { + color: purple; + font-weight: bold +} + +.gt { + color: #a00 +} + +.kc { + font-weight: bold +} + +.kd { + font-weight: bold +} + +.kn { + font-weight: bold +} + +.kp { + font-weight: bold +} + +.kr { + font-weight: bold +} + +.kt { + color: #458; + font-weight: bold +} + +.m { + color: #099 +} + +.s { + color: #d14 +} + +.n { + color: #333 +} + +.na { + color: teal +} + +.nb { + color: #0086b3 +} + +.nc { + color: #458; + font-weight: bold +} + +.no { + color: teal +} + +.ni { + color: purple +} + +.ne { + color: #900; + font-weight: bold +} + +.nf { + color: #900; + font-weight: bold +} + +.nn { + color: #555 +} + +.nt { + color: navy +} + +.nv { + color: teal +} + +.ow { + font-weight: bold +} + +.w { + color: #bbb +} + +.mf { + color: #099 +} + +.mh { + color: #099 +} + +.mi { + color: #099 +} + +.mo { + color: #099 +} + +.sb { + color: #d14 +} + +.sc { + color: #d14 +} + +.sd { + color: #d14 +} + +.s2 { + color: #d14 +} + +.se { + color: #d14 +} + +.sh { + color: #d14 +} + +.si { + color: #d14 +} + +.sx { + color: #d14 +} + +.sr { + color: #009926 +} + +.s1 { + color: #d14 +} + +.ss { + color: #990073 +} + +.bp { + color: #999 +} + +.vc { + color: teal +} + +.vg { + color: teal +} + +.vi { + color: teal +} + +.il { + color: #099 +} + +.gc { + color: #999; + background-color: #EAF2F5 +} + +.wy-breadcrumbs li { + display: inline-block +} + +.wy-breadcrumbs li.wy-breadcrumbs-aside { + float: right +} + +.wy-breadcrumbs li a { + display: inline-block; + padding: 5px +} + +.wy-breadcrumbs li a:first-child { + padding-left: 0 +} + +.wy-breadcrumbs li code, .wy-breadcrumbs li .rst-content tt, .rst-content .wy-breadcrumbs li tt { + padding: 5px; + border: none; + background: none +} + +.wy-breadcrumbs li code.literal, .wy-breadcrumbs li .rst-content tt.literal, .rst-content .wy-breadcrumbs li tt.literal { + color: #404040 +} + +.wy-breadcrumbs-extra { + margin-bottom: 0; + color: #b3b3b3; + font-size: 80%; + display: inline-block +} + +@media screen and (max-width: 480px) { + .wy-breadcrumbs-extra { + display: none + } + + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} + +@media print { + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} + +.wy-affix { + position: fixed; + top: 1.618em +} + +.wy-menu a:hover { + text-decoration: none +} + +.wy-menu-horiz { + *zoom: 1 +} + +.wy-menu-horiz:before, .wy-menu-horiz:after { + display: table; + content: "" +} + +.wy-menu-horiz:after { + clear: both +} + +.wy-menu-horiz ul, .wy-menu-horiz li { + display: inline-block +} + +.wy-menu-horiz li:hover { + background: rgba(255, 255, 255, 0.1) +} + +.wy-menu-horiz li.divide-left { + border-left: solid 1px #404040 +} + +.wy-menu-horiz li.divide-right { + border-right: solid 1px #404040 +} + +.wy-menu-horiz a { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 16px +} + +.wy-menu-vertical { + width: 300px +} + +.wy-menu-vertical header, .wy-menu-vertical p.caption { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 1.618em; + margin-bottom: 0; + display: block; + font-weight: bold; + text-transform: uppercase; + font-size: 80%; + color: #dd4814; + white-space: nowrap +} + +.wy-menu-vertical ul { + margin-bottom: 0 +} + +.wy-menu-vertical li.divide-top { + border-top: solid 1px #404040 +} + +.wy-menu-vertical li.divide-bottom { + border-bottom: solid 1px #404040 +} + +.wy-menu-vertical li.current { + background: #e3e3e3 +} + +.wy-menu-vertical li.current a { + color: gray; + border-right: solid 1px #c9c9c9; + padding: .4045em 2.427em +} + +.wy-menu-vertical li.current a:hover { + background: #d6d6d6 +} + +.wy-menu-vertical li code, .wy-menu-vertical li .rst-content tt, .rst-content .wy-menu-vertical li tt { + border: none; + background: inherit; + color: inherit; + padding-left: 0; + padding-right: 0 +} + +.wy-menu-vertical li span.toctree-expand { + display: block; + float: left; + margin-left: -1.2em; + font-size: 0.8em; + line-height: 1.6em; + color: #4d4d4d +} + +.wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { + color: #404040; + padding: .4045em 1.618em; + font-weight: bold; + position: relative; + background: #fcfcfc; + border: none; + border-bottom: solid 1px #c9c9c9; + border-top: solid 1px #c9c9c9; + padding-left: 1.618em -4px +} + +.wy-menu-vertical li.on a:hover, .wy-menu-vertical li.current > a:hover { + background: #fcfcfc +} + +.wy-menu-vertical li.on a:hover span.toctree-expand, .wy-menu-vertical li.current > a:hover span.toctree-expand { + color: gray +} + +.wy-menu-vertical li.on a span.toctree-expand, .wy-menu-vertical li.current > a span.toctree-expand { + display: block; + font-size: 0.8em; + line-height: 1.6em; + color: #333 +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2 > ul, .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > ul { + display: none +} + +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current > ul, .wy-menu-vertical li.toctree-l2.current li.toctree-l3.current > ul { + display: block +} + +.wy-menu-vertical li.toctree-l2.current > a { + background: #c9c9c9; + padding: .4045em 2.427em +} + +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { + display: block; + background: #c9c9c9; + padding: .4045em 4.045em +} + +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: gray +} + +.wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: #a3a3a3 +} + +.wy-menu-vertical li.toctree-l3 { + font-size: 0.9em +} + +.wy-menu-vertical li.toctree-l3.current > a { + background: #bdbdbd; + padding: .4045em 4.045em +} + +.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { + display: block; + background: #bdbdbd; + padding: .4045em 5.663em; + border-top: none; + border-bottom: none +} + +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: gray +} + +.wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: #969696 +} + +.wy-menu-vertical li.toctree-l4 { + font-size: 0.9em +} + +.wy-menu-vertical li.current ul { + display: block +} + +.wy-menu-vertical li ul { + margin-bottom: 0; + display: none +} + +.wy-menu-vertical .local-toc li ul { + display: block +} + +.wy-menu-vertical li ul li a { + margin-bottom: 0; + color: #b3b3b3; + font-weight: normal +} + +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: .4045em 1.618em; + display: block; + position: relative; + font-size: 90%; + color: #b3b3b3 +} + +.wy-menu-vertical a:hover { + background-color: #4e4a4a; + cursor: pointer +} + +.wy-menu-vertical a:hover span.toctree-expand { + color: #b3b3b3 +} + +.wy-menu-vertical a:active { + background-color: #dd4814; + cursor: pointer; + color: #fff +} + +.wy-menu-vertical a:active span.toctree-expand { + color: #fff +} + +.wy-side-nav-search { + display: block; + width: 300px; + padding: .809em; + margin-bottom: .809em; + z-index: 200; + background-color: #dd4814; + text-align: center; + padding: .809em; + display: block; + color: #fcfcfc; + margin-bottom: .809em +} + +.wy-side-nav-search input[type=text] { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #97310e +} + +.wy-side-nav-search img { + display: block; + margin: auto auto .809em auto; + height: 45px; + width: 45px; + background-color: #dd4814; + padding: 5px; + border-radius: 100% +} + +.wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a { + color: #fcfcfc; + font-size: 100%; + font-weight: bold; + display: inline-block; + padding: 4px 6px; + margin-bottom: .809em +} + +.wy-side-nav-search > a:hover, .wy-side-nav-search .wy-dropdown > a:hover { + background: rgba(255, 255, 255, 0.1) +} + +.wy-side-nav-search > a img.logo, .wy-side-nav-search .wy-dropdown > a img.logo { + display: block; + margin: 0 auto; + height: auto; + width: auto; + border-radius: 0; + max-width: 100%; + background: transparent +} + +.wy-side-nav-search > a.icon img.logo, .wy-side-nav-search .wy-dropdown > a.icon img.logo { + margin-top: 0.85em +} + +.wy-side-nav-search > div.version { + margin-top: -.4045em; + margin-bottom: .809em; + font-weight: normal; + color: rgba(255, 255, 255, 0.3) +} + +.wy-nav .wy-menu-vertical header { + color: #dd4814 +} + +.wy-nav .wy-menu-vertical a { + color: #b3b3b3 +} + +.wy-nav .wy-menu-vertical a:hover { + background-color: #dd4814; + color: #fff +} + +[data-menu-wrap] { + -webkit-transition: all 0.2s ease-in; + -moz-transition: all 0.2s ease-in; + transition: all 0.2s ease-in; + position: absolute; + opacity: 1; + width: 100%; + opacity: 0 +} + +[data-menu-wrap].move-center { + left: 0; + right: auto; + opacity: 1 +} + +[data-menu-wrap].move-left { + right: auto; + left: -100%; + opacity: 0 +} + +[data-menu-wrap].move-right { + right: -100%; + left: auto; + opacity: 0 +} + +.wy-body-for-nav { + background: left repeat-y #fcfcfc; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); + background-size: 300px 1px +} + +.wy-grid-for-nav { + position: absolute; + width: 100%; + height: 100% +} + +.wy-nav-side { + position: fixed; + top: 0; + bottom: 0; + left: 0; + padding-bottom: 2em; + width: 300px; + overflow-x: hidden; + overflow-y: hidden; + min-height: 100%; + background: #343131; + z-index: 200 +} + +.wy-side-scroll { + width: 320px; + position: relative; + overflow-x: hidden; + overflow-y: scroll; + height: 100% +} + +.wy-nav-top { + display: none; + background: #dd4814; + color: #fff; + padding: .4045em .809em; + position: relative; + line-height: 50px; + text-align: center; + font-size: 100%; + *zoom: 1 +} + +.wy-nav-top:before, .wy-nav-top:after { + display: table; + content: "" +} + +.wy-nav-top:after { + clear: both +} + +.wy-nav-top a { + color: #fff; + font-weight: bold +} + +.wy-nav-top img { + margin-right: 12px; + height: 45px; + width: 45px; + background-color: #dd4814; + padding: 5px; + border-radius: 100% +} + +.wy-nav-top i { + font-size: 30px; + float: left; + cursor: pointer; + padding-top: inherit +} + +.wy-nav-content-wrap { + margin-left: 300px; + background: #fcfcfc; + min-height: 100% +} + +.wy-nav-content { + padding: 1.618em 3.236em; + height: 100%; + margin: auto +} + +.wy-body-mask { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.2); + display: none; + z-index: 499 +} + +.wy-body-mask.on { + display: block +} -.rst-content dl:not(.docutils) dl dt .headerlink { color: #404040; font-size: 100% !important } +footer { + color: #999 +} + +footer p { + margin-bottom: 12px +} + +footer span.commit code, footer span.commit .rst-content tt, .rst-content footer span.commit tt { + padding: 0px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 1em; + background: none; + border: none; + color: #999 +} + +.rst-footer-buttons { + *zoom: 1 +} + +.rst-footer-buttons:before, .rst-footer-buttons:after { + display: table; + content: "" +} + +.rst-footer-buttons:after { + clear: both +} + +.rst-breadcrumbs-buttons { + margin-top: 12px; + *zoom: 1 +} + +.rst-breadcrumbs-buttons:before, .rst-breadcrumbs-buttons:after { + display: table; + content: "" +} + +.rst-breadcrumbs-buttons:after { + clear: both +} + +#search-results .search li { + margin-bottom: 24px; + border-bottom: solid 1px #e1e4e5; + padding-bottom: 24px +} + +#search-results .search li:first-child { + border-top: solid 1px #e1e4e5; + padding-top: 24px +} + +#search-results .search li a { + font-size: 120%; + margin-bottom: 12px; + display: inline-block +} + +#search-results .context { + color: gray; + font-size: 90% +} + +@media screen and (max-width: 768px) { + .wy-body-for-nav { + background: #fcfcfc + } + + .wy-nav-top { + display: block + } + + .wy-nav-side { + left: -300px + } + + .wy-nav-side.shift { + width: 85%; + left: 0 + } + + .wy-side-scroll { + width: auto + } + + .wy-side-nav-search { + width: auto + } + + .wy-menu.wy-menu-vertical { + width: auto + } + + .wy-nav-content-wrap { + margin-left: 0 + } + + .wy-nav-content-wrap .wy-nav-content { + padding: 1.618em + } + + .wy-nav-content-wrap.shift { + position: fixed; + min-width: 100%; + left: 85%; + top: 0; + height: 100%; + overflow: hidden + } +} + +@media screen and (min-width: 1400px) { + .wy-nav-content-wrap { + background: rgba(0, 0, 0, 0.05) + } + + .wy-nav-content { + margin: 0; + background: #fcfcfc + } +} + +@media print { + .rst-versions, footer, .wy-nav-side { + display: none + } + + .wy-nav-content-wrap { + margin-left: 0 + } +} + +.rst-versions { + position: fixed; + bottom: 0; + left: 0; + width: 300px; + color: #fcfcfc; + background: #1f1d1d; + border-top: solid 10px #343131; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + z-index: 400 +} + +.rst-versions a { + color: #dd4814; + text-decoration: none +} + +.rst-versions .rst-badge-small { + display: none +} + +.rst-versions .rst-current-version { + padding: 12px; + background-color: #272525; + display: block; + text-align: right; + font-size: 90%; + cursor: pointer; + color: #27AE60; + *zoom: 1 +} + +.rst-versions .rst-current-version:before, .rst-versions .rst-current-version:after { + display: table; + content: "" +} + +.rst-versions .rst-current-version:after { + clear: both +} + +.rst-versions .rst-current-version .fa, .rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, .wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, .rst-versions .rst-current-version .rst-content .admonition-title, .rst-content .rst-versions .rst-current-version .admonition-title, .rst-versions .rst-current-version .rst-content h1 .headerlink, .rst-content h1 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h2 .headerlink, .rst-content h2 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h3 .headerlink, .rst-content h3 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h4 .headerlink, .rst-content h4 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h5 .headerlink, .rst-content h5 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content h6 .headerlink, .rst-content h6 .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content dl dt .headerlink, .rst-content dl dt .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content p.caption .headerlink, .rst-content p.caption .rst-versions .rst-current-version .headerlink, .rst-versions .rst-current-version .rst-content tt.download span:first-child, .rst-content tt.download .rst-versions .rst-current-version span:first-child, .rst-versions .rst-current-version .rst-content code.download span:first-child, .rst-content code.download .rst-versions .rst-current-version span:first-child, .rst-versions .rst-current-version .icon { + color: #fcfcfc +} + +.rst-versions .rst-current-version .fa-book, .rst-versions .rst-current-version .icon-book { + float: left +} + +.rst-versions .rst-current-version .icon-book { + float: left +} + +.rst-versions .rst-current-version.rst-out-of-date { + background-color: #E74C3C; + color: #fff +} + +.rst-versions .rst-current-version.rst-active-old-version { + background-color: #F1C40F; + color: #000 +} + +.rst-versions.shift-up .rst-other-versions { + display: block +} + +.rst-versions .rst-other-versions { + font-size: 90%; + padding: 12px; + color: gray; + display: none +} + +.rst-versions .rst-other-versions hr { + display: block; + height: 1px; + border: 0; + margin: 20px 0; + padding: 0; + border-top: solid 1px #413d3d +} + +.rst-versions .rst-other-versions dd { + display: inline-block; + margin: 0 +} + +.rst-versions .rst-other-versions dd a { + display: inline-block; + padding: 6px; + color: #fcfcfc +} + +.rst-versions.rst-badge { + width: auto; + bottom: 20px; + right: 20px; + left: auto; + border: none; + max-width: 300px +} + +.rst-versions.rst-badge .icon-book { + float: none +} + +.rst-versions.rst-badge .fa-book, .rst-versions.rst-badge .icon-book { + float: none +} + +.rst-versions.rst-badge.shift-up .rst-current-version { + text-align: right +} + +.rst-versions.rst-badge.shift-up .rst-current-version .fa-book, .rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} + +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} + +.rst-versions.rst-badge .rst-current-version { + width: auto; + height: 30px; + line-height: 30px; + padding: 0 6px; + display: block; + text-align: center +} + +@media screen and (max-width: 768px) { + .rst-versions { + width: 85%; + display: none + } + + .rst-versions.shift { + display: block + } +} + +.rst-content img { + max-width: 100%; + height: auto !important +} + +.rst-content .highlight > pre { + line-height: normal +} + +.rst-content div.figure { + margin-bottom: 24px +} + +.rst-content div.figure p.caption { + font-style: italic +} + +.rst-content div.figure.align-center { + text-align: center +} + +.rst-content .section > img, .rst-content .section > a > img { + margin-bottom: 24px +} + +.rst-content blockquote { + margin-left: 24px; + line-height: 24px; + margin-bottom: 24px +} + +.rst-content .note .last, .rst-content .attention .last, .rst-content .caution .last, .rst-content .danger .last, .rst-content .error .last, .rst-content .hint .last, .rst-content .important .last, .rst-content .tip .last, .rst-content .warning .last, .rst-content .seealso .last, .rst-content .admonition-todo .last { + margin-bottom: 0 +} + +.rst-content .admonition-title:before { + margin-right: 4px +} + +.rst-content .admonition table { + border-color: rgba(0, 0, 0, 0.1) +} + +.rst-content .admonition table td, .rst-content .admonition table th { + background: transparent !important; + border-color: rgba(0, 0, 0, 0.1) !important +} + +.rst-content .section ol.loweralpha, .rst-content .section ol.loweralpha li { + list-style: lower-alpha +} + +.rst-content .section ol.upperalpha, .rst-content .section ol.upperalpha li { + list-style: upper-alpha +} + +.rst-content .section ol p, .rst-content .section ul p { + margin-bottom: 12px +} + +.rst-content .line-block { + margin-left: 24px +} + +.rst-content .topic-title { + font-weight: bold; + margin-bottom: 12px +} + +.rst-content .toc-backref { + color: #404040 +} + +.rst-content .align-right { + float: right; + margin: 0px 0px 24px 24px +} + +.rst-content .align-left { + float: left; + margin: 0px 24px 24px 0px +} + +.rst-content .align-center { + margin: auto; + display: block +} + +.rst-content h1 .headerlink, .rst-content h2 .headerlink, .rst-content .toctree-wrapper p.caption .headerlink, .rst-content h3 .headerlink, .rst-content h4 .headerlink, .rst-content h5 .headerlink, .rst-content h6 .headerlink, .rst-content dl dt .headerlink, .rst-content p.caption .headerlink { + display: none; + visibility: hidden; + font-size: 14px +} + +.rst-content h1 .headerlink:after, .rst-content h2 .headerlink:after, .rst-content .toctree-wrapper p.caption .headerlink:after, .rst-content h3 .headerlink:after, .rst-content h4 .headerlink:after, .rst-content h5 .headerlink:after, .rst-content h6 .headerlink:after, .rst-content dl dt .headerlink:after, .rst-content p.caption .headerlink:after { + visibility: visible; + content: ""; + font-family: FontAwesome; + display: inline-block +} + +.rst-content h1:hover .headerlink, .rst-content h2:hover .headerlink, .rst-content .toctree-wrapper p.caption:hover .headerlink, .rst-content h3:hover .headerlink, .rst-content h4:hover .headerlink, .rst-content h5:hover .headerlink, .rst-content h6:hover .headerlink, .rst-content dl dt:hover .headerlink, .rst-content p.caption:hover .headerlink { + display: inline-block +} + +.rst-content .sidebar { + float: right; + width: 40%; + display: block; + margin: 0 0 24px 24px; + padding: 24px; + background: #f3f6f6; + border: solid 1px #e1e4e5 +} + +.rst-content .sidebar p, .rst-content .sidebar ul, .rst-content .sidebar dl { + font-size: 90% +} + +.rst-content .sidebar .last { + margin-bottom: 0 +} + +.rst-content .sidebar .sidebar-title { + display: block; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif; + font-weight: bold; + background: #e1e4e5; + padding: 6px 12px; + margin: -24px; + margin-bottom: 24px; + font-size: 100% +} + +.rst-content .highlighted { + background: #F1C40F; + display: inline-block; + font-weight: bold; + padding: 0 6px +} + +.rst-content .footnote-reference, .rst-content .citation-reference { + vertical-align: super; + font-size: 90% +} + +.rst-content table.docutils.citation, .rst-content table.docutils.footnote { + background: none; + border: none; + color: #999 +} + +.rst-content table.docutils.citation td, .rst-content table.docutils.citation tr, .rst-content table.docutils.footnote td, .rst-content table.docutils.footnote tr { + border: none; + background-color: transparent !important; + white-space: normal +} + +.rst-content table.docutils.citation td.label, .rst-content table.docutils.footnote td.label { + padding-left: 0; + padding-right: 0; + vertical-align: top +} + +.rst-content table.docutils.citation tt, .rst-content table.docutils.citation code, .rst-content table.docutils.footnote tt, .rst-content table.docutils.footnote code { + color: #555 +} + +.rst-content table.field-list { + border: none +} + +.rst-content table.field-list td { + border: none; + padding-top: 5px +} + +.rst-content table.field-list td > strong { + display: inline-block; + margin-top: 3px +} + +.rst-content table.field-list .field-name { + padding-right: 10px; + text-align: left; + white-space: nowrap +} + +.rst-content table.field-list .field-body { + text-align: left; + padding-left: 0 +} + +.rst-content tt, .rst-content tt, .rst-content code { + color: #000; + padding: 2px 5px +} + +.rst-content tt big, .rst-content tt em, .rst-content tt big, .rst-content code big, .rst-content tt em, .rst-content code em { + font-size: 100% !important; + line-height: normal +} + +.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal { + color: #E74C3C +} + +.rst-content tt.xref, a .rst-content tt, .rst-content tt.xref, .rst-content code.xref, a .rst-content tt, a .rst-content code { + font-weight: bold; + /*color: #404040 TODO: Remove it? */ +} + +.rst-content a tt, .rst-content a tt, .rst-content a code { + color: #dd4814 +} + +.rst-content dl { + margin-bottom: 24px +} + +.rst-content dl dt { + font-weight: bold +} + +.rst-content dl p, .rst-content dl table, .rst-content dl ul, .rst-content dl ol { + margin-bottom: 12px !important +} + +.rst-content dl dd { + margin: 0 0 12px 24px +} + +.rst-content dl:not(.docutils) { + margin-bottom: 24px +} + +.rst-content dl:not(.docutils) dt { + display: inline-block; + margin: 6px 0; + font-size: 90%; + line-height: normal; + background: #fdc894; + color: #444; + border-top: solid 3px #dd4814; + padding: 6px; + position: relative +} + +.rst-content dl:not(.docutils) dt:before { + color: #8ba8af +} + +.rst-content dl:not(.docutils) dt .headerlink { + color: #404040; + font-size: 100% !important +} + +.rst-content dl:not(.docutils) dl dt { + margin-bottom: 6px; + border: none; + border-left: solid 3px #ccc; + background: #f0f0f0; + color: #555 +} + +.rst-content dl:not(.docutils) dl dt .headerlink { + color: #404040; + font-size: 100% !important +} + +.rst-content dl:not(.docutils) dt:first-child { + margin-top: 0 +} + +.rst-content dl:not(.docutils) tt, .rst-content dl:not(.docutils) tt, .rst-content dl:not(.docutils) code { + font-weight: bold +} + +.rst-content dl:not(.docutils) tt.descname, .rst-content dl:not(.docutils) tt.descclassname, .rst-content dl:not(.docutils) tt.descname, .rst-content dl:not(.docutils) code.descname, .rst-content dl:not(.docutils) tt.descclassname, .rst-content dl:not(.docutils) code.descclassname { + background-color: transparent; + border: none; + padding: 0; + font-size: 100% !important +} + +.rst-content dl:not(.docutils) tt.descname, .rst-content dl:not(.docutils) tt.descname, .rst-content dl:not(.docutils) code.descname { + font-weight: bold +} -.rst-content dl:not(.docutils) dt:first-child { margin-top: 0 } +.rst-content dl:not(.docutils) .optional { + display: inline-block; + padding: 0 4px; + color: #000; + font-weight: bold +} -.rst-content dl:not(.docutils) tt { font-weight: bold } +.rst-content dl:not(.docutils) .property { + display: inline-block; + padding-right: 8px +} -.rst-content dl:not(.docutils) tt.descname, .rst-content dl:not(.docutils) tt.descclassname { background-color: transparent; border: none; padding: 0; font-size: 100% !important } +.rst-content .viewcode-link, .rst-content .viewcode-back { + display: inline-block; + color: #27AE60; + font-size: 80%; + padding-left: 24px +} -.rst-content dl:not(.docutils) tt.descname { font-weight: bold } +.rst-content .viewcode-back { + display: block; + float: right +} -.rst-content dl:not(.docutils) .optional { display: inline-block; padding: 0 4px; color: #000000; font-weight: bold } +.rst-content p.rubric { + margin-bottom: 12px; + font-weight: bold +} -.rst-content dl:not(.docutils) .property { display: inline-block; padding-right: 8px } +.rst-content tt.download, .rst-content code.download { + background: inherit; + padding: inherit; + font-weight: normal; + font-family: inherit; + font-size: inherit; + color: inherit; + border: inherit; + white-space: inherit +} -.rst-content .viewcode-link, .rst-content .viewcode-back { display: inline-block; color: #27ae60; font-size: 80%; padding-left: 24px } +.rst-content tt.download span:first-child, .rst-content code.download span:first-child { + -webkit-font-smoothing: subpixel-antialiased +} -.rst-content .viewcode-back { display: block; float: right } +.rst-content tt.download span:first-child:before, .rst-content code.download span:first-child:before { + margin-right: 4px +} -.rst-content p.rubric { margin-bottom: 12px; font-weight: bold } +.rst-content .guilabel { + border: 1px solid #7fbbe3; + background: #e7f2fa; + font-size: 80%; + font-weight: 700; + border-radius: 4px; + padding: 2.4px 6px; + margin: auto 2px +} @media screen and (max-width: 480px) { - .rst-content .sidebar { width: 100% } + .rst-content .sidebar { + width: 100% + } +} + +span[id*='MathJax-Span'] { + color: #404040 +} + +.math { + text-align: center } -span[id*='MathJax-Span'] { color: #404040 } +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 400; + src: local("Inconsolata"), local("Inconsolata-Regular"), url(../fonts/Inconsolata-Regular.ttf) format("truetype") +} + +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 700; + src: local("Inconsolata Bold"), local("Inconsolata-Bold"), url(../fonts/Inconsolata-Bold.ttf) format("truetype") +} + +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 400; + src: local("Lato Regular"), local("Lato-Regular"), url(../fonts/Lato-Regular.ttf) format("truetype") +} -.math { text-align: center } +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Bold"), local("Lato-Bold"), url(../fonts/Lato-Bold.ttf) format("truetype") +} + +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 400; + src: local("Roboto Slab Regular"), local("RobotoSlab-Regular"), url(../fonts/RobotoSlab-Regular.ttf) format("truetype") +} + +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 700; + src: local("Roboto Slab Bold"), local("RobotoSlab-Bold"), url(../fonts/RobotoSlab-Bold.ttf) format("truetype") +} /*# sourceMappingURL=theme.css.map */ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css.map b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css.map new file mode 100644 index 000000000000..d43241724c94 --- /dev/null +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/css/theme.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "CACE,AAAE,ECQI,iBAAoB,EDPJ,SAAU,ECY1B,cAAiB,EDZD,SAAU,EC2B1B,SAAY,ED3BI,SAAU,EEFlC,uEAAiF,EAC/E,MAAO,EAAE,IAAK,EAEhB,iBAAoB,EAClB,MAAO,EAAE,WAAY,EACrB,OAAQ,EAAE,KAAM,EAChB,IAAK,EAAE,AAAC,EAEV,oBAAqB,EACnB,MAAO,EAAE,GAAI,EAEf,OAAQ,EACN,MAAO,EAAE,GAAI,EAEf,AAAC,EDLO,iBAAoB,ECMd,SAAU,EDDhB,cAAiB,ECCX,SAAU,EDchB,SAAY,ECdN,SAAU,EAExB,GAAI,EACF,QAAS,EAAE,GAAI,EACf,uBAAwB,EAAE,GAAI,EAC9B,mBAAoB,EAAE,GAAI,EAE5B,GAAI,EACF,KAAM,EAAE,AAAC,EAEX,eAAiB,EACf,MAAO,EAAE,AAAC,EAEZ,UAAW,EACT,YAAa,EAAE,SAAU,EAE3B,OAAS,EACP,UAAW,EAAE,GAAI,EAEnB,SAAU,EACR,KAAM,EAAE,AAAC,EAEX,EAAG,EACD,SAAU,EAAE,KAAM,EAGpB,EAAG,EACD,SAAU,EAAE,GAAI,EAChB,IAAK,EAAE,GAAI,EACX,cAAe,EAAE,GAAI,EAEvB,GAAI,EACF,SAAU,EAAE,GAAI,EAChB,IAAK,EAAE,GAAI,EACX,SAAU,EAAE,KAAM,EAClB,UAAW,EAAE,GAAI,EAEnB,kDAAoB,EAClB,UAAW,EAAE,cAAgB,EAC7B,WAAY,EAAE,sBAAwB,EACtC,QAAS,EAAE,EAAG,EAEhB,EAAG,EACD,UAAW,EAAE,EAAG,EAElB,AAAC,EACC,KAAM,EAAE,GAAI,EAEd,eAAiB,EACf,MAAO,EAAE,CAAE,EACX,MAAO,EAAE,GAAI,EAEf,IAAK,EACH,QAAS,EAAE,EAAG,EAEhB,MAAQ,EACN,QAAS,EAAE,EAAG,EACd,UAAW,EAAE,AAAC,EACd,OAAQ,EAAE,OAAQ,EAClB,aAAc,EAAE,OAAQ,EAE1B,EAAG,EACD,EAAG,EAAE,KAAM,EAEb,EAAG,EACD,KAAM,EAAE,MAAO,EAEjB,OAAU,EACR,KAAM,EAAE,AAAC,EACT,MAAO,EAAE,AAAC,EACV,SAAU,EAAE,GAAI,EAChB,eAAgB,EAAE,GAAI,EAExB,CAAE,EACA,SAAU,EAAE,GAAI,EAElB,CAAE,EACA,KAAM,EAAE,AAAC,EAEX,EAAG,EACD,KAAM,EAAE,AAAC,EACT,qBAAsB,EAAE,MAAO,EAC/B,aAAc,EAAE,KAAM,EACtB,QAAS,EAAE,GAAI,EAEjB,aAAc,EACZ,OAAQ,EAAE,KAAM,EAElB,KAAM,EACJ,KAAM,EAAE,AAAC,EAEX,GAAI,EACF,KAAM,EAAE,AAAC,EAEX,OAAQ,EACN,KAAM,EAAE,AAAC,EACT,KAAM,EAAE,AAAC,EACT,MAAO,EAAE,AAAC,EAEZ,IAAK,EACH,KAAM,EAAE,MAAO,EAEjB,KAAM,EACJ,KAAM,EAAE,AAAC,EACT,WAAY,EAAE,GAAI,EAClB,MAAO,EAAE,AAAC,EACV,UAAW,EAAE,KAAM,EAErB,2BAA+B,EAC7B,QAAS,EAAE,GAAI,EACf,KAAM,EAAE,AAAC,EACT,aAAc,EAAE,OAAQ,EACxB,cAAe,EAAE,KAAM,EAEzB,WAAa,EACX,UAAW,EAAE,KAAM,EAErB,mEAAuE,EACrE,KAAM,EAAE,MAAO,EACf,iBAAkB,EAAE,KAAM,EAC1B,QAAS,EAAE,MAAO,EAEpB,+BAAiC,EAC/B,KAAM,EAAE,MAAO,EAEjB,yCAA2C,EACzC,SAAU,EAAE,SAAU,EACtB,MAAO,EAAE,AAAC,EACV,KAAM,EAAE,GAAI,EACZ,MAAO,EAAE,GAAI,EAEf,mBAAoB,EAClB,iBAAkB,EAAE,QAAS,EAC7B,cAAe,EAAE,UAAW,EAC5B,iBAAkB,EAAE,UAAW,EAC/B,SAAU,EAAE,UAAW,EAEzB,iGAAmG,EACjG,iBAAkB,EAAE,GAAI,EAE1B,+CAAiD,EAC/C,KAAM,EAAE,AAAC,EACT,MAAO,EAAE,AAAC,EAEZ,OAAQ,EACN,OAAQ,EAAE,GAAI,EACd,aAAc,EAAE,EAAG,EACnB,KAAM,EAAE,OAAQ,EAElB,IAAK,EACH,cAAe,EAAE,OAAQ,EACzB,aAAc,EAAE,AAAC,EAEnB,CAAE,EACA,aAAc,EAAE,EAAG,EAErB,WAAY,EACV,KAAM,EAAE,MAAO,EACf,SAAU,EAAE,GAAI,EAChB,IAAK,EAAE,GAAK,EACZ,MAAO,EAAE,MAAO,EAElB,EAAG,EACD,MAAO,EAAE,IAAK,EACd,KAAM,EAAE,AAAC,EACT,UAAW,EAAE,KAAM,EACnB,OAAQ,EAAE,KAAM,EAChB,eAAgB,EAAE,UAAW,EAC7B,gBAAiB,EAAE,QAAS,EAC5B,SAAU,EAAE,GAAI,EAChB,QAAS,EAAE,EAAG,EACd,WAAY,EAAE,AAAC,EAEjB,KAAM,EACJ,MAAO,EAAE,GAAI,EAEf,MAAO,EACL,MAAO,EAAE,cAAe,EACxB,SAAU,EAAE,KAAM,EAEpB,cAAe,EACb,KAAM,EAAE,AAAC,EACT,GAAI,EAAE,YAAa,EACnB,KAAM,EAAE,EAAG,EACX,KAAM,EAAE,GAAI,EACZ,OAAQ,EAAE,KAAM,EAChB,MAAO,EAAE,AAAC,EACV,OAAQ,EAAE,OAAQ,EAClB,IAAK,EAAE,EAAG,EAEZ,+DAAiE,EAC/D,GAAI,EAAE,GAAI,EACV,KAAM,EAAE,GAAI,EACZ,KAAM,EAAE,AAAC,EACT,OAAQ,EAAE,MAAO,EACjB,OAAQ,EAAE,KAAM,EAChB,IAAK,EAAE,GAAI,EAEb,SAAU,EACR,SAAU,EAAE,KAAM,EAEpB,QAAS,EACP,OAAQ,EAAE,OAAQ,EAEpB,QAAU,EACR,QAAS,EAAE,GAAI,EAEjB,WAAY,EACV,gBAAmB,EACjB,SAAU,EAAE,cAAe,EAC7B,AAAC,EACC,SAAU,EAAE,cAAe,EAC3B,UAAW,EAAE,cAAe,EAC5B,KAAM,EAAE,cAAe,EACvB,SAAU,EAAE,cAAe,EAC7B,UAAY,EACV,cAAe,EAAE,QAAS,EAC5B,0DAA6D,EAC3D,MAAO,EAAE,CAAE,EACb,aAAe,EACb,gBAAiB,EAAE,IAAK,EAC1B,IAAK,EACH,MAAO,EAAE,iBAAkB,EAC7B,KAAO,EACL,gBAAiB,EAAE,IAAK,EAC1B,EAAG,EACD,QAAS,EAAE,cAAe,QAE1B,KAAM,EAAE,IAAK,EAEf,8CAAS,EACP,MAAO,EAAE,AAAC,EACV,KAAM,EAAE,AAAC,EACX,4CAAM,EACJ,eAAgB,EAAE,IAAK,GChM3B,ykDAAY,EACV,qBAAsB,EAAE,UAAW,EAqDrC,QAAS,EARP,IAAK,EAAE,AAAC,EACR,+BAAS,EAEP,MAAO,EAAE,IAAK,EACd,MAAO,EAAE,CAAE,EACb,cAAO,EACL,IAAK,EAAE,GAAI,EC7Gf;;;IAGG,DCAH,UAWC,CAVC,WAAW,CAAE,aAAa,CAC1B,GAAG,CAAE,+CAAgE,CACrE,GAAG,CAAE,wWAI8F,CAEnG,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,MAAM,CCVpB,kfAAmB,CACjB,OAAO,CAAE,YAAY,CACrB,IAAI,CAAE,uCAA8E,CACpF,SAAS,CAAE,OAAO,CAClB,cAAc,CAAE,IAAI,CACpB,sBAAsB,CAAE,WAAW,CACnC,uBAAuB,CAAE,SAAS,CCLpC,MAAsB,CACpB,SAAS,CAAE,SAAS,CACpB,WAAW,CAAE,KAAS,CACtB,cAAc,CAAE,IAAI,CAEtB,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CCVtC,MAAsB,CACpB,KAAK,CAAE,SAAW,CAClB,UAAU,CAAE,MAAM,CCDpB,MAAsB,CACpB,YAAY,CAAE,CAAC,CACf,WAAW,CCMU,SAAS,CDL9B,eAAe,CAAE,IAAI,CACrB,SAAK,CAAE,QAAQ,CAAE,QAAQ,CAE3B,MAAsB,CACpB,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,UAAa,CACnB,KAAK,CCDgB,SAAS,CDE9B,GAAG,CAAE,QAAU,CACf,UAAU,CAAE,MAAM,CAClB,YAAuB,CACrB,IAAI,CAAE,UAA0B,CEbpC,UAA0B,CACxB,OAAO,CAAE,gBAAgB,CACzB,MAAM,CAAE,iBAA4B,CACpC,aAAa,CAAE,IAAI,CAGrB,aAA6B,CAAE,KAAK,CAAE,IAAI,CAC1C,cAA8B,CAAE,KAAK,CAAE,KAAK,CAG1C,ksBAA8B,CAAE,YAAY,CAAE,IAAI,CAClD,ktBAA+B,CAAE,WAAW,CAAE,IAAI,CAIpD,WAAY,CAAE,KAAK,CAAE,KAAK,CAC1B,UAAW,CAAE,KAAK,CAAE,IAAI,CAGtB,kpBAAY,CAAE,YAAY,CAAE,IAAI,CAChC,kqBAAa,CAAE,WAAW,CAAE,IAAI,CCpBlC,QAAwB,CACtB,iBAAiB,CAAE,0BAA0B,CACrC,SAAS,CAAE,0BAA0B,CAG/C,SAAyB,CACvB,iBAAiB,CAAE,4BAA4B,CACvC,SAAS,CAAE,4BAA4B,CAGjD,0BASC,CARC,EAAG,CACD,iBAAiB,CAAE,YAAY,CACvB,SAAS,CAAE,YAAY,CAEjC,IAAK,CACH,iBAAiB,CAAE,cAAc,CACzB,SAAS,CAAE,cAAc,EAIrC,kBASC,CARC,EAAG,CACD,iBAAiB,CAAE,YAAY,CACvB,SAAS,CAAE,YAAY,CAEjC,IAAK,CACH,iBAAiB,CAAE,cAAc,CACzB,SAAS,CAAE,cAAc,EC5BrC,aAA8B,CCW5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,aAAgB,CAC/B,aAAa,CAAE,aAAgB,CAC3B,SAAS,CAAE,aAAgB,CDbrC,cAA8B,CCU5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,cAAgB,CAC/B,aAAa,CAAE,cAAgB,CAC3B,SAAS,CAAE,cAAgB,CDZrC,cAA8B,CCS5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,cAAgB,CAC/B,aAAa,CAAE,cAAgB,CAC3B,SAAS,CAAE,cAAgB,CDVrC,mBAAmC,CCcjC,UAAU,CAAE,oEAA+E,CAC3F,iBAAiB,CAAE,YAAoB,CACnC,aAAa,CAAE,YAAoB,CAC/B,SAAS,CAAE,YAAoB,CDhBzC,iBAAmC,CCajC,UAAU,CAAE,oEAA+E,CAC3F,iBAAiB,CAAE,YAAoB,CACnC,aAAa,CAAE,YAAoB,CAC/B,SAAS,CAAE,YAAoB,CDXzC,+GAIuC,CACrC,MAAM,CAAE,IAAI,CEfd,SAAyB,CACvB,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,YAAY,CACrB,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,GAAG,CACX,WAAW,CAAE,GAAG,CAChB,cAAc,CAAE,MAAM,CAExB,yBAAyD,CACvD,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,CAAC,CACP,KAAK,CAAE,IAAI,CACX,UAAU,CAAE,MAAM,CAEpB,YAA4B,CAAE,WAAW,CAAE,OAAO,CAClD,YAA4B,CAAE,SAAS,CAAE,GAAG,CAC5C,WAA2B,CAAE,KAAK,CLTZ,IAAI,CMP1B,gBAAgC,CAAE,OAAO,CNyT1B,GAAO,CMxTtB,gBAAgC,CAAE,OAAO,CNmc1B,GAAO,CMlctB,qCAAiC,CAAE,OAAO,CN8hB1B,GAAO,CM7hBvB,qBAAqC,CAAE,OAAO,CN2N1B,GAAO,CM1N3B,gBAAgC,CAAE,OAAO,CNsV1B,GAAO,CMrVtB,eAA+B,CAAE,OAAO,CNolB1B,GAAO,CMnlBrB,iBAAiC,CAAE,OAAO,CNwlB1B,GAAO,CMvlBvB,eAA+B,CAAE,OAAO,CN4qB1B,GAAO,CM3qBrB,eAA+B,CAAE,OAAO,CNqQ1B,GAAO,CMpQrB,mBAAmC,CAAE,OAAO,CNunB1B,GAAO,CMtnBzB,aAA6B,CAAE,OAAO,CNqnB1B,GAAO,CMpnBnB,kBAAkC,CAAE,OAAO,CNsnB1B,GAAO,CMrnBxB,gBAAgC,CAAE,OAAO,CNiI1B,GAAO,CMhItB,mDAEgC,CAAE,OAAO,CN0nB1B,GAAO,CMznBtB,sBAAsC,CAAE,OAAO,CNkhB1B,GAAO,CMjhB5B,uBAAuC,CAAE,OAAO,CNghB1B,GAAO,CM/gB7B,oBAAoC,CAAE,OAAO,CNme1B,GAAO,CMle1B,iBAAiC,CAAE,OAAO,CNoiB1B,GAAO,CMniBvB,8BAC8B,CAAE,OAAO,CNwJ1B,GAAO,CMvJpB,kBAAkC,CAAE,OAAO,CNkoB1B,GAAO,CMjoBxB,iCAA+B,CAAE,OAAO,CNuU1B,GAAO,CMtUrB,iBAAiC,CAAE,OAAO,CNyO1B,GAAO,CMxOvB,kBAAkC,CAAE,OAAO,CNwI1B,GAAO,CMvIxB,eAA+B,CAAE,OAAO,CNwf1B,GAAO,CMvfrB,uHAAmC,CAAE,OAAO,CNwL1B,GAAO,CMvLzB,8BAA8C,CAAE,OAAO,CNQ1B,GAAO,CMPpC,4BAA4C,CAAE,OAAO,CNU1B,GAAO,CMTlC,gBAAgC,CAAE,OAAO,CNgV1B,GAAO,CM/UtB,wBAAwC,CAAE,OAAO,CNgd1B,GAAO,CM/c9B,yCACiC,CAAE,OAAO,CN2e1B,GAAO,CM1evB,kBAAkC,CAAE,OAAO,CNqe1B,GAAO,CMpexB,mBAAmC,CAAE,OAAO,CNkX1B,GAAO,CMjXzB,eAA+B,CAAE,OAAO,CNqX1B,GAAO,CMpXrB,eAA+B,CAAE,OAAO,CN8O1B,GAAO,CM7OrB,qBAAqC,CAAE,OAAO,CNmT1B,GAAO,CMlT3B,qBAAqC,CAAE,OAAO,CN+pB1B,GAAO,CM9pB3B,sBAAsC,CAAE,OAAO,CN6pB1B,GAAO,CM5pB5B,oBAAoC,CAAE,OAAO,CN8pB1B,GAAO,CM7pB1B,iBAAiC,CAAE,OAAO,CNgd1B,GAAO,CM/cvB,kBAAkC,CAAE,OAAO,CNmB1B,GAAO,CMlBxB,cAA8B,CAAE,OAAO,CN0kB1B,GAAO,CMzkBpB,eAA+B,CAAE,OAAO,CN0kB1B,GAAO,CMzkBrB,iCAA+B,CAAE,OAAO,CNiD1B,GAAO,CMhDrB,mBAAmC,CAAE,OAAO,CNiD1B,GAAO,CMhDzB,gBAAgC,CAAE,OAAO,CNsc1B,GAAO,CMrctB,iBAAiC,CAAE,OAAO,CNmE1B,GAAO,CMlEvB,eAA+B,CAAE,OAAO,CN4O1B,GAAO,CM3OrB,eAA+B,CAAE,OAAO,CNyC1B,GAAO,CMxCrB,iBAAiC,CAAE,OAAO,CNqU1B,GAAO,CMpUvB,sBAAsC,CAAE,OAAO,CNwkB1B,GAAO,CMvkB5B,qBAAqC,CAAE,OAAO,CNwkB1B,GAAO,CMvkB3B,qBAAqC,CAAE,OAAO,CNxC1B,GAAO,CMyC3B,uBAAuC,CAAE,OAAO,CN3C1B,GAAO,CM4C7B,sBAAsC,CAAE,OAAO,CNzC1B,GAAO,CM0C5B,wBAAwC,CAAE,OAAO,CN5C1B,GAAO,CM6C9B,eAA+B,CAAE,OAAO,CNyV1B,GAAO,CMxVrB,oCACkC,CAAE,OAAO,CNmZ1B,GAAO,CMlZxB,iBAAiC,CAAE,OAAO,CNiT1B,GAAO,CMhTvB,uBAAuC,CAAE,OAAO,CNgoB1B,GAAO,CM/nB7B,sDAEoC,CAAE,OAAO,CNka1B,GAAO,CMja1B,iBAAiC,CAAE,OAAO,CN0Z1B,GAAO,CMzZvB,qBAAqC,CAAE,OAAO,CNkW1B,GAAO,CMjW3B,iBAAiC,CAAE,OAAO,CN1D1B,GAAO,CM2DvB,eAA+B,CAAE,OAAO,CNskB1B,GAAO,CMrkBrB,0CAC0C,CAAE,OAAO,CNuZ1B,GAAO,CMtZhC,yBAAyC,CAAE,OAAO,CNke1B,GAAO,CMje/B,yBAAyC,CAAE,OAAO,CNuE1B,GAAO,CMtE/B,iBAAiC,CAAE,OAAO,CN7B1B,GAAO,CM8BvB,wBAAwC,CAAE,OAAO,CNqhB1B,GAAO,CMphB9B,wBAAwC,CAAE,OAAO,CNuK1B,GAAO,CMtK9B,mBAAmC,CAAE,OAAO,CNtB1B,GAAO,CMuBzB,eAA+B,CAAE,OAAO,CN8Z1B,GAAO,CM7ZrB,gBAAgC,CAAE,OAAO,CNwY1B,GAAO,CMvYtB,eAA+B,CAAE,OAAO,CNqhB1B,GAAO,CMphBrB,kBAAkC,CAAE,OAAO,CNiN1B,GAAO,CMhNxB,uBAAuC,CAAE,OAAO,CNkK1B,GAAO,CMjK7B,uBAAuC,CAAE,OAAO,CN8gB1B,GAAO,CM7gB7B,gBAAgC,CAAE,OAAO,CNoI1B,GAAO,CMnItB,uBAAuC,CAAE,OAAO,CNiE1B,GAAO,CMhE7B,wBAAwC,CAAE,OAAO,CNiE1B,GAAO,CMhE9B,sBAAsC,CAAE,OAAO,CN0Z1B,GAAO,CMzZ5B,uBAAuC,CAAE,OAAO,CN+V1B,GAAO,CM9V7B,8FAAuC,CAAE,OAAO,CNgjB1B,GAAO,CM/iB7B,+FAAuC,CAAE,OAAO,CNmD1B,GAAO,CMlD7B,0BAA0C,CAAE,OAAO,CNga1B,GAAO,CM/ZhC,sBAAsC,CAAE,OAAO,CNqR1B,GAAO,CMpR5B,qBAAqC,CAAE,OAAO,CNkG1B,GAAO,CMjG3B,yBAAyC,CAAE,OAAO,CN4iB1B,GAAO,CM3iB/B,yBAAyC,CAAE,OAAO,CN+C1B,GAAO,CM9C/B,cAA8B,CAAE,OAAO,CNvC1B,GAAO,CMwCpB,qBAAqC,CAAE,OAAO,CNvD1B,GAAO,CMwD3B,sBAAsC,CAAE,OAAO,CNvD1B,GAAO,CMwD5B,mBAAmC,CAAE,OAAO,CNvD1B,GAAO,CMwDzB,qBAAqC,CAAE,OAAO,CN3D1B,GAAO,CM4D3B,wCACgC,CAAE,OAAO,CNgc1B,GAAO,CM/btB,iBAAiC,CAAE,OAAO,CNgI1B,GAAO,CM/HvB,mBAAmC,CAAE,OAAO,CN8E1B,GAAO,CM7EzB,eAA+B,CAAE,OAAO,CNuY1B,GAAO,CMtYrB,gBAAgC,CAAE,OAAO,CN4U1B,GAAO,CM3UtB,mBAAmC,CAAE,OAAO,CNxD1B,GAAO,CMyDzB,gNAA6C,CAAE,OAAO,CNyH1B,GAAO,CMxHnC,eAA+B,CAAE,OAAO,CNmM1B,GAAO,CMlMrB,eAA+B,CAAE,OAAO,CNqR1B,GAAO,CMpRrB,iCAA+B,CAAE,OAAO,CN+J1B,GAAO,CM9JrB,cAA8B,CAAE,OAAO,CN2H1B,GAAO,CM1HpB,oBAAoC,CAAE,OAAO,CN2H1B,GAAO,CM1H1B,kDAC+C,CAAE,OAAO,CNmH1B,GAAO,CMlHrC,gBAAgC,CAAE,OAAO,CNuX1B,GAAO,CMtXtB,mBAAmC,CAAE,OAAO,CNR1B,GAAO,CMSzB,iBAAiC,CAAE,OAAO,CN0Y1B,GAAO,CMzYvB,kBAAkC,CAAE,OAAO,CNwD1B,GAAO,CMvDxB,iBAAiC,CAAE,OAAO,CNuS1B,GAAO,CMtSvB,qBAAqC,CAAE,OAAO,CN+B1B,GAAO,CM9B3B,uBAAuC,CAAE,OAAO,CN2B1B,GAAO,CM1B7B,kBAAkC,CAAE,OAAO,CNoZ1B,GAAO,CMnZxB,wBAAwC,CAAE,OAAO,CNsb1B,GAAO,CMrb9B,iBAAiC,CAAE,OAAO,CN4J1B,GAAO,CM3JvB,sBAAsC,CAAE,OAAO,CN6J1B,GAAO,CM5J5B,mBAAmC,CAAE,OAAO,CN/E1B,GAAO,CMgFzB,mBAAmC,CAAE,OAAO,CNjF1B,GAAO,CMkFzB,2CACoC,CAAE,OAAO,CNvE1B,GAAO,CMwE1B,yBAAyC,CAAE,OAAO,CNkiB1B,GAAO,CMjiB/B,0BAA0C,CAAE,OAAO,CN8G1B,GAAO,CM7GhC,uBAAuC,CAAE,OAAO,CNjB1B,GAAO,CMkB7B,cAA8B,CAAE,OAAO,CNsP1B,GAAO,CMrPpB,gCAC+B,CAAE,OAAO,CNqC1B,GAAO,CMpCrB,mBAAmC,CAAE,OAAO,CN0C1B,GAAO,CMzCzB,sBAAsC,CAAE,OAAO,CN2f1B,GAAO,CM1f5B,wBAAwC,CAAE,OAAO,CNyf1B,GAAO,CMxf9B,oBAAoC,CAAE,OAAO,CN6c1B,GAAO,CM5c1B,kBAAkC,CAAE,OAAO,CN6M1B,GAAO,CM5MxB,mBAAmC,CAAE,OAAO,CNua1B,GAAO,CMtazB,0BAA0C,CAAE,OAAO,CNkQ1B,GAAO,CMjQhC,qBAAqC,CAAE,OAAO,CNkf1B,GAAO,CMjf3B,wBAAwC,CAAE,OAAO,CNwF1B,GAAO,CMvF9B,kBAAkC,CAAE,OAAO,CNia1B,GAAO,CMhaxB,iBAAiC,CAAE,OAAO,CNwgB1B,GAAO,CMvgBvB,wBAAwC,CAAE,OAAO,CNiK1B,GAAO,CMhK9B,iBAAiC,CAAE,OAAO,CN0hB1B,GAAO,CMzhBvB,kBAAkC,CAAE,OAAO,CNgP1B,GAAO,CM/OxB,gBAAgC,CAAE,OAAO,CNyU1B,GAAO,CMxUtB,mBAAmC,CAAE,OAAO,CN6b1B,GAAO,CM5bzB,qBAAqC,CAAE,OAAO,CNzD1B,GAAO,CM0D3B,uBAAuC,CAAE,OAAO,CNuU1B,GAAO,CMtU7B,kBAAkC,CAAE,OAAO,CNygB1B,GAAO,CMxgBxB,yCACmC,CAAE,OAAO,CNkF1B,GAAO,CMjFzB,qCAAiC,CAAE,OAAO,CNqJ1B,GAAO,CMpJvB,iBAAiC,CAAE,OAAO,CN6gB1B,GAAO,CM5gBvB,sBAAsC,CAAE,OAAO,CN4B1B,GAAO,CM3B5B,8BAC8B,CAAE,OAAO,CNoX1B,GAAO,CMnXpB,gBAAgC,CAAE,OAAO,CNmL1B,GAAO,CMlLtB,mBAAmC,CAAE,OAAO,CN7D1B,GAAO,CM8DzB,eAA+B,CAAE,OAAO,CNxF1B,GAAO,CMyFrB,sBAAsC,CAAE,OAAO,CN7B1B,GAAO,CM8B5B,uBAAuC,CAAE,OAAO,CNoK1B,GAAO,CMnK7B,sBAAsC,CAAE,OAAO,CNkK1B,GAAO,CMjK5B,oBAAoC,CAAE,OAAO,CNmK1B,GAAO,CMlK1B,sBAAsC,CAAE,OAAO,CN+J1B,GAAO,CM9J5B,2DAA4C,CAAE,OAAO,CNzI1B,GAAO,CM0IlC,6DAA6C,CAAE,OAAO,CNrI1B,GAAO,CMsInC,0BAA0C,CAAE,OAAO,CNrI1B,GAAO,CMsIhC,4BAA4C,CAAE,OAAO,CN7I1B,GAAO,CM8IlC,gBAAgC,CAAE,OAAO,CN4I1B,GAAO,CM3ItB,iBAAiC,CAAE,OAAO,CNqiB1B,GAAO,CMpiBvB,gBAAgC,CAAE,OAAO,CNsc1B,GAAO,CMrctB,iBAAiC,CAAE,OAAO,CN2F1B,GAAO,CM1FvB,oBAAoC,CAAE,OAAO,CNjF1B,GAAO,CMkF1B,qBAAqC,CAAE,OAAO,CNtI1B,GAAO,CMuI3B,iCACgC,CAAE,OAAO,CNigB1B,GAAO,CMhgBtB,kDAC+B,CAAE,OAAO,CNuN1B,GAAO,CMtNrB,gBAAgC,CAAE,OAAO,CNtB1B,GAAO,CMuBtB,gBAAgC,CAAE,OAAO,CN4F1B,GAAO,CM3FtB,kCACmC,CAAE,OAAO,CNiW1B,GAAO,CMhWzB,kCACkC,CAAE,OAAO,CN6E1B,GAAO,CM5ExB,oBAAoC,CAAE,OAAO,CNqR1B,GAAO,CMpR1B,mCACmC,CAAE,OAAO,CNuF1B,GAAO,CMtFzB,iBAAiC,CAAE,OAAO,CNkZ1B,GAAO,CMjZvB,qDAE+B,CAAE,OAAO,CNvI1B,GAAO,CMwIrB,kBAAkC,CAAE,OAAO,CNgN1B,GAAO,CM/MxB,kBAAkC,CAAE,OAAO,CN8M1B,GAAO,CM7MxB,wBAAwC,CAAE,OAAO,CNia1B,GAAO,CMha9B,oBAAoC,CAAE,OAAO,CN8d1B,GAAO,CM7d1B,gBAAgC,CAAE,OAAO,CNwa1B,GAAO,CMvatB,gBAAgC,CAAE,OAAO,CNmN1B,GAAO,CMlNtB,gBAAgC,CAAE,OAAO,CNgd1B,GAAO,CM/ctB,oBAAoC,CAAE,OAAO,CN2R1B,GAAO,CM1R1B,2BAA2C,CAAE,OAAO,CN4R1B,GAAO,CM3RjC,6BAA6C,CAAE,OAAO,CNiH1B,GAAO,CMhHnC,sBAAsC,CAAE,OAAO,CN6G1B,GAAO,CM5G5B,gBAAgC,CAAE,OAAO,CN6O1B,GAAO,CM5OtB,wEAAqC,CAAE,OAAO,CN5F1B,GAAO,CM6F3B,mBAAmC,CAAE,OAAO,CNtF1B,GAAO,CMuFzB,qBAAqC,CAAE,OAAO,CN7F1B,GAAO,CM8F3B,sBAAsC,CAAE,OAAO,CN7F1B,GAAO,CM8F5B,kBAAkC,CAAE,OAAO,CNxC1B,GAAO,CMyCxB,mCAC+B,CAAE,OAAO,CN4W1B,GAAO,CM3WrB,yCACoC,CAAE,OAAO,CNgX1B,GAAO,CM/W1B,sCACmC,CAAE,OAAO,CN6W1B,GAAO,CM5WzB,mBAAmC,CAAE,OAAO,CND1B,GAAO,CMEzB,mBAAmC,CAAE,OAAO,CNkL1B,GAAO,CMjLzB,sCAC+B,CAAE,OAAO,CNwc1B,GAAO,CMvcrB,iCACgC,CAAE,OAAO,CNqE1B,GAAO,CMpEtB,0CACqC,CAAE,OAAO,CNgZ1B,GAAO,CM/Y3B,oBAAoC,CAAE,OAAO,CNrD1B,GAAO,CMsD1B,qBAAqC,CAAE,OAAO,CNlD1B,GAAO,CMmD3B,gCAC+B,CAAE,OAAO,CN5I1B,GAAO,CM6IrB,kBAAkC,CAAE,OAAO,CNgV1B,GAAO,CM/UxB,mBAAmC,CAAE,OAAO,CN4b1B,GAAO,CM3bzB,qCACoC,CAAE,OAAO,CN7E1B,GAAO,CM8E1B,sBAAsC,CAAE,OAAO,CNgK1B,GAAO,CM/J5B,mBAAmC,CAAE,OAAO,CNX1B,GAAO,CMYzB,yBAAyC,CAAE,OAAO,CN3E1B,GAAO,CM4E/B,uBAAuC,CAAE,OAAO,CN3E1B,GAAO,CM4E7B,kBAAkC,CAAE,OAAO,CNkc1B,GAAO,CMjcxB,sBAAsC,CAAE,OAAO,CNgX1B,GAAO,CM/W5B,mBAAmC,CAAE,OAAO,CN2X1B,GAAO,CM1XzB,iBAAiC,CAAE,OAAO,CNtK1B,GAAO,CMuKvB,iBAAiC,CAAE,OAAO,CN1E1B,GAAO,CM2EvB,kBAAkC,CAAE,OAAO,CNlD1B,GAAO,CMmDxB,sBAAsC,CAAE,OAAO,CNgB1B,GAAO,CMf5B,qBAAqC,CAAE,OAAO,CNlJ1B,GAAO,CMmJ3B,qBAAqC,CAAE,OAAO,CNqG1B,GAAO,CMpG3B,oBAAoC,CAAE,OAAO,CNzO1B,GAAO,CM0O1B,iBAAiC,CAAE,OAAO,CNsL1B,GAAO,CMrLvB,sBAAsC,CAAE,OAAO,CNJ1B,GAAO,CMK5B,eAA+B,CAAE,OAAO,CNnL1B,GAAO,CMoLrB,mBAAmC,CAAE,OAAO,CNuE1B,GAAO,CMtEzB,sBAAsC,CAAE,OAAO,CNmP1B,GAAO,CMlP5B,4BAA4C,CAAE,OAAO,CNzO1B,GAAO,CM0OlC,6BAA6C,CAAE,OAAO,CNzO1B,GAAO,CM0OnC,0BAA0C,CAAE,OAAO,CNzO1B,GAAO,CM0OhC,4BAA4C,CAAE,OAAO,CN7O1B,GAAO,CM8OlC,qBAAqC,CAAE,OAAO,CNzO1B,GAAO,CM0O3B,sBAAsC,CAAE,OAAO,CNzO1B,GAAO,CM0O5B,mBAAmC,CAAE,OAAO,CNzO1B,GAAO,CM0OzB,qBAAqC,CAAE,OAAO,CN7O1B,GAAO,CM8O3B,kBAAkC,CAAE,OAAO,CN5D1B,GAAO,CM6DxB,iBAAiC,CAAE,OAAO,CNuH1B,GAAO,CMtHvB,iBAAiC,CAAE,OAAO,CNyW1B,GAAO,CMxWvB,yCACiC,CAAE,OAAO,CN+K1B,GAAO,CM9KvB,mBAAmC,CAAE,OAAO,CNjH1B,GAAO,CMkHzB,qBAAqC,CAAE,OAAO,CN+O1B,GAAO,CM9O3B,sBAAsC,CAAE,OAAO,CN+O1B,GAAO,CM9O5B,kBAAkC,CAAE,OAAO,CNiU1B,GAAO,CMhUxB,iBAAiC,CAAE,OAAO,CNtH1B,GAAO,CMuHvB,sCACgC,CAAE,OAAO,CNyP1B,GAAO,CMxPtB,qBAAqC,CAAE,OAAO,CNgC1B,GAAO,CM/B3B,mBAAmC,CAAE,OAAO,CNK1B,GAAO,CMJzB,wBAAwC,CAAE,OAAO,CNM1B,GAAO,CML9B,kBAAkC,CAAE,OAAO,CNwS1B,GAAO,CMvSxB,kBAAkC,CAAE,OAAO,CNY1B,GAAO,CMXxB,gBAAgC,CAAE,OAAO,CNyJ1B,GAAO,CMxJtB,kBAAkC,CAAE,OAAO,CNY1B,GAAO,CMXxB,qBAAqC,CAAE,OAAO,CNkG1B,GAAO,CMjG3B,iBAAiC,CAAE,OAAO,CNR1B,GAAO,CMSvB,yBAAyC,CAAE,OAAO,CNV1B,GAAO,CMW/B,mBAAmC,CAAE,OAAO,CN6V1B,GAAO,CM5VzB,eAA+B,CAAE,OAAO,CNxH1B,GAAO,CMyHrB,8CACoC,CAAE,OAAO,CN4O1B,GAAO,CM3O1B,2EAEsC,CAAE,OAAO,CNwT1B,GAAO,CMvT5B,yBAAyC,CAAE,OAAO,CNkH1B,GAAO,CMjH/B,eAA+B,CAAE,OAAO,CNxG1B,GAAO,CMyGrB,oBAAoC,CAAE,OAAO,CN/H1B,GAAO,CMgI1B,yCACuC,CAAE,OAAO,CN9J1B,GAAO,CM+J7B,mBAAmC,CAAE,OAAO,CNgN1B,GAAO,CM/MzB,eAA+B,CAAE,OAAO,CNqE1B,GAAO,CMpErB,sBAAsC,CAAE,OAAO,CNxE1B,GAAO,CMyE5B,sBAAsC,CAAE,OAAO,CNmU1B,GAAO,CMlU5B,oBAAoC,CAAE,OAAO,CN8T1B,GAAO,CM7T1B,iBAAiC,CAAE,OAAO,CN/E1B,GAAO,CMgFvB,uBAAuC,CAAE,OAAO,CNuM1B,GAAO,CMtM7B,qBAAqC,CAAE,OAAO,CNmI1B,GAAO,CMlI3B,2BAA2C,CAAE,OAAO,CNmI1B,GAAO,CMlIjC,iBAAiC,CAAE,OAAO,CN0P1B,GAAO,CMzPvB,qBAAqC,CAAE,OAAO,CNpM1B,GAAO,CMqM3B,4BAA4C,CAAE,OAAO,CNtC1B,GAAO,CMuClC,iBAAiC,CAAE,OAAO,CN4N1B,GAAO,CM3NvB,iBAAiC,CAAE,OAAO,CNuH1B,GAAO,CMtHvB,8BAA8C,CAAE,OAAO,CNtK1B,GAAO,CMuKpC,+BAA+C,CAAE,OAAO,CNtK1B,GAAO,CMuKrC,4BAA4C,CAAE,OAAO,CNtK1B,GAAO,CMuKlC,8BAA8C,CAAE,OAAO,CN1K1B,GAAO,CM2KpC,gBAAgC,CAAE,OAAO,CN6C1B,GAAO,CM5CtB,eAA+B,CAAE,OAAO,CN7H1B,GAAO,CM8HrB,iBAAiC,CAAE,OAAO,CN3S1B,GAAO,CM4SvB,qBAAqC,CAAE,OAAO,CN8W1B,GAAO,CM7W3B,mBAAmC,CAAE,OAAO,CNxN1B,GAAO,CMyNzB,qBAAqC,CAAE,OAAO,CNxG1B,GAAO,CMyG3B,qBAAqC,CAAE,OAAO,CNxG1B,GAAO,CMyG3B,qBAAqC,CAAE,OAAO,CNoN1B,GAAO,CMnN3B,sBAAsC,CAAE,OAAO,CNyK1B,GAAO,CMxK5B,iBAAiC,CAAE,OAAO,CNkU1B,GAAO,CMjUvB,uBAAuC,CAAE,OAAO,CNkH1B,GAAO,CMjH7B,wIAAyC,CAAE,OAAO,CNkH1B,GAAO,CMjH/B,mBAAmC,CAAE,OAAO,CNgE1B,GAAO,CM/DzB,qBAAqC,CAAE,OAAO,CN8D1B,GAAO,CM7D3B,uBAAuC,CAAE,OAAO,CN3L1B,GAAO,CM4L7B,wBAAwC,CAAE,OAAO,CNkJ1B,GAAO,CMjJ9B,+BAA+C,CAAE,OAAO,CNlG1B,GAAO,CMmGrC,uBAAuC,CAAE,OAAO,CN4N1B,GAAO,CM3N7B,kBAAkC,CAAE,OAAO,CNzJ1B,GAAO,CM0JxB,qDAC8C,CAAE,OAAO,CNvN1B,GAAO,CMwNpC,iDAC4C,CAAE,OAAO,CNtN1B,GAAO,CMuNlC,uDAC+C,CAAE,OAAO,CNzN1B,GAAO,CM0NrC,8BAC8B,CAAE,OAAO,CNrH1B,GAAO,CMsHpB,cAA8B,CAAE,OAAO,CN/C1B,GAAO,CMgDpB,gCAC8B,CAAE,OAAO,CNwV1B,GAAO,CMvVpB,+BAC8B,CAAE,OAAO,CNuB1B,GAAO,CMtBpB,2DAG8B,CAAE,OAAO,CN2B1B,GAAO,CM1BpB,iDAE8B,CAAE,OAAO,CNsL1B,GAAO,CMrLpB,6BAC8B,CAAE,OAAO,CN0B1B,GAAO,CMzBpB,iCAC8B,CAAE,OAAO,CNnQ1B,GAAO,CMoQpB,eAA+B,CAAE,OAAO,CN9G1B,GAAO,CM+GrB,oBAAoC,CAAE,OAAO,CNlG1B,GAAO,CMmG1B,yBAAyC,CAAE,OAAO,CN4N1B,GAAO,CM3N/B,0BAA0C,CAAE,OAAO,CN4N1B,GAAO,CM3NhC,0BAA0C,CAAE,OAAO,CN4N1B,GAAO,CM3NhC,2BAA2C,CAAE,OAAO,CN4N1B,GAAO,CM3NjC,2BAA2C,CAAE,OAAO,CN+N1B,GAAO,CM9NjC,4BAA4C,CAAE,OAAO,CN+N1B,GAAO,CM9NlC,oBAAoC,CAAE,OAAO,CNuR1B,GAAO,CMtR1B,sBAAsC,CAAE,OAAO,CNmR1B,GAAO,CMlR5B,yBAAyC,CAAE,OAAO,CNiX1B,GAAO,CMhX/B,kBAAkC,CAAE,OAAO,CN8W1B,GAAO,CM7WxB,eAA+B,CAAE,OAAO,CNmW1B,GAAO,CMlWrB,sBAAsC,CAAE,OAAO,CNmW1B,GAAO,CMlW5B,uBAAuC,CAAE,OAAO,CN4W1B,GAAO,CM3W7B,kBAAkC,CAAE,OAAO,CNlK1B,GAAO,CMmKxB,yBAAyC,CAAE,OAAO,CNgO1B,GAAO,CM/N/B,oBAAoC,CAAE,OAAO,CNL1B,GAAO,CMM1B,iBAAiC,CAAE,OAAO,CNlG1B,GAAO,CMmGvB,cAA8B,CAAE,OAAO,CN/W1B,GAAO,CMgXpB,2CAAoC,CAAE,OAAO,CNvS1B,GAAO,CMwS1B,2BAA2C,CAAE,OAAO,CNvS1B,GAAO,CMwSjC,iBAAiC,CAAE,OAAO,CNkS1B,GAAO,CMjSvB,wBAAwC,CAAE,OAAO,CNkS1B,GAAO,CMjS9B,0BAA0C,CAAE,OAAO,CN0B1B,GAAO,CMzBhC,wBAAwC,CAAE,OAAO,CN4B1B,GAAO,CM3B9B,0BAA0C,CAAE,OAAO,CNyB1B,GAAO,CMxBhC,2BAA2C,CAAE,OAAO,CNyB1B,GAAO,CMxBjC,gBAAgC,CAAE,OAAO,CNrW1B,GAAO,CMsWtB,kBAAkC,CAAE,OAAO,CN4U1B,GAAO,CM3UxB,kBAAkC,CAAE,OAAO,CNjX1B,GAAO,CMkXxB,gBAAgC,CAAE,OAAO,CNY1B,GAAO,CMXtB,mBAAmC,CAAE,OAAO,CNpL1B,GAAO,CMqLzB,gBAAgC,CAAE,OAAO,CNmL1B,GAAO,CMlLtB,qBAAqC,CAAE,OAAO,CNtG1B,GAAO,CMuG3B,iBAAiC,CAAE,OAAO,CN+Q1B,GAAO,CM9QvB,iBAAiC,CAAE,OAAO,CNpJ1B,GAAO,CMqJvB,eAA+B,CAAE,OAAO,CNuB1B,GAAO,CMtBrB,qCACmC,CAAE,OAAO,CN3E1B,GAAO,CM4EzB,gBAAgC,CAAE,OAAO,CNgO1B,GAAO,CM/NtB,iBAAiC,CAAE,OAAO,CN+C1B,GAAO,CM9CvB,kBAAkC,CAAE,OAAO,CNlX1B,GAAO,CMmXxB,cAA8B,CAAE,OAAO,CN9S1B,GAAO,CM+SpB,aAA6B,CAAE,OAAO,CN+S1B,GAAO,CM9SnB,gBAAgC,CAAE,OAAO,CNqT1B,GAAO,CMpTtB,iBAAiC,CAAE,OAAO,CNoH1B,GAAO,CMnHvB,oBAAoC,CAAE,OAAO,CN0D1B,GAAO,CMzD1B,yBAAyC,CAAE,OAAO,CN+L1B,GAAO,CM9L/B,+BAA+C,CAAE,OAAO,CNnX1B,GAAO,CMoXrC,8BAA8C,CAAE,OAAO,CNrX1B,GAAO,CMsXpC,qDAC8C,CAAE,OAAO,CNjS1B,GAAO,CMkSpC,uBAAuC,CAAE,OAAO,CN3M1B,GAAO,CM4M7B,qBAAqC,CAAE,OAAO,CN+S1B,GAAO,CM9S3B,uBAAuC,CAAE,OAAO,CNkS1B,GAAO,CMjS7B,sCAC8B,CAAE,OAAO,CN6P1B,GAAO,CM5PpB,wEAAwC,CAAE,OAAO,CNkF1B,GAAO,CMjF9B,wBAAwC,CAAE,OAAO,CN8K1B,GAAO,CM7K9B,gBAAgC,CAAE,OAAO,CNyJ1B,GAAO,CMxJtB,0BAA0C,CAAE,OAAO,CNtM1B,GAAO,CMuMhC,oBAAoC,CAAE,OAAO,CN6S1B,GAAO,CM5S1B,iBAAiC,CAAE,OAAO,CNsC1B,GAAO,CMrCvB,4DAEqC,CAAE,OAAO,CNiQ1B,GAAO,CMhQ3B,iDACyC,CAAE,OAAO,CNzG1B,GAAO,CM0G/B,gBAAgC,CAAE,OAAO,CN8S1B,GAAO,CM7StB,iBAAiC,CAAE,OAAO,CNjH1B,GAAO,CMkHvB,iBAAiC,CAAE,OAAO,CNqF1B,GAAO,CMpFvB,wBAAwC,CAAE,OAAO,CNsF1B,GAAO,CMrF9B,6BAA6C,CAAE,OAAO,CN2L1B,GAAO,CM1LnC,sBAAsC,CAAE,OAAO,CNyL1B,GAAO,CMxL5B,oBAAoC,CAAE,OAAO,CNvO1B,GAAO,CMwO1B,eAA+B,CAAE,OAAO,CNpO1B,GAAO,CMqOrB,wBAAwC,CAAE,OAAO,CNmD1B,GAAO,CMlD9B,yBAAyC,CAAE,OAAO,CNiD1B,GAAO,CMhD/B,iBAAiC,CAAE,OAAO,CNjO1B,GAAO,CMkOvB,iBAAiC,CAAE,OAAO,CN9D1B,GAAO,CM+DvB,mBAAmC,CAAE,OAAO,CNzD1B,GAAO,CM0DzB,cAA8B,CAAE,OAAO,CNpM1B,GAAO,CMqMpB,mBAAmC,CAAE,OAAO,CNrV1B,GAAO,CMsVzB,gBAAgC,CAAE,OAAO,CNlS1B,GAAO,CMmStB,cAA8B,CAAE,OAAO,CN8B1B,GAAO,CM7BpB,gBAAgC,CAAE,OAAO,CNqJ1B,GAAO,CMpJtB,eAA+B,CAAE,OAAO,CN7P1B,GAAO,CM8PrB,gBAAgC,CAAE,OAAO,CN7P1B,GAAO,CM8PtB,kBAAkC,CAAE,OAAO,CNrX1B,GAAO,CMsXxB,yBAAyC,CAAE,OAAO,CNrX1B,GAAO,CMsX/B,gBAAgC,CAAE,OAAO,CN4J1B,GAAO,CM3JtB,uBAAuC,CAAE,OAAO,CN4J1B,GAAO,CM3J7B,kBAAkC,CAAE,OAAO,CN8D1B,GAAO,CM7DxB,oCAC8B,CAAE,OAAO,CNjV1B,GAAO,CMkVpB,8BAC+B,CAAE,OAAO,CNgL1B,GAAO,CM/KrB,eAA+B,CAAE,OAAO,CN+M1B,GAAO,CM9MrB,kBAAkC,CAAE,OAAO,CNyI1B,GAAO,CMxIxB,qBAAqC,CAAE,OAAO,CN9P1B,GAAO,CM+P3B,qBAAqC,CAAE,OAAO,CNmI1B,GAAO,CMlI3B,mBAAmC,CAAE,OAAO,CNtQ1B,GAAO,CMuQzB,qBAAqC,CAAE,OAAO,CN7M1B,GAAO,CM8M3B,sBAAsC,CAAE,OAAO,CNtM1B,GAAO,CMuM5B,uBAAuC,CAAE,OAAO,CNnN1B,GAAO,CMoN7B,4BAA4C,CAAE,OAAO,CN7M1B,GAAO,CM8MlC,yEAEuC,CAAE,OAAO,CNtN1B,GAAO,CMuN7B,+CACyC,CAAE,OAAO,CN5N1B,GAAO,CM6N/B,+CACuC,CAAE,OAAO,CN7N1B,GAAO,CM8N7B,+CACuC,CAAE,OAAO,CNlN1B,GAAO,CMmN7B,sBAAsC,CAAE,OAAO,CN/N1B,GAAO,CMgO5B,eAA+B,CAAE,OAAO,CNqO1B,GAAO,CMpOrB,kBAAkC,CAAE,OAAO,CNpT1B,GAAO,CMqTxB,mBAAmC,CAAE,OAAO,CNnG1B,GAAO,CMoGzB,uGAIoC,CAAE,OAAO,CNxF1B,GAAO,CMyF1B,yBAAyC,CAAE,OAAO,CNvU1B,GAAO,CMwU/B,oDAEgC,CAAE,OAAO,CN0B1B,GAAO,CMzBtB,+BACiC,CAAE,OAAO,CN9Q1B,GAAO,CM+QvB,qBAAqC,CAAE,OAAO,CNxL1B,GAAO,CMyL3B,cAA8B,CAAE,OAAO,CN1L1B,GAAO,CM2LpB,0EAEsC,CAAE,OAAO,CNxK1B,GAAO,CMyK5B,wBAAwC,CAAE,OAAO,CN2I1B,GAAO,CM1I9B,aAA6B,CAAE,OAAO,CNQ1B,GAAO,CMPnB,mCACiC,CAAE,OAAO,CNwN1B,GAAO,CMvNvB,sCACsC,CAAE,OAAO,CNlC1B,GAAO,CMmC5B,0CACwC,CAAE,OAAO,CNnC1B,GAAO,CMoC9B,kBAAkC,CAAE,OAAO,CN3J1B,GAAO,CM4JxB,sBAAsC,CAAE,OAAO,CN1V1B,GAAO,CM2V5B,iBAAiC,CAAE,OAAO,CNlK1B,GAAO,CMmKvB,oBAAoC,CAAE,OAAO,CNrC1B,GAAO,CMsC1B,kBAAkC,CAAE,OAAO,CNkE1B,GAAO,CMjExB,oBAAoC,CAAE,OAAO,CN2C1B,GAAO,CM1C1B,2BAA2C,CAAE,OAAO,CN2C1B,GAAO,CM1CjC,eAA+B,CAAE,OAAO,CNja1B,GAAO,CMkarB,4CACmC,CAAE,OAAO,CN3N1B,GAAO,CM4NzB,cAA8B,CAAE,OAAO,CN6J1B,GAAO,CM5JpB,qBAAqC,CAAE,OAAO,CNhb1B,GAAO,CMib3B,eAA+B,CAAE,OAAO,CNpB1B,GAAO,CMqBrB,qBAAqC,CAAE,OAAO,CN0D1B,GAAO,CMzD3B,iBAAiC,CAAE,OAAO,CN8J1B,GAAO,CM7JvB,eAA+B,CAAE,OAAO,CNuN1B,GAAO,CMtNrB,sBAAsC,CAAE,OAAO,CNjE1B,GAAO,CMkE5B,eAA+B,CAAE,OAAO,CNsM1B,GAAO,CMrMrB,qBAAqC,CAAE,OAAO,CN7Z1B,GAAO,CM8Z3B,iBAAiC,CAAE,OAAO,CN/C1B,GAAO,CMgDvB,wBAAwC,CAAE,OAAO,CN1M1B,GAAO,CM2M9B,kBAAkC,CAAE,OAAO,CNpY1B,GAAO,CMqYxB,wBAAwC,CAAE,OAAO,CNxY1B,GAAO,CMyY9B,sBAAsC,CAAE,OAAO,CN3Y1B,GAAO,CM4Y5B,kBAAkC,CAAE,OAAO,CN9Y1B,GAAO,CM+YxB,oBAAoC,CAAE,OAAO,CN1Y1B,GAAO,CM2Y1B,oBAAoC,CAAE,OAAO,CN1Y1B,GAAO,CM2Y1B,qBAAqC,CAAE,OAAO,CNnc1B,GAAO,CMoc3B,uBAAuC,CAAE,OAAO,CNnc1B,GAAO,CMoc7B,gBAAgC,CAAE,OAAO,CNkI1B,GAAO,CMjItB,oBAAoC,CAAE,OAAO,CN3V1B,GAAO,CM4V1B,aAA6B,CAAE,OAAO,CNle1B,GAAO,CMmenB,qBAAqC,CAAE,OAAO,CN1S1B,GAAO,CM2S3B,sBAAsC,CAAE,OAAO,CNvE1B,GAAO,CMwE5B,wBAAwC,CAAE,OAAO,CNtc1B,GAAO,CMuc9B,qBAAqC,CAAE,OAAO,CN1f1B,GAAO,CM2f3B,oBAAoC,CAAE,OAAO,CNvD1B,GAAO,CMwD1B,qBAAqC,CAAE,OAAO,CN9I1B,GAAO,CM+I3B,iBAAiC,CAAE,OAAO,CN5J1B,GAAO,CM6JvB,wBAAwC,CAAE,OAAO,CN5J1B,GAAO,CM6J9B,qBAAqC,CAAE,OAAO,CN+G1B,GAAO,CM9G3B,oBAAoC,CAAE,OAAO,CN+G1B,GAAO,CM9G1B,kBAAkC,CAAE,OAAO,CNhd1B,GAAO,CMidxB,cAA8B,CAAE,OAAO,CNzb1B,GAAO,CM0bpB,kBAAkC,CAAE,OAAO,CN5K1B,GAAO,CM6KxB,oBAAoC,CAAE,OAAO,CN/gB1B,GAAO,CMghB1B,aAA6B,CAAE,OAAO,CNra1B,GAAO,CMsanB,kDAE8B,CAAE,OAAO,CN7L1B,GAAO,CM8LpB,mBAAmC,CAAE,OAAO,CN1H1B,GAAO,CM2HzB,qBAAqC,CAAE,OAAO,CNhc1B,GAAO,CMic3B,yBAAyC,CAAE,OAAO,CNpX1B,GAAO,CMqX/B,mBAAmC,CAAE,OAAO,CNtW1B,GAAO,CMuWzB,mBAAmC,CAAE,OAAO,CN5Q1B,GAAO,CM6QzB,kBAAkC,CAAE,OAAO,CN1K1B,GAAO,CM2KxB,iBAAiC,CAAE,OAAO,CNb1B,GAAO,CMcvB,uBAAuC,CAAE,OAAO,CND1B,GAAO,CME7B,sBAAsC,CAAE,OAAO,CNO1B,GAAO,CMN5B,mBAAmC,CAAE,OAAO,CNQ1B,GAAO,CMPzB,oBAAoC,CAAE,OAAO,CNpb1B,GAAO,CMqb1B,0BAA0C,CAAE,OAAO,CNtb1B,GAAO,CMubhC,kBAAkC,CAAE,OAAO,CNvW1B,GAAO,CMwWxB,eAA+B,CAAE,OAAO,CNR1B,GAAO,CMSrB,sBAAsC,CAAE,OAAO,CN8H1B,GAAO,CM7H5B,qBAAqC,CAAE,OAAO,CNvH1B,GAAO,CMwH3B,sBAAsC,CAAE,OAAO,CN+C1B,GAAO,CM9C5B,oBAAoC,CAAE,OAAO,CN/N1B,GAAO,CMgO1B,gBAAgC,CAAE,OAAO,CN6H1B,GAAO,CM5HtB,eAA+B,CAAE,OAAO,CNnJ1B,GAAO,CMoJrB,kBAAkC,CAAE,OAAO,CN1I1B,GAAO,CM2IxB,0CACsC,CAAE,OAAO,CNqF1B,GAAO,CMpF5B,0BAA0C,CAAE,OAAO,CNqF1B,GAAO,CMpFhC,uBAAuC,CAAE,OAAO,CNwH1B,GAAO,CMvH7B,sBAAsC,CAAE,OAAO,CNxJ1B,GAAO,CMyJ5B,qBAAqC,CAAE,OAAO,CNuH1B,GAAO,CMtH3B,sBAAsC,CAAE,OAAO,CNzJ1B,GAAO,CM0J5B,wBAAwC,CAAE,OAAO,CNxJ1B,GAAO,CMyJ9B,wBAAwC,CAAE,OAAO,CN1J1B,GAAO,CM2J9B,iBAAiC,CAAE,OAAO,CNlI1B,GAAO,CMmIvB,qBAAqC,CAAE,OAAO,CN5R1B,GAAO,CM6R3B,4BAA4C,CAAE,OAAO,CNxV1B,GAAO,CMyVlC,sBAAsC,CAAE,OAAO,CNjG1B,GAAO,CMkG5B,mBAAmC,CAAE,OAAO,CNgI1B,GAAO,CM/HzB,iBAAiC,CAAE,OAAO,CNvC1B,GAAO,CMwCvB,oBAAoC,CAAE,OAAO,CNuG1B,GAAO,CMtG1B,qBAAqC,CAAE,OAAO,CNwG1B,GAAO,CMvG3B,+BAC8B,CAAE,OAAO,CNvgB1B,GAAO,CMwgBpB,kBAAkC,CAAE,OAAO,CN0G1B,GAAO,CMzGxB,gBAAgC,CAAE,OAAO,CNiE1B,GAAO,CMhEtB,iBAAiC,CAAE,OAAO,CN0B1B,GAAO,CMzBvB,iBAAiC,CAAE,OAAO,CNpK1B,GAAO,CMqKvB,qCACuC,CAAE,OAAO,CNkI1B,GAAO,CMjI7B,wBAAwC,CAAE,OAAO,CNzI1B,GAAO,CM0I9B,mBAAmC,CAAE,OAAO,CN7I1B,GAAO,CM8IzB,uBAAuC,CAAE,OAAO,CNjX1B,GAAO,CMkX7B,4CACuC,CAAE,OAAO,CNthB1B,GAAO,CMuhB7B,sDACiD,CAAE,OAAO,CNrhB1B,GAAO,CMshBvC,4CACuC,CAAE,OAAO,CNzhB1B,GAAO,CM0hB7B,+CAC0C,CAAE,OAAO,CN1hB1B,GAAO,CM2hBhC,6CACwC,CAAE,OAAO,CN/hB1B,GAAO,CMgiB9B,wBAAwC,CAAE,OAAO,CNlK1B,GAAO,CMmK9B,mBAAmC,CAAE,OAAO,CN3P1B,GAAO,CM4PzB,uBAAuC,CAAE,OAAO,CN/J1B,GAAO,CMgK7B,yBAAyC,CAAE,OAAO,CN/J1B,GAAO,CMgK/B,sBAAsC,CAAE,OAAO,CNL1B,GAAO,CMM5B,wBAAwC,CAAE,OAAO,CNL1B,GAAO,CMM9B,iBAAiC,CAAE,OAAO,CNte1B,GAAO,CMuevB,yBAAyC,CAAE,OAAO,CNze1B,GAAO,CM0e/B,gBAAgC,CAAE,OAAO,CN3c1B,GAAO,CM4ctB,wBAAwC,CAAE,OAAO,CNrjB1B,GAAO,CMsjB9B,sBAAsC,CAAE,OAAO,CNxQ1B,GAAO,CMyQ5B,iDAC0C,CAAE,OAAO,CNzQ1B,GAAO,CM0QhC,gDACyC,CAAE,OAAO,CN7Q1B,GAAO,CM8Q/B,+CACwC,CAAE,OAAO,CNhR1B,GAAO,CMiR9B,oBAAoC,CAAE,OAAO,CNrR1B,GAAO,CMsR1B,6CACsC,CAAE,OAAO,CNvS1B,GAAO,CMwS5B,8CACuC,CAAE,OAAO,CN5S1B,GAAO,CM6S7B,0BAA0C,CAAE,OAAO,CNzS1B,GAAO,CM0ShC,wBAAwC,CAAE,OAAO,CNnT1B,GAAO,CMoT9B,uBAAuC,CAAE,OAAO,CN1S1B,GAAO,CM2S7B,yBAAyC,CAAE,OAAO,CN9S1B,GAAO,CM+S/B,uBAAuC,CAAE,OAAO,CNhT1B,GAAO,CMiT7B,oBAAoC,CAAE,OAAO,CNmB1B,GAAO,CMlB1B,qBAAqC,CAAE,OAAO,CNzH1B,GAAO,CM0H3B,2BAA2C,CAAE,OAAO,CNtc1B,GAAO,CMucjC,aAA6B,CAAE,OAAO,CNpV1B,GAAO,CMqVnB,oBAAoC,CAAE,OAAO,CNpV1B,GAAO,CMqV1B,sBAAsC,CAAE,OAAO,CNsB1B,GAAO,CMrB5B,wBAAwC,CAAE,OAAO,CN5L1B,GAAO,CM6L9B,+BAA+C,CAAE,OAAO,CN5L1B,GAAO,CM6LrC,qBAAqC,CAAE,OAAO,CN1V1B,GAAO,CM2V3B,sBAAsC,CAAE,OAAO,CNuE1B,GAAO,CMtE5B,iBAAiC,CAAE,OAAO,CN9G1B,GAAO,CM+GvB,iBAAiC,CAAE,OAAO,CNhf1B,GAAO,CMifvB,kBAAkC,CAAE,OAAO,CN3X1B,GAAO,CM4XxB,gBAAgC,CAAE,OAAO,CN/L1B,GAAO,CMgMtB,4BAA4C,CAAE,OAAO,CNxR1B,GAAO,CMyRlC,mCACqC,CAAE,OAAO,CNtB1B,GAAO,CMuB3B,iBAAiC,CAAE,OAAO,CNxd1B,GAAO,CMydvB,gBAAgC,CAAE,OAAO,CNxoB1B,GAAO,CMyoBtB,iBAAiC,CAAE,OAAO,CNloB1B,GAAO,CMmoBvB,0BAA0C,CAAE,OAAO,CNliB1B,GAAO,CMmiBhC,2BAA2C,CAAE,OAAO,CNriB1B,GAAO,CMsiBjC,2BAA2C,CAAE,OAAO,CNniB1B,GAAO,CMoiBjC,2BAA2C,CAAE,OAAO,CNxiB1B,GAAO,CMyiBjC,mBAAmC,CAAE,OAAO,CNxS1B,GAAO,CMySzB,kBAAkC,CAAE,OAAO,CNjP1B,GAAO,CMkPxB,oBAAoC,CAAE,OAAO,CNjP1B,GAAO,CMkP1B,gBAAgC,CAAE,OAAO,CNpP1B,GAAO,CMqPtB,cAA8B,CAAE,OAAO,CNvP1B,GAAO,CMwPpB,qBAAqC,CAAE,OAAO,CN3e1B,GAAO,CM4e3B,uBAAuC,CAAE,OAAO,CN3e1B,GAAO,CM4e7B,gBAAgC,CAAE,OAAO,CNtT1B,GAAO,CMuTtB,gBAAgC,CAAE,OAAO,CNgC1B,GAAO,CM/BtB,oBAAoC,CAAE,OAAO,CNzkB1B,GAAO,CM0kB1B,oBAAoC,CAAE,OAAO,CNlY1B,GAAO,CMmY1B,uBAAuC,CAAE,OAAO,CN9J1B,GAAO,CM+J7B,eAA+B,CAAE,OAAO,CN7c1B,GAAO,CM8crB,0BAA0C,CAAE,OAAO,CNve1B,GAAO,CMwehC,mBAAmC,CAAE,OAAO,CN3f1B,GAAO,CM4fzB,eAA+B,CAAE,OAAO,CNzO1B,GAAO,CM0OrB,uBAAuC,CAAE,OAAO,CNvY1B,GAAO,CMwY7B,cAA8B,CAAE,OAAO,CNQ1B,GAAO,CMPpB,uBAAuC,CAAE,OAAO,CNnL1B,GAAO,CMoL7B,mBAAmC,CAAE,OAAO,CNhP1B,GAAO,CMiPzB,iBAAiC,CAAE,OAAO,CN7I1B,GAAO,CM8IvB,uBAAuC,CAAE,OAAO,CNpN1B,GAAO,CMqN7B,yBAAyC,CAAE,OAAO,CNpN1B,GAAO,CMqN/B,sBAAsC,CAAE,OAAO,CNxE1B,GAAO,CMyE5B,wBAAwC,CAAE,OAAO,CNxE1B,GAAO,CMyE9B,uBAAuC,CAAE,OAAO,CNhI1B,GAAO,CMiI7B,0BAA0C,CAAE,OAAO,CNhI1B,GAAO,CMiIhC,kBAAkC,CAAE,OAAO,CN7V1B,GAAO,CM8VxB,oBAAoC,CAAE,OAAO,CN1lB1B,GAAO,CM2lB1B,sBAAsC,CAAE,OAAO,CN1lB1B,GAAO,CM2lB5B,kBAAkC,CAAE,OAAO,CNtN1B,GAAO,CMuNxB,qCAAiC,CAAE,OAAO,CNhY1B,GAAO,CMiYvB,qBAAqC,CAAE,OAAO,CN4B1B,GAAO,CM3B3B,kBAAkC,CAAE,OAAO,CN4B1B,GAAO,CM3BxB,iBAAiC,CAAE,OAAO,CN1d1B,GAAO,CM2dvB,2BAA2C,CAAE,OAAO,CNjB1B,GAAO,CMkBjC,yBAAyC,CAAE,OAAO,CNkB1B,GAAO,CMjB/B,4BAA4C,CAAE,OAAO,CNhM1B,GAAO,CMiMlC,gBAAgC,CAAE,OAAO,CNrmB1B,GAAO,CMsmBtB,4BAA4C,CAAE,OAAO,CNzoB1B,GAAO,CM0oBlC,+BAA+C,CAAE,OAAO,CNI1B,GAAO,CMHrC,kBAAkC,CAAE,OAAO,CN/lB1B,GAAO,CMgmBxB,sCAAsD,CAAE,OAAO,CN/oB1B,GAAO,CMgpB5C,0EAC8D,CAAE,OAAO,CNjrB1B,GAAO,CMkrBpD,8DAE+B,CAAE,OAAO,CN9f1B,GAAO,CM+frB,gBAAgC,CAAE,OAAO,CN9Y1B,GAAO,CM+YtB,kBAAkC,CAAE,OAAO,CN9Y1B,GAAO,CM+YxB,2CACwC,CAAE,OAAO,CNtJ1B,GAAO,CMuJ9B,qBAAqC,CAAE,OAAO,CN9S1B,GAAO,CM+S3B,iBAAiC,CAAE,OAAO,CNhB1B,GAAO,CMiBvB,wBAAwC,CAAE,OAAO,CNhB1B,GAAO,CMiB9B,mBAAmC,CAAE,OAAO,CN9I1B,GAAO,CM+IzB,yBAAyC,CAAE,OAAO,CN9I1B,GAAO,CM+I/B,0BAA0C,CAAE,OAAO,CN9I1B,GAAO,CM+IhC,qBAAqC,CAAE,OAAO,CN5O1B,GAAO,CM6O3B,sBAAsC,CAAE,OAAO,CNjc1B,GAAO,CMkc5B,gBAAgC,CAAE,OAAO,CNY1B,GAAO,CMXtB,oBAAoC,CAAE,OAAO,CNnF1B,GAAO,CMoF1B,6DAC+C,CAAE,OAAO,CNvZ1B,GAAO,CMwZrC,qCACuC,CAAE,OAAO,CN1b1B,GAAO,CO/R7B,QAAS,CH8BP,QAAQ,CAAE,QAAQ,CAClB,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,GAAG,CACX,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,IAAI,CACZ,QAAQ,CAAE,MAAM,CAChB,IAAI,CAAE,gBAAa,CACnB,MAAM,CAAE,CAAC,CAUT,kDACQ,CACN,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,MAAM,CAAE,CAAC,CACT,QAAQ,CAAE,OAAO,CACjB,IAAI,CAAE,IAAI,CIvDd,swBAAK,CACH,WAAW,CAAE,OAAO,CACpB,y5BAAQ,CACN,WAAW,CC+BuB,aAAa,CD9B/C,OAAO,CAAE,YAAY,CACrB,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,MAAM,CACnB,WAAW,CAAE,CAAC,CACd,eAAe,CAAE,OAAO,CAM5B,86BAAkB,CAChB,OAAO,CAAE,YAAY,CACrB,eAAe,CAAE,OAAO,CAGxB,muEAAgB,CACd,OAAO,CAAE,MAAM,CACf,2wEAAuB,CACrB,WAAW,CAAE,KAAI,CACnB,utEAAsB,CACpB,OAAO,CAAE,YAAY,CAE3B,2iBAA2B,CACzB,OAAO,CAAE,GAAE,CjBpBL,kBAAoB,CAAE,qBAAM,CAK5B,eAAiB,CAAE,qBAAM,CAezB,UAAY,CAAE,qBAAM,CiBE5B,+nBAAiC,CAC/B,OAAO,CAAE,CAAC,CAGV,mtCAAuB,CACrB,SAAS,CAAE,IAAI,CACf,cAAc,CAAE,IAAI,CEpBxB,0PAAS,CACP,OAAO,CAAE,IAAqB,CAC9B,WAAW,CDayB,IAAI,CCZxC,aAAa,CDYuB,IAAI,CCXxC,UAAU,CAAE,OAAmB,CAEjC,8CAAe,CACb,KAAK,CCe+B,IAAM,CDd1C,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,KAAK,CACd,KAAK,CCY+B,IAAM,CDX1C,UAAU,CAAE,OAAkB,CAC9B,MAAM,CAAE,KAAsB,CAC9B,OAAO,CAAE,QAA2C,CACpD,aAAa,CAAE,IAAqB,CAEtC,0ZAAyB,CACvB,UAAU,CAAE,OAAkB,CAC9B,mxCAAe,CACb,UAAU,CAAE,OAAiB,CACjC,kYAA0B,CACxB,UAAU,CAAE,OAAmB,CAC/B,ouCAAe,CACb,UAAU,CAAE,OAAoB,CAEpC,sYAAuB,CACrB,UAAU,CAAE,OAAmB,CAC/B,yuCAAe,CACb,UAAU,CAAE,OAAkB,CAElC,mZAA0B,CACxB,UAAU,CAAE,OAAuB,CACnC,swCAAe,CACb,UAAU,CAAE,OAAqB,CAErC,scAA0B,CACxB,UAAU,CCF0B,OAAmB,CDGvD,42CAAe,CACb,KAAK,CCpB6B,OAAW,CDqB7C,UAAU,CCHwB,OAAmB,CDIvD,8dAAC,CACC,KAAK,CCb6B,OAAK,CDe3C,sZAAsB,CACpB,aAAa,CAAE,CAAC,CAsBlB,kBAAkB,CAChB,QAAQ,CAAE,KAAK,CACf,MAAM,CAAE,GAAG,CACX,IAAI,CAAE,CAAC,CACP,OAAO,CDG6B,GAAG,CCFvC,qBAAE,CACA,OAAO,CAAE,KAAK,CACd,KAAK,CDT6B,KAAK,CCUvC,UAAU,CAAE,WAAW,CACvB,KAAK,CCrD6B,IAAM,CDsDxC,UAAU,CAAE,MAAM,CAClB,UAAU,CAAE,2BAA0B,CACtC,OAAO,CAAE,MAAmB,CAC5B,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,CAAC,CACT,WAAW,CAAE,IAAI,CACjB,QAAQ,CAAE,MAAM,CnB3FZ,kBAAoB,CAAE,gBAAM,CAK5B,eAAiB,CAAE,gBAAM,CAezB,UAAY,CAAE,gBAAM,CmByExB,0CAAsB,CACpB,UAAU,CC5FsB,OAAM,CD6FxC,uCAAmB,CACjB,UAAU,CC5DsB,OAAK,CD6DvC,0CAAsB,CACpB,UAAU,CDnFsB,OAAO,CCoFzC,yCAAqB,CACnB,UAAU,CDtEsB,OAAI,CCuEtC,wBAAI,CACF,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,IAAI,CEhFd,oCAAsB,CFmFxB,kBAAkB,CAChB,MAAM,CAAE,IAAI,CACZ,GAAG,CAAE,CAAC,CACN,KAAK,CAAE,IAAI,CACX,qBAAE,CACA,KAAK,CAAE,IAAI,EG3FjB,MAAM,CACJ,SAAS,CAAE,IAAI,CACf,MAAM,CAAE,CAAC,CACT,cAAc,CAAE,QAAQ,CACxB,eAAe,CAAE,MAAM,CACvB,MAAM,CAAE,OAAO,CACf,WAAW,CAAE,MAAM,CACnB,kBAAkB,CAAE,MAAM,CAC1B,SAAS,CAAE,OAAO,CACpB,gDAAiD,CAC/C,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACZ,gBAAgB,CACd,MAAM,CAAE,OAAO,CAEjB,IAAI,CAEF,OAAO,CAAE,YAAY,CACrB,aAAa,CAAE,GAAG,CAClB,WAAW,CAAE,MAAM,CACnB,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,MAAM,CAClB,MAAM,CAAE,OAAO,CACf,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,iBAA6F,CACtG,KAAK,CFf+B,IAAM,CEgB1C,MAAM,CAAE,yBAAyB,CACjC,gBAAgB,CF7CoB,OAAM,CE8C1C,eAAe,CAAE,IAAI,CACrB,WAAW,CAAE,MAAM,CACnB,WAAW,CFDyB,uDAA2D,CEE/F,UAAU,CAAE,mFAAqF,CACjG,YAAY,CAAE,KAAK,CACnB,cAAc,CAAE,MAAM,CACtB,QAAQ,CAAE,MAAM,CAChB,IAAI,CAAE,CAAC,CACP,iBAAiB,CAAE,IAAI,CtBxDjB,mBAAoB,CsByDb,IAAI,CtBpDX,gBAAiB,CsBoDV,IAAI,CtB/CX,eAAgB,CsB+CT,IAAI,CtBrCX,WAAY,CsBqCL,IAAI,CtBzDX,kBAAoB,CAAE,eAAM,CAK5B,eAAiB,CAAE,eAAM,CAezB,UAAY,CAAE,eAAM,CsByC5B,UAAU,CACR,UAAU,CAAE,OAAwB,CACpC,KAAK,CFjC+B,IAAM,CEoC1C,UAAO,CACL,UAAU,CAAE,OAAqC,CACjD,KAAK,CFtC6B,IAAM,CEuC1C,UAAO,CACL,UAAU,CAAE,OAAqC,CACjD,OAAO,CAAE,CAAC,CACZ,WAAQ,CACN,UAAU,CAAE,6EAA+E,CAC3F,OAAO,CAAE,iBAA6F,CACxG,YAAS,CACP,KAAK,CF9C6B,IAAM,CE+C1C,aAAU,CACR,gBAAgB,CAAE,IAAI,CACtB,MAAM,CAAE,2DAA2D,CACnE,MAAM,CAAE,iBAAmB,CAC3B,OAAO,CAAE,GAAG,CACZ,MAAM,CAAE,WAAW,CACnB,UAAU,CAAE,IAAI,CAEpB,aAAa,CACX,gBAAgB,CAAE,IAAI,CACtB,MAAM,CAAE,2DAA2D,CACnE,MAAM,CAAE,iBAAmB,CAC3B,OAAO,CAAE,GAAG,CACZ,MAAM,CAAE,WAAW,CACnB,UAAU,CAAE,IAAI,CAChB,4DAA0B,CACxB,gBAAgB,CAAE,IAAI,CACtB,MAAM,CAAE,2DAA2D,CACnE,MAAM,CAAE,iBAAmB,CAC3B,OAAO,CAAE,GAAI,CACb,MAAM,CAAE,WAAW,CACnB,UAAU,CAAE,IAAI,CAGpB,sBAAsB,CACpB,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,CAAC,CAEX,UAAU,CACR,SAAS,CAAE,GAAG,CAEhB,SAAS,CACP,gBAAgB,CAAE,kBAAgB,CAClC,eAAO,CACL,gBAAgB,CAAE,kBAA6B,CAEnD,YAAY,CACV,gBAAgB,CAAE,kBAA2C,CAC7D,KAAK,CAAE,kBAAsB,CAC7B,kBAAO,CACL,gBAAgB,CAAE,kBAAuD,CACzE,KAAK,CF5F6B,OAAW,CE6F/C,oBAAS,CACP,KAAK,CAAE,kBAAsB,CAEjC,YAAY,CACV,gBAAgB,CAAE,kBAAiB,CACnC,kBAAO,CACL,gBAAgB,CAAE,eAA6B,CAEnD,WAAW,CACT,gBAAgB,CAAE,kBAAe,CACjC,iBAAO,CACL,gBAAgB,CAAE,kBAA4B,CAElD,YAAY,CACV,gBAAgB,CAAE,kBAAkB,CACpC,kBAAO,CACL,gBAAgB,CAAE,kBAA+B,CACrD,WAAW,CACT,gBAAgB,CJvIoB,IAAI,CIwIxC,iBAAO,CACL,gBAAgB,CAAE,kBAAoC,CAE1D,SAAS,CACP,gBAAgB,CAAE,sBAAsB,CACxC,KAAK,CF3G+B,OAAK,CE4GzC,UAAU,CAAE,IAAI,CAChB,YAAY,CAAE,sBAAsB,CACpC,eAAO,CACL,gBAAgB,CAAE,sBAAsB,CACxC,KAAK,CAAE,kBAAoC,CAC3C,UAAU,CAAE,IAAI,CAClB,gBAAQ,CACN,gBAAgB,CAAE,sBAAsB,CACxC,KAAK,CAAE,kBAAoC,CAC3C,UAAU,CAAE,IAAI,CAClB,iBAAS,CACP,KAAK,CFtH6B,OAAO,CEwH7C,mCAAoC,CAClC,cAAc,CAAE,MAAM,CAExB,aAAa,CACX,aAAa,CJ1IuB,IAAI,ChBuExC,KAAK,CAAE,CAAC,CACR,wCAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,mBAAO,CACL,KAAK,CAAE,IAAI,CqB3Ff,YAAY,CACV,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,YAAY,CAIvB,qCAAqC,CACnC,OAAO,CAAE,KAAK,CAChB,iBAAiB,CACf,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,CAAC,CACP,OAAO,CAAE,IAAI,CACb,KAAK,CAAE,IAAI,CACX,GAAG,CAAE,IAAI,CACT,SAAS,CAAE,IAAI,CACf,UAAU,CHW0B,OAAyB,CGV7D,OAAO,CLmD6B,GAAG,CKlDvC,MAAM,CAAE,iBAAgC,CACxC,UAAU,CAAE,2BAA0B,CACtC,OAAO,CAAE,IAAqB,CAC9B,sBAAQ,CACN,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACX,KAAK,CHN6B,OAAW,CGO7C,WAAW,CAAE,MAAM,CACnB,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,MAAuB,CAChC,MAAM,CAAE,OAAO,CACf,4BAAO,CACL,UAAU,CHFsB,OAAK,CGGrC,KAAK,CHT2B,IAAM,CGU1C,4BAAY,CACV,UAAU,CAAE,iBAAgC,CAC5C,MAAM,CAAE,KAAuB,CACjC,2BAAW,CACT,cAAc,CAAE,IAAqB,CACrC,gDAAoB,CAClB,KAAK,CAAE,IAAI,CACf,mCAAmB,CACjB,UAAU,CAAE,OAA4B,CACxC,cAAc,CAAE,SAAS,CACzB,WAAW,CAAE,GAAG,CAChB,SAAS,CAAE,GAAG,CACd,yCAAO,CACL,UAAU,CAAE,OAA4B,CAC1C,wCAAI,CACF,KAAK,CHzB2B,IAAM,CG2B5C,6CAA6C,CAC3C,MAAM,CAAE,IAAI,CACZ,GAAG,CAAE,IAAI,CACT,IAAI,CAAE,IAAI,CACV,KAAK,CAAE,CAAC,CAGR,iDAAiB,CACf,UAAU,CH9BwB,OAAyB,CG+B3D,UAAU,CAAE,GAAG,CACjB,mDAAmB,CACjB,OAAO,CAAE,QAA2C,CACpD,yDAAO,CACL,UAAU,CHlCsB,OAAK,CGmCrC,KAAK,CHzC2B,IAAM,CG2C5C,+CAA+C,CAC7C,KAAK,CAAE,CAAC,CACR,IAAI,CAAE,IAAI,CACV,UAAU,CAAE,KAAK,CAGjB,yBAAQ,CACN,OAAO,CAAE,GAAG,CACZ,aAAa,CAAE,iBAA0B,CACzC,WAAW,CAAE,qBAAqB,CAClC,YAAY,CAAE,qBAAqB,CACnC,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,KAAK,CACd,GAAG,CAAE,IAAI,CACT,IAAI,CAAE,GAAG,CACT,WAAW,CAAE,IAAI,CACnB,gDAA+B,CAC7B,IAAI,CAAE,IAAI,CCtEZ,uBAAM,CACJ,OAAO,CAAE,KAAK,CAEhB,gIAA+C,CAC7C,OAAO,CAAE,YAAY,CACrB,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,CAAC,CACR,cAAc,CAAE,MAAM,CAItB,wCAAO,CACL,OAAO,CAAE,YAAY,CACrB,cAAc,CAAE,MAAM,CACtB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,YAA+C,CACvD,KAAK,CAAE,IAAI,CACf,4BAAW,CACT,KAAK,CAAE,IAAI,CACX,kCAAK,CACH,OAAO,CAAE,KAAK,CAChB,mCAAM,CACJ,UAAU,CAAE,GAAqB,CAEvC,QAAQ,CACN,MAAM,CAAE,CAAC,CACT,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACZ,MAAM,CACJ,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,MAAM,CACnB,aAAa,CN/BuB,IAAI,CMgCxC,SAAS,CAAE,IAAI,CACf,YAAY,CAAE,IAAI,CACpB,KAAK,CACH,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,aAAa,CACrB,KAAK,CNR+B,IAAU,CMS9C,SAAS,CAAE,GAAG,CAEhB,qBAAuB,CACrB,SAAS,CAAE,IAAI,CACf,MAAM,CAAE,CAAC,CACT,cAAc,CAAE,QAAQ,CACxB,eAAe,CAAE,MAAM,CAGzB,iBAAiB,CACf,aAAa,CNhDuB,IAAI,ChBuExC,KAAK,CAAE,CAAC,CuBrGR,SAAS,CCCC,IAAQ,CDChB,WAAI,CAAE,IAAI,CACV,YAAK,CAAE,IAAI,CvBkGb,KAAK,CAAE,CAAC,CACR,gDAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,uBAAO,CACL,KAAK,CAAE,IAAI,CALb,gDAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,uBAAO,CACL,KAAK,CAAE,IAAI,CsBzBf,uDAAyD,CACvD,OAAO,CAAE,IAAI,CACb,KAAK,CN/C+B,OAAI,CMoDxC,mGAA+C,CAC7C,cAAc,CAAE,IAAqB,CACrC,wHAAM,CACJ,KAAK,CAAE,IAAI,CAEX,0tEAAqP,CACnP,KAAK,CAAE,IAAI,CACnB,+BAA+B,CGlF3B,KAAK,CAAE,IAAsB,CAG3B,OAAO,CAAE,KAAK,CAed,YAAoB,CAAE,QAA+B,CACrD,KAAK,CAAE,IAAuC,CCnB5C,YAAoB,CAAE,CAAC,CDqBzB,0CAAa,CACX,YAAoB,CAAE,CAAC,CHgE/B,iCAAiC,CGtF7B,KAAK,CAAE,IAAsB,CAG3B,OAAO,CAAE,KAAK,CAed,YAAoB,CAAE,QAA+B,CACrD,KAAK,CAAE,SAAuC,CAE9C,4CAAa,CACX,YAAoB,CAAE,CAAC,CCA7B,iDAAwB,CACtB,YAAoB,CAAE,CAAC,CAEvB,mDAA0B,CACxB,KAAK,CALY,IAAkC,CJqEzD,iCAAiC,CG1F7B,KAAK,CAAE,IAAsB,CAG3B,OAAO,CAAE,KAAK,CAed,YAAoB,CAAE,QAA+B,CACrD,KAAK,CAAE,SAAuC,CAE9C,4CAAa,CACX,YAAoB,CAAE,CAAC,CCA7B,iDAAwB,CACtB,YAAoB,CAAE,CAAC,CAEvB,mDAA0B,CACxB,KAAK,CALY,IAAkC,CJ0EzD,uDAAuD,CACrD,MAAM,CAAE,SAA2B,CACnC,SAAS,CAAE,GAAG,CAEhB,oBAAoB,CAClB,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,SAA2B,CACnC,SAAS,CAAE,GAAG,CAOZ,osBAAqP,CACnP,KAAK,CAAE,IAAI,CAIjB,uBAAuB,CACrB,OAAO,CAAE,YAAY,CACrB,YAAY,CAAE,KAAK,CACnB,KAAK,CAAE,IAAI,CACX,cAAc,CAAE,MAAM,CACtB,SAAS,CAAE,GAAG,CAEhB,gBAAgB,CACd,OAAO,CAAE,KAAK,CACd,KAAK,CJrH+B,IAAW,CIsH/C,SAAS,CAAE,GAAG,CACd,UAAU,CAAE,OAAO,CACnB,UAAU,CAAE,MAAM,CAClB,kBAAC,CACC,SAAS,CAAE,OAAO,CAClB,UAAU,CAAE,MAAM,CAClB,aAAa,CAAE,GAAqB,CACtC,6BAAY,CACV,aAAa,CAAE,CAAC,CA4DpB,KAAK,CACH,WAAW,CAAE,MAAM,CAGnB,6DAAmD,CACjD,kBAAkB,CAAE,MAAM,CAC1B,MAAM,CAAE,OAAO,CACf,WAAW,CJ7JuB,uDAA2D,CI8J7F,SAAS,CAAE,OAAO,CACpB,gSAAqP,CACnP,kBAAkB,CAAE,IAAI,CACxB,OAAO,CAAE,GAAqB,CAC9B,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,cAA6B,CACrC,SAAS,CAAE,GAAG,CACd,WAAW,CJrKuB,uDAA2D,CIsK7F,UAAU,CAAE,oBAAmC,CAC/C,aAAa,CAAE,CAAC,CxBxNZ,kBAAoB,CAAE,kBAAM,CAK5B,eAAiB,CAAE,kBAAM,CAezB,UAAY,CAAE,kBAAM,CwBuM1B,4BAAwB,CACtB,OAAO,CAAE,eAAkB,CAC7B,eAAW,CACT,MAAM,CAAE,OAAO,CACjB,0CAAmC,CxB/N7B,kBAAoB,CwBgOZ,UAAU,CxB3NlB,eAAiB,CwB2NT,UAAU,CxB5MlB,UAAY,CwB4MJ,UAAU,CACtB,OAAO,CAAE,CAAC,CACV,YAAY,CAAE,OAAO,CACrB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,IAAI,CACd,oBAAgB,CxBrOV,kBAAoB,CwBsOZ,UAAU,CxBjOlB,eAAiB,CwBiOT,UAAU,CxBlNlB,UAAY,CwBkNJ,UAAU,CACtB,kGAA6D,CAC3D,kBAAkB,CAAE,IAAI,CAC5B,oXAAyU,CACvU,OAAO,CAAE,CAAC,CACV,OAAO,CAAE,cAAc,CACvB,YAAY,CNxLsB,IAAU,CMyL9C,oBAAgB,CACd,YAAY,CAAE,eAA8B,CAC9C,+EAAqE,CACnE,OAAO,CAAE,gBAAsB,CAC/B,OAAO,CAAE,gBAAgB,CAC3B,4aAAiY,CAC/X,MAAM,CAAE,WAAW,CACnB,gBAAgB,CAAE,OAAmC,CAEzD,+DAAiE,CAC/D,KAAK,CNzN+B,OAAI,CM0NxC,MAAM,CAAE,iBAAc,CACxB,iFAAmF,CACjF,YAAY,CN5NwB,OAAI,CM8NxC,yHAA+G,CAC7G,aAAa,CN/NqB,OAAI,CMiO1C,oBAAoB,CAClB,OAAO,CAAE,IAAqB,CAC9B,SAAS,CAAE,IAAI,CAKjB,QAAQ,CACN,QAAQ,CAAE,IAAI,CACd,cAAc,CAAE,GAAG,CACnB,KAAK,CAAE,IAAI,CACX,WAAW,CJzNyB,uDAA2D,CI0NjG,eAAgB,CACd,OAAO,CAAE,WAAgB,CACzB,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,cAA6B,CACrC,SAAS,CAAE,GAAG,CACd,UAAU,CAAE,oBAAmC,CxBhRzC,kBAAoB,CAAE,kBAAM,CAK5B,eAAiB,CAAE,kBAAM,CAezB,UAAY,CAAE,kBAAM,CwB+P5B,MAAM,CACJ,MAAM,CAAE,cAA6B,CACrC,gBAAgB,CJvPoB,IAAM,CIwP1C,gBAAW,CACT,MAAM,CAAE,IAAI,CAChB,2BAA4B,CAC1B,OAAO,CAAE,CAAC,CACZ,uFAA2F,CACzF,MAAM,CAAE,WAAW,CACnB,gBAAgB,CAAE,OAAmC,CAKrD,8DAAuD,CACrD,MAAM,CAAE,WAAW,CACvB,sBAAuB,CACrB,MAAM,CAAE,KAAuB,CAE/B,KAAK,CJ5Q+B,OAAW,CI6Q/C,OAAO,CAAE,KAAK,CACd,kCAAK,CACH,cAAc,CAAE,QAAQ,CAI5B,uBAAuB,CACrB,OAAO,CAAE,YAAY,CACrB,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,CAAC,CACR,cAAc,CAAE,MAAM,CAuBxB,iCAAkC,CAChC,WAAW,CAAE,MAAM,CACnB,OAAO,CAAE,GAAqB,CAC9B,qEAAiB,CACf,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,YAAY,CACrB,SAAS,CAAE,GAAG,CACd,gBAAgB,CJtSkB,OAAmB,CIuSrD,MAAM,CAAE,cAA6B,CACrC,KAAK,CJrU6B,IAAW,CIuUjD,kCAAkC,CAChC,WAAW,CAAE,CAAC,CAChB,kCAAkC,CAChC,YAAY,CAAE,CAAC,CAcjB,UAAU,CACR,KAAK,CAAE,IAAuB,CAC9B,MAAM,CAAE,IAAqB,CAC7B,MAAM,CAAE,MAAwB,CAChC,QAAQ,CAAE,QAAQ,CAClB,aAAa,CAAE,GAAG,CAClB,UAAU,CNrW0B,IAAI,CMsWxC,MAAM,CAAE,OAAO,CxB5WT,kBAAoB,CAAE,oBAAM,CAK5B,eAAiB,CAAE,oBAAM,CAezB,UAAY,CAAE,oBAAM,CwB0V1B,iBAAQ,CACN,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,EAAE,CACX,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,aAAa,CAAE,GAAG,CAClB,UAAU,CJxWwB,IAAW,CIyW7C,IAAI,CAAE,IAAI,CACV,GAAG,CAAE,IAAI,CxBvXL,kBAAoB,CAAE,oBAAM,CAK5B,eAAiB,CAAE,oBAAM,CAezB,UAAY,CAAE,oBAAM,CwBqW1B,gBAAO,CACL,OAAO,CAAE,OAAO,CAChB,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,IAAqB,CAC3B,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,IAAI,CACf,KAAK,CNzX6B,IAAI,CM0X1C,iBAAiB,CACf,UAAU,CAAE,OAAmB,CAC/B,wBAAQ,CACN,IAAI,CN5W8B,IAAI,CM6WtC,UAAU,CJlYwB,OAAM,CImY1C,uBAAO,CACL,OAAO,CAAE,MAAM,CAEnB,8CAA+C,CAC7C,MAAM,CAAE,WAAW,CAiDnB,wGAAyB,CACvB,KAAK,CN7Z6B,OAAI,CM+ZtC,81BAAqP,CACnP,MAAM,CAAE,iBAAc,CAC1B,iDAAQ,CACN,MAAM,CAAE,iBAAc,CAE1B,mBAAmB,CACjB,WAAW,CAAE,MAAM,CACnB,qCAAiB,CACf,OAAO,CAAE,WAAgB,CACzB,OAAO,CAAE,YAAY,CACrB,SAAS,CAAE,GAAG,CAClB,gEAAgE,CAC9D,KAAK,CJvc+B,OAAM,CI0c5C,+DAA+D,CAC7D,KAAK,CN/a+B,OAAI,CMkb1C,gEAAgE,CAC9D,KAAK,CNlc+B,OAAO,CMqc7C,6DAA6D,CAC3D,KAAK,CJjb+B,OAAK,CIub3C,UAAU,CxB3dF,iBAAoB,CAAE,aAAM,CAK5B,cAAiB,CAAE,aAAM,CAKzB,aAAgB,CAAE,aAAM,CAKxB,YAAe,CAAE,aAAM,CAKvB,SAAY,CAAE,aAAM,CwByc5B,WAAW,CxB7dH,iBAAoB,CAAE,cAAM,CAK5B,cAAiB,CAAE,cAAM,CAKzB,aAAgB,CAAE,cAAM,CAKxB,YAAe,CAAE,cAAM,CAKvB,SAAY,CAAE,cAAM,CwB2c5B,WAAW,CxB/dH,iBAAoB,CAAE,cAAM,CAK5B,cAAiB,CAAE,cAAM,CAKzB,aAAgB,CAAE,cAAM,CAKxB,YAAe,CAAE,cAAM,CAKvB,SAAY,CAAE,cAAM,CwB6c5B,OAAO,CxBjeC,iBAAoB,CAAE,UAAM,CAK5B,cAAiB,CAAE,UAAM,CAKzB,aAAgB,CAAE,UAAM,CAKxB,YAAe,CAAE,UAAM,CAKvB,SAAY,CAAE,UAAM,CwB+c1B,iBAAW,CxBneL,iBAAoB,CwBoeL,wBAAwB,CxB/dvC,cAAiB,CwB+dF,wBAAwB,CxB1dvC,aAAgB,CwB0dD,wBAAwB,CxBrdvC,YAAe,CwBqdA,wBAAwB,CxBhdvC,SAAY,CwBgdG,wBAAwB,CAC7C,kBAAY,CxBreN,iBAAoB,CwBseL,yBAAyB,CxBjexC,cAAiB,CwBieF,yBAAyB,CxB5dxC,aAAgB,CwB4dD,yBAAyB,CxBvdxC,YAAe,CwBudA,yBAAyB,CxBldxC,SAAY,CwBkdG,yBAAyB,CAC9C,kBAAY,CxBveN,iBAAoB,CwBweL,yBAAyB,CxBnexC,cAAiB,CwBmeF,yBAAyB,CxB9dxC,aAAgB,CwB8dD,yBAAyB,CxBzdxC,YAAe,CwBydA,yBAAyB,CxBpdxC,SAAY,CwBodG,yBAAyB,CAEhD,yCAAyC,CAErC,8BAAqB,CACnB,MAAM,CAAE,SAAS,CAEjB,8ZAAqP,CACnP,aAAa,CAAE,KAAK,CACpB,OAAO,CAAE,KAAK,CAClB,cAAK,CACH,aAAa,CAAE,KAAK,CACpB,OAAO,CAAE,KAAK,CAEhB,kYAAqO,CACnO,aAAa,CAAE,CAAC,CAElB,wCAAuB,CACrB,aAAa,CAAE,KAAK,CACpB,UAAU,CAAE,IAAI,CAChB,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACb,4BAAW,CACT,MAAM,CAAE,WAAW,CACvB,iEAAmE,CACjE,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,KAAuB,EH5ehC,oCAAsB,CQhC1B,YAAY,CAER,OAAO,CAAE,IAAI,ER8Bb,oCAAsB,CQ5B1B,YAAY,CAER,OAAO,CAAE,IAAI,EAEjB,WAAW,CACT,KAAK,CAAE,IAAI,CAEb,YAAY,CACV,KAAK,CAAE,KAAK,CAEd,WAAW,CACT,KAAK,CAAE,IAAI,CC4Cb,mEAAS,CACP,eAAe,CAAE,QAAQ,CACzB,cAAc,CAAE,CAAC,CACjB,WAAW,CAAE,IAAI,CACjB,aAAa,CZ/BuB,IAAI,CYgCxC,2FAAO,CACL,KAAK,CAAE,IAAI,CACX,IAAI,CAAE,6BAA8B,CACpC,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,MAAM,CACpB,yJAAM,CACJ,SAAS,CZjByB,GAAG,CYkBrC,MAAM,CAAE,CAAC,CACT,QAAQ,CAAE,OAAO,CACjB,OAAO,CZnB2B,QAAmC,CYoBvE,iOAA8B,CAC5B,iBAAiB,CAAE,CAAC,CACtB,qFAAK,CACH,KAAK,CAAE,IAAI,CACX,UAAU,CAAE,IAAI,CAChB,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CACnB,8FAAE,CACA,WAAW,CZnDqB,IAAI,CYoDpC,aAAa,CAAE,iBAA6B,CAChD,4EAAE,CACA,gBAAgB,CAAE,WAAW,CAC7B,cAAc,CAAE,MAAM,CAE1B,kFAAc,CACZ,WAAW,CAAE,IAAuB,CACpC,mHAAY,CACV,aAAa,CAAE,CAAC,CACpB,4HAA4B,CAC1B,KAAK,CAAE,EAAE,CACT,aAAa,CAAE,CAAC,CAChB,uXAA0C,CACxC,MAAM,CAAE,CAAC,CAEb,mBAAmB,CACjB,KAAK,CV9D+B,IAAY,CU+DhD,SAAS,CAAE,GAAG,CAChB,kBAAkB,CAChB,KAAK,CVjE+B,IAAY,CUkEhD,SAAS,CAAE,GAAG,CAIhB,2HAAyD,CACvD,gBAAgB,CVzDoB,OAAmB,CU2DzD,gBAAgB,CACd,gBAAgB,CV5DoB,OAAmB,CUiEzD,kDAAsB,CACpB,MAAM,CAAE,iBAA6B,CACrC,wDAAE,CACA,aAAa,CAAE,iBAA6B,CAC5C,WAAW,CAAE,iBAA6B,CAC5C,gGAAwB,CACtB,mBAAmB,CAAE,CAAC,CAE1B,kBAAkB,CAChB,MAAM,CAAE,iBAA6B,CAGrC,0BAAE,CACA,aAAa,CAAE,iBAA6B,CAC9C,8CAAwB,CACtB,mBAAmB,CAAE,CAAC,CAGxB,2CAAwB,CACtB,mBAAmB,CAAE,CAAC,CACxB,+CAAM,CACJ,YAAY,CAAE,SAAS,CACvB,aAAa,CAAE,iBAA6B,CAC9C,2CAAwB,CACtB,mBAAmB,CAAE,CAAC,CAG1B,oBAAoB,CAClB,aAAa,CZhHuB,IAAI,CYiHxC,SAAS,CAAE,IAAI,CACf,QAAQ,CAAE,IAAI,CACd,0BAAK,CACH,aAAa,CAAE,YAAY,CAC3B,2DAAM,CACJ,WAAW,CAAE,MAAM,CCzIzB,CAAC,CACC,KAAK,CX+B+B,OAAK,CW9BzC,eAAe,CAAE,IAAI,CACrB,MAAM,CAAE,OAAO,CACf,OAAO,CACL,KAAK,CbgD6B,OAAwB,Ca/C5D,SAAS,CACP,KAAK,CX0B6B,OAAO,CWA7C,IAAI,CACF,MAAM,CAAE,IAAI,CACZ,UAAU,CAAE,MAAM,CAEpB,IAAI,CACF,WAAW,CXOyB,uDAA2D,CWN/F,WAAW,CAAE,MAAM,CACnB,KAAK,CXlB+B,OAAW,CWmB/C,UAAU,CAAE,IAAI,CAChB,UAAU,CAAE,MAAM,CAClB,UAAU,CbnD0B,OAAO,CaqD7C,aAAa,CACX,UAAU,CAAE,IAAI,CAElB,eAAe,CACb,UAAU,CAAE,MAAM,CAEpB,cAAc,CACZ,UAAU,CAAE,KAAK,CAEnB,cAAc,CACZ,SAAS,CAAE,IAAI,CAEjB,eAAe,CACb,SAAS,CAAE,IAAI,CAEjB,oBAAqB,CACnB,SAAS,CAAE,GAAG,CAEhB,eAAe,CACb,eAAe,CAAE,YAAY,CAE/B,gBAAgB,CACd,KAAK,CAAE,kBAAkB,CAC3B,uBAAuB,CACrB,KAAK,CAAE,kBAAgC,CACzC,aAAa,CACX,KAAK,CAAE,kBAAgB,CACzB,oBAAoB,CAClB,KAAK,CAAE,kBAA8B,CACvC,gBAAgB,CACd,KAAK,CAAE,kBAAiB,CAC1B,uBAAuB,CACrB,KAAK,CAAE,kBAA+B,CACxC,eAAe,CACb,KAAK,CAAE,kBAAe,CACxB,sBAAsB,CACpB,KAAK,CAAE,kBAA6B,CACtC,gBAAgB,CACd,KAAK,CAAE,kBAAsB,CAC/B,uBAAuB,CACrB,KAAK,CAAE,kBAAoC,CAkB7C,gEAAyB,CACvB,UAAU,CAAE,CAAC,CACb,WAAW,CAAE,GAAG,CAChB,WAAW,CX5DyB,0DAA8D,CW8DpG,CAAC,CACC,WAAW,Cb1FyB,IAAI,Ca2FxC,MAAM,CAAE,CAAC,CACT,SAAS,Cb/F2B,IAAI,CagGxC,aAAa,Cb7FuB,IAAI,Ca+F1C,EAAE,CACA,SAAS,CAAE,IAAI,CAEjB,0CAAE,CACA,SAAS,CAAE,IAAI,CAEjB,EAAE,CACA,SAAS,CAAE,IAAI,CAEjB,EAAE,CACA,SAAS,CAAE,IAAI,CAEjB,EAAE,CACA,SAAS,CAAE,IAAI,CAEjB,EAAE,CACA,SAAS,CAAE,IAAI,CAEjB,EAAE,CACA,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,GAAG,CACX,MAAM,CAAE,CAAC,CACT,UAAU,CAAE,iBAA6B,CACzC,MAAM,CAAE,MAAmB,CAC3B,OAAO,CAAE,CAAC,CAEZ,sCAAI,CACF,WAAW,CAAE,MAAM,CACnB,SAAS,CAAE,IAAI,CACf,UAAU,CXrH0B,IAAM,CWsH1C,MAAM,CAAE,iBAAiC,CACzC,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,KAAK,CACd,WAAW,CXnGyB,wMAAoN,CWoGxP,KAAK,Cb1H+B,OAAI,Ca2HxC,UAAU,CAAE,IAAI,CAChB,0CAAY,CACV,SAAS,CAAE,GAAG,CAmClB,wFAAmB,CACjB,UAAU,CAAE,IAAI,CAChB,WAAW,CbzKyB,IAAI,Ca0KxC,aAAa,Cb1KuB,IAAI,Ca2KxC,oGAAE,CACA,UAAU,CAAE,IAAI,CAChB,WAAW,Cb7KuB,IAAI,Ca8KtC,wJAAY,CACV,aAAa,CAAE,CAAC,CAClB,gHAAE,CACA,aAAa,CAAE,CAAC,CAClB,gHAAE,CACA,UAAU,CAAE,MAAM,CAClB,4HAAE,CACA,UAAU,CAAE,MAAM,CACtB,4HAAK,CACH,UAAU,CAAE,OAAO,CAEzB,iFAAsB,CACpB,UAAU,CAAE,OAAO,CACnB,WAAW,Cb3LyB,IAAI,Ca4LxC,aAAa,Cb5LuB,IAAI,Ca6LxC,6FAAE,CACA,UAAU,CAAE,OAAO,CACnB,WAAW,Cb/LuB,IAAI,CagMtC,iJAAY,CACV,aAAa,CAAE,CAAC,CAClB,yGAAE,CACA,aAAa,CAAE,CAAC,CAChB,qHAAE,CACA,UAAU,CAAE,IAAI,CCrOxB,kBAAkB,CAChB,MAAM,CAAE,iBAA6B,CACrC,aAAa,CAAE,IAAI,CACnB,OAAO,Cd6B6B,IAAI,Cc5BxC,WAAW,CAAE,IAAqB,CAClC,WAAW,CAAE,GAAG,CAChB,UAAU,CZiC0B,IAAM,CYhC1C,QAAQ,CAAE,QAAQ,CAClB,wBAAO,CACL,OAAO,CAAE,SAAS,CAClB,QAAQ,CAAE,QAAQ,CAClB,GAAG,CAAE,GAAG,CACR,IAAI,CAAE,GAAG,CACT,UAAU,CZiCwB,OAAO,CYhCzC,KAAK,CAAE,IAAoB,CAC3B,OAAO,CAAE,QAA2C,CACtD,2CAA0B,CACxB,MAAM,CAAE,iBAA6B,CACrC,aAAa,CdcqB,IAAI,CcZ1C,+GAAmC,CACjC,MAAM,CAAE,iBAA6B,CACrC,OAAO,CAAE,GAAG,CACZ,UAAU,CAAE,IAAI,CAChB,UAAU,CZe0B,IAAM,CYb1C,MAAM,CAAE,YAAyB,CACjC,gLAAuB,CACrB,MAAM,CAAE,IAAI,CACZ,UAAU,CAAE,IAAI,CAChB,MAAM,CAAE,CAAC,CAEb,+BAA+B,CAC7B,KAAK,CAAE,IAAI,CACb,cAAc,CACZ,YAAY,CAAE,iBAA0C,CACxD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,SAA2C,CACpD,WAAW,CZuByB,wMAAoN,CYtBxP,SAAS,CAAE,IAAI,CACf,WAAW,CAAE,GAAG,CAChB,KAAK,CdI+B,OAAwB,CcH9D,2BAA2B,CACzB,WAAW,CAAE,GAAG,CAChB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,SAA2C,CACpD,WAAW,CZeyB,wMAAoN,CYdxP,SAAS,CAAE,IAAI,CACf,WAAW,CAAE,GAAG,CAChB,OAAO,CAAE,KAAK,CACd,QAAQ,CAAE,IAAI,CACd,KAAK,CZhB+B,OAAW,CYoBjD,YAAY,CACV,2IAAgE,CAC9D,WAAW,CAAE,QAAQ,ECzDzB,IAAI,CACF,gBAAgB,CAAE,IAAO,CACzB,MAAM,CAAE,OAAO,CACf,OAAO,CAAE,MAAM,CACf,OAAO,CAAE,KAAK,CAChB,EAAE,CACA,KAAK,CAAE,IAAO,CACd,UAAU,CAAE,MAAM,CACpB,IAAI,CACF,KAAK,CAAE,OAAO,CACd,gBAAgB,CAAE,OAAO,CAC3B,EAAE,CACA,WAAW,CAAE,IAAI,CACnB,EAAE,CACA,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,UAAU,CAAE,MAAM,CACpB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,UAAU,CAAE,MAAM,CACpB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACjB,UAAU,CAAE,MAAM,CACpB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,gBAAgB,CAAE,IAAO,CAC3B,MAAM,CACJ,KAAK,CAAE,IAAO,CACd,gBAAgB,CAAE,IAAO,CAC3B,GAAG,CACD,UAAU,CAAE,MAAM,CACpB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,gBAAgB,CAAE,IAAO,CAC3B,MAAM,CACJ,KAAK,CAAE,IAAO,CACd,gBAAgB,CAAE,IAAO,CAC3B,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,MAAO,CACd,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACnB,EAAE,CACA,KAAK,CAAE,IAAO,CAChB,EAAE,CACA,KAAK,CAAE,IAAO,CAChB,EAAE,CACA,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,OAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,MAAM,CACf,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CACd,WAAW,CAAE,IAAI,CACnB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,WAAW,CAAE,IAAI,CACnB,EAAE,CACA,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,OAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,OAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,IAAI,CACb,GAAG,CACD,KAAK,CAAE,IAAO,CAChB,GAAG,CACD,KAAK,CAAE,IAAI,CACX,gBAAgB,CAAE,OAAO,CCjJ3B,kBAAkB,CAChB,OAAO,CAAE,YAAY,CACrB,uCAAsB,CACpB,KAAK,CAAE,KAAK,CACd,oBAAC,CACC,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,GAAG,CACZ,gCAAa,CACX,YAAY,CAAE,CAAC,CACnB,6FAAI,CACF,OAAO,CAAE,GAAG,CACZ,MAAM,CAAE,IAAI,CACZ,UAAU,CAAE,IAAI,CAChB,qHAAS,CACP,KAAK,CdqB2B,OAAW,CcpBjD,qBAAqB,CACnB,aAAa,CAAE,CAAC,CAChB,KAAK,CdqB+B,OAAW,CcpB/C,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,YAAY,CbanB,oCAAsB,CaTxB,qBAAqB,CACnB,OAAO,CAAE,IAAI,CACf,uCAAuC,CACrC,OAAO,CAAE,IAAI,EAEjB,YAAY,CACV,uCAAuC,CACrC,OAAO,CAAE,IAAI,EC9BjB,SAAS,CACP,QAAQ,CAAE,KAAK,CACf,GAAG,CCAO,OAAO,CDGjB,gBAAO,CACL,eAAe,CAAE,IAAI,CAEzB,cAAc,CjC+FZ,KAAK,CAAE,CAAC,CACR,0CAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,oBAAO,CACL,KAAK,CAAE,IAAI,CiCnGb,mCAAM,CACJ,OAAO,CAAE,YAAY,CACvB,uBAAQ,CACN,UAAU,CAAE,qBAAoB,CAEhC,6BAAa,CACX,WAAW,CAAE,iBAAyB,CACxC,8BAAc,CACZ,YAAY,CAAE,iBAAyB,CAC3C,gBAAC,CACC,MAAM,CAAE,IAAmB,CAC3B,OAAO,CAAE,YAAY,CACrB,WAAW,CAAE,IAAmB,CAChC,OAAO,CAAE,MAAiB,CAE9B,iBAAiB,CACf,KAAK,CjBuD+B,KAAK,CiBtDzC,oDAAiB,CACf,MAAM,CAAE,IAAmB,CAC3B,OAAO,CAAE,YAAY,CACrB,WAAW,CAAE,IAAmB,CAChC,OAAO,CAAE,SAAS,CAClB,aAAa,CAAE,CAAC,CAChB,OAAO,CAAE,KAAK,CACd,WAAW,CAAE,IAAI,CACjB,cAAc,CAAE,SAAS,CACzB,SAAS,CAAE,GAAG,CACd,KAAK,CfT6B,IAAK,CeUvC,WAAW,CAAE,MAAM,CAErB,oBAAE,CACA,aAAa,CAAE,CAAC,CAEhB,+BAAY,CACV,UAAU,CAAE,iBAAyB,CACvC,kCAAe,CACb,aAAa,CAAE,iBAAyB,CAC1C,4BAAS,CACP,UAAU,CAAE,OAA4C,CACxD,8BAAC,CACC,KAAK,CfbyB,IAAY,Cec1C,YAAY,CAAE,iBAAsD,CACpE,OAAO,CAAE,eAAyB,CAClC,oCAAO,CACL,UAAU,CAAE,OAA4C,CAC9D,mGAAI,CACF,MAAM,CAAE,IAAI,CACZ,UAAU,CAAE,OAAO,CACnB,KAAK,CAAE,OAAO,CACd,YAAY,CAAE,CAAC,CACf,aAAa,CAAE,CAAC,CAElB,wCAAmB,CACjB,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACX,WAAW,CAAE,MAAM,CAGnB,SAAS,CAAE,KAAI,CACf,WAAW,CAAE,KAAK,CAClB,KAAK,CAAE,OAA8B,CAGzC,wDAAuB,CACrB,KAAK,CfvC6B,OAAW,CewC7C,OAAO,CAAE,eAAmB,CAC5B,WAAW,CAAE,IAAI,CACjB,QAAQ,CAAE,QAAQ,CAClB,UAAU,CflCwB,OAAyB,CemC3D,MAAM,CAAE,IAAI,CACZ,aAAa,CAAE,iBAAsD,CACrE,UAAU,CAAE,iBAAsD,CAClE,YAAY,CAAE,YAAY,CAE1B,oEAAO,CACL,UAAU,CfzCsB,OAAyB,Ce0CzD,4GAAmB,CACjB,KAAK,CflDyB,IAAY,CemD9C,gGAAmB,CAGjB,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,KAAI,CACf,WAAW,CAAE,KAAK,CAClB,KAAK,CAAE,IAA8B,CAIvC,iHAAI,CACF,OAAO,CAAE,IAAI,CACf,iIAAc,CACZ,OAAO,CAAE,KAAK,CAGd,yCAAG,CACD,UAAU,CAAE,OAA4C,CACxD,OAAO,CAAE,eAAyB,CACpC,uDAAiB,CACf,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,OAA4C,CACxD,OAAO,CAAE,eAAyB,CACtC,2DAA2B,CACzB,KAAK,Cf3E2B,IAAY,Ce4E9C,mDAAmB,CACjB,KAAK,CAAE,OAA4C,CACvD,+BAAa,CACX,SAAS,CAAE,KAAI,CAEb,yCAAG,CACD,UAAU,CAAE,OAA4C,CACxD,OAAO,CAAE,eAAyB,CACpC,uDAAiB,CACf,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,OAA4C,CACxD,OAAO,CAAE,eAAyB,CAClC,UAAU,CAAE,IAAI,CAChB,aAAa,CAAE,IAAI,CACvB,2DAA2B,CACzB,KAAK,Cf3F2B,IAAY,Ce4F9C,mDAAmB,CACjB,KAAK,CAAE,OAA4C,CACvD,+BAAa,CACX,SAAS,CAAE,KAAI,CAEjB,+BAAa,CACX,OAAO,CAAE,KAAK,CAChB,uBAAK,CACH,aAAa,CAAE,CAAC,CAChB,OAAO,CAAE,IAAI,CAEb,kCAAK,CACH,OAAO,CAAE,KAAK,CAClB,4BAAU,CACR,aAAa,CAAE,CAAC,CAChB,KAAK,Cf1G6B,OAAW,Ce2G7C,WAAW,CAAE,MAAM,CACrB,mBAAC,CACC,OAAO,CAAE,YAAY,CACrB,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,eAAmB,CAC5B,OAAO,CAAE,KAAK,CACd,QAAQ,CAAE,QAAQ,CAClB,SAAS,CAAE,GAAG,CACd,KAAK,CfnH6B,OAAW,CeoH7C,yBAAO,CACL,gBAAgB,CAAE,OAAoC,CACtD,MAAM,CAAE,OAAO,CACf,6CAAmB,CACjB,KAAK,CfxHyB,OAAW,CeyH7C,0BAAQ,CACN,gBAAgB,CfnHgB,OAAK,CeoHrC,MAAM,CAAE,OAAO,CACf,KAAK,Cf3H2B,IAAM,Ce4HtC,8CAAmB,CACjB,KAAK,Cf7HyB,IAAM,Ce+H5C,mBAAmB,CACjB,OAAO,CAAE,KAAK,CACd,KAAK,CjBvF+B,KAAK,CiBwFzC,OAAO,CAAE,MAAW,CACpB,aAAa,CAAE,MAAW,CAC1B,OAAO,CjBrF6B,GAAG,CiBsFvC,gBAAgB,Cf/HoB,OAAK,CegIzC,UAAU,CAAE,MAAM,CAClB,OAAO,CAAE,MAAW,CACpB,OAAO,CAAE,KAAK,CACd,KAAK,CfpI+B,OAAyB,CeqI7D,aAAa,CAAE,MAAW,CAC1B,oCAAgB,CACd,KAAK,CAAE,IAAI,CACX,aAAa,CAAE,IAAI,CACnB,OAAO,CAAE,QAAQ,CACjB,YAAY,CAAE,OAAuB,CACvC,uBAAG,CACD,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,qBAA0B,CAClC,MAAM,CAAE,IAAI,CACZ,KAAK,CAAE,IAAI,CACX,gBAAgB,Cf/IkB,OAAK,CegJvC,OAAO,CAAE,GAAG,CACZ,aAAa,CAAE,IAAI,CACrB,wDAAqB,CACnB,KAAK,CfpJ6B,OAAyB,CeqJ3D,SAAS,CAAE,IAAI,CACf,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,OAA2C,CACpD,aAAa,CAAE,MAAW,CAE1B,oEAAO,CACL,UAAU,CAAE,qBAAoB,CAClC,0EAAQ,CACN,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,MAAM,CACd,MAAM,CAAE,IAAI,CACZ,KAAK,CAAE,IAAI,CACX,aAAa,CAAE,CAAC,CAChB,SAAS,CAAE,IAAI,CACf,UAAU,CAAE,WAAa,CAEzB,oFAAQ,CACN,UAAU,CAAE,MAAM,CACxB,+BAAa,CACX,UAAU,CAAE,QAAkB,CAC9B,aAAa,CAAE,MAAW,CAC1B,WAAW,CAAE,MAAM,CACnB,KAAK,CAAE,qBAAoB,CAI7B,gCAAM,CACJ,KAAK,CfhL6B,OAAK,CeiLzC,2BAAC,CACC,KAAK,CfzL6B,OAAW,Ce0L7C,iCAAO,CACL,gBAAgB,CfpLgB,OAAK,CeqLrC,KAAK,Cf3L2B,IAAM,Ce6L5C,gBAAgB,CnC3NR,kBAAoB,CAAE,gBAAM,CAK5B,eAAiB,CAAE,gBAAM,CAezB,UAAY,CAAE,gBAAM,CmCyM1B,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,CAAC,CACV,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,CAAC,CACV,4BAAa,CACX,IAAI,CAAE,CAAC,CACP,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,CAAC,CACZ,0BAAW,CACT,KAAK,CAAE,IAAI,CACX,IAAI,CAAE,KAAK,CACX,OAAO,CAAE,CAAC,CACZ,2BAAY,CACV,KAAK,CAAE,KAAK,CACZ,IAAI,CAAE,IAAI,CACV,OAAO,CAAE,CAAC,CAGd,gBAAgB,CACd,UAAU,CAAE,qBAAuC,CACnD,gBAAgB,CAAE,2uCAA2uC,CAC7vC,eAAe,CAAE,SAAsB,CAEzC,gBAAgB,CACd,QAAQ,CAAE,QAAQ,CAClB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CAEd,YAAY,CACV,QAAQ,CAAE,KAAK,CACf,GAAG,CAAE,CAAC,CACN,MAAM,CAAE,CAAC,CACT,IAAI,CAAE,CAAC,CACP,cAAc,CAAE,GAAG,CACnB,KAAK,CjBvL+B,KAAK,CiBwLzC,UAAU,CAAE,MAAM,CAClB,UAAU,CAAE,MAAM,CAClB,UAAU,CAAE,IAAI,CAChB,UAAU,CflO0B,OAAsB,CemO1D,OAAO,CjBvL6B,GAAG,CiByLzC,eAAe,CACb,KAAK,CAAE,KAAyB,CAChC,QAAQ,CAAE,QAAQ,CAClB,UAAU,CAAE,MAAM,CAClB,UAAU,CAAE,MAAM,CAClB,MAAM,CAAE,IAAI,CAEd,WAAW,CACT,OAAO,CAAE,IAAI,CACb,UAAU,Cf3O0B,OAAK,Ce4OzC,KAAK,CflP+B,IAAM,CemP1C,OAAO,CAAE,cAAuB,CAChC,QAAQ,CAAE,QAAQ,CAClB,WAAW,CAAE,IAAI,CACjB,UAAU,CAAE,MAAM,CAClB,SAAS,CAAE,IAAI,CjCvLf,KAAK,CAAE,CAAC,CACR,oCAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,iBAAO,CACL,KAAK,CAAE,IAAI,CiCmLb,aAAC,CACC,KAAK,Cf1P6B,IAAM,Ce2PxC,WAAW,CAAE,IAAI,CAEnB,eAAG,CACD,YAAY,CAAE,IAAqB,CACnC,MAAM,CAAE,IAAI,CACZ,KAAK,CAAE,IAAI,CACX,gBAAgB,Cf3PkB,OAAK,Ce4PvC,OAAO,CAAE,GAAG,CACZ,aAAa,CAAE,IAAI,CACrB,aAAC,CACC,SAAS,CAAE,IAAI,CACf,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,OAAO,CACf,WAAW,CAAE,OAAO,CAExB,oBAAoB,CAClB,WAAW,CjBjOyB,KAAK,CiBkOzC,UAAU,CfvQ0B,OAAyB,CewQ7D,UAAU,CAAE,IAAI,CAElB,eAAe,CACb,OAAO,CAAE,eAAmB,CAC5B,MAAM,CAAE,IAAI,CACZ,SAAS,CAAE,KAAK,CAChB,MAAM,CAAE,IAAI,CAEd,aAAa,CACX,QAAQ,CAAE,KAAK,CACf,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,UAAU,CAAE,eAAc,CAC1B,OAAO,CAAE,IAAI,CACb,OAAO,CAAE,GAAkB,CAC3B,gBAAI,CACF,OAAO,CAAE,KAAK,CAClB,MAAM,CACJ,KAAK,CfhT+B,IAAW,CeiT/C,QAAC,CACC,aAAa,CAAE,IAAqB,CACtC,6FAAgB,CACd,OAAO,CAAE,GAAG,CACZ,WAAW,Cf9QuB,wMAAoN,Ce+QtP,SAAS,CAAE,GAAG,CACd,UAAU,CAAE,IAAI,CAChB,MAAM,CAAE,IAAI,CACZ,KAAK,CfzT6B,IAAW,Ce2TjD,mBAAmB,CjC1OjB,KAAK,CAAE,CAAC,CACR,oDAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,yBAAO,CACL,KAAK,CAAE,IAAI,CiCuOf,wBAAwB,CACtB,UAAU,CAAE,IAAI,CjC9OhB,KAAK,CAAE,CAAC,CACR,8DAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,8BAAO,CACL,KAAK,CAAE,IAAI,CiC4Ob,0BAAU,CACR,aAAa,CjB1TqB,IAAI,CiB2TtC,aAAa,CAAE,iBAA6B,CAC5C,cAAc,CjB5ToB,IAAI,CiB6TxC,sCAAsB,CACpB,UAAU,CAAE,iBAA6B,CACzC,WAAW,CjB/TuB,IAAI,CiBgUxC,4BAAY,CACV,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,IAAqB,CACpC,OAAO,CAAE,YAAY,CACvB,wBAAQ,CACN,KAAK,CfhU6B,IAAY,CeiU9C,SAAS,CAAE,GAAG,CdtUd,oCAAsB,Cc0UxB,gBAAgB,CACd,UAAU,Cf/TwB,OAAyB,CegU7D,WAAW,CACT,OAAO,CAAE,KAAK,CAChB,YAAY,CAER,IAAI,CAAE,MAAmB,CAG3B,kBAAO,CACL,KAAK,CAAE,GAAG,CACV,IAAI,CAAE,CAAC,CACX,eAAe,CACb,KAAK,CAAE,IAAI,CACb,mBAAmB,CACjB,KAAK,CAAE,IAAI,CACb,yBAAyB,CACvB,KAAK,CAAE,IAAI,CACb,oBAAoB,CAClB,WAAW,CAAE,CAAC,CACd,oCAAe,CACb,OAAO,CC7XD,OAAO,CD8Xf,0BAAO,CACL,QAAQ,CAAE,KAAK,CACf,SAAS,CAAE,IAAI,CACf,IAAI,CAAE,GAAG,CACT,GAAG,CAAE,CAAC,CACN,MAAM,CAAE,IAAI,CACZ,QAAQ,CAAE,MAAM,EdtWlB,qCAAsB,CcyWxB,oBAAoB,CAClB,UAAU,CAAE,gBAAe,CAC7B,eAAe,CACb,MAAM,CAAE,CAAC,CACT,UAAU,CfjWwB,OAAyB,EemW/D,YAAY,CACV,iCAAmC,CACjC,OAAO,CAAE,IAAI,CACf,oBAAoB,CAClB,WAAW,CAAE,CAAC,EEnZlB,aAAa,CACX,QAAQ,CAAE,KAAK,CACf,MAAM,CAAE,CAAC,CACT,IAAI,CAAE,CAAC,CACP,KAAK,CnB6E+B,KAAK,CmB5EzC,KAAK,CjBuC+B,OAAyB,CiBtC7D,UAAU,CAAE,OAAkC,CAC9C,UAAU,CAAE,kBAAiC,CAC7C,WAAW,CjBkDyB,uDAA2D,CiBjD/F,OAAO,CnB+E6B,GAAG,CmB9EvC,eAAC,CACC,KAAK,CjBkC6B,OAAK,CiBjCvC,eAAe,CAAE,IAAI,CACvB,8BAAgB,CACd,OAAO,CAAE,IAAI,CACf,kCAAoB,CAClB,OAAO,CAAE,IAAqB,CAC9B,gBAAgB,CAAE,OAAkC,CACpD,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,KAAK,CACjB,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,OAAO,CACf,KAAK,CjBX6B,OAAM,ClB4F1C,KAAK,CAAE,CAAC,CACR,kFAAS,CAEP,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,EAAE,CACb,wCAAO,CACL,KAAK,CAAE,IAAI,CmCrFX,uqDAAG,CACD,KAAK,CjBmB2B,OAAyB,CiBlB3D,yFAAQ,CACN,KAAK,CAAE,IAAI,CACb,6CAAU,CACR,KAAK,CAAE,IAAI,CACb,kDAAiB,CACf,gBAAgB,CnBQgB,OAAI,CmBPpC,KAAK,CjBO2B,IAAM,CiBNxC,yDAAwB,CACtB,gBAAgB,CjBsBgB,OAAO,CiBrBvC,KAAK,CnBzB2B,IAAI,CmB0BxC,0CAA8B,CAC5B,OAAO,CAAE,KAAK,CAChB,iCAAmB,CACjB,SAAS,CAAE,GAAG,CACd,OAAO,CAAE,IAAqB,CAC9B,KAAK,CjBJ6B,IAAY,CiBK9C,OAAO,CAAE,IAAI,CACb,oCAAE,CACA,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,GAAG,CACX,MAAM,CAAE,CAAC,CACT,MAAM,CAAE,MAAM,CACd,OAAO,CAAE,CAAC,CACV,UAAU,CAAE,iBAA6C,CAC3D,oCAAE,CACA,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,CAAC,CACT,sCAAC,CACC,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,GAAqB,CAC9B,KAAK,CjBZyB,OAAyB,CiBa7D,uBAAW,CACT,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,KAAK,CAAE,IAAI,CACX,IAAI,CAAE,IAAI,CACV,MAAM,CAAE,IAAI,CACZ,SAAS,CnBkByB,KAAK,CmBjBvC,kCAAU,CACR,KAAK,CAAE,IAAI,CACb,mEAAQ,CACN,KAAK,CAAE,IAAI,CACb,qDAA+B,CAC7B,UAAU,CAAE,KAAK,CACjB,+HAAQ,CACN,KAAK,CAAE,IAAI,CACb,gEAAU,CACR,KAAK,CAAE,IAAI,CACf,4CAAoB,CAClB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,KAAuB,CAChC,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,MAAM,ChBhDpB,oCAAsB,CgBmDxB,aAAa,CACX,KAAK,CAAE,GAAG,CACV,OAAO,CAAE,IAAI,CACb,mBAAO,CACL,OAAO,CAAE,KAAK,ECtElB,gBAAG,CACD,SAAS,CAAE,IAAI,CACf,MAAM,CAAE,eAAe,CAEzB,2BAAgB,CACd,WAAW,CAAE,MAAM,CAErB,uBAAU,CACR,aAAa,CpBOqB,IAAI,CoBNtC,iCAAS,CACP,UAAU,CAAE,MAAM,CAEtB,oCAAuB,CACrB,UAAU,CAAE,MAAM,CAGpB,qDAAoC,CAClC,aAAa,CpBFqB,IAAI,CoBaxC,uBAAU,CACR,WAAW,CpBduB,IAAI,CoBetC,WAAW,CpBfuB,IAAI,CoBgBtC,aAAa,CpBhBqB,IAAI,CoBsBtC,kTAAK,CACH,aAAa,CAAE,CAAC,CAKlB,qCAAQ,CACN,YAAY,CAAE,GAAG,CAUrB,8BAAiB,CACf,YAAY,CAAE,eAAc,CAC5B,mEAAM,CACJ,UAAU,CAAE,sBAAsB,CAClC,YAAY,CAAE,0BAAyB,CAG3C,0EAAiD,CAC/C,UAAU,CAAE,WAAW,CACzB,0EAAiD,CAC/C,UAAU,CAAE,WAAW,CAGzB,qDAA4B,CAC1B,aAAa,CAAE,IAAqB,CACtC,wBAAW,CACT,WAAW,CpBvDuB,IAAI,CoB0DxC,yBAAY,CACV,WAAW,CAAE,IAAI,CACjB,aAAa,CAAE,IAAqB,CACtC,yBAAY,CACV,KAAK,ClB3D6B,OAAW,CkB4D/C,yBAAY,CACV,KAAK,CAAE,KAAK,CACZ,MAAM,CAAE,iBAA2C,CACrD,wBAAW,CACT,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,iBAA2C,CACrD,0BAAa,CACX,MAAM,CAAE,IAAI,CACZ,OAAO,CAAE,KAAK,CAMd,6RAAW,CACT,OAAO,CAAE,IAAI,CACb,UAAU,CAAE,MAAM,CAClB,SAAS,CAAE,IAAI,CAEf,mVAAO,CACL,UAAU,CAAE,OAAO,CACnB,OAAO,CAAE,GAAO,CAChB,WAAW,CAAE,WAAW,CACxB,OAAO,CAAE,YAAY,CACzB,mVAAmB,CACjB,OAAO,CAAE,YAAY,CAGzB,qBAAQ,CACN,KAAK,CAAE,KAAK,CACZ,KAAK,CAAE,GAAG,CACV,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,aAAuC,CAC/C,OAAO,CpBhG2B,IAAI,CoBiGtC,UAAU,ClB9EwB,OAAmB,CkB+ErD,MAAM,CAAE,iBAA+B,CAEvC,yEAAS,CACP,SAAS,CAAE,GAAG,CAChB,2BAAK,CACH,aAAa,CAAE,CAAC,CAClB,oCAAc,CACZ,OAAO,CAAE,KAAK,CACd,WAAW,ClB/EqB,0DAA8D,CkBgF9F,WAAW,CAAE,IAAI,CACjB,UAAU,ClBvFsB,OAAmB,CkBwFnD,OAAO,CAAE,QAA2C,CACpD,MAAM,CAAE,KAAkB,CAC1B,aAAa,CpB/GmB,IAAI,CoBgHpC,SAAS,CAAE,IAAI,CAEnB,yBAAY,CACV,UAAU,ClB3FwB,OAAO,CkB4FzC,OAAO,CAAE,YAAY,CACrB,WAAW,CAAE,IAAI,CACjB,OAAO,CAAE,KAAuB,CAGlC,iEAAwC,CACtC,cAAc,CAAE,KAAK,CACrB,SAAS,CAAE,GAAG,CAIhB,yEAAgD,CAC9C,UAAU,CAAE,IAAI,CAChB,MAAM,CAAE,IAAI,CACZ,KAAK,ClB5I6B,IAAW,CkB6I7C,+JAAM,CACJ,MAAM,CAAE,IAAI,CACZ,gBAAgB,CAAE,sBAAsB,CACxC,WAAW,CAAE,MAAM,CACrB,2FAAQ,CACN,YAAY,CAAE,CAAC,CACf,aAAa,CAAE,CAAC,CAChB,cAAc,CAAE,GAAG,CACrB,mKAAI,CACF,KAAK,ClBhJ2B,IAAK,CkBuJzC,6BAAgB,CAEd,MAAM,CAAE,IAAI,CACZ,gCAAE,CACA,MAAM,CAAE,IAAI,CACZ,WAAW,CAAE,GAAG,CAClB,uCAAW,CACT,OAAO,CAAE,YAAY,CACrB,UAAU,CAAE,GAAG,CACjB,yCAAW,CACT,aAAa,CAAE,IAAI,CACnB,UAAU,CAAE,IAAI,CAChB,WAAW,CAAE,MAAM,CACrB,yCAAW,CACT,UAAU,CAAE,IAAI,CAChB,YAAY,CAAE,CAAC,CAGnB,iDAAQ,CAEN,KAAK,CpB7L6B,IAAI,CoB8LtC,OAAO,CAAE,OAAO,CAChB,wHAAO,CACL,SAAS,CAAE,eAAe,CAC1B,WAAW,CAAE,MAAM,CAErB,yEAAS,CACP,KAAK,CpBvK2B,OAAI,CoBwKtC,wHAAW,CACT,WAAW,CAAE,IAAI,CACjB,KAAK,ClB9K2B,OAAW,CkBgL/C,uDAAY,CACV,KAAK,ClBvK6B,OAAK,CkBwKzC,eAAE,CACA,aAAa,CpBtLqB,IAAI,CoBuLtC,kBAAE,CACA,WAAW,CAAE,IAAI,CAEnB,6EAAgB,CACd,aAAa,CAAE,eAAgC,CAEjD,kBAAE,CACA,MAAM,CAAE,aAA4C,CAMxD,8BAAiB,CACf,aAAa,CpBrMqB,IAAI,CoBuMtC,iCAAE,CACA,OAAO,CAAE,YAAY,CACrB,MAAM,CAAE,KAAuB,CAC/B,SAAS,CAAE,GAAG,CACd,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,OAA0B,CACtC,KAAK,ClBhM2B,OAAK,CkBiMrC,UAAU,CAAE,iBAAoC,CAChD,OAAO,CAAE,GAAqB,CAC9B,QAAQ,CAAE,QAAQ,CAClB,wCAAQ,CACN,KAAK,CAAE,OAA0B,CACnC,6CAAW,CACT,KAAK,ClBjNyB,OAAW,CkBkNzC,SAAS,CAAE,eAAe,CAE9B,oCAAK,CACH,aAAa,CAAE,GAAqB,CACpC,MAAM,CAAE,IAAI,CACZ,WAAW,CAAE,cAAuB,CACpC,UAAU,CAAE,OAAa,CACzB,KAAK,ClBhO2B,IAAK,CkBiOrC,gDAAW,CACT,KAAK,ClB3NyB,OAAW,CkB4NzC,SAAS,CAAE,eAAe,CAC9B,6CAAc,CACZ,UAAU,CAAE,CAAC,CAEf,uGAAQ,CACN,WAAW,CAAE,IAAI,CACjB,oRAA2B,CACzB,gBAAgB,CAAE,WAAW,CAC7B,MAAM,CAAE,IAAI,CACZ,OAAO,CAAE,CAAC,CACV,SAAS,CAAE,eAAe,CAC5B,kIAAU,CACR,WAAW,CAAE,IAAI,CAErB,wCAAS,CACP,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,KAAK,CACd,KAAK,CpBtQ2B,IAAI,CoBuQpC,WAAW,CAAE,IAAI,CACnB,wCAAS,CACP,OAAO,CAAE,YAAY,CACrB,aAAa,CAAE,GAAG,CAEtB,uDAA8B,CAC5B,OAAO,CAAE,YAAY,CACrB,KAAK,ClB7Q6B,OAAM,CkB8QxC,SAAS,CAAE,GAAG,CACd,YAAY,CpB1PsB,IAAI,CoB2PxC,2BAAc,CACZ,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,KAAK,CACd,qBAAQ,CACN,aAAa,CAAE,IAAI,CACnB,WAAW,CAAE,IAAI,CAEnB,mDAAa,CACX,UAAU,CAAE,OAAO,CACnB,OAAO,CAAE,OAAO,CAChB,WAAW,CAAE,MAAM,CACnB,WAAW,CAAE,OAAO,CACpB,SAAS,CAAE,OAAO,CAClB,KAAK,CAAE,OAAO,CACd,MAAM,CAAE,OAAO,CACf,WAAW,CAAE,OAAO,CACpB,qFAAgB,CACd,sBAAsB,CAAE,oBAAoB,CAG5C,mGAAQ,CACN,YAAY,CAAE,GAAG,CACvB,sBAAS,CACP,MAAM,CAAE,iBAAuC,CAC/C,UAAU,CAAE,OAA6B,CACzC,SAAS,CAAE,GAAG,CACd,WAAW,CAAE,GAAG,CAChB,aAAa,CAAE,GAAqB,CACpC,OAAO,CAAE,SAA4C,CACrD,MAAM,CAAE,QAA2B,CjBxRnC,oCAAsB,CiB8RtB,qBAAQ,CACN,KAAK,CAAE,IAAI,EC/TjB,wBAAwB,CACtB,KAAK,CnBkC+B,OAAW,CmBhCjD,KAAK,CACH,UAAU,CAAE,MAAM,YCHlB,WAAW,CAAE,aAAa,CAC1B,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,0GAA4G,YAGjH,WAAW,CAAE,aAAa,CAC1B,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,yGAA2G,YAGhH,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,6FAA+F,YAGpG,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,oFAAsF,YAG3F,WAAW,CAAE,aAAa,CAC1B,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,gHAAkH,YAGvH,WAAW,CAAE,aAAa,CAC1B,UAAU,CAAE,MAAM,CAClB,WAAW,CAAE,GAAG,CAChB,GAAG,CAAE,uGAAyG", +"sources": ["../../../bower_components/neat/app/assets/stylesheets/grid/_grid.scss","../../../bower_components/bourbon/dist/addons/_prefixer.scss","../../../bower_components/wyrm/sass/wyrm_core/_reset.sass","../../../bower_components/wyrm/sass/wyrm_core/_mixin.sass","../../../bower_components/font-awesome/scss/font-awesome.scss","../../../bower_components/font-awesome/scss/_path.scss","../../../bower_components/font-awesome/scss/_core.scss","../../../bower_components/font-awesome/scss/_larger.scss","../../../bower_components/font-awesome/scss/_fixed-width.scss","../../../bower_components/font-awesome/scss/_list.scss","../../../bower_components/font-awesome/scss/_variables.scss","../../../bower_components/font-awesome/scss/_bordered-pulled.scss","../../../bower_components/font-awesome/scss/_animated.scss","../../../bower_components/font-awesome/scss/_rotated-flipped.scss","../../../bower_components/font-awesome/scss/_mixins.scss","../../../bower_components/font-awesome/scss/_stacked.scss","../../../bower_components/font-awesome/scss/_icons.scss","../../../bower_components/font-awesome/scss/_screen-reader.scss","../../../bower_components/wyrm/sass/wyrm_core/_font_icon_defaults.sass","../../../bower_components/wyrm/sass/wyrm_core/_wy_variables.sass","../../../bower_components/wyrm/sass/wyrm_core/_alert.sass","../../../sass/_theme_variables.sass","../../../bower_components/neat/app/assets/stylesheets/grid/_media.scss","../../../bower_components/wyrm/sass/wyrm_core/_button.sass","../../../bower_components/wyrm/sass/wyrm_core/_dropdown.sass","../../../bower_components/wyrm/sass/wyrm_core/_form.sass","../../../bower_components/neat/app/assets/stylesheets/grid/_outer-container.scss","../../../bower_components/neat/app/assets/stylesheets/settings/_grid.scss","../../../bower_components/neat/app/assets/stylesheets/grid/_span-columns.scss","../../../bower_components/wyrm/sass/wyrm_core/_neat_extra.sass","../../../bower_components/wyrm/sass/wyrm_core/_generic.sass","../../../bower_components/wyrm/sass/wyrm_core/_table.sass","../../../bower_components/wyrm/sass/wyrm_core/_type.sass","../../../bower_components/wyrm/sass/wyrm_addons/pygments/_pygments.sass","../../../bower_components/wyrm/sass/wyrm_addons/pygments/_pygments_light.sass","../../../sass/_theme_breadcrumbs.sass","../../../sass/_theme_layout.sass","../../../bower_components/neat/app/assets/stylesheets/grid/_private.scss","../../../sass/_theme_badge.sass","../../../sass/_theme_rst.sass","../../../sass/_theme_mathjax.sass","../../../sass/_theme_font_local.sass"], +"names": [], +"file": "theme.css" +} diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/FontAwesome.otf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/FontAwesome.otf index 8b0f54e47e1d..d4de13e832d5 100644 Binary files a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/FontAwesome.otf and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/FontAwesome.otf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Bold.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Bold.ttf new file mode 100644 index 000000000000..809c1f5828f8 Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Bold.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Regular.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Regular.ttf new file mode 100644 index 000000000000..fc981ce7ad6c Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Inconsolata-Regular.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Bold.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Bold.ttf new file mode 100644 index 000000000000..1d23c7066e09 Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Bold.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Regular.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Regular.ttf new file mode 100644 index 000000000000..0f3d0f837d24 Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/Lato-Regular.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Bold.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Bold.ttf new file mode 100644 index 000000000000..df5d1df27304 Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Bold.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Regular.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Regular.ttf new file mode 100644 index 000000000000..eb52a7907362 Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/RobotoSlab-Regular.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot index 7c79c6a6bc9a..c7b00d2ba889 100644 Binary files a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.eot differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.svg b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.svg index 45fdf3383012..8b66187fe067 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.svg +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.svg @@ -14,10 +14,11 @@ + - + - + @@ -30,7 +31,7 @@ - + @@ -52,7 +53,7 @@ - + @@ -77,11 +78,11 @@ - - - - - + + + + + @@ -109,8 +110,8 @@ - - + + @@ -143,17 +144,17 @@ - - + + - + - + - + @@ -168,7 +169,7 @@ - + @@ -176,14 +177,14 @@ - - + + - + @@ -218,8 +219,8 @@ - - + + @@ -247,10 +248,10 @@ - + - + @@ -274,7 +275,7 @@ - + @@ -345,8 +346,8 @@ - - + + @@ -361,14 +362,14 @@ - - + + - - + + @@ -379,7 +380,7 @@ - + @@ -398,17 +399,287 @@ - + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf index e89738de5eaf..f221e50a2ef6 100644 Binary files a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.ttf differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff index 8c1748aab7a7..6e7483cf61b4 100644 Binary files a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff2 b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000000..7eb74fd127ee Binary files /dev/null and b/user_guide_src/source/_themes/sphinx_rtd_theme/static/fonts/fontawesome-webfont.woff2 differ diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/modernizr.min.js b/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/modernizr.min.js new file mode 100644 index 000000000000..f65d47974786 --- /dev/null +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/modernizr.min.js @@ -0,0 +1,4 @@ +/* Modernizr 2.6.2 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f
            "); -}); - -window.SphinxRtdTheme = (function (jquery) { - var stickyNav = (function () { - var navBar, - win, - stickyNavCssClass = 'stickynav', - applyStickNav = function () { - if (navBar.height() <= win.height()) { - navBar.addClass(stickyNavCssClass); - } else { - navBar.removeClass(stickyNavCssClass); - } - }, - enable = function () { - applyStickNav(); - win.on('resize', applyStickNav); - }, - init = function () { - navBar = jquery('nav.wy-nav-side:first'); - win = jquery(window); - }; - jquery(init); - return { - enable : enable - }; - }()); - return { - StickyNav : stickyNav - }; -}($)); diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/theme.js b/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/theme.js index b77789d0605b..af661a92365a 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/theme.js +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/static/js/theme.js @@ -1,105 +1,169 @@ -$(document).ready(function () { - // Shift nav in mobile when clicking the menu. - $(document).on('click', "[data-toggle='wy-nav-top']", function () { - $("[data-toggle='wy-nav-shift']").toggleClass("shift"); - $("[data-toggle='rst-versions']").toggleClass("shift"); - }); - // Close menu when you click a link. - $(document).on('click', ".wy-menu-vertical .current ul li a", function () { - $("[data-toggle='wy-nav-shift']").removeClass("shift"); - $("[data-toggle='rst-versions']").toggleClass("shift"); - }); - $(document).on('click', "[data-toggle='rst-current-version']", function () { - $("[data-toggle='rst-versions']").toggleClass("shift-up"); - }); - // Make tables responsive - $("table.docutils:not(.field-list)").wrap("
            "); - // --- - // START DOC MODIFICATION BY RUFNEX - // v1.0 04.02.2015 - // Add ToogleButton to get FullWidth-View by Johannes Gamperl codeigniter.de - - $('#openToc').click(function () { - $('#nav').slideToggle(); - }); - $('#closeMe').toggle( - function () - { - setCookie('ciNav', true, 365); - $('#nav2').show(); - $('#topMenu').remove(); - $('body').css({background: 'none'}); - $('.wy-nav-content-wrap').css({background: 'none', 'margin-left': 0}); - $('.wy-breadcrumbs').append('
            ' + $('.wy-form').parent().html() + '
            '); - $('.wy-nav-side').toggle(); - }, - function () - { - setCookie('ciNav', false, 365); - $('#topMenu').remove(); - $('#nav').hide(); - $('#nav2').hide(); - $('body').css({background: '#edf0f2;'}); - $('.wy-nav-content-wrap').css({background: 'none repeat scroll 0 0 #fcfcfc;', 'margin-left': '300px'}); - $('.wy-nav-side').show(); +require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o
            "); + + // Add expand links to all parents of nested ul + $('.wy-menu-vertical ul').not('.simple').siblings('a').each(function () { + var link = $(this); + expand = $(''); + expand.on('click', function (ev) { + self.toggleCurrent(link); + ev.stopPropagation(); + return false; + }); + link.prepend(expand); + }); + }; + + nav.reset = function () { + // Get anchor from URL and open up nested nav + var anchor = encodeURI(window.location.hash); + if (anchor) { + try { + var link = $('.wy-menu-vertical') + .find('[href="' + anchor + '"]'); + // If we didn't find a link, it may be because we clicked on + // something that is not in the sidebar (eg: when using + // sphinxcontrib.httpdomain it generates headerlinks but those + // aren't picked up and placed in the toctree). So let's find + // the closest header in the document and try with that one. + if (link.length === 0) { + var doc_link = $('.document a[href="' + anchor + '"]'); + var closest_section = doc_link.closest('div.section'); + // Try again with the closest section entry. + link = $('.wy-menu-vertical') + .find('[href="#' + closest_section.attr("id") + '"]'); + + } + $('.wy-menu-vertical li.toctree-l1 li.current') + .removeClass('current'); + link.closest('li.toctree-l2').addClass('current'); + link.closest('li.toctree-l3').addClass('current'); + link.closest('li.toctree-l4').addClass('current'); + } + catch (err) { + console.log("Error expanding nav for anchor", err); + } } - ); - if (getCookie('ciNav') == 'true') - { - $('#closeMe').trigger('click'); - //$('#nav').slideToggle(); - } - // END MODIFICATION --- -}); - -// Rufnex Cookie functions -function setCookie(cname, cvalue, exdays) { - var d = new Date(); - d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); - var expires = "expires=" + d.toGMTString(); - document.cookie = cname + "=" + cvalue + "; " + expires; -} -function getCookie(cname) { - var name = cname + "="; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) == ' ') - c = c.substring(1); - if (c.indexOf(name) == 0) { - return c.substring(name.length, c.length); + }; + + nav.onScroll = function () { + this.winScroll = false; + var newWinPosition = this.win.scrollTop(), + winBottom = newWinPosition + this.winHeight, + navPosition = this.navBar.scrollTop(), + newNavPosition = navPosition + (newWinPosition - this.winPosition); + if (newWinPosition < 0 || winBottom > this.docHeight) { + return; } + this.navBar.scrollTop(newNavPosition); + this.winPosition = newWinPosition; + }; + + nav.onResize = function () { + this.winResize = false; + this.winHeight = this.win.height(); + this.docHeight = $(document).height(); + }; + + nav.hashChange = function () { + this.linkScroll = true; + this.win.one('hashchange', function () { + this.linkScroll = false; + }); + }; + + nav.toggleCurrent = function (elem) { + var parent_li = elem.closest('li'); + parent_li.siblings('li.current').removeClass('current'); + parent_li.siblings().find('li.current').removeClass('current'); + parent_li.find('> ul li.current').removeClass('current'); + parent_li.toggleClass('current'); } - return false; + + return nav; +}; + +module.exports.ThemeNav = ThemeNav(); + +if (typeof(window) != 'undefined') { + window.SphinxRtdTheme = { StickyNav: module.exports.ThemeNav }; } -// End - -window.SphinxRtdTheme = (function (jquery) { - var stickyNav = (function () { - var navBar, - win, - stickyNavCssClass = 'stickynav', - applyStickNav = function () { - if (navBar.height() <= win.height()) { - navBar.addClass(stickyNavCssClass); - } else { - navBar.removeClass(stickyNavCssClass); - } - }, - enable = function () { - applyStickNav(); - win.on('resize', applyStickNav); - }, - init = function () { - navBar = jquery('nav.wy-nav-side:first'); - win = jquery(window); - }; - jquery(init); - return { - enable: enable - }; - }()); - return { - StickyNav: stickyNav - }; -}($)); + +},{"jquery":"jquery"}]},{},["sphinx-rtd-theme"]); diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/theme.conf b/user_guide_src/source/_themes/sphinx_rtd_theme/theme.conf index 5814ac963e50..7a2aec399a2f 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/theme.conf +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/theme.conf @@ -4,5 +4,11 @@ stylesheet = css/citheme.css [options] typekit_id = hiw1hhg -analytics_id = +analytics_id = sticky_navigation = False +logo_only = +collapse_navigation = False +display_version = False +navigation_depth = 4 +prev_next_buttons_location = bottom +canonical_url = diff --git a/user_guide_src/source/_themes/sphinx_rtd_theme/versions.html b/user_guide_src/source/_themes/sphinx_rtd_theme/versions.html index 8b3eb79d2592..4d78287ade92 100644 --- a/user_guide_src/source/_themes/sphinx_rtd_theme/versions.html +++ b/user_guide_src/source/_themes/sphinx_rtd_theme/versions.html @@ -8,28 +8,28 @@
            -
            Versions
            +
            {{ _('Versions') }}
            {% for slug, url in versions %}
            {{ slug }}
            {% endfor %}
            -
            Downloads
            +
            {{ _('Downloads') }}
            {% for type, url in downloads %}
            {{ type }}
            {% endfor %}
            -
            On Read the Docs
            +
            {{ _('On Read the Docs') }}
            - Project Home + {{ _('Project Home') }}
            - Builds + {{ _('Builds') }}

            - Free document hosting provided by Read the Docs. + {% trans %}Free document hosting provided by Read the Docs.{% endtrans %}
            diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst index d4967f6d598e..e60d2cd9103f 100644 --- a/user_guide_src/source/changelog.rst +++ b/user_guide_src/source/changelog.rst @@ -2,69 +2,107 @@ Change Log ########## -Version 4.0-Pre-Alpha1 -====================== +Version 4.0.0-alpha.1 +================================= + +Release Date: September 28, 2018 **Rewrite of the CodeIgniter framework** -Release Date: Not Released +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 New core classes: - - CodeIgniter (bootstrap) - Common (shared functions) - ComposerScripts (integrate third party tools) - Controller (base controller) - Model (base model) + - Entity (entity encapsulation) New packages: - - - Autoloader \\ AutoLoader, FileLocator - - CLI \\ CLI - - Commands \\ MigrationsCommand - - Config \\ AutoloadConfig, BaseConfig, DotEnv, Routes - - Database - - - \\ BaseBuilder, BaseConnection, BaseResult, BaseUtils, Config, - ConnectionInterface, Database, Forge, Migration, MigrationRunner, Query, + - API + - \\ ResponseTrait + - Autoloader + - \\ AutoLoader, FileLocator + - CLI + - \\ BaseCommand, CLI, CommandRunner, Console + - Cache + - \\ CacheFactory, CacheInterface + - \\ Handlers ... Dummy, File, Memcached, Predis, Redis, Wincache + - Commands + - \\ Help, ListCommands + - \\ Database \\ CreateMigration, MigrateCurrent, MigrateLatest, MigrateRefresh, + MigrateRollback, MigrateStatus, MigrateVersion, Seed + - \\ Server \\ Serve + - \\ Sessions \\ CreateMigration + - \\ Utilities \\ Namespaces, Routes + - Config + - \\ AutoloadConfig, BaseConfig, BaseService, Config, DotEnv, ForeignCharacters, + Routes, Services, View + - Database + - \\ BaseBuilder, BaseConnection, BasePreparedQuery, BaseResult, BaseUtils, Config, + ConnectionInterface, Database, Forge, Migration, MigrationRunner, PreparedQueryInterface, Query, QueryInterface, ResultInterface, Seeder - - \\ MySQLi \\ Builder, Connection, Forge, Result - - \\ Postgre \\ Builder, Connection, Forge, Result, Utils - - - - Debug - - - \\ CustomExceptions, Exceptions, Iterator, Timer, Toolbar - - Kint \\ Kint **third party** - + - \\ MySQLi \\ Builder, Connection, Forge, PreparedQuery, Result + - \\ Postgre \\ Builder, Connection, Forge, PreparedQuery, Result, Utils + - \\ SQLite3 \\ Builder, Connection, Forge, PreparedQuery, Result, Utils + - Debug + - \\ Exceptions, Iterator, Timer, Toolbar + - \\ Toolbar \\ Collectors... + - Email + - \\ Email + - Events + - \\ Events + - Files + - \\ File + - Filters + - \\ FilterInterface, Filters + - Format + - \\ FormatterInterface, JSONFormatter, XMLFormatter - HTTP - - - \\ CLIRequest, CURLRequest, ContentSecurityPolicy, Header, + - \\ CLIRequest, CURLRequest, ContentSecurityPolicy, Header, IncomingRequest, Message, Negotiate, Request, RequestInterface, - Response, ResponseInterface, URI + Response, ResponseInterface, URI, UserAgent - \\ Files \\ FileCollection, UploadedFile, UploadedFileInterface - - - Helpers ... uri - - Hooks \\ Hooks - - Log - + - Helpers + - ... array, cookie, date, filesystem, form, html, inflector, number, + security, text, url + - Honeypot + - \\ Honeypot + - I18n + - \\ Time, TimeDifference + - Images + - \\ Image, ImageHandlerInterface + - \\ Handlers ... Base, GD, ImageMagick + - Language + - \\ Language + - Log - Logger, LoggerAwareTrait - - \\ Handlers \\ BaseHandler, ChromeLoggerHandler, FileHandler, HandlerInterface - - Psr \\ Log **third party** - - - Router \\ RouteCollection, RouteCollectionInterface, Router, RouterInterface - - Security \\ Security + - \\ Handlers ... Base, ChromeLogger, File, HandlerInterface + - Pager + - \\ Pager, PagerInterface, PagerRenderer + - Router + - \\ RouteCollection, RouteCollectionInterface, Router, RouterInterface + - Security + - \\ Security - Session - - \\ Session, SessionInterface - - \\ Handlers \\ BaseHandler, FileHandler, MemcachedHandler, RedisHandler - - - Test \\ CIDatabaseTestCase, CIUnitTestCase, ReflectionHelper + - \\ Handlers ... Base, File, Memcached, Redis + - Test + - \\ CIDatabaseTestCase, CIUnitTestCase, FeatureResponse, FeatureTestCase, ReflectionHelper + - \\ Filters \\ CITestStreamFilter + - ThirdParty (bundled) + - \\ Kint (for \\Debug) + - \\ PSR \\ Log (for \\Log) + - \\ ZendEscaper \\ Escaper (for \\View) + - Throttle + - \\ Throttler, ThrottlerInterface + - Typography + - \\ Typography + - Validation + - \\ CreditCardRules, FileRules, FormatRules, Rules, Validation, ValidationInterface - View - - - Zend \\ Escaper, Exception \\ ... **third party** - - RenderableInterface, View - - -User Guide adapted or rewritten. - + - \\ Cell, Filters, Parser, Plugins, RendererInterface, View diff --git a/user_guide_src/source/general/cli.rst b/user_guide_src/source/cli/cli.rst similarity index 86% rename from user_guide_src/source/general/cli.rst rename to user_guide_src/source/cli/cli.rst index 22b03ac44aac..a11b380ba9c7 100644 --- a/user_guide_src/source/general/cli.rst +++ b/user_guide_src/source/cli/cli.rst @@ -1,12 +1,14 @@ -################### -Running via the CLI -################### +############################ +Running via the Command Line +############################ -As well as calling an applications :doc:`Controllers <./controllers>` +As well as calling an applications :doc:`Controllers ` via the URL in a browser they can also be loaded via the command-line interface (CLI). -.. contents:: Page Contents +.. contents:: + :local: + :depth: 2 What is the CLI? ================ @@ -21,7 +23,7 @@ Why run via the command-line? There are many reasons for running CodeIgniter from the command-line, but they are not always obvious. -- Run your cron-jobs without needing to use *wget* or *curl* +- Run your cron-jobs without needing to use *wget* or *curl*. - Make your cron-jobs inaccessible from being loaded in the URL by checking the return value of :php:func:`is_cli()`. - Make interactive "tasks" that can do things like set permissions, @@ -87,11 +89,11 @@ CLI-Only Routing In your **Routes.php** file you can create routes that are only accessible from the CLI as easily as you would create any other route. Instead of using the ``get()``, ``post()``, or similar method, you would use the ``cli()`` method. Everything else -works exactly like a normal route definition.:: +works exactly like a normal route definition:: $routes->cli('tools/message/(:segment)', 'Tools::message/$1'); -For more information, see the :doc:`Routes ` page. +For more information, see the :doc:`Routes ` page. The CLI Library --------------- @@ -100,4 +102,4 @@ The CLI library makes working with the CLI interface simple. It provides easy ways to output text in multiple colors to the terminal window. It also allows you to prompt a user for information, making it easy to build flexible, smart tools. -See the :doc:`CLI Library ` page for detailed information. +See the :doc:`CLI Library ` page for detailed information. diff --git a/user_guide_src/source/cli/cli_commands.rst b/user_guide_src/source/cli/cli_commands.rst new file mode 100644 index 000000000000..07e48a0c00d1 --- /dev/null +++ b/user_guide_src/source/cli/cli_commands.rst @@ -0,0 +1,192 @@ +################### +Custom CLI Commands +################### + +While the ability to use cli commands like any other route is convenient, you might find times where you +need a little something different. That's where CLI Commands come in. They are simple classes that do not +need to have routes defined for, making them perfect for building tools that developers can use to make +their jobs simpler, whether by handling migrations or database seeding, checking cronjob status, or even +building out custom code generators for your company. + +.. contents:: + :local: + :depth: 2 + +**************** +Running Commands +**************** + +Commands are run from the command line, in the root directory. The same one that holds the **/application** +and **/system** directories. A custom script, **spark** has been provided that is used to run any of the +cli commands:: + + > php spark + +When called without specifying a command, a simple help page is displayed that also provides a list of +available commands. You should pass the name of the command as the first argument to run that command:: + + > php spark migrate + +Some commands take additional arguments, which should be provided directly after the command, separated by spaces:: + + > php spark db:seed DevUserSeeder + +For all of the commands CodeIgniter provides, if you do not provide the required arguments, you will be prompted +for the information it needs to run correctly:: + + > php spark migrate:version + > Version? + +****************** +Using Help Command +****************** + +You can get help about any CLI command using the help command as follows:: + + > php spark help db:seed + +********************* +Creating New Commands +********************* + +You can very easily create new commands to use in your own development. Each class must be in its own file, +and must extend ``CodeIgniter\CLI\BaseCommand``, and implement the ``run()`` method. + +The following properties should be used in order to get listed in CLI commands and to add help functionality to your command: + +* ($group): a string to describe the group the command is lumped under when listing commands. For example (Database) +* ($name): a string to describe the command's name. For example (migrate:create) +* ($description): a string to describe the command. For example (Creates a new migration file.) +* ($usage): a string to describe the command usage. For example (migrate:create [migration_name] [Options]) +* ($arguments): an array of strings to describe each command argument. For example ('migration_name' => 'The migration file name') +* ($options): an array of strings to describe each command option. For example ('-n' => 'Set migration namespace') + +**Help description will be automatically generated according to the above parameters.** + +File Location +============= + +Commands must be stored within a directory named **Commands**. However, that directory can be located anywhere +that the :doc:`Autoloader ` can locate it. This could be in **/application/Commands**, or +a directory that you keep commands in to use in all of your project development, like **Acme/Commands**. + +.. note:: When the commands are executed, the full CodeIgniter cli environment has been loaded, making it + possible to get environment information, path information, and to use any of the tools you would use when making a Controller. + +An Example Command +================== + +Let's step through an example command whose only function is to report basic information about the application +itself, for demonstration purposes. Start by creating a new file at **/application/Commands/AppInfo.php**. It +should contain the following code:: + + php spark foo bar baz + +Then **foo** is the command name, and the ``$params`` array would be:: + + $params = ['bar', 'baz']; + +This can also be accessed through the :doc:`CLI ` library, but this already has your command removed +from the string. These parameters can be used to customize how your scripts behave. + +Our demo command might have a ``run`` method something like:: + + public function run(array $params) + { + CLI::write('PHP Version: '. CLI::color(phpversion(), 'yellow')); + CLI::write('CI Version: '. CLI::color(CodeIgniter::CI_VERSION, 'yellow')); + CLI::write('APPPATH: '. CLI::color(APPPATH, 'yellow')); + CLI::write('BASEPATH: '. CLI::color(BASEPATH, 'yellow')); + CLI::write('ROOTPATH: '. CLI::color(ROOTPATH, 'yellow')); + CLI::write('Included files: '. CLI::color(count(get_included_files()), 'yellow')); + } + +*********** +BaseCommand +*********** + +The ``BaseCommand`` class that all commands must extend have a couple of helpful utility methods that you should +be familiar with when creating your own commands. It also has a :doc:`Logger ` available at +**$this->logger**. + +.. php:class:: CodeIgniter\CLI\BaseCommand + + .. php:method:: call(string $command[, array $params=[] ]) + + :param string $command: The name of another command to call. + :param array $params: Additional cli arguments to make available to that command. + + This method allows you to run other commands during the execution of your current command:: + + $this->call('command_one'); + $this->call('command_two', $params); + + .. php:method:: showError(\Exception $e) + + :param Exception $e: The exception to use for error reporting. + + A convenience method to maintain a consistent and clear error output to the cli:: + + try + { + . . . + } + catch (\Exception $e) + { + $this->showError($e); + } + + .. php:method:: showHelp() + + A method to show command help: (usage,arguments,description,options) + + .. php:method:: getPad($array, $pad) + + :param Exception $array: The $key => $value array. + :param Exception $pad: The pad spaces. + + A method to calculate padding for $key => $value array output. The padding can be used to output a will formatted table in CLI:: + + $pad = $this->getPad($this->options, 6); + foreach ($this->options as $option => $description) + { + CLI::write($tab . CLI::color(str_pad($option, $pad), 'green') . $description, 'yellow'); + } + + // Output will be + -n Set migration namespace + -r override file diff --git a/user_guide_src/source/libraries/cli.rst b/user_guide_src/source/cli/cli_library.rst similarity index 76% rename from user_guide_src/source/libraries/cli.rst rename to user_guide_src/source/cli/cli_library.rst index c9aba88d0dea..6ff932f5dae5 100644 --- a/user_guide_src/source/libraries/cli.rst +++ b/user_guide_src/source/cli/cli_library.rst @@ -11,6 +11,8 @@ CodeIgniter's CLI library makes creating interactive command-line scripts simple * Wrapping long text lines to fit the window. .. contents:: + :local: + :depth: 2 Initializing the Class ====================== @@ -35,13 +37,7 @@ Sometimes you need to ask the user for more information. They might not have pro arguments, or the script may have encountered an existing file and needs confirmation before overwriting. This is handled with the ``prompt()`` method. -The most basic use case is to simply wait for the user to press a key:: - - // Wait for the user to press any key... - CLI::prompt(); - -You can get a little more specific and provide a question for them to answer by passing the question in -as the first parameter:: +You can provide a question by passing it in as the first parameter:: $color = CLI::prompt('What is your favorite color?'); @@ -50,9 +46,13 @@ second parameter:: $color = CLI::prompt('What is your favorite color?', 'blue'); -Finally, you can restrict the acceptable answers by passing in an array of allowed answers as the second parameter:: +You can restrict the acceptable answers by passing in an array of allowed answers as the second parameter:: + + $overwrite = CLI::prompt('File exists. Overwrite?', ['y','n']); - $overwrite = CLI::prompt('File exists. Overwrite?', array('y','n')); +Finally, you can pass validation rules to the answer input as the third parameter:: + + $email = CLI::prompt('What is your email?', null, 'required|valid_email'); Providing Feedback ================== @@ -119,7 +119,7 @@ exactly as you would the ``write()`` method:: This command will take a string, start printing it on the current line, and wrap it to a set length on new lines. This might be useful when displaying a list of options with descriptions that you want to wrap in the current -window and not go off screen.:: +window and not go off screen:: CLI::color("task1\t", 'yellow'); CLI::wrap("Some long description goes here that might be longer than the current window."); @@ -136,14 +136,13 @@ will break the string at the nearest word barrier so that words are not broken. You may find that you want a column on the left of titles, files, or tasks, while you want a column of text on the right with their descriptions. By default, this will wrap back to the left edge of the window, which doesn't allow things to line up in columns. In cases like this, you can pass in a number of spaces to pad -every line after the first line, so that you will have a crisp column edge on the left. -:: +every line after the first line, so that you will have a crisp column edge on the left:: // Determine the maximum length of all titles // to determine the width of the left column $maxlen = max(array_map('strlen', $titles)); - for ($i=0; $ <= count($titles); $i++) + for ($i=0; $i <= count($titles); $i++) { CLI::write( // Display the title on the left of the row @@ -151,16 +150,19 @@ every line after the first line, so that you will have a crisp column edge on th // Wrap the descriptions in a right-hand column // with its left side 3 characters wider than // the longest item on the left. - CLI::wrap($descriptions[$i], 40, $maxlen+3 + CLI::wrap($descriptions[$i], 40, $maxlen+3) ); } - // Would create something like this: - task1a Lorem Ipsum is simply dummy - text of the printing and typesetting - industry. - task1abc Lorem Ipsum has been the industry's - standard dummy text ever since the +Would create something like this: + +.. code-block:: none + + task1a Lorem Ipsum is simply dummy + text of the printing and typesetting + industry. + task1abc Lorem Ipsum has been the industry's + standard dummy text ever since the **newLine()** @@ -172,7 +174,7 @@ The ``newLine()`` method displays a blank line to the user. It does not take any You can clear the current terminal window with the ``clearScreen()`` method. In most versions of Windows, this will simply insert 40 blank lines since Windows doesn't support this feature. Windows 10 bash integration should change -this.:: +this:: CLI::clearScreen(); @@ -181,7 +183,7 @@ this.:: If you have a long-running task that you would like to keep the user updated with the progress, you can use the ``showProgress()`` method which displays something like the following: -.. code-block:: bash +.. code-block:: none [####......] 40% Complete @@ -193,7 +195,7 @@ pass ``false`` as the first parameter and the progress bar will be removed. :: $totalSteps = count($tasks); - $currStep = 1; + $currStep = 1; foreach ($tasks as $task) { @@ -204,4 +206,39 @@ pass ``false`` as the first parameter and the progress bar will be removed. // Done, so erase it... CLI::showProgress(false); +**table()** + +:: + + $thead = ['ID', 'Title', 'Updated At', 'Active']; + $tbody = [ + [7, 'A great item title', '2017-11-15 10:35:02', 1], + [8, 'Another great item title', '2017-11-16 13:46:54', 0] + ]; + + CLI::table($tbody, $thead); + +.. code-block:: none + + +----+--------------------------+---------------------+--------+ + | ID | Title | Updated At | Active | + +----+--------------------------+---------------------+--------+ + | 7 | A great item title | 2017-11-16 10:35:02 | 1 | + | 8 | Another great item title | 2017-11-16 13:46:54 | 0 | + +----+--------------------------+---------------------+--------+ + +**wait()** + +Waits a certain number of seconds, optionally showing a wait message and +waiting for a key press. + +:: + + // wait for specified interval, with countdown displayed + CLI::wait($seconds, true); + + // show continuation message and wait for input + CLI::wait(0, false); + // wait for specified interval + CLI::wait($seconds, false); diff --git a/user_guide_src/source/cli/index.rst b/user_guide_src/source/cli/index.rst new file mode 100644 index 000000000000..c702405e9586 --- /dev/null +++ b/user_guide_src/source/cli/index.rst @@ -0,0 +1,12 @@ +################## +Command Line Usage +################## + +CodeIgniter 4 can also be used with command line programs. + +.. toctree:: + :titlesonly: + + cli + cli_commands + cli_library diff --git a/user_guide_src/source/concepts/autoloader.rst b/user_guide_src/source/concepts/autoloader.rst index 86a28fe1c195..300200bc30f0 100644 --- a/user_guide_src/source/concepts/autoloader.rst +++ b/user_guide_src/source/concepts/autoloader.rst @@ -10,10 +10,10 @@ hard-coding that location into your files in a series of ``requires()`` is a mas headache and very error-prone. That's where autoloaders come in. CodeIgniter provides a very flexible autoloader that can be used with very little configuration. -It can locate individual non-namespaced classes, namespaced classes that adhere to +It can locate individual non-namespaced classes, namespaced classes that adhere to `PSR4 `_ autoloading directory structures, and will even attempt to locate classes in common directories (like Controllers, -Models, etc). +Models, etc). For performance improvement, the core CodeIgniter components have been added to the classmap. @@ -53,7 +53,7 @@ have a trailing slash. By default, the application folder is namespace to the ``App`` namespace. While you are not forced to namespace the controllers, libraries, or models in the application directory, if you do, they will be found under the ``App`` namespace. You may change this namespace by editing the **/application/Config/Constants.php** file and setting the -new namespace value under the ``APP_NAMESPACE`` setting.:: +new namespace value under the ``APP_NAMESPACE`` setting:: define('APP_NAMESPACE', 'App'); diff --git a/user_guide_src/source/concepts/http.rst b/user_guide_src/source/concepts/http.rst index a1261124d3e9..a8d6370042d6 100644 --- a/user_guide_src/source/concepts/http.rst +++ b/user_guide_src/source/concepts/http.rst @@ -12,18 +12,18 @@ how to work with the requests and responses within CodeIgniter. What is HTTP? ============= -HTTP is simply a text-based language that allows two machines to talk to each other. When a browser +HTTP is simply a text-based convention that allows two machines to talk to each other. When a browser requests a page, it asks the server if it can get the page. The server then prepares the page and sends -response back to the browser that asked for it. That's pretty much it. Obviously, there are some complexities +a response back to the browser that asked for it. That's pretty much it. Obviously, there are some complexities that you can use, but the basics are really pretty simple. -HTTP is the term used to describe that language. It stands for HyperText Transfer Protocol. Your goal when +HTTP is the term used to describe that exchange convention. It stands for HyperText Transfer Protocol. Your goal when you develop web applications is to always understand what the browser is requesting, and be able to respond appropriately. The Request ----------- -Whenever a client makes a request (a web browser, smartphone app, etc), it is sending a small text message +Whenever a client (a web browser, smartphone app, etc) makes a request, it sends a small text message to the server and waits for a response. The request would look something like this:: @@ -57,7 +57,6 @@ a simple text message that looks something like this:: . . . - The response tells the client what version of the HTTP specification that it's using and, probably most importantly, the status code (200). The status code is one of a number of codes that have been standardized to have a very specific meaning to the client. This can tell them that it was successful (200), or that the page @@ -68,38 +67,37 @@ Working with Requests and Responses ----------------------------------- While PHP provides ways to interact with the request and response headers, CodeIgniter, like most frameworks, -abstract them so that you have a consistent, simple interface to them. The :doc:`IncomingRequest class ` +abstracts them so that you have a consistent, simple interface to them. The :doc:`IncomingRequest class ` is an object-oriented representation of the HTTP request. It provides everything you need:: - use CodeIgniter\HTTP\IncomingRequest; - - $request = new IncomingRequest(new \Config\App(), new \CodeIgniter\HTTP\URI()); + use CodeIgniter\HTTP\IncomingRequest; - // the URI being requested (i.e. /about) - $request->uri->getPath(); + $request = new IncomingRequest(new \Config\App(), new \CodeIgniter\HTTP\URI()); - // Retrieve $_GET and $_POST variables - $request->getVar('foo'); - $request->getGet('foo'); - $request->getPost('foo'); + // the URI being requested (i.e. /about) + $request->uri->getPath(); - // Retrieve JSON from AJAX calls - $request->getJSON(); + // Retrieve $_GET and $_POST variables + $request->getVar('foo'); + $request->getGet('foo'); + $request->getPost('foo'); - // Retrieve server variables - $request->getServer('Host'); + // Retrieve JSON from AJAX calls + $request->getJSON(); - // Retrieve an HTTP Request header, with case-insensitive names - $request->getHeader('host'); - $request->getHeader('Content-Type'); + // Retrieve server variables + $request->getServer('Host'); - $request->getMethod(); // GET, POST, PUT, etc + // Retrieve an HTTP Request header, with case-insensitive names + $request->getHeader('host'); + $request->getHeader('Content-Type'); + $request->getMethod(); // GET, POST, PUT, etc The request class does a lot of work in the background for you, that you never need to worry about. The ``isAJAX()`` and ``isSecure()`` methods check several different methods to determine the correct answer. -CodeIgniter also provides a :doc:`Response class ` that is an object-oriented representation +CodeIgniter also provides a :doc:`Response class ` that is an object-oriented representation of the HTTP response. This gives you an easy and powerful way to construct your response to the client:: use CodeIgniter\HTTP\Response; @@ -114,4 +112,4 @@ of the HTTP response. This gives you an easy and powerful way to construct your // Sends the output to the browser $response->send(); -In addition, the Response class allows you to work the HTTP cache layer for the best performance. \ No newline at end of file +In addition, the Response class allows you to work the HTTP cache layer for the best performance. diff --git a/user_guide_src/source/concepts/mvc.rst b/user_guide_src/source/concepts/mvc.rst index 03ced8fd09af..9cc6b16dcda9 100644 --- a/user_guide_src/source/concepts/mvc.rst +++ b/user_guide_src/source/concepts/mvc.rst @@ -13,7 +13,7 @@ each piece as you need. **Views** are simple files, with little to no logic, that display the information to the user. -**Controllers** act as glue code, marshalling data back and forth between the view (or the user that's seeing it) and +**Controllers** act as glue code, marshaling data back and forth between the view (or the user that's seeing it) and the data storage. At their most basic, controllers and models are simply classes that have a specific job. They are not the only class @@ -23,7 +23,6 @@ wherever you desire, as long as they are properly namespaced. We will discuss th Let's take a closer look at each of these three main components. - ************** The Components ************** @@ -47,8 +46,7 @@ You might store the view file for this method in **/application/Views/User/Profi That type of organization works great as a base habit to get into. At times you might need to organize it differently. That's not a problem. As long as CodeIgniter can find the file, it can display it. -:doc:`Find out more about views ` - +:doc:`Find out more about views ` Models ====== @@ -65,8 +63,7 @@ miss updating an area. Models are typically stored in **/application/Models**, though they can use a namespace to be grouped however you need. -:doc:`Find out more about models ` - +:doc:`Find out more about models ` Controllers =========== @@ -76,11 +73,11 @@ then determine what to do with it. This often involves passing the data to a mod the model that is then passed on to the view to be displayed. This also includes loading up other utility classes, if needed, to handle specialized tasks that is outside of the purview of the model. -The other responsibility of the controller is to handles everything that pertains to HTTP requests - redirects, +The other responsibility of the controller is to handle everything that pertains to HTTP requests - redirects, authentication, web safety, encoding, etc. In short, the controller is where you make sure that people are allowed to be there, and they get the data they need in a format they can use. Controllers are typically stored in **/application/Controllers**, though they can use a namespace to be grouped however you need. -:doc:`Find out more about controllers ` \ No newline at end of file +:doc:`Find out more about controllers ` diff --git a/user_guide_src/source/concepts/security.rst b/user_guide_src/source/concepts/security.rst index 78639a61a368..7942eea5aecf 100644 --- a/user_guide_src/source/concepts/security.rst +++ b/user_guide_src/source/concepts/security.rst @@ -7,9 +7,9 @@ CodeIgniter incorporates a number of features and techniques to either enforce good security practices, or to enable you to do so easily. We respect the `Open Web Application Security Project (OWASP) `_ -and follow their recommendations as much as possible. +and follow their recommendations as much as possible. -The following comes from +The following comes from `OWASP Top Ten Cheat Sheet `_, identifying the top vulnerabilities for web applications. For each, we provide a brief description, the OWASP recommendations, and then @@ -34,7 +34,7 @@ OWASP recommendations CodeIgniter provisions ---------------------- -- `HTTP library <../libraries/message.html>`_ provides for input field filtering & content metadata +- `HTTP library `_ provides for input field filtering & content metadata - Form validation library ********************************************* @@ -48,7 +48,7 @@ OWASP recommendations --------------------- - Presentation: validate authentication & role; send CSRF token with forms -- Design: only use inbuilt session management +- Design: only use built-in session management - Controller: validate user, role, CSRF token - Model: validate role - Tip: consider the use of a request governor @@ -57,37 +57,37 @@ CodeIgniter provisions ---------------------- - `Session <../libraries/sessions.html>`_ library -- `HTTP library <../libraries/message.html>`_ provides for CSRF validation +- `HTTP library `_ provides for CSRF validation - Easy to add third party authentication ***************************** A3 Cross Site Scripting (XSS) ***************************** -Insufficient input validation where one user can add content to a web site +Insufficient input validation where one user can add content to a web site that can be malicious when viewed by other users to the web site. OWASP recommendations --------------------- -- Presentation: validate authentication & role; set input constraints +- Presentation: output encode all user data as per output context; set input constraints - Controller: positive input validation - Tips: only process trustworthy data; do not store data HTML encoded in DB CodeIgniter provisions ---------------------- +- esc function - Form validation library -- Easy to add third party authentication *********************************** A4 Insecure Direct Object Reference *********************************** -Insecure Direct Object References occur when an application provides direct -access to objects based on user-supplied input. As a result of this vulnerability -attackers can bypass authorization and access resources in the system directly, -for example database records or files. +Insecure Direct Object References occur when an application provides direct +access to objects based on user-supplied input. As a result of this vulnerability +attackers can bypass authorization and access resources in the system directly, +for example database records or files. OWASP recommendations --------------------- @@ -106,13 +106,13 @@ CodeIgniter provisions A5 Security Misconfiguration **************************** -Improper configuration of an application architecture can lead to mistakes -that might compromise the security of the whole architecture. +Improper configuration of an application architecture can lead to mistakes +that might compromise the security of the whole architecture. OWASP recommendations --------------------- -- Presentation: harden web and application servers; use HTTP strict transport secutiry +- Presentation: harden web and application servers; use HTTP strict transport security - Controller: harden web and application servers; protect your XML stack - Model: harden database servers @@ -125,10 +125,10 @@ CodeIgniter provisions A6 Sensitive Data Exposure ************************** -Sensitive data must be protected when it is transmitted through the network. -Such data can include user credentials and credit cards. As a rule of thumb, -if data must be protected when it is stored, it must be protected also during -transmission. +Sensitive data must be protected when it is transmitted through the network. +Such data can include user credentials and credit cards. As a rule of thumb, +if data must be protected when it is stored, it must be protected also during +transmission. OWASP recommendations --------------------- @@ -146,10 +146,10 @@ CodeIgniter provisions A7 Missing Function Level Access Control **************************************** -Sensitive data must be protected when it is transmitted through the network. -Such data can include user credentials and credit cards. As a rule of thumb, -if data must be protected when it is stored, it must be protected also during -transmission. +Sensitive data must be protected when it is transmitted through the network. +Such data can include user credentials and credit cards. As a rule of thumb, +if data must be protected when it is stored, it must be protected also during +transmission. OWASP recommendations --------------------- @@ -162,13 +162,13 @@ CodeIgniter provisions ---------------------- - Public folder, with application and system outside -- `HTTP library <../libraries/message.html>`_ provides for CSRF validation +- `HTTP library `_ provides for CSRF validation ************************************ A8 Cross Site Request Forgery (CSRF) ************************************ -CSRF is an attack that forces an end user to execute unwanted actions on a web +CSRF is an attack that forces an end user to execute unwanted actions on a web application in which he/she is currently authenticated. OWASP recommendations @@ -181,14 +181,14 @@ OWASP recommendations CodeIgniter provisions ---------------------- -- `HTTP library <../libraries/message.html>`_ provides for CSRF validation +- `HTTP library `_ provides for CSRF validation ********************************************** A9 Using Components with Known Vulnerabilities ********************************************** -Many applications have known vulnerabilities and known attack strategies that -can be exploited in order to gain remote control or to exploit data. +Many applications have known vulnerabilities and known attack strategies that +can be exploited in order to gain remote control or to exploit data. OWASP recommendations --------------------- @@ -217,6 +217,6 @@ OWASP recommendations CodeIgniter provisions ---------------------- -- `HTTP library <../libraries/message.html>`_ provides for ... +- `HTTP library `_ provides for ... - `Session <../libraries/sessions.html>`_ library provides flashdata diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index 729fa583da02..b15efa8364a5 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -2,6 +2,10 @@ Services ######## +.. contents:: + :local: + :depth: 2 + Introduction ============ @@ -12,7 +16,7 @@ configuration file. This file acts as a type of factory to create new instances A quick example will probably make things clearer, so imagine that you need to pull in an instance of the Timer class. The simplest method would simply be to create a new instance of that class:: - $timer = new CodeIgniter\Debug\Timer(); + $timer = new \CodeIgniter\Debug\Timer(); And this works great. Until you decide that you want to use a different timer class in its place. Maybe this one has some advanced reporting the default timer does not provide. In order to do this, @@ -23,19 +27,18 @@ come in handy. Instead of creating the instance ourself, we let a central class create an instance of the class for us. This class is kept very simple. It only contains a method for each class that we want -to use as a service. The method typically returns a new instance of that class, passing any dependencies +to use as a service. The method typically returns a shared instance of that class, passing any dependencies it might have into it. Then, we would replace our timer creation code with code that calls this new class:: - $timer = Config\Services::timer(); + $timer = \Config\Services::timer(); When you need to change the implementation used, you can modify the services configuration file, and the change happens automatically throughout your application without you having to do anything. Now you just need to take advantage of any new functionality and you're good to go. Very simple and error-resistant. -.. note:: It is recommended to only create services within controllers. Other files, like models - and libraries should have the dependencies either passed into the constructor or through a - setter method. +.. note:: It is recommended to only create services within controllers. Other files, like models and libraries should have the dependencies either passed into the constructor or through a setter method. + Convenience Functions --------------------- @@ -44,7 +47,8 @@ Two functions have been provided for getting a service. These functions are alwa The first is ``service()`` which returns a new instance of the requested service. The only required parameter is the service name. This is the same as the method name within the Services -file:: +file always returns a SHARED instance of the class, so calling the function multiple times should +always return the same instance:: $logger = service('logger'); @@ -52,13 +56,10 @@ If the creation method requires additional parameters, they can be passed after $renderer = service('renderer', APPPATH.'views/'); -The second function, ``shared_service()`` works just like ``service()`` but returns a shared -instance of the desired service:: - - $logger = shared_service('logger'); - - +The second function, ``single_service()`` works just like ``service()`` but returns a new instance of +the class:: + $logger = single_service('logger'); Defining Services ================= @@ -75,7 +76,7 @@ create a new class that implements the ``RouterCollectionInterface``:: class MyRouter implements \CodeIgniter\Router\RouteCollectionInterface { - // Implement required methods here. + // Implement required methods here. } Finally, modify **/application/Config/Services.php** to create a new instance of ``MyRouter`` @@ -83,12 +84,9 @@ instead of ``CodeIgniter\Router\RouterCollection``:: public static function routes() { - return new \App\Router\MyRouter(); + return new \App\Router\MyRouter(); } - //-------------------------------------------------------------------- - - Allowing Parameters ------------------- @@ -102,7 +100,7 @@ as a constructor parameter. The service method looks like this:: public static function renderer($viewPath=APPPATH.'views/') { - return new \CodeIgniter\View\View($viewPath); + return new \CodeIgniter\View\View($viewPath); } This sets the default path in the constructor method, but allows for easily changing @@ -117,7 +115,7 @@ There are occasions where you need to require that only a single instance of a s is created. This is easily handled with the ``getSharedInstance()`` method that is called from within the factory method. This handles checking if an instance has been created and saved within the class, and, if not, creates a new one. All of the factory methods provide a -``$getShared = false`` value as the last parameter. You should stick to the method also.:: +``$getShared = true`` value as the last parameter. You should stick to the method also:: class Services { @@ -132,4 +130,38 @@ within the class, and, if not, creates a new one. All of the factory methods pro } } +Service Discovery +----------------- + +CodeIgniter can automatically discover any Config\Services.php files you may have created within any defined namespaces. +This allows simple use of any module Services files. In order for custom Services files to be discovered, they must +meet these requirements: + +- It's namespace must be defined ``Config\Autoload.php`` +- Inside the namespace, the file must be found at ``Config\Services.php`` +- It must extend ``CodeIgniter\Config\BaseService`` + +A small example should clarify this. + +Imagine that you've created a new directory, ``Blog`` in your root directory. This will hold a blog module with controllers, +models, etc, and you'd like to make some of the classes available as a service. The first step is to create a new file: +``Blog\Config\Services.php``. The skeleton of the file should be:: + + `_ in August, 2015. - -Phase 1: Essentials (done) -========================== - -The first phase focused on nailing the essentials in the framework. -This ensures that all of the basic parts needed to make it work are in place -and working well. - -Phase 1 packages include: - -- Autoloader -- Logging -- Exception Handling -- HTTP Request/Response -- Routing -- Controllers -- Models -- Database (MySQL & Postgres) -- Config -- Security -- Views -- Sessions -- Basic debugging and profiling - -This phase is complete, and the repository is being opened up for the -community to help. - -Completed: June 2016 - -Phase 2: Core Components (in progress) -====================================== - -The second phase focuses on providing and refining the existing classes and -features that CodeIgniter users know and love. - -Phase 2 packages include: - -- The helpers -- Language/Localization features -- Caching -- Email -- Encryption -- Form Validation -- Image Library -- Pagination -- Uploader - -During this phase, we will be looking for PRs for the planned and approved -components, and for bug reports filed as github issues. - -Those packages that we are ready to implement will appear as issues -in the github repository, with whatever direction we can provide. -As they get implemented and tested, and merged into the framework, -they will show up in the changelog. - -Target completion: December 2016 - -Phase 3: Expansion (not started) -================================ - -The third phase includes fleshing out and working on the optional packages. -At this point, the framework can be released and need not wait for these l -ibraries to be brought up to date. - -Each optional package will have its own repository, and will be developed -and managed independently of the main framework. - -Planned optional packages: - -- FTP -- XML-RPC -- Zip -- Typography -- Template Parser - -These optional packages will constitute the "official" addins for CodeIgniter4. -Developers will undoubtedly create their own as well. We have not settled -on the best way to promote/integrate these. - -Target completion: April 2017 - -.. note:: Any target completion dates shown are speculative, and depend - very much on the quantity and quality of community contributions. - - Results may not be as depicted. Your mileage may vary. Contents will settle - during shipping. \ No newline at end of file diff --git a/user_guide_src/source/database/call_function.rst b/user_guide_src/source/database/call_function.rst index e5efb574a697..dce294d80a60 100644 --- a/user_guide_src/source/database/call_function.rst +++ b/user_guide_src/source/database/call_function.rst @@ -35,5 +35,5 @@ database result ID. The connection ID can be accessed using:: The result ID can be accessed from within your result object, like this:: $query = $db->query("SOME QUERY"); - + $query->resultID; \ No newline at end of file diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index deca7d23f8be..2ae9a3c6cdeb 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -2,6 +2,10 @@ Database Configuration ###################### +.. contents:: + :local: + :depth: 2 + CodeIgniter has a config file that lets you store your database connection values (username, password, database name, etc.). The config file is located at application/Config/Database.php. You can also set @@ -11,7 +15,7 @@ The config settings are stored in a class property that is an array with this prototype:: public $default = [ - 'DSN' => '', + 'DSN' => '', 'hostname' => 'localhost', 'username' => 'root', 'password' => '', @@ -29,7 +33,6 @@ prototype:: 'compress' => FALSE, 'strictOn' => FALSE, 'failover' => array(), - 'saveQueries' => true ]; The name of the class property is the connection name, and can be used @@ -104,7 +107,7 @@ connection group for each, then switch between groups as needed. For example, to set up a "test" environment you would do this:: public $test = [ - 'DSN' => '', + 'DSN' => '', 'hostname' => 'localhost', 'username' => 'root', 'password' => '', @@ -174,7 +177,7 @@ Explanation of Values: **username** The username used to connect to the database. **password** The password used to connect to the database. **database** The name of the database you want to connect to. -**DBDiver** The database type. eg: MySQLi, Postgre, etc. The case must match the driver name +**DBDriver** The database type. eg: MySQLi, Postgre, etc. The case must match the driver name **DBPrefix** An optional table prefix which will added to the table name when running :doc:`Query Builder ` queries. This permits multiple CodeIgniter installations to share one database. @@ -195,11 +198,11 @@ Explanation of Values: - 'sqlsrv' and 'pdo/sqlsrv' drivers accept TRUE/FALSE - 'MySQLi' and 'pdo/mysql' drivers accept an array with the following options: - + - 'ssl_key' - Path to the private key file - 'ssl_cert' - Path to the public key certificate file - 'ssl_ca' - Path to the certificate authority file - - 'ssl_capath' - Path to a directory containing trusted CA certificats in PEM format + - 'ssl_capath' - Path to a directory containing trusted CA certificates in PEM format - 'ssl_cipher' - List of *allowed* ciphers to be used for the encryption, separated by colons (':') - 'ssl_verify' - TRUE/FALSE; Whether to verify the server certificate or not ('MySQLi' only) diff --git a/user_guide_src/source/database/connecting.rst b/user_guide_src/source/database/connecting.rst index e3008feb0c1d..c7ae039309fa 100644 --- a/user_guide_src/source/database/connecting.rst +++ b/user_guide_src/source/database/connecting.rst @@ -2,7 +2,6 @@ Connecting to your Database ########################### - You can connect to your database by adding this line of code in any function where it is needed, or in your class constructor to make the database available globally in that class. @@ -44,7 +43,6 @@ to the same database, send ``false`` as the second parameter:: $db = \Config\Database::connect('group_name', false); - Connecting to Multiple Databases ================================ @@ -52,7 +50,7 @@ If you need to connect to more than one database simultaneously you can do so as follows:: $db1 = \Config\Database::connect('group_one'); - $db = \Config\Database::connect('group_two'); + $db = \Config\Database::connect('group_two'); Note: Change the words "group_one" and "group_two" to the specific group names you are connecting to. @@ -61,7 +59,7 @@ group names you are connecting to. only need to use a different database on the same connection. You can switch to a different database when you need to, like this: - | $db->dbSelect($database2_name); + | $db->setDatabase($database2_name); Reconnecting / Keeping the Connection Alive =========================================== @@ -72,6 +70,9 @@ consider pinging the server by using the reconnect() method before sending further queries, which can gracefully keep the connection alive or re-establish it. +.. important:: If you are using MySQLi database driver, the reconnect() method + does not ping the server but it closes the connection then connects again. + :: $db->reconnect(); diff --git a/user_guide_src/source/database/events.rst b/user_guide_src/source/database/events.rst new file mode 100644 index 000000000000..ea99528b9e33 --- /dev/null +++ b/user_guide_src/source/database/events.rst @@ -0,0 +1,28 @@ +############### +Database Events +############### + +The Database classes contain a few :doc:`Events ` that you can tap into in +order to learn more about what is happening during the database execution. These events can +be used to collect data for analysis and reporting. The :doc:`Debug Toolbar ` +uses this to collect the queries to display in the Toolbar. + +========== +The Events +========== + +**DBQuery** + +This event is triggered whenever a new query has been run, whether successful or not. The only parameter is +a :doc:`Query ` instance of the current query. You could use this to display all queries +in STDOUT, or logging to a file, or even creating tools to do automatic query analysis to help you spot +potentially missing indexes, slow queries, etc. An example usage might be:: + + // In Config\Events.php + Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect'); + + // Collect the queries so something can be done with them later. + public static function collect(CodeIgniter\Database\Query $query) + { + static::$queries[] = $query; + } diff --git a/user_guide_src/source/database/examples.rst b/user_guide_src/source/database/examples.rst index f978a942bcf0..17e73917ac55 100644 --- a/user_guide_src/source/database/examples.rst +++ b/user_guide_src/source/database/examples.rst @@ -33,7 +33,7 @@ Standard Query With Multiple Results (Object Version) echo $row->name; echo $row->email; } - + echo 'Total Results: ' . count($results); The above getResult() function returns an array of **objects**. Example: @@ -44,7 +44,7 @@ Standard Query With Multiple Results (Array Version) :: - $query = $db->query('SELECT name, title, email FROM my_table'); + $query = $db->query('SELECT name, title, email FROM my_table'); $results = $query->getResultArray(); foreach ($results as $row) @@ -63,7 +63,7 @@ Standard Query With Single Result :: $query = $db->query('SELECT name FROM my_table LIMIT 1'); - $row = $query->getRow(); + $row = $query->getRow(); echo $row->name; The above getRow() function returns an **object**. Example: $row->name @@ -74,7 +74,7 @@ Standard Query With Single Result (Array version) :: $query = $db->query('SELECT name FROM my_table LIMIT 1'); - $row = $query->getRowArray(); + $row = $query->getRowArray(); echo $row['name']; The above getRowArray() function returns an **array**. Example: @@ -87,7 +87,7 @@ Standard Insert $sql = "INSERT INTO mytable (title, name) VALUES (".$db->escape($title).", ".$db->escape($name).")"; $db->query($sql); - echo $this->db->getAffectedRows(); + echo $db->getAffectedRows(); Query Builder Query =================== @@ -95,8 +95,8 @@ Query Builder Query The :doc:`Query Builder Pattern ` gives you a simplified means of retrieving data:: - $query = $this->db->table('table_name')->get(); - + $query = $db->table('table_name')->get(); + foreach ($query->getResult() as $row) { echo $row->title; @@ -113,9 +113,9 @@ Query Builder Insert $data = array( 'title' => $title, - 'name' => $name, - 'date' => $date + 'name' => $name, + 'date' => $date ); - - $this->db->table('mytable')->insert($data); // Produces: INSERT INTO mytable (title, name, date) VALUES ('{$title}', '{$name}', '{$date}') + + $db->table('mytable')->insert($data); // Produces: INSERT INTO mytable (title, name, date) VALUES ('{$title}', '{$name}', '{$date}') diff --git a/user_guide_src/source/database/helpers.rst b/user_guide_src/source/database/helpers.rst index 1e7967c47bf2..297c75151fcd 100644 --- a/user_guide_src/source/database/helpers.rst +++ b/user_guide_src/source/database/helpers.rst @@ -10,7 +10,7 @@ Information From Executing a Query The insert ID number when performing database inserts. .. note:: If using the PDO driver with PostgreSQL, or using the Interbase - driver, this function requires a $name parameter, which specifies the + driver, this function requires a $name parameter, which specifies the appropriate sequence to check for the insert id. **$db->affectedRows()** @@ -27,17 +27,6 @@ Displays the number of affected rows, when doing "write" type queries Returns a Query object that represents the last query that was run (the query string, not the result). - -.. note:: Disabling the **saveQueries** setting in your database - configuration will render this function useless. - -**$db->getQueries()** - -Returns an array of Query objects that represent all of the queries ran on this connection. - -.. note:: Disabling the **saveQueries** setting in your database - configuration will render this function useless. - Information About Your Database =============================== @@ -47,8 +36,8 @@ Permits you to determine the number of rows in a particular table. Submit the table name in the first parameter. This is part of Query Builder. Example:: - echo $db->table('my_table')->count_all(); - + echo $db->table('my_table')->countAll(); + // Produces an integer, like 25 **$db->getPlatform()** diff --git a/user_guide_src/source/database/index.rst b/user_guide_src/source/database/index.rst index 51fee9bafe43..15b15fae92a8 100644 --- a/user_guide_src/source/database/index.rst +++ b/user_guide_src/source/database/index.rst @@ -1,25 +1,22 @@ -################## -Database Reference -################## +###################### +Working With Databases +###################### CodeIgniter comes with a full-featured and very fast abstracted database class that supports both traditional structures and Query Builder patterns. The database functions offer clear, simple syntax. .. toctree:: - :titlesonly: - - Quick Start: Usage Examples - Database Configuration - Connecting to a Database - Running Queries - Generating Query Results - Query Helper Functions - Query Builder Class - Transactions - Getting MetaData - Custom Function Calls - Using CodeIgniter's Model - Database Manipulation with Database Forge - Database Migrations - Database Seeding \ No newline at end of file + :titlesonly: + + Quick Start: Usage Examples + Database Configuration + Connecting to a Database + Running Queries + Generating Query Results + Query Helper Functions + Query Builder Class + Transactions + Getting MetaData + Custom Function Calls + Database Events diff --git a/user_guide_src/source/database/metadata.rst b/user_guide_src/source/database/metadata.rst index abaaa95cdd29..9b71c0c9eb79 100644 --- a/user_guide_src/source/database/metadata.rst +++ b/user_guide_src/source/database/metadata.rst @@ -2,6 +2,10 @@ Database Metadata ################# +.. contents:: + :local: + :depth: 2 + ************** Table MetaData ************** @@ -17,13 +21,12 @@ Returns an array containing the names of all the tables in the database you are currently connected to. Example:: $tables = $db->listTables(); - + foreach ($tables as $table) { echo $table; } - Determine If a Table Exists =========================== @@ -39,7 +42,6 @@ running an operation on it. Returns a boolean TRUE/FALSE. Usage example:: .. note:: Replace *table_name* with the name of the table you are looking for. - ************** Field MetaData ************** @@ -56,7 +58,7 @@ two ways: object:: $fields = $db->getFieldNames('table_name'); - + foreach ($fields as $field) { echo $field; @@ -66,13 +68,12 @@ object:: calling the function from your query result object:: $query = $db->query('SELECT * FROM some_table'); - + foreach ($query->getFieldNames() as $field) { echo $field; } - Determine If a Field is Present in a Table ========================================== @@ -90,11 +91,10 @@ performing an action. Returns a boolean TRUE/FALSE. Usage example:: for, and replace *table_name* with the name of the table you are looking for. - Retrieve Field Metadata ======================= -**$db->fieldData()** +**$db->getFieldData()** Returns an array of objects containing field information. @@ -106,7 +106,7 @@ the column type, max length, etc. Usage example:: $fields = $db->getFieldData('table_name'); - + foreach ($fields as $field) { echo $field->name; @@ -118,7 +118,7 @@ Usage example:: If you have run a query already you can use the result object instead of supplying the table name:: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); $fields = $query->fieldData(); The following data is available from this function if supported by your @@ -128,3 +128,10 @@ database: - max_length - maximum length of the column - primary_key - 1 if the column is a primary key - type - the type of the column + +List the Indexes in a Table +=========================== + +**$db->getIndexData()** + +please write this, someone... diff --git a/user_guide_src/source/database/model.rst b/user_guide_src/source/database/model.rst deleted file mode 100644 index ecda8c529802..000000000000 --- a/user_guide_src/source/database/model.rst +++ /dev/null @@ -1,431 +0,0 @@ -######################### -Using CodeIgniter's Model -######################### - -.. contents:: - :local: - :depth: 2 - -Manual Model Creation -===================== - -You do not need to extend any special class to create a model for your application. All you need is to get an -instance of the database connection and you're good to go. - -:: - - use \CodeIgniter\Database\ConnectionInterface; - - class UserModel - { - protected $db; - - public function __construct(ConnectionInterface &$db) - { - $this->db =& $db; - } - } - -CodeIgniter's Model -=================== - -CodeIgniter does provide a model class that provides a few nice features, including: - -- automatic database connection -- basic CRUD methods -- in-model validation -- and more - -This class provides a solid base from which to build your own models, allowing you to -rapidly build out your application's model layer. - -Creating Your Model -=================== - -To take advantage of CodeIgniter's model, you would simply create a new model class -that extends ``CodeIgniter\Model``:: - - class UserModel extends \CodeIgniter\Model - { - - } - -This empty class provides convenient access to the database connection, the Query Builder, -and a number of additional convenience methods. - -Connecting to the Database --------------------------- - -When the class is first instantiated, if no database connection instance is passed to constructor, -it will automatically connect to the default database group, as set in the configuration. You can -modify which group is used on a per-model basis by adding the DBGroup property to your class. -This ensures that within the model any references to ``$this->db`` are made through the appropriate -connection. -:: - - class UserModel extends \CodeIgniter\Model - { - protected $DBGroup = 'group_name'; - } - -You would replace "group_name" with the name of a defined database group from the database -configuration file. - -Configuring Your Model ----------------------- - -The model class has a few configuration options that can be set to allow the class' methods -to work seamlessly for you. The first two are used by all of the CRUD methods to determine -what table to use and how we can find the required records.:: - - class UserModel extends \CodeIgniter\Model - { - protected $table = 'users'; - protected $primaryKey = 'id'; - - protected $returnType = 'array'; - protected $useSoftDeletes = true; - - protected $allowedFields = ['name', 'email']; - - protected $useTimestamps = false; - } - -**$table** - -Specifies the database table that this model primarily works with. This only applies to the -built-in CRUD methods. You are not restricted to using only this table in your own -queries. - -**$primaryKey** - -This is the name of the column that uniquely identifies the records in this table. This -does not necessarilly have to match the primary key that is specified in the database, but -is used with methods like ``find()`` to know what column to match the specified value to. - -**$returnType** - -The Model's CRUD methods will take a step of work away from you and automatically return -the resulting data, instead of the Result object. This setting allows you to define -the type of data that is returned. Valid values are 'array', 'object', or the fully -qualified name of a class that can be used with the Result object's getCustomResultObject() -method. - -**$useSoftDeletes** - -If true, then any delete* method calls will simply set a flag in the database, instead of -actually deleting the row. This can preserve data when it might be referenced elsewhere, or -can maintain a "recylce bin" of objects that can be restored, or even simply preserve it as -part of a security trail. If true, the find* methods will only return non-deleted rows, unless -the withDeleted() method is called prior to calling the find* method. - -This requires an INT or TINYINT field named ``deleted`` to be present in the table. - -**$allowedFields** - -This array should be updated with the field names that can be set during save, insert, or -update methods. Any field names other than these will be discarded. This helps to protect -against just taking input from a form and throwing it all at the model, resulting in -potential mass assignment vulnerabilities. - -**$useTimestamps** - -This boolean value determines whether the current date is automatically added to all inserts -and updates. If true, will set the current time in the format specified by $dateFormat. This -requires that the table have columns named 'created_at' and 'updated_at' in the appropriate -data type. - -**$dateFormat** - -This value works with $useTimestamps to ensure that the correct type of date value gets -inserted into the database. By default, this creates DATETIME values, but valid options -are: datetime, date, or int (a PHP timestamp). - -Working With Data -================= - -Finding Data ------------- - -Several functions are provided for doing basic CRUD work on your tables, including find(), -insert(), update(), delete() and more. - -**find()** - -Returns a single row where the primary key matches the value passed in as the first parameter:: - - $user = $userModel->find($user_id); - -The value is returned in the format specified in $returnType. - -You can specify more than one row to return by passing an array of primaryKey values instead -of just one.:: - - $users = $userModel->find([1,2,3]); - -**findWhere()** - -Allows you to specify one or more criteria that must be matched against the data. Returns -all rows that match:: - - // Use simple where - $users = $userModel->findWhere('role_id >', '10'); - - // Use array of where values - $users = $userModel->findWhere([ - 'status' => 'active', - 'deleted' => 0 - ]); - -**findAll()** - -Returns all results. - - $users = $userModel->findAll(); - -This query may be modified by interjecting Query Builder commands as needed prior to calling this method.:: - - $users = $userModel->where('active', 1) - ->findAll(); - -You can pass in a limit and offset values as the first and second -parameters, respectively:: - - $users = $userModel->findAll($limit, $offset); - -**first()** - -Returns the first row in the result set. This is best used in combination with the query builder. -:: - - $user = $userModel->where('deleted', 0) - ->first(); - - -**withDeleted()** - -If $useSoftDeletes is true, then the find* methods will not return any rows where 'deleted = 1'. To -temporarily override this, you can use the withDeleted() method prior to calling the find* method. -:: - - // Only gets non-deleted rows (deleted = 0) - $activeUsers = $userModel->findAll(); - - // Gets all rows - $allUsers = $userModel->withDeleted() - ->findAll(); - -**onlyDeleted()** - -Whereas withDeleted() will return both deleted and not-deleted rows, this method modifies -the next find* methods to return only soft deleted rows.:: - - $deletedUsers = $userModel->onlyDeleted() - ->findAll(); - -Saving Data ------------ - -**insert()** - -An associative array of data is passed into this method as the only parameter to create a new -row of data in the database. The array's keys must match the name of the columns in $table, while -the array's values are the values to save for that key:: - - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; - - $userModel->insert($data); - -**update()** - -Updates an existing record in the database. The first parameter is the $primaryKey of the record to update. -An associative array of data is passed into this method as the second parameter. The array's keys must match the name -of the columns in $table, while the array's values are the values to save for that key:: - - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; - - $userModel->update($id, $data); - -**save()** - -This is a wrapper around the insert() and update() methods that handles inserting or updating the record -automatically, based on whether it finds an array key matching the $primaryKey value:: - - // Defined as a model property - $primaryKey = 'id'; - - // Does an insert() - $data = [ - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; - - $userModel->save($data); - - // Performs an update, since the primary key, 'id', is found. - $data = [ - 'id' => 3, - 'username' => 'darth', - 'email' => 'd.vader@theempire.com' - ]; - $userModel->save($data); - -Deleting Data -------------- - -**delete()** - -Takes a primary key value as the first parameter and deletes the matching record from the model's table:: - - $userModel->delete(12); - -If the model's $useSoftDeletes value is true, this will update the row to set 'deleted = 1'. You can force -a permanent delete by setting the second parameter as true. - -**deleteWhere()** - -Deletes multiple records from the model's table based on the criteria pass into the first two parameters. -:: - - // Simple where - $userMdoel->deleteWhere('status', 'inactive'); - - // Complex where - $userModel->deleteWhere([ - 'status' => 'inactive', - 'warn_lvl >=' => 50 - ]); - -If the model's $useSoftDeletes value is true, this will update the rows to set 'deleted = 1'. You can force -a permanent delete by setting the third parameter as true. - -**purgeDeleted()** - -Cleans out the database table by permanently removing all rows that have 'deleted = 1'. :: - - $userModel->purgeDeleted(); - -Protecting Fields ------------------ - -To help protect against Mass Assignment Attacks, the Model class requires that you list all of the field names -that can be changed during inserts and updates in the ``$allowedFields`` class property. Any data provided -in addition to these will be removed prior to hitting the database. This is great for ensuring that timestamps, -or primary keys do not get changed. -:: - - protected $allowedFields = ['name', 'email', 'address']; - -Occasionally, you will find times where you need to be able to change these elements. This is often during -testing, migrations, or seeds. In these cases, you can turn the protection on or off:: - - $model->protect(false) - ->insert($data) - ->protect(true); - -Working With Query Builder --------------------------- - -You can get access to a shared instance of the Query Builder for that model's database connection any time you -need it:: - - $builder = $userModel->builder(); - -This builder is already setup with the model's $table. - -You can also use Query Builder methods and the Model's CRUD methods in the same chained call, allowing for -very elegant use:: - - $users = $userModel->where('status', 'active') - ->orderBy('last_login', 'asc') - ->findAll(); - -.. note:: You can also access the model's database connection seamlessly:: - - $user_name = $userModel->escape($name); - - -Runtime Return Type Changes ----------------------------- - -You can specify the format that data should be returned as when using the find*() methods as the class property, -$returnType. There may be times that you would like the data back in a different format, though. The Model -provides methods that allow you to do just that. - -.. note:: These methods only change the return type for the next find*() method call. After that, - it is reset to its default value. - -**asArray()** - -Returns data from the next find*() method as associative arrays:: - - $users = $userModel->asArray()->findWhere('status', 'active'); - -**asObject()** - -Returns data from the next find*() method as standard objects or custom class intances:: - - // Return as standard objects - $users = $userModel->asObject()->findWhere('status', 'active'); - - // Return as custom class instances - $users = $userModel->asObject('User')->findWhere('status', 'active'); - - -Processing Large Amounts of Data --------------------------------- - -Sometimes, you need to process large amounts of data and would run the risk of running out of memory. -To make this simpler, you may use the chunk() method to get smaller chunks of data that you can then -do your work on. The first parameter is the number of rows to retrieve in a single chunk. The second -parameter is a Closure that will be called for each row of data. - -This is best used during cronjobs, data exports, or other large tasks. -:: - - $userModel->chunk(100, function ($data) - { - // do something. - // $data is a single row of data. - }); - -Obfuscating IDs in URLs ------------------------ - -Instead of displaying the resource's ID in the URL (i.e. /users/123), the model provides a simple -way to obfuscate the ID. This provides some protection against attackers simply incrementing IDs in the -URL to do bad things to your data. - -This is not a valid security use, but another simple layer of protection. Determined attackers could very easily -determine the actual ID. - -The data is not stored in the database at any time, it is simply used to disguise the ID. When creating a URL -you can use the **encodeID()** method to get the hashed ID. -:: - - // Creates something like: http://exmample.com/users/MTIz - $url = '/users/'. $model->encodeID($user->id); - -When you need to grab the item in your controller, you can use the **findByHashedID()** method instead of the -**find()** method. -:: - - public function show($hashedID) - { - $user = $this->model->findByHashedID($hashedID); - } - -If you ever need to decode the hash, you may do so with the **decodeID()** method. -:: - - $hash = $model->encodeID(123); - $check = $model->decodeID($hash); - -.. note:: While the name is "hashed id", this is not actually a hashed variable, but that term has become - common in many circles to represent the encoding of an ID into a short, unique, identifier. \ No newline at end of file diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 8353da6e64b3..56bb2896980a 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -2,6 +2,10 @@ Queries ####### +.. contents:: + :local: + :depth: 2 + ************ Query Basics ************ @@ -14,7 +18,7 @@ To submit a query, use the **query** function:: $db->query('YOUR QUERY HERE'); The query() function returns a database result **object** when "read" -type queries are run, which you can use to :doc:`show your +type queries are run which you can use to :doc:`show your results `. When "write" type queries are run it simply returns TRUE or FALSE depending on success or failure. When retrieving data you will typically assign the query to your own variable, like @@ -49,8 +53,8 @@ fetchable results. } .. note:: PostgreSQL's ``pg_exec()`` function (for example) always - returns a resource on success, even for write type queries. - So take that in mind if you're looking for a boolean value. + returns a resource on success even for write type queries. + So keep that in mind if you're looking for a boolean value. *************************************** Working with Database prefixes manually @@ -62,27 +66,25 @@ the following:: $db->prefixTable('tablename'); // outputs prefix_tablename - -If for any reason you would like to change the prefix programatically -without needing to create a new connection, you can use this method:: +If for any reason you would like to change the prefix programmatically +without needing to create a new connection you can use this method:: $db->setPrefix('newprefix'); $db->prefixTable('tablename'); // outputs newprefix_tablename - ********************** Protecting identifiers ********************** In many databases it is advisable to protect table and field names - for example with backticks in MySQL. **Query Builder queries are -automatically protected**, however if you need to manually protect an +automatically protected**, but if you need to manually protect an identifier you can use:: $db->protectIdentifiers('table_name'); .. important:: Although the Query Builder will try its best to properly - quote any field and table names that you feed it, note that it + quote any field and table names that you feed it. Note that it is NOT designed to work with arbitrary user input. DO NOT feed it with unsanitized user data. @@ -92,7 +94,6 @@ prefixing set TRUE (boolean) via the second parameter:: $db->protectIdentifiers('table_name', TRUE); - **************** Escaping Queries **************** @@ -121,9 +122,9 @@ this: :: - $search = '20% raise'; + $search = '20% raise'; $sql = "SELECT id FROM table WHERE column LIKE '%" . - $db->escapeLikeString($search)."%' ESCAPE '!'"; + $db->escapeLikeString($search)."%' ESCAPE '!'"; .. important:: The ``escapeLikeString()`` method uses '!' (exclamation mark) to escape special characters for *LIKE* conditions. Because this @@ -131,7 +132,6 @@ this: yourself, it cannot automatically add the ``ESCAPE '!'`` condition for you, and so you'll have to manually do that. - ************** Query Bindings ************** @@ -155,9 +155,8 @@ The resulting query will be:: SELECT * FROM some_table WHERE id IN (3,6) AND status = 'live' AND author = 'Rick' The secondary benefit of using binds is that the values are -automatically escaped, producing safer queries. You don't have to -remember to manually escape data; the engine does it automatically for -you. +automatically escaped producing safer queries. +You don't have to remember to manually escape data — the engine does it automatically for you. Named Bindings ============== @@ -166,10 +165,14 @@ Instead of using the question mark to mark the location of the bound values, you can name the bindings, allowing the keys of the values passed in to match placeholders in the query:: - $sql = "SELECT * FROM some_table WHERE id = :id AND status = :status AND author = :name"; - $db->query($sql, ['id' => 3, - 'status' => 'live', - 'name' => 'Rick']); + $sql = "SELECT * FROM some_table WHERE id = :id: AND status = :status: AND author = :name:"; + $db->query($sql, [ + 'id' => 3, + 'status' => 'live', + 'name' => 'Rick' + ]); + +.. note:: Each name in the query MUST be surrounded by colons. *************** Handling Errors @@ -177,7 +180,7 @@ Handling Errors **$db->error();** -If you need to get the last error that has occured, the error() method +If you need to get the last error that has occurred, the error() method will return an array containing its code and message. Here's a quick example:: @@ -186,33 +189,119 @@ example:: $error = $db->error(); // Has keys 'code' and 'message' } +**************** +Prepared Queries +**************** -************************** -Working with Query Objects -************************** +Most database engines support some form of prepared statements, that allow you to prepare a query once, and then run +that query multiple times with new sets of data. This eliminates the possibility of SQL injection since the data is +passed to the database in a different format than the query itself. When you need to run the same query multiple times +it can be quite a bit faster, too. However, to use it for every query can have major performance hits, since you're calling +out to the database twice as often. Since the Query Builder and Database connections already handle escaping the data +for you, the safety aspect is already taken care of for you. There will be times, though, when you need to ability +to optimize the query by running a prepared statement, or prepared query. -Internally, all queries are processed and stored as instances of -\CodeIgniter\Database\Query. This class is responsible for binding -the parameters, otherwise preparing the query, and storing performance -data about its query. +Preparing the Query +=================== + +This can be easily done with the ``prepare()`` method. This takes a single parameter, which is a Closure that returns +a query object. Query objects are automatically generated by any of the "final" type queries, including **insert**, +**update**, **delete**, **replace**, and **get**. This is handled the easiest by using the Query Builder to +run a query. The query is not actually run, and the values don't matter since they're never applied, acting instead +as placeholders. This returns a PreparedQuery object:: + + $pQuery = $db->prepare(function($db) + { + return $db->table('user') + ->insert([ + 'name' => 'x', + 'email' => 'y', + 'country' => 'US' + ]); + }); + +If you don't want to use the Query Builder you can create the Query object manually using question marks for +value placeholders:: + + $pQuery = $db->prepare(function($db) + { + $sql = "INSERT INTO user (name, email, country) VALUES (?, ?, ?)"; + + return new Query($db)->setQuery($sql); + }); + +If the database requires an array of options passed to it during the prepare statement phase you can pass that +array through in the second parameter:: + + $pQuery = $db->prepare(function($db) + { + $sql = "INSERT INTO user (name, email, country) VALUES (?, ?, ?)"; -**getQueries()** + return new Query($db)->setQuery($sql); + }, $options); -You can retrieve all Query objects from the database connection with the -getQueries() method. This has no parameters and returns an array of -all of the queries that have ran:: +Executing the Query +=================== - $queries = $db->getQueries(); +Once you have a prepared query you can use the ``execute()`` method to actually run the query. You can pass in as +many variables as you need in the query parameters. The number of parameters you pass must match the number of +placeholders in the query. They must also be passed in the same order as the placeholders appear in the original +query:: -.. note:: If the saveQueries setting in the database configuraiton is set - to false, this and the following two methods, will not return any results. + // Prepare the Query + $pQuery = $db->prepare(function($db) + { + return $db->table('user') + ->insert([ + 'name' => 'x', + 'email' => 'y', + 'country' => 'US' + ]); + }); -**getQueryCount()** + // Collect the Data + $name = 'John Doe'; + $email = 'j.doe@example.com'; + $country = 'US'; -Returns the total number of queries that have been ran on this connection -during this request:: + // Run the Query + $results = $pQuery->execute($name, $email, $country); - $count = $db->getQueryCount(); +This returns a standard :doc:`result set `. + +Other Methods +============= + +In addition to these two primary methods, the prepared query object also has the following methods: + +**close()** + +While PHP does a pretty good job of closing all open statements with the database it's always a good idea to +close out the prepared statement when you're done with it:: + + $pQuery->close(); + +**getQueryString()** + +This returns the prepared query as a string. + +**hasError()** + +Returns boolean true/false if the last execute() call created any errors. + +**getErrorCode()** +**getErrorMessage()** + +If any errors were encountered these methods can be used to retrieve the error code and string. + +************************** +Working with Query Objects +************************** + +Internally, all queries are processed and stored as instances of +\CodeIgniter\Database\Query. This class is responsible for binding +the parameters, otherwise preparing the query, and storing performance +data about its query. **getLastQuery()** @@ -231,7 +320,7 @@ as well. **getQuery()** -Returns the final query, after all processing has happened. This is the exact +Returns the final query after all processing has happened. This is the exact query that was sent to the database:: $sql = $query->getQuery(); @@ -249,7 +338,7 @@ binds in it, or prefixes swapped out, etc:: **hasError()** -If an error was encountered during the execution of this query, this method +If an error was encountered during the execution of this query this method will return true:: if ($query->hasError()) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst old mode 100644 new mode 100755 index ba4d5fa4bb3b..db9fe5163eba --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -17,7 +17,7 @@ system. .. contents:: :local: - :depth: 1 + :depth: 2 ************************* Loading the Query Builder @@ -27,7 +27,7 @@ The Query Builder is loaded through the ``table()`` method on the database connection. This sets the ``FROM`` portion of the query for you and returns a new instance of the Query Builder class:: - $db = \Config\Database::connect(); + $db = \Config\Database::connect(); $builder = $db->table('users'); The Query Builder is only loaded into memory when you specifically request @@ -45,7 +45,7 @@ Runs the selection query and returns the result. Can be used by itself to retrieve all records from a table:: $builder = $db->table('mytable'); - $query = $builder->get(); // Produces: SELECT * FROM mytable + $query = $builder->get(); // Produces: SELECT * FROM mytable The first and second parameters enable you to set a limit and offset clause:: @@ -128,7 +128,7 @@ escaping of fields may break them. :: - $builder->select('(SELECT SUM(payments.amount) FROM payments WHERE payments.invoice_id=4') AS amount_paid', FALSE); + $builder->select('(SELECT SUM(payments.amount) FROM payments WHERE payments.invoice_id=4) AS amount_paid', FALSE); $query = $builder->get(); **$builder->selectMax()** @@ -144,7 +144,6 @@ include a second parameter to rename the resulting field. $builder->selectMax('age', 'member_age'); $query = $builder->get(); // Produces: SELECT MAX(age) as member_age FROM mytable - **$builder->selectMin()** Writes a "SELECT MIN(field)" portion for your query. As with @@ -156,7 +155,6 @@ the resulting field. $builder->selectMin('age'); $query = $builder->get(); // Produces: SELECT MIN(age) as age FROM mytable - **$builder->selectAvg()** Writes a "SELECT AVG(field)" portion for your query. As with @@ -168,7 +166,6 @@ the resulting field. $builder->selectAvg('age'); $query = $builder->get(); // Produces: SELECT AVG(age) as age FROM mytable - **$builder->selectSum()** Writes a "SELECT SUM(field)" portion for your query. As with @@ -197,12 +194,12 @@ Permits you to write the FROM portion of your query:: Permits you to write the JOIN portion of your query:: $builder->db->table('blog'); - $builder->select('*'); - $builder->join('comments', 'comments.id = blogs.id'); - $query = $builder->get(); + $builder->select('*'); + $builder->join('comments', 'comments.id = blogs.id'); + $query = $builder->get(); - // Produces: - // SELECT * FROM blogs JOIN comments ON comments.id = blogs.id + // Produces: + // SELECT * FROM blogs JOIN comments ON comments.id = blogs.id Multiple function calls can be made if you need several joins in one query. @@ -277,7 +274,6 @@ methods: $where = "name='Joe' AND status='boss' OR status='active'"; $builder->where($where); - ``$builder->where()`` accepts an optional third parameter. If you set it to FALSE, CodeIgniter will not try to protect your field or table names. @@ -304,7 +300,6 @@ appropriate $builder->whereIn('username', $names); // Produces: WHERE username IN ('Frank', 'Todd', 'James') - **$builder->orWhereIn()** Generates a WHERE field IN ('item', 'item') SQL query joined with OR if @@ -327,7 +322,6 @@ AND if appropriate $builder->whereNotIn('username', $names); // Produces: WHERE username NOT IN ('Frank', 'Todd', 'James') - **$builder->orWhereNotIn()** Generates a WHERE field NOT IN ('item', 'item') SQL query joined with OR @@ -350,6 +344,11 @@ searches. .. note:: All values passed to this method are escaped automatically. +.. note:: All ``like*`` method variations can be forced to be perform case-insensitive searches by passing + a fifth parameter of ``true`` to the method. This will use platform-specific features where available + otherwise, will force the values to be lowercase, i.e. ``WHERE LOWER(column) LIKE '%search%'``. This + may require indexes to be made for ``LOWER(column)`` instead of ``column`` to be effective. + #. **Simple key/value method:** :: @@ -382,7 +381,6 @@ searches. $builder->like($array); // WHERE `title` LIKE '%match%' ESCAPE '!' AND `page1` LIKE '%match%' ESCAPE '!' AND `page2` LIKE '%match%' ESCAPE '!' - **$builder->orLike()** This method is identical to the one above, except that multiple @@ -417,7 +415,6 @@ You can also pass an array of multiple values as well:: $builder->groupBy(array("title", "date")); // Produces: GROUP BY title, date - **$builder->distinct()** Adds the "DISTINCT" keyword to a query @@ -440,7 +437,6 @@ You can also pass an array of multiple values as well:: $builder->having(['title =' => 'My Title', 'id <' => $id]); // Produces: HAVING title = 'My Title', id < 45 - If you are using a database that CodeIgniter escapes queries for, you can prevent escaping content by passing an optional third argument, and setting it to FALSE. @@ -450,7 +446,6 @@ setting it to FALSE. $builder->having('user_id', 45); // Produces: HAVING `user_id` = 45 in some databases such as MySQL $builder->having('user_id', 45, FALSE); // Produces: HAVING user_id = 45 - **$builder->orHaving()** Identical to having(), only separates multiple clauses with "OR". @@ -518,8 +513,8 @@ The second parameter lets you set a result offset. **$builder->countAllResults()** -Permits you to determine the number of rows in a particular Active -Record query. Queries will accept Query Builder restrictors such as +Permits you to determine the number of rows in a particular Query +Builder query. Queries will accept Query Builder restrictors such as ``where()``, ``orWhere()``, ``like()``, ``orLike()``, etc. Example:: echo $builder->countAllResults('my_table'); // Produces an integer, like 25 @@ -548,7 +543,7 @@ Query grouping allows you to create groups of WHERE clauses by enclosing them in you to create queries with complex WHERE clauses. Nested groups are supported. Example:: $builder->select('*')->from('my_table') - ->group_start() + ->groupStart() ->where('a', 'a') ->orGroupStart() ->where('b', 'b') @@ -561,7 +556,7 @@ you to create queries with complex WHERE clauses. Nested groups are supported. E // Generates: // SELECT * FROM (`my_table`) WHERE ( `a` = 'a' OR ( `b` = 'b' AND `c` = 'c' ) ) AND `d` = 'd' -.. note:: groups need to be balanced, make sure every group_start() is matched by a group_end(). +.. note:: groups need to be balanced, make sure every groupStart() is matched by a groupEnd(). **$builder->groupStart()** @@ -595,8 +590,8 @@ function. Here is an example using an array:: $data = array( 'title' => 'My title', - 'name' => 'My Name', - 'date' => 'My date' + 'name' => 'My Name', + 'date' => 'My date' ); $builder->insert($data); @@ -608,9 +603,9 @@ Here is an example using an object:: /* class Myclass { - public $title = 'My Title'; + public $title = 'My Title'; public $content = 'My Content'; - public $date = 'My Date'; + public $date = 'My Date'; } */ @@ -618,8 +613,7 @@ Here is an example using an object:: $builder->insert($object); // Produces: INSERT INTO mytable (title, content, date) VALUES ('My Title', 'My Content', 'My Date') -The first parameter will contain the table name, the second is an -object. +The first parameter is an object. .. note:: All values are escaped automatically producing safer queries. @@ -653,7 +647,7 @@ will be reset (by default it will be--just like $builder->insert()):: // Produces string: INSERT INTO mytable (`title`, `content`) VALUES ('My Title', 'My Content') The key thing to notice in the above example is that the second query did not -utlize `$builder->from()` nor did it pass a table name into the first +utilize `$builder->from()` nor did it pass a table name into the first parameter. The reason this worked is because the query has not been executed using `$builder->insert()` which resets values or reset directly using `$builder->resetQuery()`. @@ -669,13 +663,13 @@ function. Here is an example using an array:: $data = array( array( 'title' => 'My title', - 'name' => 'My Name', - 'date' => 'My date' + 'name' => 'My Name', + 'date' => 'My date' ), array( 'title' => 'Another title', - 'name' => 'Another Name', - 'date' => 'Another date' + 'name' => 'Another Name', + 'date' => 'Another date' ) ); @@ -747,7 +741,7 @@ parameter. $builder->set('field', 'field+1', FALSE); $builder->where('id', 2); - $builder->update(); // gives UPDATE mytable SET field = field+1 WHERE id = 2 + $builder->update(); // gives UPDATE mytable SET field = field+1 WHERE `id` = 2 $builder->set('field', 'field+1'); $builder->where('id', 2); @@ -756,8 +750,8 @@ parameter. You can also pass an associative array to this function:: $array = array( - 'name' => $name, - 'title' => $title, + 'name' => $name, + 'title' => $title, 'status' => $status ); @@ -768,9 +762,9 @@ Or an object:: /* class Myclass { - public $title = 'My Title'; + public $title = 'My Title'; public $content = 'My Content'; - public $date = 'My Date'; + public $date = 'My Date'; } */ @@ -786,8 +780,8 @@ is an example using an array:: $data = array( 'title' => $title, - 'name' => $name, - 'date' => $date + 'name' => $name, + 'date' => $date ); $builder->where('id', $id); @@ -802,9 +796,9 @@ Or you can supply an object:: /* class Myclass { - public $title = 'My Title'; + public $title = 'My Title'; public $content = 'My Content'; - public $date = 'My Date'; + public $date = 'My Date'; } */ @@ -841,13 +835,13 @@ Here is an example using an array:: $data = array( array( 'title' => 'My title' , - 'name' => 'My Name 2' , - 'date' => 'My date 2' + 'name' => 'My Name 2' , + 'date' => 'My date 2' ), array( 'title' => 'Another title' , - 'name' => 'Another Name 2' , - 'date' => 'Another date 2' + 'name' => 'Another Name 2' , + 'date' => 'Another date 2' ) ); @@ -893,9 +887,9 @@ Generates a delete SQL string and runs the query. $builder->delete(array('id' => $id)); // Produces: // DELETE FROM mytable // WHERE id = $id -The first parameter is the table name, the second is the where clause. +The first parameter is the where clause. You can also use the where() or or_where() functions instead of passing -the data to the second parameter of the function:: +the data to the first parameter of the function:: $builder->where('id', $id); $builder->delete(); @@ -910,7 +904,7 @@ function, or empty_table(). **$builder->emptyTable()** Generates a delete SQL string and runs the -query.:: +query:: $builder->emptyTable('mytable'); // Produces: DELETE FROM mytable @@ -943,13 +937,12 @@ Method chaining allows you to simplify your syntax by connecting multiple functions. Consider this example:: $query = $builder->select('title') - ->where('id', $id) - ->limit(10, 20) - ->get(); + ->where('id', $id) + ->limit(10, 20) + ->get(); .. _ar-caching: - *********************** Resetting Query Builder *********************** @@ -963,20 +956,20 @@ This is useful in situations where you are using Query Builder to generate SQL (ex. ``$builder->getCompiledSelect()``) but then choose to, for instance, run the query:: - // Note that the second parameter of the get_compiled_select method is FALSE - $sql = $builder->select(array('field1','field2')) - ->where('field3',5) - ->getCompiledSelect(false); + // Note that the second parameter of the get_compiled_select method is FALSE + $sql = $builder->select(array('field1','field2')) + ->where('field3',5) + ->getCompiledSelect(false); - // ... - // Do something crazy with the SQL code... like add it to a cron script for - // later execution or something... - // ... + // ... + // Do something crazy with the SQL code... like add it to a cron script for + // later execution or something... + // ... - $data = $builder->get()->getResultArray(); + $data = $builder->get()->getResultArray(); - // Would execute and return an array of results of the following query: - // SELECT field1, field1 from mytable where field3 = 5; + // Would execute and return an array of results of the following query: + // SELECT field1, field1 from mytable where field3 = 5; *************** Class Reference @@ -990,7 +983,7 @@ Class Reference :rtype: BaseBuilder Resets the current Query Builder state. Useful when you want - to build a query that can be cancelled under certain conditions. + to build a query that can be canceled under certain conditions. .. php:method:: countAllResults([$reset = TRUE]) @@ -1005,8 +998,8 @@ Class Reference :param int $limit: The LIMIT clause :param int $offset: The OFFSET clause - :returns: CI_DB_result instance (method chaining) - :rtype: CI_DB_result + :returns: \CodeIgniter\Database\ResultInterface instance (method chaining) + :rtype: \CodeIgniter\Database\ResultInterface Compiles and runs SELECT statement based on the already called Query Builder methods. @@ -1016,8 +1009,8 @@ Class Reference :param string $where: The WHERE clause :param int $limit: The LIMIT clause :param int $offset: The OFFSET clause - :returns: CI_DB_result instance (method chaining) - :rtype: CI_DB_result + :returns: \CodeIgniter\Database\ResultInterface instance (method chaining) + :rtype: \CodeIgniter\Database\ResultInterface Same as ``get()``, but also allows the WHERE to be added directly. diff --git a/user_guide_src/source/database/results.rst b/user_guide_src/source/database/results.rst index 7f45e5f28f30..54ed85938b8b 100644 --- a/user_guide_src/source/database/results.rst +++ b/user_guide_src/source/database/results.rst @@ -18,14 +18,14 @@ This method returns the query result as an array of **objects**, or **an empty array** on failure. Typically you'll use this in a foreach loop, like this:: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); - foreach ($query->getResult() as $row) - { - echo $row->title; - echo $row->name; - echo $row->body; - } + foreach ($query->getResult() as $row) + { + echo $row->title; + echo $row->name; + echo $row->body; + } The above method is an alias of ``getResultObject()``. @@ -34,12 +34,12 @@ as an array of arrays:: $query = $db->query("YOUR QUERY"); - foreach ($query->getResult() as $row) - { - echo $row['title']; - echo $row['name']; - echo $row['body']; - } + foreach ($query->getResult('array') as $row) + { + echo $row['title']; + echo $row['name']; + echo $row['body']; + } The above usage is an alias of ``getResultArray()``. @@ -48,13 +48,13 @@ instantiate for each result object :: - $query = $db->query("SELECT * FROM users;"); + $query = $db->query("SELECT * FROM users;"); - foreach ($query->getResult('User') as $user) - { - echo $user->name; // access attributes - echo $user->reverseName(); // or methods defined on the 'User' class - } + foreach ($query->getResult('User') as $user) + { + echo $user->name; // access attributes + echo $user->reverseName(); // or methods defined on the 'User' class + } The above method is an alias of ``getCustomResultObject()``. @@ -64,14 +64,14 @@ This method returns the query result as a pure array, or an empty array when no result is produced. Typically you'll use this in a foreach loop, like this:: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); - foreach ($query->getResultArray() as $row) - { - echo $row['title']; - echo $row['name']; - echo $row['body']; - } + foreach ($query->getResultArray() as $row) + { + echo $row['title']; + echo $row['name']; + echo $row['body']; + } *********** Result Rows @@ -83,16 +83,16 @@ This method returns a single result row. If your query has more than one row, it returns only the first row. The result is returned as an **object**. Here's a usage example:: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); - $row = $query->getRow(); + $row = $query->getRow(); - if (isset($row)) - { - echo $row->title; - echo $row->name; - echo $row->body; - } + if (isset($row)) + { + echo $row->title; + echo $row->name; + echo $row->body; + } If you want a specific row returned you can submit the row number as a digit in the first parameter:: @@ -104,7 +104,7 @@ to instantiate the row with:: $query = $db->query("SELECT * FROM users LIMIT 1;"); $row = $query->getRow(0, 'User'); - + echo $row->name; // access attributes echo $row->reverse_name(); // or methods defined on the 'User' class @@ -113,16 +113,16 @@ to instantiate the row with:: Identical to the above ``row()`` method, except it returns an array. Example:: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); - $row = $query->getRowArray(); + $row = $query->getRowArray(); - if (isset($row)) - { - echo $row['title']; - echo $row['name']; - echo $row['body']; - } + if (isset($row)) + { + echo $row['title']; + echo $row['name']; + echo $row['body']; + } If you want a specific row returned you can submit the row number as a digit in the first parameter:: @@ -153,18 +153,18 @@ parameter: This method returns a single result row without prefetching the whole result in memory as ``row()`` does. If your query has more than one row, -it returns the current row and moves the internal data pointer ahead. +it returns the current row and moves the internal data pointer ahead. :: - $query = $db->query("YOUR QUERY"); + $query = $db->query("YOUR QUERY"); - while ($row = $query->getUnbufferedRow()) - { - echo $row->title; - echo $row->name; - echo $row->body; - } + while ($row = $query->getUnbufferedRow()) + { + echo $row->title; + echo $row->name; + echo $row->body; + } You can optionally pass 'object' (default) or 'array' in order to specify the returned value's type:: @@ -264,7 +264,7 @@ Example:: ********************* Result Helper Methods ********************* - + **getFieldCount()** The number of FIELDS (columns) returned by the query. Make sure to call @@ -378,7 +378,7 @@ Class Reference :returns: The requested row or NULL if it doesn't exist :rtype: mixed - A wrapper for the ``getRowArray()``, ``getRowObject() and + A wrapper for the ``getRowArray()``, ``getRowObject()`` and ``getCustomRowObject()`` methods. Usage: see `Result Rows`_. diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 16ebcfd6dcf2..4b8cff884641 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -1,5 +1,127 @@ -##################### -Handling Transactions -##################### +############ +Transactions +############ -TODO +CodeIgniter's database abstraction allows you to use transactions with +databases that support transaction-safe table types. In MySQL, you'll +need to be running InnoDB or BDB table types rather than the more common +MyISAM. Most other database platforms support transactions natively. + +If you are not familiar with transactions we recommend you find a good +online resource to learn about them for your particular database. The +information below assumes you have a basic understanding of +transactions. + +CodeIgniter's Approach to Transactions +====================================== + +CodeIgniter utilizes an approach to transactions that is very similar to +the process used by the popular database class ADODB. We've chosen that +approach because it greatly simplifies the process of running +transactions. In most cases all that is required are two lines of code. + +Traditionally, transactions have required a fair amount of work to +implement since they demand that you keep track of your queries and +determine whether to commit or rollback based on the success or failure +of your queries. This is particularly cumbersome with nested queries. In +contrast, we've implemented a smart transaction system that does all +this for you automatically (you can also manage your transactions +manually if you choose to, but there's really no benefit). + +Running Transactions +==================== + +To run your queries using transactions you will use the +$this->db->transStart() and $this->db->transComplete() functions as +follows:: + + $this->db->transStart(); + $this->db->query('AN SQL QUERY...'); + $this->db->query('ANOTHER QUERY...'); + $this->db->query('AND YET ANOTHER QUERY...'); + $this->db->transComplete(); + +You can run as many queries as you want between the start/complete +functions and they will all be committed or rolled back based on success +or failure of any given query. + +Strict Mode +=========== + +By default CodeIgniter runs all transactions in Strict Mode. When strict +mode is enabled, if you are running multiple groups of transactions, if +one group fails all groups will be rolled back. If strict mode is +disabled, each group is treated independently, meaning a failure of one +group will not affect any others. + +Strict Mode can be disabled as follows:: + + $this->db->transStrict(false); + +Managing Errors +=============== + +If you have error reporting enabled in your Config/Database.php file +you'll see a standard error message if the commit was unsuccessful. If +debugging is turned off, you can manage your own errors like this:: + + $this->db->transStart(); + $this->db->query('AN SQL QUERY...'); + $this->db->query('ANOTHER QUERY...'); + $this->db->transComplete(); + + if ($this->db->transStatus() === FALSE) + { + // generate an error... or use the log_message() function to log your error + } + +Enabling Transactions +===================== + +Transactions are enabled automatically the moment you use +$this->db->transStart(). If you would like to disable transactions you +can do so using $this->db->transOff():: + + $this->db->transOff(); + + $this->db->trans_Start(); + $this->db->query('AN SQL QUERY...'); + $this->db->transComplete(); + +When transactions are disabled, your queries will be auto-commited, just +as they are when running queries without transactions. + +Test Mode +========= + +You can optionally put the transaction system into "test mode", which +will cause your queries to be rolled back -- even if the queries produce +a valid result. To use test mode simply set the first parameter in the +$this->db->transStart() function to TRUE:: + + $this->db->transStart(true); // Query will be rolled back + $this->db->query('AN SQL QUERY...'); + $this->db->transComplete(); + +Running Transactions Manually +============================= + +If you would like to run transactions manually you can do so as follows:: + + $this->db->transBegin(); + + $this->db->query('AN SQL QUERY...'); + $this->db->query('ANOTHER QUERY...'); + $this->db->query('AND YET ANOTHER QUERY...'); + + if ($this->db->transStatus() === FALSE) + { + $this->db->transRollback(); + } + else + { + $this->db->transCommit(); + } + +.. note:: Make sure to use $this->db->transBegin() when running manual + transactions, **NOT** $this->db->transStart(). diff --git a/user_guide_src/source/database/forge.rst b/user_guide_src/source/dbmgmt/forge.rst similarity index 77% rename from user_guide_src/source/database/forge.rst rename to user_guide_src/source/dbmgmt/forge.rst index de635c104608..b65fa3428941 100644 --- a/user_guide_src/source/database/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -5,8 +5,9 @@ Database Forge Class The Database Forge Class contains methods that help you manage your database. -.. contents:: Table of Contents - :depth: 3 +.. contents:: + :local: + :depth: 2 **************************** Initializing the Forge Class @@ -51,7 +52,6 @@ Returns TRUE/FALSE based on success or failure:: echo 'Database deleted!'; } - **************************** Creating and Dropping Tables **************************** @@ -72,13 +72,12 @@ also require a 'constraint' key. $fields = array( 'users' => array( - 'type' => 'VARCHAR', + 'type' => 'VARCHAR', 'constraint' => '100', ), ); // will translate to "users VARCHAR(100)" when the field is added. - Additionally, the following key/values can be used: - unsigned/true : to generate "UNSIGNED" in the field definition. @@ -93,29 +92,28 @@ Additionally, the following key/values can be used: :: $fields = array( - 'blog_id' => array( - 'type' => 'INT', - 'constraint' => 5, - 'unsigned' => TRUE, + 'blog_id' => array( + 'type' => 'INT', + 'constraint' => 5, + 'unsigned' => TRUE, 'auto_increment' => TRUE ), - 'blog_title' => array( - 'type' => 'VARCHAR', - 'constraint' => '100', - 'unique' => TRUE, + 'blog_title' => array( + 'type' => 'VARCHAR', + 'constraint' => '100', + 'unique' => TRUE, ), - 'blog_author' => array( - 'type' =>'VARCHAR', - 'constraint' => '100', - 'default' => 'King of Town', + 'blog_author' => array( + 'type' =>'VARCHAR', + 'constraint' => '100', + 'default' => 'King of Town', ), 'blog_description' => array( - 'type' => 'TEXT', - 'null' => TRUE, + 'type' => 'TEXT', + 'null' => TRUE, ), ); - After the fields have been defined, they can be added using ``$forge->addField($fields);`` followed by a call to the ``createTable()`` method. @@ -124,7 +122,6 @@ After the fields have been defined, they can be added using The add fields method will accept the above array. - Passing strings as fields ------------------------- @@ -135,7 +132,6 @@ string into the field definitions with addField() $forge->addField("label varchar(100) NOT NULL DEFAULT 'default label'"); - .. note:: Passing raw strings as fields cannot be followed by ``add_key()`` calls on those fields. .. note:: Multiple calls to add_field() are cumulative. @@ -152,13 +148,13 @@ Primary Key. $forge->addField('id'); // gives id INT(9) NOT NULL AUTO_INCREMENT - Adding Keys =========== Generally speaking, you'll want your table to have Keys. This is -accomplished with $forge->addKey('field'). An optional second -parameter set to TRUE will make it a primary key. Note that addKey() +accomplished with $forge->addKey('field'). The optional second +parameter set to TRUE will make it a primary key and the third +parameter set to TRUE will make it a unique key. Note that addKey() must be followed by a call to createTable(). Multiple column non-primary keys must be sent as an array. Sample output @@ -179,6 +175,34 @@ below is for MySQL. $forge->addKey(array('blog_name', 'blog_label')); // gives KEY `blog_name_blog_label` (`blog_name`, `blog_label`) + $forge->addKey(array('blog_id', 'uri'), FALSE, TRUE); + // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) + +To make code reading more objective it is also possible to add primary +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(array('blog_id', 'uri')); + // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) + + +Adding Foreign Keys +=================== + +:: + + $forge->addForeignKey('users_id','users','id'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) + +You can specify the desired action for the "on delete" and "on update" properties of the constraint:: + + $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); + // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE Creating a table ================ @@ -191,7 +215,6 @@ with $forge->createTable('table_name'); // gives CREATE TABLE table_name - An optional second parameter set to TRUE adds an "IF NOT EXISTS" clause into the definition @@ -210,7 +233,6 @@ You could also pass optional table attributes, such as MySQL's ``ENGINE``:: ``createTable()`` will always add them with your configured *charset* and *DBCollat* values, as long as they are not empty (MySQL only). - Dropping a table ================ @@ -224,6 +246,17 @@ Execute a DROP TABLE statement and optionally add an IF EXISTS clause. // Produces: DROP TABLE IF EXISTS table_name $forge->dropTable('table_name',TRUE); +Dropping a Foreign Key +====================== + +Execute a DROP FOREIGN KEY. + +:: + + // Produces: ALTER TABLE 'tablename' DROP FOREIGN KEY 'users_foreign' + $forge->dropForeignKey('tablename','users_foreign'); + +.. note:: SQlite database driver does not support dropping of foreign keys. Renaming a table ================ @@ -235,7 +268,6 @@ Executes a TABLE rename $forge->renameTable('old_table_name', 'new_table_name'); // gives ALTER TABLE old_table_name RENAME TO new_table_name - **************** Modifying Tables **************** @@ -272,7 +304,6 @@ Examples:: 'preferences' => array('type' => 'TEXT', 'first' => TRUE) ); - Dropping a Column From a Table ============================== @@ -284,8 +315,6 @@ Used to remove a column from a table. $forge->dropColumn('table_name', 'column_to_drop'); - - Modifying a Column in a Table ============================= @@ -306,18 +335,16 @@ change the name you can add a "name" key into the field defining array. $forge->modifyColumn('table_name', $fields); // gives ALTER TABLE table_name CHANGE old_name new_name TEXT - *************** Class Reference *************** .. php:class:: \CodeIgniter\Database\Forge - .. php:method:: addColumn($table[, $field = array()[, $_after = NULL]]) + .. php:method:: addColumn($table[, $field = array()]) :param string $table: Table name to add the column to :param array $field: Column definition(s) - :param string $_after: Column for AFTER clause (deprecated) :returns: TRUE on success, FALSE on failure :rtype: bool @@ -331,15 +358,32 @@ Class Reference Adds a field to the set that will be used to create a table. Usage: See `Adding fields`_. - .. php:method:: addKey($key[, $primary = FALSE]) + .. php:method:: addKey($key[, $primary = FALSE[, $unique = FALSE]]) - :param array $key: Name of a key field + :param mixed $key: Name of a key field or an array of fields :param bool $primary: Set to TRUE if it should be a primary key or a regular one + :param bool $unique: Set to TRUE if it should be a unique key or a regular one :returns: \CodeIgniter\Database\Forge instance (method chaining) :rtype: \CodeIgniter\Database\Forge Adds a key to the set that will be used to create a table. Usage: See `Adding Keys`_. + .. php:method:: addPrimaryKey($key) + + :param mixed $key: Name of a key field or an array of fields + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge + + Adds a primary key to the set that will be used to create a table. Usage: See `Adding Keys`_. + + .. php:method:: addUniqueKey($key) + + :param mixed $key: Name of a key field or an array of fields + :returns: \CodeIgniter\Database\Forge instance (method chaining) + :rtype: \CodeIgniter\Database\Forge + + Adds an unique key to the set that will be used to create a table. Usage: See `Adding Keys`_. + .. php:method:: createDatabase($db_name) :param string $db_name: Name of the database to create diff --git a/user_guide_src/source/dbmgmt/index.rst b/user_guide_src/source/dbmgmt/index.rst new file mode 100644 index 000000000000..ef9e94e5b242 --- /dev/null +++ b/user_guide_src/source/dbmgmt/index.rst @@ -0,0 +1,12 @@ +################## +Managing Databases +################## + +CodeIgniter comes with tools to restructure or sees your database. + +.. toctree:: + :titlesonly: + + Database Manipulation with Database Forge + Database Migrations + Database Seeding diff --git a/user_guide_src/source/database/migration.rst b/user_guide_src/source/dbmgmt/migration.rst similarity index 54% rename from user_guide_src/source/database/migration.rst rename to user_guide_src/source/dbmgmt/migration.rst index 56ebbc17e920..f469a18a7868 100644 --- a/user_guide_src/source/database/migration.rst +++ b/user_guide_src/source/dbmgmt/migration.rst @@ -2,14 +2,14 @@ Database Migrations ################### -Migrations are a convenient way for you to alter your database in a -structured and organized manner. You could edit fragments of SQL by hand -but you would then be responsible for telling other developers that they -need to go and run them. You would also have to keep track of which changes +Migrations are a convenient way for you to alter your database in a +structured and organized manner. You could edit fragments of SQL by hand +but you would then be responsible for telling other developers that they +need to go and run them. You would also have to keep track of which changes need to be run against the production machines next time you deploy. -The database table **migration** tracks which migrations have already been -run so all you have to do is update your application files and +The database table **migration** tracks which migrations have already been +run so all you have to do is update your application files and call ``$migration->current()`` to work out which migrations should be run. The current version is found in **application/Config/Migrations.php**. @@ -48,34 +48,34 @@ name for the migration. For example: ****************** Create a Migration ****************** - -This will be the first migration for a new site which has a blog. All + +This will be the first migration for a new site which has a blog. All migrations go in the **application/Database/Migrations/** directory and have names such as *20121031100537_Add_blog.php*. :: - forge->addField(array( - 'blog_id' => array( - 'type' => 'INT', - 'constraint' => 5, - 'unsigned' => TRUE, + $this->forge->addField([ + 'blog_id' => [ + 'type' => 'INT', + 'constraint' => 5, + 'unsigned' => TRUE, 'auto_increment' => TRUE - ), - 'blog_title' => array( - 'type' => 'VARCHAR', - 'constraint' => '100', - ), - 'blog_description' => array( - 'type' => 'TEXT', - 'null' => TRUE, - ), - )); + ], + 'blog_title' => [ + 'type' => 'VARCHAR', + 'constraint' => '100', + ], + 'blog_description' => [ + 'type' => 'TEXT', + 'null' => TRUE, + ], + ]); $this->forge->addKey('blog_id', TRUE); $this->forge->createTable('blog'); } @@ -91,10 +91,13 @@ Then in **application/Config/Migrations.php** set ``$currentVersion = 2012103110 The database connection and the database Forge class are both available to you through ``$this->db`` and ``$this->forge``, respectively. +Alternatively, you can use a command-line call to generate a skeleton migration file. See +below for more details. + Using $currentVersion ===================== -The $currentVersion setting allows you to mark a location that your application should be set at. +The $currentVersion setting allows you to mark a location that your main application namespace should be set at. This is especially helpful for use in a production setting. In your application, you can always update the migration to the current version, and not latest to ensure your production and staging servers are running the correct schema. On your development servers, you can add additional migrations @@ -104,7 +107,7 @@ that your development machines are always running the bleeding edge schema. Database Groups =============== -A migration will only be ran against a single database group. If you have multiple groups defined in +A migration will only be run against a single database group. If you have multiple groups defined in **application/Config/Database.php**, then it will run against the ``$defaultGroup`` as specified in that same configuration file. There may be times when you need different schemas for different database groups. Perhaps you have one database that is used for all general site information, while @@ -121,16 +124,37 @@ match the name of the database group exactly:: public function down() { . . . } } +Namespaces +========== + +The migration library can automatically scan all namespaces you have defined within +**application/Config/Autoload.php** and its ``$psr4`` property for matching directory +names. It will include all migrations it finds in Database/Migrations. + +Each namespace has it's own version sequence, this will help you upgrade and downgrade each module (namespace) without affecting other namespaces. + +For example, assume that we have the the following namespaces defined in our Autoload +configuration file:: + + $psr4 = [ + 'App' => APPPATH, + 'MyCompany' => ROOTPATH.'MyCompany' + ]; + +This will look for any migrations located at both **APPPATH/Database/Migrations** and +**ROOTPATH/Database/Migrations**. This makes it simple to include migrations in your +re-usable, modular code suites. + ************* Usage Example ************* -In this example some simple code is placed in **application/controllers/Migrate.php** -to update the schema.:: +In this example some simple code is placed in **application/Controllers/Migrate.php** +to update the schema:: ` 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 +that wish to use them. The tools primarily provide access to the same methods that are available within the MigrationRunner class. **latest** Migrates all database groups to the latest available migrations:: - > php index.php migrations latest +> php spark migrate:latest + +You can use (latest) with the following options: + +- (-g) to chose database group, otherwise default database group will be used. +- (-n) to choose namespace, otherwise (App) namespace will be used. +- (all) to migrate all namespaces to the latest migration + +This example will migrate Blog namespace to latest:: + +> php spark migrate:latest -g test -n Blog **current** -Migrates all database groups to match the version set in ``$currentVersion``. This will migrate both -up and down as needed to match the specified version.:: +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 index.php migrations current +You can use (current) with the following options: + +- (-g) to chose database group, otherwise default database group will be used. **version** -Migrates all database groups to the specified version. If no version is provided, you will be prompted +Migrates to the specified version. If no version is provided, you will be prompted for the version. :: // Asks you for the version... - > php index.php migrations version + > php spark migrate:version > Version: // Sequential - > php index.php migrations version 007 + > php spark migrate:version 007 // Timestamp - > php index.php migrations version 20161426211300 + > php spark migrate:version 20161426211300 + +You can use (version) with the following options: + +- (-g) to chose database group, otherwise default database group will be used. +- (-n) to choose namespace, , otherwise (App) namespace will be used. **rollback** Rolls back all migrations, taking all database groups to a blank slate, effectively migration 0:: - > php index.php migrations rollback + > php spark migrate:rollback + +You can use (rollback) with the following options: + +- (-g) to chose database group, otherwise default database group will be used. +- (-n) to choose namespace, otherwise (App) namespace will be used. +- (all) to migrate all namespaces to the latest migration **refresh** -Refreshes the database state by first rolling back all migrations, and then migrating to the latest version.:: +Refreshes the database state by first rolling back all migrations, and then migrating to the latest version:: + + > php spark migrate:refresh + +You can use (refresh) with the following options: - > php index.php migrations refresh +- (-g) to chose database group, otherwise default database group will be used. +- (-n) to choose namespace, otherwise (App) namespace will be used. +- (all) to migrate all namespaces to the latest migration **status** -Displays a list of all migrations and the date and time they were ran, or '--' if they have not be ran.:: +Displays a list of all migrations and the date and time they ran, or '--' if they have not been run:: + + > php spark migrate:status + Filename Migrated On + First_migration.php 2016-04-25 04:44:22 + +You can use (refresh) with the following options: + +- (-g) to chose database group, otherwise default database group will be used. + +**create** - > php index.php migrations status - Filename Migrated On - 20150101101500_First_migration.php 2016-04-25 04:44:22 +Creates a skeleton migration file in **application/Database/Migrations** using the timestamp format:: + > php spark migrate:create [filename] + +You can use (create) with the following options: + +- (-n) to choose namespace, otherwise (App) namespace will be used. ********************* Migration Preferences @@ -217,10 +283,10 @@ The following is a table of all the config options for migrations, available in Preference Default Options Description ========================== ====================== ========================== ============================================================= **enabled** FALSE TRUE / FALSE Enable or disable migrations. -**path** APPPATH.'migrations/' None The path to your migrations folder. +**path** 'Database/Migrations/' None The path to your migrations folder. **currentVersion** 0 None The current version your database should use. **table** migrations None The table name for storing the schema version number. -**type** 'timestamp' 'timestamp' / 'sequential' The type of numeric identifier used to name migration files. +**type** 'timestamp' 'timestamp' / 'sequential' The type of numeric identifier used to name migration files. ========================== ====================== ========================== ============================================================= *************** @@ -229,8 +295,9 @@ Class Reference .. php:class:: CodeIgniter\Database\MigrationRunner - .. php:method:: current() + .. php:method:: current($group) + :param mixed $group: database group name, if null (App) namespace will be used. :returns: TRUE if no migrations are found, current version string on success, FALSE on failure :rtype: mixed @@ -244,35 +311,56 @@ Class Reference An array of migration filenames are returned that are found in the **path** property. - .. php:method:: latest() + .. php:method:: latest($namespace, $group) + :param mixed $namespace: application namespace, if null (App) namespace will be used. + :param mixed $group: database group name, if null default database group will be used. :returns: Current version string on success, FALSE on failure :rtype: mixed - This works much the same way as ``current()`` but instead of looking for + This works much the same way as ``current()`` but instead of looking for the ``$currentVersion`` the Migration class will use the very newest migration found in the filesystem. + .. php:method:: latestAll($group) - .. php:method:: version($target_version) + :param mixed $group: database group name, if null default database group will be used. + :returns: TRUE on success, FALSE on failure + :rtype: mixed + This works much the same way as ``latest()`` but instead of looking for + one namespace, the Migration class will use the very + newest migration found for all namespaces. + .. php:method:: version($target_version, $namespace, $group) + + :param mixed $namespace: application namespace, if null (App) namespace will be used. + :param mixed $group: database group name, if null default database group will be used. :param mixed $target_version: Migration version to process :returns: TRUE if no migrations are found, current version string on success, FALSE on failure :rtype: mixed - Version can be used to roll back changes or step forwards programmatically to + Version can be used to roll back changes or step forwards programmatically to specific versions. It works just like ``current()`` but ignores ``$currentVersion``. :: $migration->version(5); - .. php:method:: setPath($path) + .. php:method:: setNamespace($namespace) - :param string $path: The directory where migration files can be found. + :param string $namespace: application namespace. :returns: The current MigrationRunner instance :rtype: CodeIgniter\Database\MigrationRunner - Sets the path the library should look for migration files.:: + Sets the path the library should look for migration files:: - $migration->setPath($path) + $migration->setNamespace($path) ->latest(); + .. php:method:: setGroup($group) + :param string $group: database group name. + :returns: The current MigrationRunner instance + :rtype: CodeIgniter\Database\MigrationRunner + + Sets the path the library should look for migration files:: + + $migration->setNamespace($path) + ->latest(); diff --git a/user_guide_src/source/database/seeds.rst b/user_guide_src/source/dbmgmt/seeds.rst similarity index 84% rename from user_guide_src/source/database/seeds.rst rename to user_guide_src/source/dbmgmt/seeds.rst index 462796a10a6f..3b47ee41f824 100644 --- a/user_guide_src/source/database/seeds.rst +++ b/user_guide_src/source/dbmgmt/seeds.rst @@ -20,7 +20,7 @@ stored within the **application/Database/Seeds** directory. The name of the file { $data = [ 'username' => 'darth', - 'email' => 'darth@theempire.com' + 'email' => 'darth@theempire.com' ]; // Simple Queries @@ -49,6 +49,15 @@ but organize the tasks into separate seeder files:: } } +You can also use a fully-qualified class name in the **call()** method, allowing you to keep your seeders +anywhere the autoloader can find them. This is great for more modular code bases:: + + public function run() + { + $this->call('UserSeeder'); + $this->call('My\Database\Seeds\CountrySeeder'); + } + Using Seeders ============= @@ -63,5 +72,5 @@ Command Line Seeding You can also seed data from the command line, as part of the Migrations CLI tools, if you don't want to create a dedicated controller:: - > php index.php migrations seed TestSeeder + > php spark db:seed TestSeeder diff --git a/user_guide_src/source/general/core_classes.rst b/user_guide_src/source/extending/core_classes.rst similarity index 98% rename from user_guide_src/source/general/core_classes.rst rename to user_guide_src/source/extending/core_classes.rst index cd5f4d1dbb75..8829bfd50145 100644 --- a/user_guide_src/source/general/core_classes.rst +++ b/user_guide_src/source/extending/core_classes.rst @@ -23,7 +23,7 @@ The following is a list of the core system files that are invoked every time Cod * CodeIgniter\\Controller * CodeIgniter\\Debug\\Exceptions * CodeIgniter\\Debug\\Timer -* CodeIgniter\\Hooks\\Hooks +* CodeIgniter\\Events\\Events * CodeIgniter\\HTTP\\CLIRequest (if launched from command line only) * CodeIgniter\\HTTP\\IncomingRequest (if launched over HTTP) * CodeIgniter\\HTTP\\Request @@ -97,7 +97,7 @@ If you need to use a constructor in your class make sure you extend the parent c instead of the native ones (this is known as “method overriding”). This allows you to substantially alter the CodeIgniter core. If you are extending the Controller core class, then be sure to extend your new class in your application controller’s -constructors.:: +constructors:: class Home extends App\BaseController { diff --git a/user_guide_src/source/extending/events.rst b/user_guide_src/source/extending/events.rst new file mode 100644 index 000000000000..9e8fe0a630fa --- /dev/null +++ b/user_guide_src/source/extending/events.rst @@ -0,0 +1,112 @@ +##################################### +Events - Extending the Framework Core +##################################### + +CodeIgniter's Events feature provides a means to tap into and modify the inner workings of the framework without hacking +core files. When CodeIgniter runs it follows a specific execution process. There may be instances, however, when you'd +like to cause some action to take place at a particular stage in the execution process. For example, you might want to run +a script right before your controllers get loaded, or right after, or you might want to trigger one of your own scripts +in some other location. + +Events work on a *publish/subscribe* pattern, where an event, is triggered at some point during the script execution. +Other scripts can "subscribe" to that event by registering with the Events class to let it know they want to perform an +action when that event is triggered. + +Enabling Events +=============== + +Events are always enabled, and are available globally. + +Defining an Event +================= + +Most events are defined within the **application/Config/Events.php** file. You can subscribe an action to an event with +the Events class' ``on()`` method. The first parameter is the name of the event to subscribe to. The second parameter is +a callable that will be run when that event is triggered:: + + use CodeIgniter\Events\Events; + + Events::on('pre_system', ['MyClass', 'MyFunction']); + +In this example, whenever the **pre_controller** event is executed, an instance of ``MyClass`` is created and the +``MyFunction`` method is run. Note that the second parameter can be *any* form of +`callable `_ that PHP recognizes:: + + // Call a standalone function + Events::on('pre_system', 'some_function'); + + // Call on an instance method + $user = new User(); + Events::on('pre_system', [$user, 'some_method']); + + // Call on a static method + Events::on('pre_system', 'SomeClass::someMethod'); + + // Use a Closure + Events::on('pre_system', function(...$params) + { + . . . + }); + +Setting Priorities +------------------ + +Since multiple methods can be subscribed to a single event, you will need a way to define in what order those methods +are called. You can do this by passing a priority value as the third parameter of the ``on()`` method. Lower values +are executed first, with a value of 1 having the highest priority, and there being no limit on the lower values:: + + Events::on('post_controller_constructor', 'some_function', 25); + +Any subscribers with the same priority will be executed in the order they were defined. + +Three constants are defined for your use, that set some helpful ranges on the values. You are not required to use these +but you might find they aid readability:: + + define('EVENT_PRIORITY_LOW', 200); + define('EVENT_PRIORITY_NORMAL', 100); + define('EVENT_PRIORITY_HIGH', 10); + +Once sorted, all subscribers are executed in order. If any subscriber returns a boolean false value, then execution of +the subscribers will stop. + +Publishing your own Events +========================== + +The Events library makes it simple for you to create events in your own code, also. To use this feature, you would simply +need to call the ``trigger()`` method on the **Events** class with the name of the event:: + + \CodeIgniter\Events\Events::trigger('some_event'); + +You can pass any number of arguments to the subscribers by adding them as additional parameters. Subscribers will be +given the arguments in the same order as defined:: + + \CodeIgniter\Events\Events::trigger('some_events', $foo, $bar, $baz); + + Events::on('some_event', function($foo, $bar, $baz) { + ... + }); + +Simulating Events +================= + +During testing, you might not want the events to actually fire, as sending out hundreds of emails a day is both slow +and counter-productive. You can tell the Events class to only simulate running the events with the ``simulate()`` method. +When **true**, all events will be skipped over during the trigger method. Everything else will work as normal, though. + +:: + + Events::simulate(true); + +You can stop simulation by passing false:: + + Events::simulate(false); + +Event Points +============ + +The following is a list of available event points within the CodeIgniter core code: + +* **pre_system** Called very early during system execution. Only the benchmark and events class have been loaded at this point. No routing or other processes have happened. +* **post_controller_constructor** Called immediately after your controller is instantiated, but prior to any method calls happening. +* **post_system** Called after the final rendered page is sent to the browser, at the end of system execution after the finalized data is sent to the browser. + diff --git a/user_guide_src/source/extending/index.rst b/user_guide_src/source/extending/index.rst new file mode 100644 index 000000000000..adbf8b11cea3 --- /dev/null +++ b/user_guide_src/source/extending/index.rst @@ -0,0 +1,11 @@ +##################### +Extending CodeIgniter +##################### + +CodeIgniter 4 has been designed to be easy to extend or build upon. + +.. toctree:: + :titlesonly: + + core_classes + events diff --git a/user_guide_src/source/general/caching.rst b/user_guide_src/source/general/caching.rst new file mode 100644 index 000000000000..000900381fae --- /dev/null +++ b/user_guide_src/source/general/caching.rst @@ -0,0 +1,57 @@ +################ +Web Page Caching +################ + +CodeIgniter lets you cache your pages in order to achieve maximum +performance. + +Although CodeIgniter is quite fast, the amount of dynamic information +you display in your pages will correlate directly to the server +resources, memory, and processing cycles utilized, which affect your +page load speeds. By caching your pages, since they are saved in their +fully rendered state, you can achieve performance much closer to that of +static web pages. + +How Does Caching Work? +====================== + +Caching can be enabled on a per-page basis, and you can set the length +of time that a page should remain cached before being refreshed. When a +page is loaded for the first time, the file will be cached using the +currently configured cache engine. On subsequent page loads the cache file +will be retrieved and sent to the requesting user's browser. If it has +expired, it will be deleted and refreshed before being sent to the +browser. + +.. note:: The Benchmark tag is not cached so you can still view your page + load speed when caching is enabled. + +Enabling Caching +================ + +To enable caching, put the following tag in any of your controller +methods:: + + $this->cachePage($n); + +Where ``$n`` is the number of **seconds** you wish the page to remain +cached between refreshes. + +The above tag can go anywhere within a method. It is not affected by +the order that it appears, so place it wherever it seems most logical to +you. Once the tag is in place, your pages will begin being cached. + +.. important:: If you change configuration options that might affect + your output, you have to manually delete your cache files. + +.. note:: Before the cache files can be written you must set the cache + engine up by editing **application/Config/Cache.php**. + +Deleting Caches +=============== + +If you no longer wish to cache a file you can remove the caching tag and +it will no longer be refreshed when it expires. + +.. note:: Removing the tag will not delete the cache immediately. It will + have to expire normally. diff --git a/user_guide_src/source/general/common_functions.rst b/user_guide_src/source/general/common_functions.rst old mode 100644 new mode 100755 index e3daf5491af8..5338af2e7e29 --- a/user_guide_src/source/general/common_functions.rst +++ b/user_guide_src/source/general/common_functions.rst @@ -5,8 +5,10 @@ Global Functions and Constants CodeIgniter uses provides a few functions and variables that are globally defined, and are available to you at any point. These do not require loading any additional libraries or helpers. -.. contents:: Page Contents - :local: +.. contents:: + :local: + :depth: 2 + ================ Global Functions @@ -15,73 +17,140 @@ Global Functions Service Accessors ================= +.. php:function:: cache ( [$key] ) + + :param string $key: The cache name of the item to retrieve from cache (Optional) + :returns: Either the cache object, or the item retrieved from the cache + :rtype: mixed + + If no $key is provided, will return the Cache engine instance. If a $key + is provided, will return the value of $key as stored in the cache currently, + or false if no value is found. + + Examples:: + + $foo = cache('foo'); + $cache = cache(); + +.. php:function:: env ( $key[, $default=null]) + + :param string $key: The name of the environment variable to retrieve + :param mixed $default: The default value to return if no value is found. + :returns: The environment variable, the default value, or null. + :rtype: mixed + + Used to retrieve values that have previously been set to the environment, + or return a default value if it is not found. Will format boolean values + to actual booleans instead of string representations. + + Especially useful when used in conjunction with .env files for setting + values that are specific to the environment itself, like database + settings, API keys, etc. + .. php:function:: esc ( $data, $context='html' [, $encoding]) :param string|array $data: The information to be escaped. - :param string $context: The escaping context. Default is 'html'. - :param string $encoding: The character encoding of the string. - :returns: The escaped data. - :rtype: string + :param string $context: The escaping context. Default is 'html'. + :param string $encoding: The character encoding of the string. + :returns: The escaped data. + :rtype: string - Escapes data for inclusion in web pages, to help prevent XSS attacks. - This uses the Zend Escaper library to handle the actual filtering of the data. + Escapes data for inclusion in web pages, to help prevent XSS attacks. + This uses the Zend Escaper library to handle the actual filtering of the data. - If $data is a string, then it simply escapes and returns it. - If $data is an array, then it loops over it, escaping each 'value' of the key/value pairs. + If $data is a string, then it simply escapes and returns it. + If $data is an array, then it loops over it, escaping each 'value' of the key/value pairs. - Valid context values: html, js, css, url, attr, raw, null + Valid context values: html, js, css, url, attr, raw, null .. php:function:: helper( $filename ) - :param string $filename: The name of the helper file to load. + :param string|array $filename: The name of the helper file to load, or an array of names. + + Loads a helper file. + + For full details, see the :doc:`helpers` page. + +.. php:function:: lang(string $line[, array $args]): string + + :param string $line: The line of text to retrieve + :param array $args: An array of data to substitute for placeholders. + + Retrieves a locale-specific file based on an alias string. + + For more information, see the :doc:`Localization ` page. + +.. php:function:: old( $key[, $default = null, [, $escape = 'html' ]] ) + + :param string $key: The name of the old form data to check for. + :param mixed $default: The default value to return if $key doesn't exist. + :param mixed $escape: An `escape <#esc>`_ context or false to disable it. + :returns: The value of the defined key, or the default value. + :rtype: mixed + + Provides a simple way to access "old input data" from submitting a form. + + Example:: + + // in controller, checking form submittal + if (! $model->save($user)) + { + // 'withInput' is what specifies "old data" + // should be saved. + return redirect()->back()->withInput(); + } - Loads a helper file. + // In the view + + // Or with arrays + - For full details, see the :doc:`helpers` page. +.. note:: If you are using the :doc:`form helper `, this feature is built-in. You only + need to use this function when not using the form helper. .. php:function:: session( [$key] ) :param string $key: The name of the session item to check for. - :returns: An instance of the Session object if no $key, - the value found in the session for $key, or null. - :rtype: mixed + :returns: An instance of the Session object if no $key, the value found in the session for $key, or null. + :rtype: mixed - Provides a convenient way to access the session class and to retrieve a - stored value. For more information, see the :doc:`Sessions ` page. + Provides a convenient way to access the session class and to retrieve a + stored value. For more information, see the :doc:`Sessions ` page. .. php:function:: timer( [$name] ) :param string $name: The name of the benchmark point. - :returns: The Timer instance - :rtype: CodeIgniter\Debug\Timer + :returns: The Timer instance + :rtype: CodeIgniter\Debug\Timer - A convenience method that provides quick access to the Timer class. You can pass in the name - of a benchmark point as the only parameter. This will start timing from this point, or stop - timing if a timer with this name is already running. - :: + A convenience method that provides quick access to the Timer class. You can pass in the name + of a benchmark point as the only parameter. This will start timing from this point, or stop + timing if a timer with this name is already running. - // Get an instance - $timer = timer(); + Example:: + + // Get an instance + $timer = timer(); - // Set timer start and stop points - timer('controller_loading'); // Will start the timer - . . . - timer('controller_loading'); // Will stop the running timer + // Set timer start and stop points + timer('controller_loading'); // Will start the timer + . . . + timer('controller_loading'); // Will stop the running timer .. php:function:: view ($name [, $data [, $options ]]) :param string $name: The name of the file to load - :param array $data: An array of key/value pairs to make available within the view. - :param array $options: An array of options that will be passed to the rendering class. - :returns: The output from the view. - :rtype: string + :param array $data: An array of key/value pairs to make available within the view. + :param array $options: An array of options that will be passed to the rendering class. + :returns: The output from the view. + :rtype: string - Grabs the current RenderableInterface-compatible class - and tells it to render the specified view. Simply provides - a convenience method that can be used in Controllers, - libraries, and routed closures. + Grabs the current RendererInterface-compatible class + and tells it to render the specified view. Simply provides + a convenience method that can be used in Controllers, + libraries, and routed closures. - Currently, only one option is available for use within the `$options` array, `saveData` which specifies + Currently, only one option is available for use within the `$options` array, `saveData` which specifies that data will persistent between multiple calls to `view()` within the same request. By default, the data for that view is forgotten after displaying that single view file. @@ -94,7 +163,7 @@ Service Accessors echo view('user_profile', $data); - For more details, see the :doc:`Views ` page. + For more details, see the :doc:`Views ` page. Miscellaneous Functions ======================= @@ -102,32 +171,41 @@ Miscellaneous Functions .. php:function:: csrf_token () :returns: The name of the current CSRF token. - :rtype: string + :rtype: string - Returns the name of the current CSRF token. + Returns the name of the current CSRF token. .. php:function:: csrf_hash () :returns: The current value of the CSRF hash. - :rtype: string + :rtype: string + + Returns the current CSRF hash value. - Returns the current CSRF hash value. +.. php:function:: csrf_field () + + :returns: A string with the HTML for hidden input with all required CSRF information. + :rtype: string + + Returns a hidden input with the CSRF information already inserted: + + .. php:function:: force_https ( $duration = 31536000 [, $request = null [, $response = null]] ) :param int $duration: The number of seconds browsers should convert links to this resource to HTTPS. - :param RequestInterface $request: An instance of the current Request object. - :param ResponseInterface $response: An instance of the current Response object. + :param RequestInterface $request: An instance of the current Request object. + :param ResponseInterface $response: An instance of the current Response object. - Checks to see if the page is currently being accessed via HTTPS. If it is, then - nothing happens. If it is not, then the user is redirected back to the current URI - but through HTTPS. Will set the HTTP Strict Transport Security header, which instructs - modern browsers to automatically modify any HTTP requests to HTTPS requests for the $duration. + Checks to see if the page is currently being accessed via HTTPS. If it is, then + nothing happens. If it is not, then the user is redirected back to the current URI + but through HTTPS. Will set the HTTP Strict Transport Security header, which instructs + modern browsers to automatically modify any HTTP requests to HTTPS requests for the $duration. .. php:function:: is_cli () :returns: TRUE if the script is being executed from the command line or FALSE otherwise. - :rtype: bool + :rtype: bool .. php:function:: log_message ($level, $message [, array $context]) @@ -145,33 +223,41 @@ Miscellaneous Functions Context can be used to substitute values in the message string. For full details, see the :doc:`Logging Information ` page. -.. php:function:: redirect( $uri[, ...$params ] ) +.. php:function:: redirect( string $uri ) :param string $uri: The URI to redirect the user to. - :param mixed $params: one or more additional parameters that can be used with the :meth:`RouteCollection::reverseRoute` method. - Convenience method that works with the current global ``$request`` and - ``$router`` instances to redirect using named/reverse-routed routes - to determine the URL to go to. If nothing is found, will treat - as a traditional redirect and pass the string in, letting - ``$response->redirect()`` determine the correct method and code. + Returns a RedirectResponse instance allowing you to easily create redirects:: + + // Go back to the previous page + return redirect()->back(); - If more control is needed, you must use ``$response->redirect()`` explicitly. + // Go to specific UI + return redirect()->to('/admin'); + + // Go to a named/reverse-routed URI + return redirect()->route('named_route'); + + // Keep the old input values upon redirect so they can be used by the `old()` function + return redirect()->back()->withInput(); + + // Set a flash message + return redirect()->back()->with('foo', 'message'); .. php:function:: remove_invisible_characters($str[, $url_encoded = TRUE]) :param string $str: Input string - :param bool $url_encoded: Whether to remove URL-encoded characters as well - :returns: Sanitized string - :rtype: string + :param bool $url_encoded: Whether to remove URL-encoded characters as well + :returns: Sanitized string + :rtype: string - This function prevents inserting NULL characters between ASCII - characters, like Java\\0script. + This function prevents inserting NULL characters between ASCII + characters, like Java\\0script. - Example:: + Example:: - remove_invisible_characters('Java\\0script'); - // Returns: 'Javascript' + remove_invisible_characters('Java\\0script'); + // Returns: 'Javascript' .. php:function:: route_to ( $method [, ...$params] ) @@ -181,7 +267,7 @@ Miscellaneous Functions Generates a relative URI for you based on either a named route alias, or a controller::method combination. Will take parameters into effect, if provided. - For full details, see the :doc:`routing` page. + For full details, see the :doc:`/incoming/routing` page. .. php:function:: service ( $name [, ...$params] ) @@ -191,13 +277,15 @@ Miscellaneous Functions :rtype: mixed Provides easy access to any of the :doc:`Services <../concepts/services>` defined in the system. + This will always return a shared instance of the class, so no matter how many times this is called + during a single request, only one class instance will be created. Example:: $logger = service('logger'); $renderer = service('renderer', APPPATH.'views/'); -.. php:function:: shared_service ( $name [, ...$params] ) +.. php:function:: single_service ( $name [, ...$params] ) :param string $name: The name of the service to load :param mixed $params: One or more parameters to pass to the service method. @@ -205,9 +293,17 @@ Miscellaneous Functions :rtype: mixed Identical to the **service()** function described above, except that all calls to this - function will share the same instance of the service, where **service** returns a new + function will return a new instance of the class, where **service** returns the same instance every time. +.. php:function:: stringify_attributes ( $attributes [, $js] ) + + :param mixed $attributes: string, array of key value pairs, or object + :param boolean $js: TRUE if values do not need quotes (Javascript-style) + :returns: String containing the attribute key/value pairs, comma-separated + :rtype: string + + Helper function used to convert a string, array, or object of attributes to a string. ================ Global Constants @@ -216,7 +312,11 @@ Global Constants The following constants are always available anywhere within your application. Core Constants --------------- +============== + +.. php:const:: ROOTPATH + + The path to the main application directory. Just above ``public``. .. php:const:: APPPATH @@ -238,9 +338,8 @@ Core Constants The path to the **writable** directory. - Time Constants --------------- +============== .. php:const:: SECOND @@ -269,3 +368,7 @@ Time Constants .. php:const:: YEAR Equals 31536000. + +.. php:const:: DECADE + + Equals 315360000. diff --git a/user_guide_src/source/general/configuration.rst b/user_guide_src/source/general/configuration.rst index b44470e8574f..f07758f5c3c1 100644 --- a/user_guide_src/source/general/configuration.rst +++ b/user_guide_src/source/general/configuration.rst @@ -8,14 +8,28 @@ hold a class that contains its settings as public properties. Unlike in many oth there is no single class that you need to use to access your settings. Instead, you simply create an instance of the class and all your settings are there for you. +.. contents:: + :local: + :depth: 2 + Accessing Config Files ====================== -You can access config files within your classes by creating a new instance. All of the properties +You can access config files within your classes by creating a new instance or using the config function. All of the properties are public, so you access the settings like any other property:: - $config = new Config\EmailConfig(); - + // Creating new class by hand + $config = new \Config\EmailConfig(); + + // Creating new class with config function + $config = config( 'EmailConfig', false ); + + // Get shared instance with config function + $config = config( 'EmailConfig' ); + + // Access config class with namespace + $config = config( 'Config\\EmailConfig' ); + // Access settings as class properties $protocol = $config->protocol; $mailpath = $config->mailpath; @@ -37,14 +51,14 @@ If you need to create a new configuration file you would create a new file at yo **/application/Config** by default. Then create the class and fill it with public properties that represent your settings:: - 45, 'actual' => 72]; + } + } - // Namespaced for the 'Database' config file - database.db_name = "my_db" +With the above example, when `MySalesConfig` is instantiated, it will end up with +the two properties declared, but the value of the `$target` property will be over-ridden +by treating `RegionalSalesModel` as a "registrar". The resulting configuration properties:: + $target = 45; + $campaign = "Winter Wonderland"; diff --git a/user_guide_src/source/general/environments.rst b/user_guide_src/source/general/environments.rst index 9a4bf9762917..5fbfe4c534c1 100644 --- a/user_guide_src/source/general/environments.rst +++ b/user_guide_src/source/general/environments.rst @@ -13,30 +13,46 @@ The ENVIRONMENT Constant ======================== By default, CodeIgniter comes with the environment constant set to use -the value provided in ``$_SERVER['CI_ENV']``, otherwise defaulting to -'production'. At the top of index.php, you will see:: +the value provided in ``$_SERVER['CI_ENVIRONMENT']``, otherwise defaulting to +'production'. This can be set in several ways depending on your server setup. - define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development'); +.env +---- + +The simplest method to set the variable is in your :doc:`.env file `. + +.. code-block:: ini + + CI_ENVIRONMENT = development + +Apache +------ This server variable can be set in your ``.htaccess`` file, or Apache config using `SetEnv `_. -:: - SetEnv CI_ENV development +.. code-block:: apache + + SetEnv CI_ENVIRONMENT development + +nginx +----- Under nginx, you must pass the environment variable through the ``fastcgi_params`` in order for it to show up under the `$_SERVER` variable. This allows it to work on the virtual-host level, instead of using `env` to set it for the entire server, though that would work fine on a dedicated server. You would then modify your server config to something -like:: +like: + +.. code-block:: nginx server { server_name localhost; include conf/defaults.conf; root /var/www; - location ~* "\.php$" { - fastcgi_param CI_ENV "production"; + location ~* \.php$ { + fastcgi_param CI_ENVIRONMENT "production"; include conf/fastcgi-php.conf; } } @@ -55,7 +71,7 @@ Boot Files CodeIgniter requires that a PHP script matching the environment's name is located under **APPPATH/Config/Boot**. These files can contain any customizations that you would like to make for your environment, whether it's updating the error display -settings, loading addtional developer tools, or anything else. These are +settings, loading additional developer tools, or anything else. These are automatically loaded by the system. The following files are already created in a fresh install: @@ -76,7 +92,7 @@ Error Reporting Setting the ENVIRONMENT constant to a value of 'development' will cause all PHP errors to be rendered to the browser when they occur. Conversely, setting the constant to 'production' will disable all error -output. Disabling error reporting in production is a +output. Disabling error reporting in production is a :doc:`good security practice `. Configuration Files @@ -86,4 +102,4 @@ Optionally, you can have CodeIgniter load environment-specific configuration files. This may be useful for managing things like differing API keys across multiple environments. This is described in more detail in the Handling Different Environments section of the -:doc:`Working with Configuration Files ` documentation. \ No newline at end of file +:doc:`Working with Configuration Files ` documentation. diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index 99849019318a..7d6f2ab64112 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -8,6 +8,9 @@ default action when an error or exception is thrown is to display a detailed err is running under the ``production`` environment. In this case, a more generic message is displayed to keep the best user experience for your users. +.. contents:: + :local: + :depth: 2 Using Exceptions ================ @@ -19,7 +22,7 @@ execution is then sent to the error handler which displays the appropriate error throw new \Exception("Some message goes here"); -If you are calling a method that might throw an exception, you can catch that exception using a ``try/catch block``:: +If you are calling a method that might throw an exception, you can catch that exception using a ``try/catch`` block:: try { $user = $userModel->find($id); @@ -41,14 +44,14 @@ not child classes of the caught exception will be passed on to the error handler // do something here... } -This can be handy for handling the error yourself, or for performaing cleanup before the script ends. If you want +This can be handy for handling the error yourself, or for performing cleanup before the script ends. If you want the error handler to function as normal, you can throw a new exception within the catch block:: catch (\CodeIgniter\UnknownFileException $e) { // do something here... - throw new \RuntimeException($e->getMessage(), $e->getCode()); + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); } Configuration @@ -60,6 +63,27 @@ portion at the top of the main ``index.php`` file. .. important:: Disabling error reporting DOES NOT stop logs from being written if there are errors. +Logging Exceptions +------------------ + +By default, all Exceptions other than 404 - Page Not Found exceptions are logged. This can be turned on and off +by setting the **$log** value of ``Config\Exceptions``:: + + class Exceptions + { + public $log = true; + } + +To ignore logging on other status codes, you can set the status code to ignore in the same file:: + + class Exceptions + { + public $ignoredCodes = [ 404 ]; + } + +.. note:: It is possible that logging still will not happen for exceptions if your current Log settings + are not setup to log **critical** errors, which all exceptions are logged as. + Custom Exceptions ================= @@ -71,11 +95,11 @@ PageNotFoundException This is used to signal a 404, Page Not Found error. When thrown, the system will show the view found at ``/application/views/errors/html/error_404.php``. You should customize all of the error views for your site. If, in ``Config/Routes.php``, you have specified a 404 Override, that will be called instead of the standard -404 page.:: +404 page:: if (! $page = $pageModel->find($id)) { - throw new \CodeIgniter\PageNotFoundException(); + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } You can pass a message into the exception that will be displayed in place of the default message on the 404 page. @@ -84,54 +108,18 @@ ConfigException --------------- This exception should be used when the values from the configuration class are invalid, or when the config class -is not the right type, etc.:: +is not the right type, etc:: - throw new \CodeIgniter\ConfigException(); + throw new \CodeIgniter\Exceptions\ConfigException(); This provides an HTTP status code of 500, and an exit code of 3. -UnknownFileException --------------------- - -Use this exception when a file cannot be found.:: - - throw new \CodeIgniter\UnknownFileException(); - -This provides an HTTP status code of 500, and an exit code of 4. - -UnknownClassException ---------------------- - -Use this exception when a class cannot be found.:: - - throw new \CodeIgniter\UnknownClassException($className); - -This provides an HTTP status code of 500, and an exit code of 5. - -UnknownMethodException ----------------------- - -Use this exception when a class' method does not exist:: - - throw new \CodeIgniter\UnknownMethodException(); - -This provides an HTTP status code of 500, and an exit code of 6. - -UserInputException ------------------- - -Use this exception when the user's input is not valid.:: - - throw new \CodeIgniter\UserInputException(); - -This provides an HTTP status code of 500, and an exit code of 7. - DatabaseException ----------------- This exception is thrown for database errors, such as when the database connection cannot be created, or when it is temporarily lost:: - throw new \CodeIgniter\DatabaseException(); + throw new \CodeIgniter\Database\Exceptions\DatabaseException(); -This provides an HTTP status code of 500, and an exit code of 4. +This provides an HTTP status code of 500, and an exit code of 8. diff --git a/user_guide_src/source/general/helpers.rst b/user_guide_src/source/general/helpers.rst index b55d5e078cc2..09a6580001b3 100644 --- a/user_guide_src/source/general/helpers.rst +++ b/user_guide_src/source/general/helpers.rst @@ -9,6 +9,10 @@ you create form elements, **Text Helpers** perform various text formatting routines, **Cookie Helpers** set and read cookies, **File Helpers** help you deal with files, etc. +.. contents:: + :local: + :depth: 2 + Unlike most other systems in CodeIgniter, Helpers are not written in an Object Oriented format. They are simple, procedural functions. Each helper function performs one specific task, with no dependence on other @@ -16,8 +20,8 @@ functions. CodeIgniter does not load Helper Files by default, so the first step in using a Helper is to load it. Once loaded, it becomes globally available -in your :doc:`controller <../general/controllers>` and -:doc:`views <../general/views>`. +in your :doc:`controller ` and +:doc:`views `. Helpers are typically stored in your **system/Helpers**, or **application/Helpers directory**. CodeIgniter will look first in your @@ -35,10 +39,15 @@ Loading a helper file is quite simple using the following method:: Where **name** is the file name of the helper, without the .php file extension or the "helper" part. -For example, to load the **URL Helper** file, which is named -**url_helper.php**, you would do this:: +For example, to load the **Cookie Helper** file, which is named +**cookie_helper.php**, you would do this:: + + helper('cookie'); + +If you need to load more than one helper at a time, you can pass +an array of file names in and all of them will be loaded:: - helper('url'); + helper(['cookie', 'date']); A helper can be loaded anywhere within your controller methods (or even within your View files, although that's not a good practice), as @@ -50,6 +59,8 @@ it. .. note:: The Helper loading method above does not return a value, so don't try to assign it to a variable. Just use it as shown. +.. note:: The URL helper is always loaded so you do not need to load it yourself. + Loading from Non-standard Locations ----------------------------------- @@ -89,7 +100,7 @@ URI to the controller/method you wish to link to. "Extending" Helpers =================== -TODO: Determine how these can be extended... namespaces, etc? +@todo: Determine how these can be extended... namespaces, etc? To "extend" Helpers, create a file in your **application/helpers/** folder with an identical name to the existing Helper, but prefixed with **MY\_** @@ -132,9 +143,8 @@ functions:: return array_pop($array); } - Now What? ========= In the Table of Contents you'll find a list of all the available Helper -Files. Browse each one to see what they do. \ No newline at end of file +Files. Browse each one to see what they do. diff --git a/user_guide_src/source/general/hooks.rst b/user_guide_src/source/general/hooks.rst deleted file mode 100644 index 9c7a95e72649..000000000000 --- a/user_guide_src/source/general/hooks.rst +++ /dev/null @@ -1,98 +0,0 @@ -#################################### -Hooks - Extending the Framework Core -#################################### - -CodeIgniter's Hooks feature provides a means to tap into and modify the inner workings of the framework without hacking -core files. When CodeIgniter runs it follows a specific execution process. There may be instances, however, when you'd -like to cause some action to take place at a particular stage in the execution process. For example, you might want to run -a script right before your controllers get loaded, or right after, or you might want to trigger one of your own scripts -in some other location. - -Hooks work on a *publish/subscribe* pattern, where a hook, or event, is triggered at some point during the script execution. -Other scripts can "subscribe" to that event by registering with the Hooks class to let it know they want to perform an -action when that hook is triggered. - -Enabling Hooks -============== - -Hooks are always enabled, and are available globally. - -Defining a Hook -=============== - -Most hooks are defined within the **application/Config/Hooks.php** file. You can subscribe an action to a hook with -the Hooks class' ``on()`` method. The first parameter is the name of the hook to subscribe to. The second parameter is -a callable that will be run when that event is triggered.:: - - use CodeIgniter\Hooks\Hooks; - - Hooks::on('pre_controller', ['MyClass', 'MyFunction']); - -In this example, whenever the **pre_controller** hook is executed, an instance of ``MyClass`` is created and the -``MyFunction`` method is ran. Note that the second parameter can be *any* form of -`callable `_ that PHP recognizes:: - - // Call a standalone function - Hooks::on('pre_controller', 'some_function'); - - // Call on an instance method - $user = new User(); - Hooks::on('pre_controller', [$user, 'some_method']); - - // Call on a static method - Hooks::on('pre_controller', 'SomeClass::someMethod'); - - // Use a Closure - Hooks::on('pre_controller', function(...$params) - { - . . . - }); - -Setting Priorities ------------------- - -Since multiple methods can be subscribed to a single event, you will need a way to define in what order those methods -are called. You can do this by passing a priority value as the third parameter of the ``on()`` method. Lower values -are executed first, with a value of 1 having the highest priority, and there being no limit on the lower values.:: - - Hooks::on('pre_controller', 'some_function', 25); - -Any subscribers with the same priority will be executed in the order they were defined. - -Three constants are defined for your use, that set some helpful ranges on the values. You are not required to use these -but you might find they aid readability.:: - - define('HOOKS_PRIORITY_LOW', 200); - define('HOOKS_PRIORITY_NORMAL', 100); - define('HOOKS_PRIORITY_HIGH', 10); - -Once sorted, all subscribers are executed in order. If any subscriber returns a boolean false value, then execution of -the subscribers will stop. - -Publishing your own Hooks -========================= - -The Hooks library makes it simple for you to create hooks into your own code, also. To use this feature, you would simply -need to call the ``trigger()`` method on the **Hooks** class with the name of the hook:: - - \CodeIgniter\Hooks\Hooks::trigger('some_hook'); - -You can pass any number of arguments to the subscribers by adding them as additional parameters. Subscribers will be -given the arguments in the same order as defined.:: - - \CodeIgniter\Hooks\Hooks::trigger('some_hook', $foo, $bar, $baz); - - Hooks::on('some_hook', function($foo, $bar, $baz) { - ... - }); - -Hook Points -=========== - -The following is a list of available hook points: - -* **pre_system** Called very early during system execution. Only the benchmark and hooks class have been loaded at this point. No routing or other processes have happened. -* **pre_controller** Called immediately prior to any of your controllers being called. All base classes, routing, and security checks have been done. -* **post_controller_constructor** Called immediately after your controller is instantiated, but prior to any method calls happening. -* **post_controller** Called immediately after your controller is fully executed. -* **post_system** Called after the final rendered page is sent to the browser, at the end of system execution after the finalized data is sent to the browser. diff --git a/user_guide_src/source/general/index.rst b/user_guide_src/source/general/index.rst index eed5cd3e9f03..dcd667457fea 100644 --- a/user_guide_src/source/general/index.rst +++ b/user_guide_src/source/general/index.rst @@ -3,22 +3,15 @@ General Topics ############## .. toctree:: - :titlesonly: + :titlesonly: - configuration - urls - controllers - views - Helpers - core_classes - hooks - common_functions - routing - logging - errors - debugging - cli - managing_apps - environments - alternative_php - testing + configuration + urls + helpers + common_functions + logging + errors + caching + modules + managing_apps + environments diff --git a/user_guide_src/source/general/logging.rst b/user_guide_src/source/general/logging.rst index 639b9cc92983..1a73e66edf32 100644 --- a/user_guide_src/source/general/logging.rst +++ b/user_guide_src/source/general/logging.rst @@ -2,6 +2,10 @@ Logging Information ################### +.. contents:: + :local: + :depth: 2 + You can log information to the local log files by using the ``log_message()`` method. You must supply the "level" of the error in the first parameter, indicating what type of message it is (debug, error, etc). The second parameter is the message itself:: @@ -13,14 +17,14 @@ The second parameter is the message itself:: There are eight different log levels, matching to the `RFC 5424 `_ levels, and they are as follows: -* debug - Detailed debug information -* info - Interesting events in your application, like a user logging in, logging SQL queries, etc. -* notice - Normal, but significant events in your application. -* warning - Exceptional occurrences that are not errors, like the user of deprecated APIs, poor use of an API, or other undesirable things that are not necessarily wrong. -* error - Runtime errors that do not require immediate action but should typically be logged and monitored. -* critical - Critical conditions, like an application component not available, or an unexpected exception. -* alert - Action must be taken immediately, like when an entire website is down, the database unavailable, etc. -* emergency - The system is unusable. +* **debug** - Detailed debug information. +* **info** - Interesting events in your application, like a user logging in, logging SQL queries, etc. +* **notice** - Normal, but significant events in your application. +* **warning** - Exceptional occurrences that are not errors, like the user of deprecated APIs, poor use of an API, or other undesirable things that are not necessarily wrong. +* **error** - Runtime errors that do not require immediate action but should typically be logged and monitored. +* **critical** - Critical conditions, like an application component not available, or an unexpected exception. +* **alert** - Action must be taken immediately, like when an entire website is down, the database unavailable, etc. +* **emergency** - The system is unusable. The logging system does not provide ways to alert sysadmins or webmasters about these events, they solely log the information. For many of the more critical event levels, the logging happens automatically by the @@ -37,7 +41,7 @@ are requested to be logged by the application, but the threshold doesn't allow t ignored. The simplest method to use is to set this value to the minimum level that you want to have logged. For example, if you want to log debug messages, and not information messages, you would set the threshold to ``5``. Any log requests with a level of 5 or less (which includes runtime errors, system errors, etc) would be logged and info, notices, and warnings -would be ignored.:: +would be ignored:: public $threshold = 5; @@ -56,16 +60,16 @@ The logging system can support multiple methods of handling logging running at t be set to handle specific levels and ignore the rest. Currently, two handlers come with a default install: - **File Handler** is the default handler and will create a single file for every day locally. This is the - recommended method of logging. + recommended method of logging. - **ChromeLogger Handler** If you have the `ChromeLogger extension `_ - installed in the Chrome web browser, you can use this handler to display the log information in - Chrome's console window. + installed in the Chrome web browser, you can use this handler to display the log information in + Chrome's console window. The handlers are configured in the main configuration file, in the ``$handlers`` property, which is simply an array of handlers and their configuration. Each handler is specified with the key being the fully name-spaced class name. The value will be an array of varying properties, specific to each handler. Each handler's section will have one property in common: ``handles``, which is an array of log level -__names__ that the handler will log information for. +*names* that the handler will log information for. :: public $handlers = [ @@ -80,8 +84,6 @@ __names__ that the handler will log information for. ] ]; - - Modifying the Message With Context ================================== @@ -103,11 +105,11 @@ If you want to log an Exception or an Error, you can use the key of 'exception', Exception or Error itself. A string will be generated from that object containing the error message, the file name and line number. You must still provide the exception placeholder in the message:: - try + try { ... Something throws error here } - catch (\Exception #e) + catch (\Exception $e) { log_message('error', '[ERROR] {exception}', ['exception' => $e]); } @@ -132,7 +134,6 @@ Several core placeholders exist that will be automatically expanded for you base | {env:foo} | The value of 'foo' in $_ENV | +----------------+---------------------------------------------------+ - Using Third-Party Loggers ========================= @@ -141,8 +142,8 @@ You can use any other logger that you might like as long as it extends from eith that you can easily drop in use for any PSR3-compatible logger, or create your own. You must ensure that the third-party logger can be found by the system, by adding it to either -the ``/application/config/autoload.php`` configuration file, or through another autoloader, -like Composer. Next, you should modify ``/application/config/services.php`` to point the ``logger`` +the ``/application/Config/Autoload.php`` configuration file, or through another autoloader, +like Composer. Next, you should modify ``/application/Config/Services.php`` to point the ``logger`` alias to your new class name. Now, any call that is done through the ``log_message()`` function will use your library instead. @@ -155,6 +156,3 @@ the ``CodeIgniter\Log\LoggerAwareTrait`` which implements the ``setLogger()`` me Then, when you use your library under different environments for frameworks, your library should still be able to log as it would expect, as long as it can find a PSR3 compatible logger. - - - diff --git a/user_guide_src/source/general/managing_apps.rst b/user_guide_src/source/general/managing_apps.rst index c1e09c2fdaaf..3bde3b86d4e6 100644 --- a/user_guide_src/source/general/managing_apps.rst +++ b/user_guide_src/source/general/managing_apps.rst @@ -3,7 +3,7 @@ Managing your Applications ########################## By default it is assumed that you only intend to use CodeIgniter to -manage one application, which you will build in your **application/** +manage one application, which you will build in your **application** directory. It is possible, however, to have multiple sets of applications that share a single CodeIgniter installation, or even to rename or relocate your application directory. @@ -12,7 +12,7 @@ Renaming the Application Directory ================================== If you would like to rename your application directory you may do so -as long as you open your main **index.php** file and set its name using +as long as you open **application/Config/Paths.php** file and set its name using the ``$application_directory`` variable:: $application_directory = 'application'; diff --git a/user_guide_src/source/general/modules.rst b/user_guide_src/source/general/modules.rst new file mode 100644 index 000000000000..8024a65c3038 --- /dev/null +++ b/user_guide_src/source/general/modules.rst @@ -0,0 +1,189 @@ +############ +Code Modules +############ + +CodeIgniter supports a form of code modularization to help you create reusable code. Modules are typically +centered around a specific subject, and can be thought of as mini-applications within your larger application. Any +of the standard file types within the framework are supported, like controllers, models, views, config files, helpers, +language files, etc. Modules may contain as few, or as many, of these as you like. + +.. contents:: + :local: + :depth: 2 + +========== +Namespaces +========== + +The core element of the modules functionality comes from the :doc:`PSR4-compatible autoloading ` +that CodeIgniter uses. While any code can use the PSR4 autoloader and namespaces, the only way to take full advantage of +modules is to namespace your code and add it to **application/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 +folder with our company name, Acme, to store all of our modules within. We will put it right alongside our **application** +directory in the main project root:: + + /acme // New modules directory + /application + /public + /system + /tests + /writable + +Open **application/Config/Autoload.php** and add the **Acme** namespace to the ``psr4`` array property:: + + public $psr4 = [ + 'Acme' => ROOTPATH.'acme' + ]; + +Now that this is setup we can access any file within the **acme** folder through the ``Acme`` namespace. This alone +takes care of 80% of what is needed for modules to work, so you should be sure to familiarize yourself within namespaces +and become comfortable with their use. A number of the file types will be scanned for automatically through all defined +namespaces here, making this crucial to working with modules at all. + +A common directory structure within a module will mimic the main application folder:: + + /acme + /Blog + /Config + /Controllers + /Database + /Migrations + /Seeds + /Helpers + /Language + /en + /Libraries + /Models + /Views + +Of course, there is nothing forcing you to use this exact structure, and you should organize it in the manner that +best suits your module, leaving out directories you don't need, creating new directories for Entities, Interfaces, +or Repositories, etc. + +============== +Auto-Discovery +============== + +Many times, you will need to specify the full namespace to files you want to include, but CodeIgniter can be +configured to make integrating modules into your applications simpler by automatically discovering many different +file types, including: + +- :doc:`Events ` +- :doc:`Registrars ` +- :doc:`Route files ` +- :doc:`Services ` + +This is configured in the file **application/Config/Modules.php**. + +The auto-discovery system works by scanning any psr4 namespaces that have been defined within **Config/Autoload.php** +for familiar directories/files. + +When at the **acme** namespace above, we would need to make one small adjustment to make it so the files could be found: +each "module" within the namespace would have to have it's own namespace defined there. **Acme** would be changed +to **Acme\Blog**. Once your module folder has been defined, the discover process would look for a Routes file, for example, +at **/acme/Blog/Config/Routes.php**, just as if it was another application. + +Enable/Disable Discover +======================= + +You can turn on or off all auto-discovery in the system with the **$enabled** class variable. False will disable +all discovery, optimizing performance, but negating the special capabilities of your modules. + +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. + +================== +Working With Files +================== + +This section will take a look at each of the file types (controllers, views, language files, etc) and how they can +be used within the module. Some of this information is described in more detail in the relevant location of the user +guide, but is being reproduced here so that it's easier to grasp how all of the pieces fit together. + +Routes +====== + +By default, :doc:`routes ` are automatically scanned for within modules. If can be turned off in +the **Modules** config file, described above. + +.. note:: Since the files are being included into the current scope, the ``$routes`` instance is already defined for you. + It will cause errors if you attempt to redefine that class. + +Controllers +=========== + +Controllers outside of the main **application/Controllers** directory cannot be automatically routed by URI detection, +but must be specified within the Routes file itself:: + + // Routes.php + $routes->get('blog', 'Acme\Blog\Controllers\Blog::index'); + +To reduce the amount of typing needed here, the **group** routing feature is helpful:: + + $routes->group('blog', ['namespace' => 'Acme\Blog\Controllers'], function($routes) + { + $routes->get('/', 'Blog::index'); + }); + +Config Files +============ + +No special change is needed when working with configuration files. These are still namespaced classes and loaded +with the ``new`` command:: + + $config = new \Acme\Blog\Config\Blog(); + +Config files are automatically discovered whenever using the **config()** function that is always available. + +Migrations +========== + +Migration files will be automatically discovered within defined namespaces. All migrations found across all +namespaces will be run every time. + +Seeds +===== + +Seed files can be used from both the CLI and called from within other seed files as long as the full namespace +is provided. If calling on the CLI, you will need to provide double backslashes:: + + > php public/index.php migrations seed Acme\\Blog\\Database\\Seeds\\TestPostSeeder + +Helpers +======= + +Helpers will be located automatically from defined namespaces when using the ``helper()`` method, as long as it +is within the namespaces **Helpers** directory:: + + helper('blog'); + +Language Files +============== + +Language files are located automatically from defined namespaces when using the ``lang()`` method, as long as the +file follows the same directory structures as the main application directory. + +Libraries +========= + +Libraries are always instantiated by their fully-qualified class name, so no special access is provided:: + + $lib = new \Acme\Blog\Libraries\BlogLib(); + +Models +====== + +Models are always instantiated by their fully-qualified class name, so no special access is provided:: + + $model = new \Acme\Blog\Models\PostModel(); + +Views +===== + +Views can be loaded using the class namespace as described in the :doc:`views ` documentation:: + + echo view('Acme\Blog\Views\index'); diff --git a/user_guide_src/source/general/urls.rst b/user_guide_src/source/general/urls.rst index 9630db4b007e..5152e099140a 100644 --- a/user_guide_src/source/general/urls.rst +++ b/user_guide_src/source/general/urls.rst @@ -18,11 +18,10 @@ The segments in the URL, in following with the Model-View-Controller approach, u 2. The second segment represents the class **method** that should be called. 3. The third, and any additional segments, represent the ID and any variables that will be passed to the controller. -The :doc:`URL Library <../libraries/url>` and the :doc:`URL Helper <../helpers/url>` contain functions that make it easy -to work with your URI data. In addition, your URLs can be remapped using the :doc:`URI Routing ` +The :doc:`URI Library <../libraries/uri>` and the :doc:`URL Helper <../helpers/url_helper>` contain functions that make it easy +to work with your URI data. In addition, your URLs can be remapped using the :doc:`URI Routing ` feature for more flexibility. - Removing the index.php file =========================== @@ -38,7 +37,9 @@ Apache Web Server Apache must have the *mod_rewrite* extension enabled. If it does, you can use a ``.htaccess`` file with some simple rules. Here is an example of such a file, using the "negative" method in which everything is redirected except the specified -items:: +items: + +.. code-block:: apache RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f @@ -56,10 +57,12 @@ NGINX ----- Under NGINX, you can define a location block and use the ``try_files`` directive to get the same effect as we did with -the above Apache configuration:: +the above Apache configuration: + +.. code-block:: nginx location / { - try_files $uri $uri/ /index.php/$args; + try_files $uri $uri/ /index.php/$args; } This will first look for a file or directory matching the URI (constructing the full path to each file from the diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst new file mode 100644 index 000000000000..340b5ac1aed6 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper.rst @@ -0,0 +1,58 @@ +############ +Array Helper +############ + +The array helper provides several functions to simplify more complex usages of arrays. It is not intended to duplicate +any of the existing functionality that PHP provides - unless it is to vastly simplify their usage. + +.. contents:: + :local: + +Loading this Helper +=================== + +This helper is loaded using the following code:: + + helper('array'); + +Available Functions +=================== + +The following functions are available: + +.. php:function:: dot_array_search(string $search, array $values) + + :param string $search: The dot-notation string describing how to search the array + :param array $values: The array to search + :returns: The value found within the array, or null + :rtype: mixed + + This method allows you to use dot-notation to search through an array for a specific-key, + and allows the use of a the '*' wildcard. Given the following array:: + + $data = [ + 'foo' => [ + 'buzz' => [ + 'fizz' => 11 + ], + 'bar' => [ + 'baz' => 23 + ] + ] + ] + + We can locate the value of 'fizz' by using the search string "foo.buzz.fizz". Likewise, the value + of baz can be found with "foo.bar.baz":: + + // Returns: 11 + $fizz = dot_array_search('foo.buzz.fizz', $data); + + // Returns: 23 + $baz = dot_array_search('foo.bar.baz', $data); + + You can use the asterisk as a wildcard to replace any of the segments. When found, it will search through all + of the child nodes until it finds it. This is handy if you don't know the values, or if your values + have a numeric index:: + + // Returns: 23 + $baz = dot_array_search('foo.*.baz', $data); diff --git a/user_guide_src/source/helpers/cookie_helper.rst b/user_guide_src/source/helpers/cookie_helper.rst new file mode 100755 index 000000000000..e4af32deea1f --- /dev/null +++ b/user_guide_src/source/helpers/cookie_helper.rst @@ -0,0 +1,78 @@ +############# +Cookie Helper +############# + +The Cookie Helper file contains functions that assist in working with +cookies. + +.. contents:: + :local: + +.. raw:: html + +
            + +Loading this Helper +=================== + +This helper is loaded using the following code:: + + helper('cookie'); + +Available Functions +=================== + +The following functions are available: + +.. php:function:: set_cookie($name[, $value = ''[, $expire = ''[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httpOnly = false]]]]]]]) + + :param mixed $name: Cookie name *or* associative array of all of the parameters available to this function + :param string $value: Cookie value + :param int $expire: Number of seconds until expiration + :param string $domain: Cookie domain (usually: .yourdomain.com) + :param string $path: Cookie path + :param string $prefix: Cookie name prefix + :param bool $secure: Whether to only send the cookie through HTTPS + :param bool $httpOnly: Whether to hide the cookie from JavaScript + :rtype: void + + This helper function gives you friendlier syntax to set browser + cookies. Refer to the :doc:`Response Library ` for + a description of its use, as this function is an alias for + ``Response::setCookie()``. + +.. php:function:: get_cookie($index[, $xssClean = false]) + + :param string $index: Cookie name + :param bool $xss_clean: Whether to apply XSS filtering to the returned value + :returns: The cookie value or NULL if not found + :rtype: mixed + + This helper function gives you friendlier syntax to get browser + cookies. Refer to the :doc:`IncomingRequest Library ` for + detailed description of its use, as this function acts very + similarly to ``IncomingRequest::getCookie()``, except it will also prepend + the ``$cookiePrefix`` that you might've set in your + *application/Config/App.php* file. + +.. php:function:: delete_cookie($name[, $domain = ''[, $path = '/'[, $prefix = '']]]) + + :param string $name: Cookie name + :param string $domain: Cookie domain (usually: .yourdomain.com) + :param string $path: Cookie path + :param string $prefix: Cookie name prefix + :rtype: void + + Lets you delete a cookie. Unless you've set a custom path or other + values, only the name of the cookie is needed. + :: + + delete_cookie('name'); + + This function is otherwise identical to ``set_cookie()``, except that it + does not have the value and expiration parameters. You can submit an + array of values in the first parameter or you can set discrete + parameters. + :: + + delete_cookie($name, $domain, $path, $prefix); \ No newline at end of file diff --git a/user_guide_src/source/helpers/date_helper.rst b/user_guide_src/source/helpers/date_helper.rst new file mode 100644 index 000000000000..e6d451f4fee8 --- /dev/null +++ b/user_guide_src/source/helpers/date_helper.rst @@ -0,0 +1,47 @@ +########### +Date Helper +########### + +The Date Helper file contains functions that assist in working with +dates. + +.. contents:: + :local: + +.. raw:: html + +
            + +Loading this Helper +=================== + +This helper is loaded using the following code:: + + helper('date'); + +Available Functions +=================== + +The following functions are available: + +.. php:function:: now([$timezone = NULL]) + + :param string $timezone: Timezone + :returns: UNIX timestamp + :rtype: int + + Returns the current time as a UNIX timestamp, referenced either to your server's + local time or any PHP supported timezone, based on the "time reference" setting + in your config file. If you do not intend to set your master time reference to + any other PHP supported timezone (which you'll typically do if you run a site + that lets each user set their own timezone settings) there is no benefit to using + this function over PHP's ``time()`` function. + :: + + echo now('Australia/Victoria'); + + If a timezone is not provided, it will return ``time()`` based on the + **time_reference** setting. + +Many functions previously found in the CodeIgniter 3 ``date_helper`` have been moved to the ``I18n`` +module in CodeIgniter 4. diff --git a/user_guide_src/source/helpers/filesystem_helper.rst b/user_guide_src/source/helpers/filesystem_helper.rst new file mode 100644 index 000000000000..af9aebbc9c3d --- /dev/null +++ b/user_guide_src/source/helpers/filesystem_helper.rst @@ -0,0 +1,244 @@ +################# +Filesystem Helper +################# + +The Directory Helper file contains functions that assist in working with +directories. + +.. contents:: + :local: + +.. raw:: html + +
            + +Loading this Helper +=================== + +This helper is loaded using the following code: + +:: + + helper('filesystem'); + +Available Functions +=================== + +The following functions are available: + +.. php:function:: directory_map($source_dir[, $directory_depth = 0[, $hidden = FALSE]]) + + :param string $source_dir: Path to the source directory + :param int $directory_depth: Depth of directories to traverse (0 = fully recursive, 1 = current dir, etc) + :param bool $hidden: Whether to include hidden directories + :returns: An array of files + :rtype: array + + Examples:: + + $map = directory_map('./mydirectory/'); + + .. note:: Paths are almost always relative to your main index.php file. + + Sub-folders contained within the directory will be mapped as well. If + you wish to control the recursion depth, you can do so using the second + parameter (integer). A depth of 1 will only map the top level directory:: + + $map = directory_map('./mydirectory/', 1); + + By default, hidden files will not be included in the returned array. To + override this behavior, you may set a third parameter to true (boolean):: + + $map = directory_map('./mydirectory/', FALSE, TRUE); + + Each folder name will be an array index, while its contained files will + be numerically indexed. Here is an example of a typical array:: + + Array ( + [libraries] => Array + ( + [0] => benchmark.html + [1] => config.html + ["database/"] => Array + ( + [0] => query_builder.html + [1] => binds.html + [2] => configuration.html + [3] => connecting.html + [4] => examples.html + [5] => fields.html + [6] => index.html + [7] => queries.html + ) + [2] => email.html + [3] => file_uploading.html + [4] => image_lib.html + [5] => input.html + [6] => language.html + [7] => loader.html + [8] => pagination.html + [9] => uri.html + ) + + If no results are found, this will return an empty array. + +.. php:function:: write_file($path, $data[, $mode = 'wb']) + + :param string $path: File path + :param string $data: Data to write to file + :param string $mode: ``fopen()`` mode + :returns: TRUE if the write was successful, FALSE in case of an error + :rtype: bool + + Writes data to the file specified in the path. If the file does not exist then the + function will create it. + + Example:: + + $data = 'Some file data'; + if ( ! write_file('./path/to/file.php', $data)) + {      + echo 'Unable to write the file'; + } + else + {      + echo 'File written!'; + } + + You can optionally set the write mode via the third parameter:: + + write_file('./path/to/file.php', $data, 'r+'); + + The default mode is 'wb'. Please see the `PHP user guide `_ + for mode options. + + .. note:: In order for this function to write data to a file, its permissions must + be set such that it is writable. If the file does not already exist, + then the directory containing it must be writable. + + .. note:: The path is relative to your main site index.php file, NOT your + controller or view files. CodeIgniter uses a front controller so paths + are always relative to the main site index. + + .. note:: This function acquires an exclusive lock on the file while writing to it. + +.. php:function:: delete_files($path[, $del_dir = FALSE[, $htdocs = FALSE]]) + + :param string $path: Directory path + :param bool $del_dir: Whether to also delete directories + :param bool $htdocs: Whether to skip deleting .htaccess and index page files + :returns: TRUE on success, FALSE in case of an error + :rtype: bool + + Deletes ALL files contained in the supplied path. + + Example:: + + delete_files('./path/to/directory/'); + + If the second parameter is set to TRUE, any directories contained within the supplied + root path will be deleted as well. + + Example:: + + delete_files('./path/to/directory/', TRUE); + + .. note:: The files must be writable or owned by the system in order to be deleted. + +.. php:function:: get_filenames($source_dir[, $include_path = FALSE]) + + :param string $source_dir: Directory path + :param bool $include_path: Whether to include the path as part of the filenames + :returns: An array of file names + :rtype: array + + Takes a server path as input and returns an array containing the names of all files + contained within it. The file path can optionally be added to the file names by setting + the second parameter to TRUE. + + Example:: + + $controllers = get_filenames(APPPATH.'controllers/'); + +.. php:function:: get_dir_file_info($source_dir, $top_level_only) + + :param string $source_dir: Directory path + :param bool $top_level_only: Whether to look only at the specified directory (excluding sub-directories) + :returns: An array containing info on the supplied directory's contents + :rtype: array + + Reads the specified directory and builds an array containing the filenames, filesize, + dates, and permissions. Sub-folders contained within the specified path are only read + if forced by sending the second parameter to FALSE, as this can be an intensive + operation. + + Example:: + + $models_info = get_dir_file_info(APPPATH.'models/'); + +.. php:function:: get_file_info($file[, $returned_values = array('name', 'server_path', 'size', 'date')]) + + :param string $file: File path + :param array $returned_values: What type of info to return + :returns: An array containing info on the specified file or FALSE on failure + :rtype: array + + Given a file and path, returns (optionally) the *name*, *path*, *size* and *date modified* + information attributes for a file. Second parameter allows you to explicitly declare what + information you want returned. + + Valid ``$returned_values`` options are: `name`, `size`, `date`, `readable`, `writeable`, + `executable` and `fileperms`. + +.. php:function:: symbolic_permissions($perms) + + :param int $perms: Permissions + :returns: Symbolic permissions string + :rtype: string + + Takes numeric permissions (such as is returned by ``fileperms()``) and returns + standard symbolic notation of file permissions. + + :: + + echo symbolic_permissions(fileperms('./index.php')); // -rw-r--r-- + +.. php:function:: octal_permissions($perms) + + :param int $perms: Permissions + :returns: Octal permissions string + :rtype: string + + Takes numeric permissions (such as is returned by ``fileperms()``) and returns + a three character octal notation of file permissions. + + :: + + echo octal_permissions(fileperms('./index.php')); // 644 + +.. php:function:: set_realpath($path[, $check_existance = FALSE]) + + :param string $path: Path + :param bool $check_existance: Whether to check if the path actually exists + :returns: An absolute path + :rtype: string + + This function will return a server path without symbolic links or + relative directory structures. An optional second argument will + cause an error to be triggered if the path cannot be resolved. + + Examples:: + + $file = '/etc/php5/apache2/php.ini'; + echo set_realpath($file); // Prints '/etc/php5/apache2/php.ini' + + $non_existent_file = '/path/to/non-exist-file.txt'; + echo set_realpath($non_existent_file, TRUE); // Shows an error, as the path cannot be resolved + echo set_realpath($non_existent_file, FALSE); // Prints '/path/to/non-exist-file.txt' + + $directory = '/etc/php5'; + echo set_realpath($directory); // Prints '/etc/php5/' + + $non_existent_directory = '/path/to/nowhere'; + echo set_realpath($non_existent_directory, TRUE); // Shows an error, as the path cannot be resolved + echo set_realpath($non_existent_directory, FALSE); // Prints '/path/to/nowhere' diff --git a/user_guide_src/source/helpers/form_helper.rst b/user_guide_src/source/helpers/form_helper.rst new file mode 100644 index 000000000000..2c7b4f9f4e27 --- /dev/null +++ b/user_guide_src/source/helpers/form_helper.rst @@ -0,0 +1,676 @@ +########### +Form Helper +########### + +The Form Helper file contains functions that assist in working with +forms. + +.. contents:: + :local: + +.. raw:: html + +
            + +Loading this Helper +=================== + +This helper is loaded using the following code:: + + helper('form'); + +Escaping field values +===================== + +You may need to use HTML and characters such as quotes within your form +elements. In order to do that safely, you'll need to use +:doc:`common function <../general/common_functions>` +:func:`esc()`. + +Consider the following example:: + + $string = 'Here is a string containing "quoted" text.'; + + + +Since the above string contains a set of quotes, it will cause the form +to break. The :php:func:`esc()` function converts HTML special +characters so that it can be used safely:: + + + +.. note:: If you use any of the form helper functions listed on this page, + and you pass values as an associative array, + the form values will be automatically escaped, so there is no need + to call this function. Use it only if you are creating your own + form elements, which you would pass as strings. + +Available Functions +=================== + +The following functions are available: + +.. php:function:: form_open([$action = ''[, $attributes = ''[, $hidden = array()]]]) + + :param string $action: Form action/target URI string + :param mixed $attributes: HTML attributes, as an array or escaped string + :param array $hidden: An array of hidden fields' definitions + :returns: An HTML form opening tag + :rtype: string + + Creates an opening form tag with a base URL **built from your config preferences**. + It will optionally let you add form attributes and hidden input fields, and + will always add the `accept-charset` attribute based on the charset value in your + config file. + + The main benefit of using this tag rather than hard coding your own HTML is that + it permits your site to be more portable in the event your URLs ever change. + + Here's a simple example:: + + echo form_open('email/send'); + + The above example would create a form that points to your base URL plus the + "email/send" URI segments, like this:: + +
            + + **Adding Attributes** + + Attributes can be added by passing an associative array to the second + parameter, like this:: + + $attributes = array('class' => 'email', 'id' => 'myform'); + echo form_open('email/send', $attributes); + + Alternatively, you can specify the second parameter as a string:: + + echo form_open('email/send', 'class="email" id="myform"'); + + The above examples would create a form similar to this:: + + + + **Adding Hidden Input Fields** + + Hidden fields can be added by passing an associative array to the + third parameter, like this:: + + $hidden = array('username' => 'Joe', 'member_id' => '234'); + echo form_open('email/send', '', $hidden); + + You can skip the second parameter by passing any false value to it. + + The above example would create a form similar to this:: + + + + + +.. php:function:: form_open_multipart([$action = ''[, $attributes = ''[, $hidden = array()]]]) + + :param string $action: Form action/target URI string + :param mixed $attributes: HTML attributes, as an array or escaped string + :param array $hidden: An array of hidden fields' definitions + :returns: An HTML multipart form opening tag + :rtype: string + + This function is identical to :php:func:`form_open()` above, + except that it adds a *multipart* attribute, which is necessary if you + would like to use the form to upload files with. + +.. php:function:: form_hidden($name[, $value = '']) + + :param string $name: Field name + :param string $value: Field value + :returns: An HTML hidden input field tag + :rtype: string + + Lets you generate hidden input fields. You can either submit a + name/value string to create one field:: + + form_hidden('username', 'johndoe'); + // Would produce: + + ... or you can submit an associative array to create multiple fields:: + + $data = array( + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'url' => 'http://example.com' + ); + + echo form_hidden($data); + + /* + Would produce: + + + + */ + + You can also pass an associative array to the value field:: + + $data = array( + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'url' => 'http://example.com' + ); + + echo form_hidden('my_array', $data); + + /* + Would produce: + + + + + */ + + If you want to create hidden input fields with extra attributes:: + + $data = array( + 'type' => 'hidden', + 'name' => 'email', + 'id' => 'hiddenemail', + 'value' => 'john@example.com', + 'class' => 'hiddenemail' + ); + + echo form_input($data); + + /* + Would produce: + + + */ + +.. php:function:: form_input([$data = ''[, $value = ''[, $extra = ''[, $type = 'text']]]]) + + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :param string $type: The type of input field. i.e. 'text', 'email', 'number', etc. + :returns: An HTML text input field tag + :rtype: string + + Lets you generate a standard text input field. You can minimally pass + the field name and value in the first and second parameter:: + + echo form_input('username', 'johndoe'); + + Or you can pass an associative array containing any data you wish your + form to contain:: + + $data = array( + 'name' => 'username', + 'id' => 'username', + 'value' => 'johndoe', + 'maxlength' => '100', + 'size' => '50', + 'style' => 'width:50%' + ); + + echo form_input($data); + + /* + Would produce: + + + */ + + If you would like your form to contain some additional data, like + JavaScript, you can pass it as a string in the third parameter:: + + $js = 'onClick="some_function()"'; + echo form_input('username', 'johndoe', $js); + + Or you can pass it as an array:: + + $js = array('onClick' => 'some_function();'); + echo form_input('username', 'johndoe', $js); + + To support the expanded range of HTML5 input fields, you can pass an input type in as the fourth parameter:: + + echo form_input('email', 'joe@example.com', ['placeholder' => 'Email Address...'], 'email'); + + /* + Would produce: + + + */ + +.. php:function:: form_password([$data = ''[, $value = ''[, $extra = '']]]) + + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML password input field tag + :rtype: string + + This function is identical in all respects to the :php:func:`form_input()` + function above except that it uses the "password" input type. + +.. php:function:: form_upload([$data = ''[, $value = ''[, $extra = '']]]) + + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML file upload input field tag + :rtype: string + + This function is identical in all respects to the :php:func:`form_input()` + function above except that it uses the "file" input type, allowing it to + be used to upload files. + +.. php:function:: form_textarea([$data = ''[, $value = ''[, $extra = '']]]) + + :param array $data: Field attributes data + :param string $value: Field value + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML textarea tag + :rtype: string + + This function is identical in all respects to the :php:func:`form_input()` + function above except that it generates a "textarea" type. + + .. note:: Instead of the *maxlength* and *size* attributes in the above example, + you will instead specify *rows* and *cols*. + +.. php:function:: form_dropdown([$name = ''[, $options = array()[, $selected = array()[, $extra = '']]]]) + + :param string $name: Field name + :param array $options: An associative array of options to be listed + :param array $selected: List of fields to mark with the *selected* attribute + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML dropdown select field tag + :rtype: string + + Lets you create a standard drop-down field. The first parameter will + contain the name of the field, the second parameter will contain an + associative array of options, and the third parameter will contain the + value you wish to be selected. You can also pass an array of multiple + items through the third parameter, and the helper will create a + multiple select for you. + + Example:: + + $options = array( + 'small' => 'Small Shirt', + 'med' => 'Medium Shirt', + 'large' => 'Large Shirt', + 'xlarge' => 'Extra Large Shirt', + ); + + $shirts_on_sale = array('small', 'large'); + echo form_dropdown('shirts', $options, 'large'); + + /* + Would produce: + + + */ + + echo form_dropdown('shirts', $options, $shirts_on_sale); + + /* + Would produce: + + + */ + + If you would like the opening + + The third parameter contains a boolean TRUE/FALSE to determine whether + the box should be checked or not. + + Similar to the other form functions in this helper, you can also pass an + array of attributes to the function:: + + $data = array( + 'name' => 'newsletter', + 'id' => 'newsletter', + 'value' => 'accept', + 'checked' => TRUE, + 'style' => 'margin:10px' + ); + + echo form_checkbox($data); + // Would produce: + + Also as with other functions, if you would like the tag to contain + additional data like JavaScript, you can pass it as a string in the + fourth parameter:: + + $js = 'onClick="some_function()"'; + echo form_checkbox('newsletter', 'accept', TRUE, $js); + + Or you can pass it as an array:: + + $js = array('onClick' => 'some_function();'); + echo form_checkbox('newsletter', 'accept', TRUE, $js); + +.. php:function:: form_radio([$data = ''[, $value = ''[, $checked = FALSE[, $extra = '']]]]) + + :param array $data: Field attributes data + :param string $value: Field value + :param bool $checked: Whether to mark the radio button as being *checked* + :param mixed $extra: Extra attributes to be added to the tag either as an array or a literal string + :returns: An HTML radio input tag + :rtype: string + + This function is identical in all respects to the :php:func:`form_checkbox()` + function above except that it uses the "radio" input type. + +.. php:function:: form_label([$label_text = ''[, $id = ''[, $attributes = array()]]]) + + :param string $label_text: Text to put in the
            '; + echo form_close($string); + // Would produce: + +.. php:function:: set_value($field[, $default = ''[, $html_escape = TRUE]]) + + :param string $field: Field name + :param string $default: Default value + :param bool $html_escape: Whether to turn off HTML escaping of the value + :returns: Field value + :rtype: string + + Permits you to set the value of an input form or textarea. You must + supply the field name via the first parameter of the function. The + second (optional) parameter allows you to set a default value for the + form. The third (optional) parameter allows you to turn off HTML escaping + of the value, in case you need to use this function in combination with + i.e. :php:func:`form_input()` and avoid double-escaping. + + Example:: + + + + The above form will show "0" when loaded for the first time. + + .. note:: If you've loaded the :doc:`Form Validation Library <../libraries/validation>` and + have set a validation rule for the field name in use with this helper, then it will + forward the call to the :doc:`Form Validation Library <../libraries/validation>`'s + own ``set_value()`` method. Otherwise, this function looks in ``$_POST`` for the + field value. + +.. php:function:: set_select($field[, $value = ''[, $default = FALSE]]) + + :param string $field: Field name + :param string $value: Value to check for + :param string $default: Whether the value is also a default one + :returns: 'selected' attribute or an empty string + :rtype: string + + If you use a + + + + + +.. php:function:: set_checkbox($field[, $value = ''[, $default = FALSE]]) + + :param string $field: Field name + :param string $value: Value to check for + :param string $default: Whether the value is also a default one + :returns: 'checked' attribute or an empty string + :rtype: string + + Permits you to display a checkbox in the state it was submitted. + + The first parameter must contain the name of the checkbox, the second + parameter must contain its value, and the third (optional) parameter + lets you set an item as the default (use boolean TRUE/FALSE). + + Example:: + + /> + /> + +.. php:function:: set_radio($field[, $value = ''[, $default = FALSE]]) + + :param string $field: Field name + :param string $value: Value to check for + :param string $default: Whether the value is also a default one + :returns: 'checked' attribute or an empty string + :rtype: string + + Permits you to display radio buttons in the state they were submitted. + This function is identical to the :php:func:`set_checkbox()` function above. + + Example:: + + /> + /> + + .. note:: If you are using the Form Validation class, you must always specify + a rule for your field, even if empty, in order for the ``set_*()`` + functions to work. This is because if a Form Validation object is + defined, the control for ``set_*()`` is handed over to a method of the + class instead of the generic helper function. diff --git a/user_guide_src/source/helpers/html_helper.rst b/user_guide_src/source/helpers/html_helper.rst new file mode 100755 index 000000000000..d5ad51f6f23a --- /dev/null +++ b/user_guide_src/source/helpers/html_helper.rst @@ -0,0 +1,480 @@ +########### +HTML Helper +########### + +The HTML Helper file contains functions that assist in working with +HTML. + +.. contents:: + :local: + +.. raw:: html + +
            + +Loading this Helper +=================== + +This helper is loaded using the following code:: + + helper('html'); + +Available Functions +=================== + +The following functions are available: + +.. php:function:: img([$src = ''[, $indexPage = false[, $attributes = '']]]) + + :param mixed $src: Image source data + :param bool $indexPage: Whether to treat $src as a routed URI string + :param mixed $attributes: HTML attributes + :returns: HTML image tag + :rtype: string + + Lets you create HTML tags. The first parameter contains the + image source. Example:: + + echo img('images/picture.jpg'); + // + + There is an optional second parameter that is a true/false value that + specifics if the *src* should have the page specified by + ``$config['indexPage']`` added to the address it creates. + Presumably, this would be if you were using a media controller:: + + echo img('images/picture.jpg', true); + // + + Additionally, an associative array can be passed as the first parameter, + for complete control over all attributes and values. If an *alt* attribute + is not provided, CodeIgniter will generate an empty string. + + Example:: + + $imageProperties = array( + 'src' => 'images/picture.jpg', + 'alt' => 'Me, demonstrating how to eat 4 slices of pizza at one time', + 'class' => 'post_images', + 'width' => '200', + 'height' => '200', + 'title' => 'That was quite a night', + 'rel' => 'lightbox' + ); + + img($imageProperties); + // Me, demonstrating how to eat 4 slices of pizza at one time + +.. php:function:: link_tag([$href = ''[, $rel = 'stylesheet'[, $type = 'text/css'[, $title = ''[, $media = ''[, $indexPage = false]]]]]]) + + :param string $href: The source of the link file + :param string $rel: Relation type + :param string $type: Type of the related document + :param string $title: Link title + :param string $media: Media type + :param bool $indexPage: Whether to treat $src as a routed URI string + :returns: HTML link tag + :rtype: string + + Lets you create HTML tags. This is useful for stylesheet links, + as well as other links. The parameters are *href*, with optional *rel*, + *type*, *title*, *media* and *indexPage*. + + *indexPage* is a boolean value that specifies if the *href* should have + the page specified by ``$config['indexPage']`` added to the address it creates. + + Example:: + + echo link_tag('css/mystyles.css'); + // + + Further examples:: + + echo link_tag('favicon.ico', 'shortcut icon', 'image/ico'); + // + + echo link_tag('feed', 'alternate', 'application/rss+xml', 'My RSS Feed'); + // + + Alternately, an associative array can be passed to the ``link_tag()`` function + for complete control over all attributes and values:: + + $link = array( + 'href' => 'css/printer.css', + 'rel' => 'stylesheet', + 'type' => 'text/css', + 'media' => 'print' + ); + + echo link_tag($link); + // + +.. php:function:: script_tag([$src = ''[, $indexPage = false]]) + + :param mixed $src: The source name of a JavaScript file + :param bool $indexPage: Whether to treat $src as a routed URI string + :returns: HTML script tag + :rtype: string + + Lets you create HTML tags. The parameters is *src*, with optional *indexPage*. + + *indexPage* is a boolean value that specifies if the *src* should have + the page specified by ``$config['indexPage']`` added to the address it creates. + + Example:: + + echo script_tag('js/mystyles.js'); + // + + Alternately, an associative array can be passed to the ``script_tag()`` function + for complete control over all attributes and values:: + + $script = array('src' => 'js/printer.js'); + + echo script_tag($script); + // + +.. php:function:: ul($list[, $attributes = '']) + + :param array $list: List entries + :param array $attributes: HTML attributes + :returns: HTML-formatted unordered list + :rtype: string + + Permits you to generate unordered HTML lists from simple or + multi-dimensional arrays. Example:: + + $list = array( + 'red', + 'blue', + 'green', + 'yellow' + ); + + $attributes = array( + 'class' => 'boldlist', + 'id' => 'mylist' + ); + + echo ul($list, $attributes); + + The above code will produce this: + + .. code-block:: html + +
              +
            • red
            • +
            • blue
            • +
            • green
            • +
            • yellow
            • +
            + + Here is a more complex example, using a multi-dimensional array:: + + $attributes = array( + 'class' => 'boldlist', + 'id' => 'mylist' + ); + + $list = array( + 'colors' => array( + 'red', + 'blue', + 'green' + ), + 'shapes' => array( + 'round', + 'square', + 'circles' => array( + 'ellipse', + 'oval', + 'sphere' + ) + ), + 'moods' => array( + 'happy', + 'upset' => array( + 'defeated' => array( + 'dejected', + 'disheartened', + 'depressed' + ), + 'annoyed', + 'cross', + 'angry' + ) + ) + ); + + echo ul($list, $attributes); + + The above code will produce this: + + .. code-block:: html + +
              +
            • colors +
                +
              • red
              • +
              • blue
              • +
              • green
              • +
              +
            • +
            • shapes +
                +
              • round
              • +
              • suare
              • +
              • circles +
                  +
                • elipse
                • +
                • oval
                • +
                • sphere
                • +
                +
              • +
              +
            • +
            • moods +
                +
              • happy
              • +
              • upset +
                  +
                • defeated +
                    +
                  • dejected
                  • +
                  • disheartened
                  • +
                  • depressed
                  • +
                  +
                • +
                • annoyed
                • +
                • cross
                • +
                • angry
                • +
                +
              • +
              +
            • +
            + +.. php:function:: ol($list, $attributes = '') + + :param array $list: List entries + :param array $attributes: HTML attributes + :returns: HTML-formatted ordered list + :rtype: string + + Identical to :php:func:`ul()`, only it produces the
              tag for + ordered lists instead of
                . + +.. php:function:: video($src[, $unsupportedMessage = ''[, $attributes = ''[, $tracks = [][, $indexPage = false]]]]) + + :param mixed $src: Either a source string or an array of sources. See :php:func:`source()` function + :param string $unsupportedMessage: The message to display if the media tag is not supported by the browser + :param string $attributes: HTML attributes + :param array $tracks: Use the track function inside an array. See :php:func:`track()` function + :param bool $indexPage: + :returns: HTML-formatted video element + :rtype: string + + Permits you to generate HTML video element from simple or + source arrays. Example:: + + $tracks = + [ + track('subtitles_no.vtt', 'subtitles', 'no', 'Norwegian No'), + track('subtitles_yes.vtt', 'subtitles', 'yes', 'Norwegian Yes') + ]; + + echo video('test.mp4', 'Your browser does not support the video tag.', 'controls'); + + echo video + ( + 'http://www.codeigniter.com/test.mp4', + 'Your browser does not support the video tag.', + 'controls', + $tracks + ); + + echo video + ( + [ + source('movie.mp4', 'video/mp4', 'class="test"'), + source('movie.ogg', 'video/ogg'), + source('movie.mov', 'video/quicktime'), + source('movie.ogv', 'video/ogv; codecs=dirac, speex') + ], + 'Your browser does not support the video tag.', + 'class="test" controls', + $tracks + ); + + The above code will produce this: + + .. code-block:: html + + + + + + + +.. php:function:: audio($src[, $unsupportedMessage = ''[, $attributes = ''[, $tracks = [][, $indexPage = false]]]]) + + :param mixed $src: Either a source string or an array of sources. See :php:func:`source()` function + :param string $unsupportedMessage: The message to display if the media tag is not supported by the browser + :param string $attributes: + :param array $tracks: Use the track function inside an array. See :php:func:`track()` function + :param bool $indexPage: + :returns: HTML-formatted audio element + :rtype: string + + Identical to :php:func:`video()`, only it produces the