diff --git a/.github/scripts/deploy-appstarter b/.github/scripts/deploy-appstarter new file mode 100644 index 000000000000..eccc1fc46964 --- /dev/null +++ b/.github/scripts/deploy-appstarter @@ -0,0 +1,34 @@ +#!/bin/bash + +## Deploy codeigniter4/appstarter + +# Setup variables +SOURCE=$1 +TARGET=$2 +RELEASE=$3 + +echo "Preparing for version $3" +echo "Merging files from $1 to $2" + +# Prepare the source +cd $SOURCE +git checkout master + +# Prepare the target +cd $TARGET +git checkout master +rm -rf * + +# Copy common files +releasable='app public writable env license.txt spark .gitignore' +for fff in $releasable ; do + cp -Rf ${SOURCE}/$fff ./ +done + +# Copy repo-specific files +cp -Rf ${SOURCE}/admin/starter/. ./ + +# Commit the changes +git add . +git commit -m "Release ${RELEASE}" +git push diff --git a/.github/scripts/deploy-framework b/.github/scripts/deploy-framework new file mode 100644 index 000000000000..89aaeecfaa81 --- /dev/null +++ b/.github/scripts/deploy-framework @@ -0,0 +1,34 @@ +#!/bin/bash + +## Deploy codeigniter4/framework + +# Setup variables +SOURCE=$1 +TARGET=$2 +RELEASE=$3 + +echo "Preparing for version $3" +echo "Merging files from $1 to $2" + +# Prepare the source +cd $SOURCE +git checkout master + +# Prepare the target +cd $TARGET +git checkout master +rm -rf * + +# Copy common files +releasable='app public writable env license.txt spark .gitignore system' +for fff in $releasable ; do + cp -Rf ${SOURCE}/$fff ./ +done + +# Copy repo-specific files +cp -Rf ${SOURCE}/admin/framework/. ./ + +# Commit the changes +git add . +git commit -m "Release ${RELEASE}" +git push diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000000..b7bc1892ea37 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,94 @@ +name: Deploy + +on: + release: + types: [published] + +jobs: + framework: + runs-on: ubuntu-latest + + steps: + - name: Identify + run: | + git config --global user.email "action@github.com" + git config --global user.name "${GITHUB_ACTOR}" + + - name: Checkout source + uses: actions/checkout@v2 + with: + path: source + + - name: Checkout target + uses: actions/checkout@v2 + with: + repository: codeigniter4/framework + token: ${{ secrets.ACCESS_TOKEN }} + path: framework + + - name: Chmod + run: chmod +x ./source/.github/scripts/deploy-framework + + - name: Deploy + run: ./source/.github/scripts/deploy-framework ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/framework ${GITHUB_REF##*/} + + - name: Release + uses: actions/github-script@0.8.0 + with: + github-token: ${{secrets.ACCESS_TOKEN}} + script: | + const release = await github.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }) + github.repos.createRelease({ + owner: context.repo.owner, + repo: 'framework', + tag_name: release.data.tag_name, + name: release.data.name, + body: release.data.body + }) + + appstarter: + runs-on: ubuntu-latest + + steps: + - name: Identify + run: | + git config --global user.email "action@github.com" + git config --global user.name "${GITHUB_ACTOR}" + + - name: Checkout source + uses: actions/checkout@v2 + with: + path: source + + - name: Checkout target + uses: actions/checkout@v2 + with: + repository: codeigniter4/appstarter + token: ${{ secrets.ACCESS_TOKEN }} + path: appstarter + + - name: Chmod + run: chmod +x ./source/.github/scripts/deploy-appstarter + + - name: Deploy + run: ./source/.github/scripts/deploy-appstarter ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/appstarter ${GITHUB_REF##*/} + + - name: Release + uses: actions/github-script@0.8.0 + with: + github-token: ${{secrets.ACCESS_TOKEN}} + script: | + const release = await github.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }) + github.repos.createRelease({ + owner: context.repo.owner, + repo: 'appstarter', + tag_name: release.data.tag_name, + name: release.data.name, + body: release.data.body + }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c23a7e13b1c..e7b2d605f90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,220 @@ # Changelog -## [4.0.1](https://github.com/codeigniter4/CodeIgniter4/tree/HEAD) +## [4.0.3](https://github.com/codeigniter4/CodeIgniter4/tree/HEAD) -[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/4.0.0...4.0.1) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/4.0.3...HEAD) **Fixed bugs:** +- Bug: is\_unique validation rule with model-\>save\(\) fails [\#2906](https://github.com/codeigniter4/CodeIgniter4/issues/2906) +- Bug: Debugging toolbar not showing [\#2893](https://github.com/codeigniter4/CodeIgniter4/issues/2893) +- Bug: database query bug [\#2890](https://github.com/codeigniter4/CodeIgniter4/issues/2890) +- Bug: Routes - missing \(:id\) [\#2889](https://github.com/codeigniter4/CodeIgniter4/issues/2889) +- Bug: Typo example "Using Named Routes" [\#2888](https://github.com/codeigniter4/CodeIgniter4/issues/2888) +- Bug: I am able to make POST request to a GET route [\#2880](https://github.com/codeigniter4/CodeIgniter4/issues/2880) +- Bug: Argument 1 passed to CodeIgniter\Database\BaseResult::getFirstRow\(\) must be of the type string, null given, called in /system/Model.php on line 383 [\#2877](https://github.com/codeigniter4/CodeIgniter4/issues/2877) +- Bug: Can't override already defined language in pager [\#2875](https://github.com/codeigniter4/CodeIgniter4/issues/2875) +- Installation Problem: Composer installation halts while installing kint-php/kint [\#2863](https://github.com/codeigniter4/CodeIgniter4/issues/2863) +- Bug: `composer require codeigniter4/translations` without stable version [\#2862](https://github.com/codeigniter4/CodeIgniter4/issues/2862) +- Bug: Method Spoofing Validation [\#2855](https://github.com/codeigniter4/CodeIgniter4/issues/2855) +- Bug: You made it just as impossible to get going as Laravel – BUMMER! [\#2850](https://github.com/codeigniter4/CodeIgniter4/issues/2850) +- Bug: localised validation messages [\#2845](https://github.com/codeigniter4/CodeIgniter4/issues/2845) +- Bug: Commands discovery in custom namespaces [\#2840](https://github.com/codeigniter4/CodeIgniter4/issues/2840) +- Bug: When the $index parameter of getGetPost or getPostGet is null, you will get an error result [\#2839](https://github.com/codeigniter4/CodeIgniter4/issues/2839) +- Bug: delete\_cookie\(\) doesn't work [\#2836](https://github.com/codeigniter4/CodeIgniter4/issues/2836) +- Bug: Model save method not working if PRIMARY KEY is VARCHAR [\#2835](https://github.com/codeigniter4/CodeIgniter4/issues/2835) +- Bug: Alias Filter with Multiple Filter Class Doesn't Work [\#2831](https://github.com/codeigniter4/CodeIgniter4/issues/2831) +- Bug: Kint Config isnt loaded [\#2830](https://github.com/codeigniter4/CodeIgniter4/issues/2830) +- Bug: RouteCollection::resource\(\) doesn't work with grouped rules [\#2829](https://github.com/codeigniter4/CodeIgniter4/issues/2829) +- Bug: $forge Property in Seeder Class Never Initialize [\#2825](https://github.com/codeigniter4/CodeIgniter4/issues/2825) +- Bug: getSegments\(\) returns an array with 2 empty strings when accessing / [\#2822](https://github.com/codeigniter4/CodeIgniter4/issues/2822) +- Bug: Cell Caching in View Cells and SOLVE! [\#2821](https://github.com/codeigniter4/CodeIgniter4/issues/2821) +- Bug: saveData option doesn't work in Views [\#2818](https://github.com/codeigniter4/CodeIgniter4/issues/2818) +- Bug: Validation placeholder not being replaced [\#2817](https://github.com/codeigniter4/CodeIgniter4/issues/2817) +- Bug: Problems with QueryBuilder when run multiple queries one by one [\#2800](https://github.com/codeigniter4/CodeIgniter4/issues/2800) +- Bug: Routing placeholder in "controller"part of route doesn't work [\#2787](https://github.com/codeigniter4/CodeIgniter4/issues/2787) +- Bug: session\(\)-\>push\(\) Strange behavior [\#2786](https://github.com/codeigniter4/CodeIgniter4/issues/2786) +- Bug: php spark serve [\#2784](https://github.com/codeigniter4/CodeIgniter4/issues/2784) +- Bug: Can't paginate query with group by [\#2776](https://github.com/codeigniter4/CodeIgniter4/issues/2776) +- Bug: negotiateLocale bug in Safari with fr-ca locale [\#2774](https://github.com/codeigniter4/CodeIgniter4/issues/2774) +- Bug: Controller in Sub Directory is not working [\#2764](https://github.com/codeigniter4/CodeIgniter4/issues/2764) +- Bug: rename release By "v" [\#2763](https://github.com/codeigniter4/CodeIgniter4/issues/2763) +- Bug: db query '?' bind is not working when use sql with ':=' operator. [\#2762](https://github.com/codeigniter4/CodeIgniter4/issues/2762) +- Bug: Multiple select validation problem [\#2757](https://github.com/codeigniter4/CodeIgniter4/issues/2757) +- Bug: Official Site is not working [\#2749](https://github.com/codeigniter4/CodeIgniter4/issues/2749) +- Bug: Logger context placeholders {file} and {line} are wrong [\#2743](https://github.com/codeigniter4/CodeIgniter4/issues/2743) +- Bug: Decimal validation fails without leading digit [\#2740](https://github.com/codeigniter4/CodeIgniter4/issues/2740) +- Bug: Model insert Created\_at and updated\_at get when new record added [\#2737](https://github.com/codeigniter4/CodeIgniter4/issues/2737) +- Bug: appendHeader 500 error if header does not exist [\#2730](https://github.com/codeigniter4/CodeIgniter4/issues/2730) +- Bug: codeigniter4 download link 404 resource not found [\#2727](https://github.com/codeigniter4/CodeIgniter4/issues/2727) +- Bug: Logger `path` property ignored [\#2725](https://github.com/codeigniter4/CodeIgniter4/issues/2725) +- Bug: $this-\>request-\>getPost\(\) is empty when json is send by postman [\#2720](https://github.com/codeigniter4/CodeIgniter4/issues/2720) +- Bug: open path /0 at uri got error "Class Home does not exist" on development environment [\#2716](https://github.com/codeigniter4/CodeIgniter4/issues/2716) +- Bug: calling countAllResults after find\($id\) produce wrong result [\#2705](https://github.com/codeigniter4/CodeIgniter4/issues/2705) +- Bug: $routes-\>cli\(\) accessible via web browser if autoroute is true [\#2704](https://github.com/codeigniter4/CodeIgniter4/issues/2704) +- Bug: Controllers and Views in subdirectories not working [\#2701](https://github.com/codeigniter4/CodeIgniter4/issues/2701) +- Bug: undefined model method should throw exception [\#2688](https://github.com/codeigniter4/CodeIgniter4/issues/2688) +- Bug: The custom error config of validation is not working [\#2678](https://github.com/codeigniter4/CodeIgniter4/issues/2678) +- Bug: Can't test redirect\(\)-\>route\('routename'\), while redirect\(\)-\>to\('path'\) is working with ControllerTester [\#2676](https://github.com/codeigniter4/CodeIgniter4/issues/2676) +- Bug: php spark migrate:create File -n NameSpace doesn't create migration class under NameSpace/Database/Migrations directory with composer autoload [\#2664](https://github.com/codeigniter4/CodeIgniter4/issues/2664) +- Bug: \I18n\Time object displaying +1 year when object date is set to 2021-12-31 [\#2663](https://github.com/codeigniter4/CodeIgniter4/issues/2663) +- Bug: Route options filter didn't working [\#2654](https://github.com/codeigniter4/CodeIgniter4/issues/2654) +- Bug: Error in Seeder [\#2653](https://github.com/codeigniter4/CodeIgniter4/issues/2653) +- Bug: spark no longer lists function when used by without any parameters [\#2645](https://github.com/codeigniter4/CodeIgniter4/issues/2645) +- Bug: Number Helper, Currency Fraction issue [\#2634](https://github.com/codeigniter4/CodeIgniter4/issues/2634) +- Bug: forceHTTPS method ignores baseURL configuration when redirecting [\#2633](https://github.com/codeigniter4/CodeIgniter4/issues/2633) +- While serving Application on CLI using different port debugbar is still using a default 8080 port [\#2630](https://github.com/codeigniter4/CodeIgniter4/issues/2630) +- Bug: spark migrate -all with appstarter [\#2627](https://github.com/codeigniter4/CodeIgniter4/issues/2627) +- Bug: Problem when compiled vendor as PHAR file [\#2623](https://github.com/codeigniter4/CodeIgniter4/issues/2623) +- Bug: debugbar javascript error [\#2621](https://github.com/codeigniter4/CodeIgniter4/issues/2621) +- Bug: ResourceController json response always empty [\#2617](https://github.com/codeigniter4/CodeIgniter4/issues/2617) +- Bug: Chrome logger does not work. [\#2616](https://github.com/codeigniter4/CodeIgniter4/issues/2616) +- Bug: [\#2608](https://github.com/codeigniter4/CodeIgniter4/issues/2608) +- User Guide is not in HTML in the download file [\#2607](https://github.com/codeigniter4/CodeIgniter4/issues/2607) +- Unnecessary files in the download installation [\#2606](https://github.com/codeigniter4/CodeIgniter4/issues/2606) +- Bug: Class 'Kint\Renderer\Renderer' not found [\#2605](https://github.com/codeigniter4/CodeIgniter4/issues/2605) +- Bug: Codeigniter4/framework composer.json not updated [\#2601](https://github.com/codeigniter4/CodeIgniter4/issues/2601) +- \[Docs\] Loading Environment into Configuration documentation described wrong [\#2554](https://github.com/codeigniter4/CodeIgniter4/issues/2554) +- Bug: Sessions dont work on PostgreSQL [\#2546](https://github.com/codeigniter4/CodeIgniter4/issues/2546) +- Bug: router service adds backslash to controllername if route is configured [\#2520](https://github.com/codeigniter4/CodeIgniter4/issues/2520) +- Bug: JSONFormatter-\>format\(\) cannot handle errordata, only outputs it's own error [\#2434](https://github.com/codeigniter4/CodeIgniter4/issues/2434) +- Bug: HTTP Feature Testing only runs the FIRST test [\#2393](https://github.com/codeigniter4/CodeIgniter4/issues/2393) +- Bug: Spark issue with PHP install location [\#2367](https://github.com/codeigniter4/CodeIgniter4/issues/2367) +- spark route issue [\#2194](https://github.com/codeigniter4/CodeIgniter4/issues/2194) + +**Closed issues:** + +- Modular MVP on CI4 [\#2900](https://github.com/codeigniter4/CodeIgniter4/issues/2900) +- About javascript: void \(0\); [\#2887](https://github.com/codeigniter4/CodeIgniter4/issues/2887) +- Entity returns null when used on the model [\#2838](https://github.com/codeigniter4/CodeIgniter4/issues/2838) +- php spark migrate -g does not work [\#2832](https://github.com/codeigniter4/CodeIgniter4/issues/2832) +- Bug: Namespacing of app/Config folder vs. app/Controller [\#2826](https://github.com/codeigniter4/CodeIgniter4/issues/2826) +- Controller Call to a member function getPost\(\) on null [\#2823](https://github.com/codeigniter4/CodeIgniter4/issues/2823) +- QueryBuilder - Does not support JOIN in UPDATE [\#2799](https://github.com/codeigniter4/CodeIgniter4/issues/2799) +- Database model error when limiting delete\(\) [\#2780](https://github.com/codeigniter4/CodeIgniter4/issues/2780) +- codeigniter4/codeigniter4 package not exists in packagist [\#2753](https://github.com/codeigniter4/CodeIgniter4/issues/2753) +- datamap Entities not works! [\#2747](https://github.com/codeigniter4/CodeIgniter4/issues/2747) +- Error: Call to undefined function CodeIgniter\CLI\mb\_strpos\(\) [\#2746](https://github.com/codeigniter4/CodeIgniter4/issues/2746) +- CodeIgniter\Log\Logger::logPath property is never used. [\#2738](https://github.com/codeigniter4/CodeIgniter4/issues/2738) +- Bug: set\_radio\(\) in Form Helper does not work when radio button value equals "0" [\#2728](https://github.com/codeigniter4/CodeIgniter4/issues/2728) +- Array validation has a problem [\#2714](https://github.com/codeigniter4/CodeIgniter4/issues/2714) +- delete cookie not working [\#2700](https://github.com/codeigniter4/CodeIgniter4/issues/2700) +- remove default language local from url [\#2682](https://github.com/codeigniter4/CodeIgniter4/issues/2682) +- OpenSSLHandler: Encrypt/Decrypt [\#2680](https://github.com/codeigniter4/CodeIgniter4/issues/2680) +- RESTFUL API with CORS problem [\#2667](https://github.com/codeigniter4/CodeIgniter4/issues/2667) +- I guess there's no the third parameter [\#2657](https://github.com/codeigniter4/CodeIgniter4/issues/2657) +- set ci4 repo default branch = master [\#2643](https://github.com/codeigniter4/CodeIgniter4/issues/2643) +- BUG: 4.0.2 Kint not found [\#2639](https://github.com/codeigniter4/CodeIgniter4/issues/2639) +- Feature: Migrate:Rollback/Refresh confirmation in production environment [\#2385](https://github.com/codeigniter4/CodeIgniter4/issues/2385) + +**Merged pull requests:** + +- url\_title\(\) used CI3 style in user guide [\#2911](https://github.com/codeigniter4/CodeIgniter4/pull/2911) ([jreklund](https://github.com/jreklund)) +- fix undefined class 'CodeIgniter' [\#2910](https://github.com/codeigniter4/CodeIgniter4/pull/2910) ([PingZii](https://github.com/PingZii)) +- Improved subjects in Controller and Routing chapter [\#2908](https://github.com/codeigniter4/CodeIgniter4/pull/2908) ([jreklund](https://github.com/jreklund)) +- Fix Model::first\(\) only use orderBy\(\) when group by is not empty [\#2907](https://github.com/codeigniter4/CodeIgniter4/pull/2907) ([samsonasik](https://github.com/samsonasik)) +- Allow bypassing content negotiation during API responses. [\#2904](https://github.com/codeigniter4/CodeIgniter4/pull/2904) ([lonnieezell](https://github.com/lonnieezell)) +- Ugtweaks [\#2903](https://github.com/codeigniter4/CodeIgniter4/pull/2903) ([lonnieezell](https://github.com/lonnieezell)) +- Carbonads [\#2902](https://github.com/codeigniter4/CodeIgniter4/pull/2902) ([lonnieezell](https://github.com/lonnieezell)) +- Added information about the new features of the Pagination library [\#2901](https://github.com/codeigniter4/CodeIgniter4/pull/2901) ([jlamim](https://github.com/jlamim)) +- New features for pagination [\#2899](https://github.com/codeigniter4/CodeIgniter4/pull/2899) ([jlamim](https://github.com/jlamim)) +- Fixed lang\(\) example in user guide [\#2898](https://github.com/codeigniter4/CodeIgniter4/pull/2898) ([nmolinos](https://github.com/nmolinos)) +- Make validation placeholders always available [\#2897](https://github.com/codeigniter4/CodeIgniter4/pull/2897) ([jreklund](https://github.com/jreklund)) +- \[ci skip\] Add `make.bat` for Windows users [\#2895](https://github.com/codeigniter4/CodeIgniter4/pull/2895) ([paulbalandan](https://github.com/paulbalandan)) +- Added ability to delete row with string primary key via Model::delete\($id\) [\#2894](https://github.com/codeigniter4/CodeIgniter4/pull/2894) ([samsonasik](https://github.com/samsonasik)) +- Update of the pagination template to make the correct use of the locale [\#2892](https://github.com/codeigniter4/CodeIgniter4/pull/2892) ([jlamim](https://github.com/jlamim)) +- \[ci skip\] route placeholders 'id' to 'num' [\#2891](https://github.com/codeigniter4/CodeIgniter4/pull/2891) ([Instrye](https://github.com/Instrye)) +- \[ci skip\] fix warnings on compiling user guide [\#2886](https://github.com/codeigniter4/CodeIgniter4/pull/2886) ([paulbalandan](https://github.com/paulbalandan)) +- Added more Common functions and improved rendering in userguide [\#2884](https://github.com/codeigniter4/CodeIgniter4/pull/2884) ([jreklund](https://github.com/jreklund)) +- Build Your First Application used url\_title incorrectly [\#2883](https://github.com/codeigniter4/CodeIgniter4/pull/2883) ([jreklund](https://github.com/jreklund)) +- \[User guide\] Correcting some details in the part that talks about model and entities [\#2878](https://github.com/codeigniter4/CodeIgniter4/pull/2878) ([jlamim](https://github.com/jlamim)) +- Shifted basic URI Routing examples down [\#2874](https://github.com/codeigniter4/CodeIgniter4/pull/2874) ([nmolinos](https://github.com/nmolinos)) +- Better locale matching against broad groups. Fixes \#2774 [\#2872](https://github.com/codeigniter4/CodeIgniter4/pull/2872) ([lonnieezell](https://github.com/lonnieezell)) +- Fixes session active detection on force\_https function and add more test CodeIgniter::forceSecureAccess\(\) run force\_https\(\) [\#2871](https://github.com/codeigniter4/CodeIgniter4/pull/2871) ([samsonasik](https://github.com/samsonasik)) +- clean up use statements: remove unused and sort [\#2870](https://github.com/codeigniter4/CodeIgniter4/pull/2870) ([samsonasik](https://github.com/samsonasik)) +- more test for View::renderString\(\) for null tempData [\#2869](https://github.com/codeigniter4/CodeIgniter4/pull/2869) ([samsonasik](https://github.com/samsonasik)) +- Localized label in validation rules [\#2868](https://github.com/codeigniter4/CodeIgniter4/pull/2868) ([michalsn](https://github.com/michalsn)) +- \[ci skip\] update translations version [\#2867](https://github.com/codeigniter4/CodeIgniter4/pull/2867) ([Instrye](https://github.com/Instrye)) +- Initialize $forge property in Seeder Class - fixes \#2825 [\#2864](https://github.com/codeigniter4/CodeIgniter4/pull/2864) ([jlamim](https://github.com/jlamim)) +- fix. saveData not work [\#2861](https://github.com/codeigniter4/CodeIgniter4/pull/2861) ([Instrye](https://github.com/Instrye)) +- fix. getGetPost and getPostGet can't work in index empty [\#2860](https://github.com/codeigniter4/CodeIgniter4/pull/2860) ([Instrye](https://github.com/Instrye)) +- \[ci skip\]fix. getHeader return header object [\#2859](https://github.com/codeigniter4/CodeIgniter4/pull/2859) ([Instrye](https://github.com/Instrye)) +- fix. filters alias multiple [\#2857](https://github.com/codeigniter4/CodeIgniter4/pull/2857) ([Instrye](https://github.com/Instrye)) +- \[ci skip\] typo fix Initial Configuration & Set Up [\#2856](https://github.com/codeigniter4/CodeIgniter4/pull/2856) ([samsonasik](https://github.com/samsonasik)) +- Enclose file paths in double quotes to capture spaces [\#2853](https://github.com/codeigniter4/CodeIgniter4/pull/2853) ([paulbalandan](https://github.com/paulbalandan)) +- Strip directory separators from auto-generated cell cache name. Fixes… [\#2851](https://github.com/codeigniter4/CodeIgniter4/pull/2851) ([lonnieezell](https://github.com/lonnieezell)) +- Normalize dir separator of Exceptions::cleanPath and added more paths to clean [\#2847](https://github.com/codeigniter4/CodeIgniter4/pull/2847) ([paulbalandan](https://github.com/paulbalandan)) +- Improve readability in the userguide with a fixed size [\#2846](https://github.com/codeigniter4/CodeIgniter4/pull/2846) ([jreklund](https://github.com/jreklund)) +- Fixed Issue \#2840 on discovery of classes by FileLocator [\#2844](https://github.com/codeigniter4/CodeIgniter4/pull/2844) ([paulbalandan](https://github.com/paulbalandan)) +- add $segment parameter in pager call by Model.php [\#2843](https://github.com/codeigniter4/CodeIgniter4/pull/2843) ([paul45](https://github.com/paul45)) +- Improve flash of unstyled content in userguide [\#2842](https://github.com/codeigniter4/CodeIgniter4/pull/2842) ([jreklund](https://github.com/jreklund)) +- Add English message for "string" validation rule [\#2841](https://github.com/codeigniter4/CodeIgniter4/pull/2841) ([rmilecki](https://github.com/rmilecki)) +- more tests for Common functions [\#2837](https://github.com/codeigniter4/CodeIgniter4/pull/2837) ([samsonasik](https://github.com/samsonasik)) +- Pagination: open page \> pageCount get last page [\#2834](https://github.com/codeigniter4/CodeIgniter4/pull/2834) ([samsonasik](https://github.com/samsonasik)) +- add ability for nested language definition [\#2833](https://github.com/codeigniter4/CodeIgniter4/pull/2833) ([samsonasik](https://github.com/samsonasik)) +- Documentation fixes [\#2827](https://github.com/codeigniter4/CodeIgniter4/pull/2827) ([pjio](https://github.com/pjio)) +- fix. URI path is empty [\#2824](https://github.com/codeigniter4/CodeIgniter4/pull/2824) ([Instrye](https://github.com/Instrye)) +- ignore coverage on exit and die [\#2820](https://github.com/codeigniter4/CodeIgniter4/pull/2820) ([samsonasik](https://github.com/samsonasik)) +- add respondUpdated\(\) method into API\ResponseTrait [\#2816](https://github.com/codeigniter4/CodeIgniter4/pull/2816) ([samsonasik](https://github.com/samsonasik)) +- ignore coverage on !CI\_DEBUG [\#2814](https://github.com/codeigniter4/CodeIgniter4/pull/2814) ([samsonasik](https://github.com/samsonasik)) +- Fix missing InvalidArgumentException in Database\BaseBuilder [\#2813](https://github.com/codeigniter4/CodeIgniter4/pull/2813) ([samsonasik](https://github.com/samsonasik)) +- Ensure $\_SERVER\['SCRIPT\_NAME'\] ends with PHP [\#2810](https://github.com/codeigniter4/CodeIgniter4/pull/2810) ([willnode](https://github.com/willnode)) +- make named constructor in Exception classes consistent: use return instead of throw [\#2809](https://github.com/codeigniter4/CodeIgniter4/pull/2809) ([samsonasik](https://github.com/samsonasik)) +- Check if dataset is empty before Model update. [\#2808](https://github.com/codeigniter4/CodeIgniter4/pull/2808) ([vibbow](https://github.com/vibbow)) +- test Controller::validate\(\) with string rules [\#2807](https://github.com/codeigniter4/CodeIgniter4/pull/2807) ([samsonasik](https://github.com/samsonasik)) +- clean up buffer tweak in FeatureTestCaseTest [\#2805](https://github.com/codeigniter4/CodeIgniter4/pull/2805) ([samsonasik](https://github.com/samsonasik)) +- using realpath\(\) for define $pathsPath in index.php [\#2804](https://github.com/codeigniter4/CodeIgniter4/pull/2804) ([samsonasik](https://github.com/samsonasik)) +- add ext-mbstring to required and update regex that sanitize file name [\#2803](https://github.com/codeigniter4/CodeIgniter4/pull/2803) ([samsonasik](https://github.com/samsonasik)) +- Add resetting QBFrom part [\#2802](https://github.com/codeigniter4/CodeIgniter4/pull/2802) ([michalsn](https://github.com/michalsn)) +- Update Routes.php [\#2801](https://github.com/codeigniter4/CodeIgniter4/pull/2801) ([mostafakhudair](https://github.com/mostafakhudair)) +- add more test for Entity : 100% tested [\#2798](https://github.com/codeigniter4/CodeIgniter4/pull/2798) ([samsonasik](https://github.com/samsonasik)) +- \[ci skip\] Fix download badge total shows [\#2797](https://github.com/codeigniter4/CodeIgniter4/pull/2797) ([samsonasik](https://github.com/samsonasik)) +- test for I18n\Time::toFormattedDateString [\#2796](https://github.com/codeigniter4/CodeIgniter4/pull/2796) ([samsonasik](https://github.com/samsonasik)) +- test Logger::determineFile\(\) for no stack trace [\#2795](https://github.com/codeigniter4/CodeIgniter4/pull/2795) ([samsonasik](https://github.com/samsonasik)) +- test CLI\CLI::strlen\(null\) [\#2794](https://github.com/codeigniter4/CodeIgniter4/pull/2794) ([samsonasik](https://github.com/samsonasik)) +- test for API\ResponseTrait::format\(\) with format is not json or xml [\#2793](https://github.com/codeigniter4/CodeIgniter4/pull/2793) ([samsonasik](https://github.com/samsonasik)) +- test for View\Cell::render\(\) with class has initController\(\) method [\#2792](https://github.com/codeigniter4/CodeIgniter4/pull/2792) ([samsonasik](https://github.com/samsonasik)) +- test Autoloader::initialize\(\) with composer path not found [\#2791](https://github.com/codeigniter4/CodeIgniter4/pull/2791) ([samsonasik](https://github.com/samsonasik)) +- add ability to replace {locale} to request-\>getLocale\(\) in form\_open\('action'\) [\#2790](https://github.com/codeigniter4/CodeIgniter4/pull/2790) ([samsonasik](https://github.com/samsonasik)) +- test for IncomingRequest::getFileMultiple\(\) [\#2789](https://github.com/codeigniter4/CodeIgniter4/pull/2789) ([samsonasik](https://github.com/samsonasik)) +- add MockEmail class [\#2788](https://github.com/codeigniter4/CodeIgniter4/pull/2788) ([samsonasik](https://github.com/samsonasik)) +- test for CodeIgniter\Config\Services::email\(\) [\#2785](https://github.com/codeigniter4/CodeIgniter4/pull/2785) ([samsonasik](https://github.com/samsonasik)) +- make Model::paginate\(\) use default perPage from Config\Pager-\>perPage if $perPage parameter not passed [\#2782](https://github.com/codeigniter4/CodeIgniter4/pull/2782) ([samsonasik](https://github.com/samsonasik)) +- \#2780 - LIMIT. [\#2781](https://github.com/codeigniter4/CodeIgniter4/pull/2781) ([nowackipawel](https://github.com/nowackipawel)) +- \[ci skip\] \_remap method must have return [\#2779](https://github.com/codeigniter4/CodeIgniter4/pull/2779) ([Instrye](https://github.com/Instrye)) +- Rework get\_filenames [\#2778](https://github.com/codeigniter4/CodeIgniter4/pull/2778) ([MGatner](https://github.com/MGatner)) +- Fix \#2776 add ability to paginate\(\) query with group by [\#2777](https://github.com/codeigniter4/CodeIgniter4/pull/2777) ([samsonasik](https://github.com/samsonasik)) +- Update on "Build Your First Application" [\#2775](https://github.com/codeigniter4/CodeIgniter4/pull/2775) ([jreklund](https://github.com/jreklund)) +- Fix ? bind with := bind [\#2773](https://github.com/codeigniter4/CodeIgniter4/pull/2773) ([musmanikram](https://github.com/musmanikram)) +- Fixed some styling in "Installation" chapter \[ci skip\] [\#2772](https://github.com/codeigniter4/CodeIgniter4/pull/2772) ([jreklund](https://github.com/jreklund)) +- Uncommented tests, mistake in my previous PR :\( [\#2767](https://github.com/codeigniter4/CodeIgniter4/pull/2767) ([musmanikram](https://github.com/musmanikram)) +- Don't show message 404 error, eg: Controller or its method not found in "production" environment and ! is\_cli\(\) [\#2760](https://github.com/codeigniter4/CodeIgniter4/pull/2760) ([samsonasik](https://github.com/samsonasik)) +- fix. MYSQLI::DBDebug can't woker [\#2755](https://github.com/codeigniter4/CodeIgniter4/pull/2755) ([Instrye](https://github.com/Instrye)) +- fix. delete\_cookite can't delete alreday set Cookie [\#2709](https://github.com/codeigniter4/CodeIgniter4/pull/2709) ([Instrye](https://github.com/Instrye)) +- Re-write userguide to support Python 3 and future proofing Sphinx [\#2671](https://github.com/codeigniter4/CodeIgniter4/pull/2671) ([jreklund](https://github.com/jreklund)) +- Colored table in CLI [\#2624](https://github.com/codeigniter4/CodeIgniter4/pull/2624) ([enix-app](https://github.com/enix-app)) + +## [4.0.2](https://github.com/codeigniter4/CodeIgniter4/tree/4.0.2) (2020-02-25) + +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.0.1...4.0.2) + +**Fixed bugs:** + +- Bug: Your requirements could not be resolved to an installable set of packages. [\#2613](https://github.com/codeigniter4/CodeIgniter4/issues/2613) + +**Merged pull requests:** + +- Removed unused test class that was causing appstarter not to work from CLI. [\#2614](https://github.com/codeigniter4/CodeIgniter4/pull/2614) ([lonnieezell](https://github.com/lonnieezell)) +- \[UG\] Fix all Sphinx warnings [\#2611](https://github.com/codeigniter4/CodeIgniter4/pull/2611) ([LittleJ](https://github.com/LittleJ)) +- \[UG\] Sphinx\_rtd\_theme fixes and improvements [\#2610](https://github.com/codeigniter4/CodeIgniter4/pull/2610) ([LittleJ](https://github.com/LittleJ)) + +## [v4.0.1](https://github.com/codeigniter4/CodeIgniter4/tree/v4.0.1) (2020-02-24) + +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/4.0.0...v4.0.1) + +**Fixed bugs:** + +- Bug: Difficult architecture of the codeigniter4 project [\#2602](https://github.com/codeigniter4/CodeIgniter4/issues/2602) - Bug: mentioned rc4 in the changelog file of V4 userguide [\#2599](https://github.com/codeigniter4/CodeIgniter4/issues/2599) **Merged pull requests:** @@ -80,7 +289,7 @@ - MySQLi: Incorrect DBDebug flag used for connection charset [\#2558](https://github.com/codeigniter4/CodeIgniter4/pull/2558) ([jreklund](https://github.com/jreklund)) - Update File.php [\#2552](https://github.com/codeigniter4/CodeIgniter4/pull/2552) ([thanhtaivtt](https://github.com/thanhtaivtt)) - disable buffer check on "testing" environment [\#2551](https://github.com/codeigniter4/CodeIgniter4/pull/2551) ([samsonasik](https://github.com/samsonasik)) -- Improved view: welcome\_message.php [\#2550](https://github.com/codeigniter4/CodeIgniter4/pull/2550) ([GianVizzielli](https://github.com/GianVizzielli)) +- Improved view: welcome\_message.php [\#2550](https://github.com/codeigniter4/CodeIgniter4/pull/2550) ([Vizzielli](https://github.com/Vizzielli)) - Add retry creation server when the port is used [\#2544](https://github.com/codeigniter4/CodeIgniter4/pull/2544) ([thanhtaivtt](https://github.com/thanhtaivtt)) - New "welcome" page [\#2541](https://github.com/codeigniter4/CodeIgniter4/pull/2541) ([LittleJ](https://github.com/LittleJ)) - valid\_ip removed $data which was causing exception [\#2540](https://github.com/codeigniter4/CodeIgniter4/pull/2540) ([nowackipawel](https://github.com/nowackipawel)) @@ -135,7 +344,6 @@ - Bug: Usage of `static::methodName` in CodeIgniter\Config\Services prevents Service overriding [\#2376](https://github.com/codeigniter4/CodeIgniter4/issues/2376) - Bug: Duplicate headers in response [\#2375](https://github.com/codeigniter4/CodeIgniter4/issues/2375) - Bug: Nothing work with minimal config \(DIRECTORY SEPARATOR\) [\#2370](https://github.com/codeigniter4/CodeIgniter4/issues/2370) -- Bug: Spark issue with PHP install location [\#2367](https://github.com/codeigniter4/CodeIgniter4/issues/2367) - Bug: current\_url function not working as expected. [\#2365](https://github.com/codeigniter4/CodeIgniter4/issues/2365) - Bug: localhost development server after edit the content not updated or reloaded [\#2363](https://github.com/codeigniter4/CodeIgniter4/issues/2363) - Bug: with the parser, nl2br in a foreach duplicates entries. [\#2360](https://github.com/codeigniter4/CodeIgniter4/issues/2360) @@ -337,6 +545,7 @@ - Don't restrict model's access to properties in a read-only manner [\#2289](https://github.com/codeigniter4/CodeIgniter4/pull/2289) ([lonnieezell](https://github.com/lonnieezell)) - Fix line numbering in Debug/Exceptions class [\#2288](https://github.com/codeigniter4/CodeIgniter4/pull/2288) ([michalsn](https://github.com/michalsn)) - Fix error with Host header for CURLRequest class [\#2285](https://github.com/codeigniter4/CodeIgniter4/pull/2285) ([michalsn](https://github.com/michalsn)) +- Fix getErrors\(\) for validation with redirect [\#2284](https://github.com/codeigniter4/CodeIgniter4/pull/2284) ([michalsn](https://github.com/michalsn)) - Bug in CSRF parameter cleanup [\#2279](https://github.com/codeigniter4/CodeIgniter4/pull/2279) ([michalsn](https://github.com/michalsn)) - WIP fix store\(\) default value bug [\#2123](https://github.com/codeigniter4/CodeIgniter4/pull/2123) ([lucifergit](https://github.com/lucifergit)) - WIP Added validation on exists database before created for MySQLi… [\#2100](https://github.com/codeigniter4/CodeIgniter4/pull/2100) ([oleg1540](https://github.com/oleg1540)) @@ -347,7 +556,6 @@ **Merged pull requests:** -- Fix getErrors\(\) for validation with redirect [\#2284](https://github.com/codeigniter4/CodeIgniter4/pull/2284) ([michalsn](https://github.com/michalsn)) - Fix user guide for Message class [\#2282](https://github.com/codeigniter4/CodeIgniter4/pull/2282) ([michalsn](https://github.com/michalsn)) - Handle X-CSRF-TOKEN - CSRF [\#2272](https://github.com/codeigniter4/CodeIgniter4/pull/2272) ([nowackipawel](https://github.com/nowackipawel)) - QUICKFIX Batch Update Where Reset [\#2252](https://github.com/codeigniter4/CodeIgniter4/pull/2252) ([searchy2](https://github.com/searchy2)) @@ -531,7 +739,6 @@ - Added app/Common.php [\#2110](https://github.com/codeigniter4/CodeIgniter4/pull/2110) ([jason-napolitano](https://github.com/jason-napolitano)) - Fix typo in checking if exists db\_connect\(\) [\#2109](https://github.com/codeigniter4/CodeIgniter4/pull/2109) ([xbotkaj](https://github.com/xbotkaj)) - Original email port [\#2092](https://github.com/codeigniter4/CodeIgniter4/pull/2092) ([jim-parry](https://github.com/jim-parry)) -- Fix prevent soft delete all without conditions set [\#2090](https://github.com/codeigniter4/CodeIgniter4/pull/2090) ([rino7](https://github.com/rino7)) - Update BaseConfig.php [\#2082](https://github.com/codeigniter4/CodeIgniter4/pull/2082) ([zl59503020](https://github.com/zl59503020)) - WIP: Migration updates for more wholistic functionality [\#2065](https://github.com/codeigniter4/CodeIgniter4/pull/2065) ([lonnieezell](https://github.com/lonnieezell)) - clean base controller code [\#2046](https://github.com/codeigniter4/CodeIgniter4/pull/2046) ([behnampro](https://github.com/behnampro)) @@ -585,6 +792,7 @@ - Prep for beta.4 [\#2107](https://github.com/codeigniter4/CodeIgniter4/pull/2107) ([jim-parry](https://github.com/jim-parry)) - File & UploadFile Fixes [\#2104](https://github.com/codeigniter4/CodeIgniter4/pull/2104) ([MGatner](https://github.com/MGatner)) - Timezone select [\#2091](https://github.com/codeigniter4/CodeIgniter4/pull/2091) ([MGatner](https://github.com/MGatner)) +- Fix prevent soft delete all without conditions set [\#2090](https://github.com/codeigniter4/CodeIgniter4/pull/2090) ([rino7](https://github.com/rino7)) - JSON format checking improved [\#2081](https://github.com/codeigniter4/CodeIgniter4/pull/2081) ([nowackipawel](https://github.com/nowackipawel)) - Update config\(\) to check all namespaces [\#2079](https://github.com/codeigniter4/CodeIgniter4/pull/2079) ([MGatner](https://github.com/MGatner)) - Throttler can access bucket for bucket life time [\#2074](https://github.com/codeigniter4/CodeIgniter4/pull/2074) ([MohKari](https://github.com/MohKari)) @@ -620,6 +828,7 @@ - Entity refactor [\#2002](https://github.com/codeigniter4/CodeIgniter4/pull/2002) ([lonnieezell](https://github.com/lonnieezell)) - use CodeIgniter\Controller; not needed since Home Controller extends … [\#1999](https://github.com/codeigniter4/CodeIgniter4/pull/1999) ([titounnes](https://github.com/titounnes)) - Attempting to fix CURLRequest debug issue. \#1994 [\#1996](https://github.com/codeigniter4/CodeIgniter4/pull/1996) ([lonnieezell](https://github.com/lonnieezell)) +- argument set\(\) must by type of string - cannot agree [\#1989](https://github.com/codeigniter4/CodeIgniter4/pull/1989) ([nowackipawel](https://github.com/nowackipawel)) - Prevent reverseRoute from searching closures [\#1959](https://github.com/codeigniter4/CodeIgniter4/pull/1959) ([MGatner](https://github.com/MGatner)) ## [v4.0.0-beta.3](https://github.com/codeigniter4/CodeIgniter4/tree/v4.0.0-beta.3) (2019-05-06) @@ -694,7 +903,6 @@ **Merged pull requests:** - Prep for beta.3 [\#1990](https://github.com/codeigniter4/CodeIgniter4/pull/1990) ([jim-parry](https://github.com/jim-parry)) -- argument set\(\) must by type of string - cannot agree [\#1989](https://github.com/codeigniter4/CodeIgniter4/pull/1989) ([nowackipawel](https://github.com/nowackipawel)) - Correct API docblock problems for phpdocs [\#1987](https://github.com/codeigniter4/CodeIgniter4/pull/1987) ([jim-parry](https://github.com/jim-parry)) - Update docblock version to 4.0.0 [\#1986](https://github.com/codeigniter4/CodeIgniter4/pull/1986) ([jim-parry](https://github.com/jim-parry)) - Fix filter processing. Fixes \#1907 [\#1985](https://github.com/codeigniter4/CodeIgniter4/pull/1985) ([jim-parry](https://github.com/jim-parry)) @@ -1148,7 +1356,6 @@ - Fix Controller use validate bug Fixes \#1419 [\#1423](https://github.com/codeigniter4/CodeIgniter4/pull/1423) ([bangbangda](https://github.com/bangbangda)) - normalize composer.json [\#1418](https://github.com/codeigniter4/CodeIgniter4/pull/1418) ([samsonasik](https://github.com/samsonasik)) - add php 7.3 to travis config [\#1394](https://github.com/codeigniter4/CodeIgniter4/pull/1394) ([samsonasik](https://github.com/samsonasik)) -- Caching Driver Document ,isSupported method has no parameters [\#629](https://github.com/codeigniter4/CodeIgniter4/pull/629) ([bangbangda](https://github.com/bangbangda)) - Add Header Link Pagination [\#622](https://github.com/codeigniter4/CodeIgniter4/pull/622) ([natanfelles](https://github.com/natanfelles)) ## [v4.0.0-alpha.2](https://github.com/codeigniter4/CodeIgniter4/tree/v4.0.0-alpha.2) (2018-10-26) @@ -1242,7 +1449,9 @@ - Release notes & process [\#1269](https://github.com/codeigniter4/CodeIgniter4/pull/1269) ([jim-parry](https://github.com/jim-parry)) - Fix \#1244 \(form\_hidden declaration\) [\#1245](https://github.com/codeigniter4/CodeIgniter4/pull/1245) ([bvrignaud](https://github.com/bvrignaud)) - 【Unsolicited PR】I changed the download method to testable. [\#1239](https://github.com/codeigniter4/CodeIgniter4/pull/1239) ([ytetsuro](https://github.com/ytetsuro)) +- Docs/contributing [\#1218](https://github.com/codeigniter4/CodeIgniter4/pull/1218) ([jim-parry](https://github.com/jim-parry)) - Optional parameter for resetSelect\(\) call in Builder's countAll\(\); [\#1217](https://github.com/codeigniter4/CodeIgniter4/pull/1217) ([nowackipawel](https://github.com/nowackipawel)) +- Fix undefined function xml\_convert at Database\BaseUtils [\#1209](https://github.com/codeigniter4/CodeIgniter4/pull/1209) ([samsonasik](https://github.com/samsonasik)) ## [v4.0.0-alpha.1](https://github.com/codeigniter4/CodeIgniter4/tree/v4.0.0-alpha.1) (2018-09-29) @@ -1755,11 +1964,9 @@ - use short array syntax [\#1223](https://github.com/codeigniter4/CodeIgniter4/pull/1223) ([samsonasik](https://github.com/samsonasik)) - doc fix: FormatterInterface namespace [\#1222](https://github.com/codeigniter4/CodeIgniter4/pull/1222) ([samsonasik](https://github.com/samsonasik)) - Improved division logic of validation rules. [\#1220](https://github.com/codeigniter4/CodeIgniter4/pull/1220) ([ytetsuro](https://github.com/ytetsuro)) -- Docs/contributing [\#1218](https://github.com/codeigniter4/CodeIgniter4/pull/1218) ([jim-parry](https://github.com/jim-parry)) - Niggly fixes [\#1216](https://github.com/codeigniter4/CodeIgniter4/pull/1216) ([jim-parry](https://github.com/jim-parry)) - Autodiscovery [\#1215](https://github.com/codeigniter4/CodeIgniter4/pull/1215) ([lonnieezell](https://github.com/lonnieezell)) - Fix warnings in welcome\_message.php [\#1211](https://github.com/codeigniter4/CodeIgniter4/pull/1211) ([puschie286](https://github.com/puschie286)) -- Fix undefined function xml\_convert at Database\BaseUtils [\#1209](https://github.com/codeigniter4/CodeIgniter4/pull/1209) ([samsonasik](https://github.com/samsonasik)) - Correct helper tests namespace [\#1207](https://github.com/codeigniter4/CodeIgniter4/pull/1207) ([jim-parry](https://github.com/jim-parry)) - Validation Class - corresponding about the escaped separator. [\#1203](https://github.com/codeigniter4/CodeIgniter4/pull/1203) ([ytetsuro](https://github.com/ytetsuro)) - Fixes FileRules::max\_size\(\) to use file-\>getSize\(\) instead of number\_formatted size [\#1199](https://github.com/codeigniter4/CodeIgniter4/pull/1199) ([samsonasik](https://github.com/samsonasik)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0cf482fd530..8a03902d8fec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,3 +92,11 @@ If you are using the command-line, you can do the following to update your fork 3. `git push origin develop` Your fork is now up to date. This should be done regularly and, at the least, before you submit a pull request. + +## Translations Installation + +If you wish to contribute to the system message translations, +then fork and clone the [translations repository](https://github.com/codeigniter4/translations>) +separately from the codebase. + +These are two independent repositories! diff --git a/README.md b/README.md index e09072817397..a4319ced0345 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,29 @@ [![Build Status](https://travis-ci.org/codeigniter4/CodeIgniter4.svg?branch=develop)](https://travis-ci.org/codeigniter4/CodeIgniter4) [![Coverage Status](https://coveralls.io/repos/github/codeigniter4/CodeIgniter4/badge.svg?branch=develop)](https://coveralls.io/github/codeigniter4/CodeIgniter4?branch=develop) +[![Downloads](https://poser.pugx.org/codeigniter4/framework/downloads)](https://packagist.org/packages/codeigniter4/framework) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/codeigniter4/CodeIgniter4)](https://packagist.org/packages/codeigniter4/framework) +[![GitHub stars](https://img.shields.io/github/stars/codeigniter4/CodeIgniter4)](https://packagist.org/packages/codeigniter4/framework) +[![GitHub license](https://img.shields.io/github/license/codeigniter4/CodeIgniter4)](https://github.com/codeigniter4/CodeIgniter4/blob/develop/license.txt) +[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/codeigniter4/CodeIgniter4/pulls)
## What is CodeIgniter? -CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure. +CodeIgniter is a PHP full-stack web framework that is light, fast, flexible, and secure. More information can be found at the [official site](http://codeigniter.com). -This repository holds the source code for CodeIgniter 4 only. -Version 4 is a complete rewrite to bring the quality and the code into a more modern version, -while still keeping as many of the things intact that has made people love the framework over the years. - -**This is pre-release code and should not be used in production sites.** +This repository holds the source code for CodeIgniter 4 only. +Version 4 is a complete rewrite to bring the quality and the code into a more modern version, +while still keeping as many of the things intact that has made people love the framework over the years. 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 [User Guide](https://codeigniter4.github.io/userguide/) is the primary documentation for CodeIgniter 4. +The [User Guide](https://codeigniter4.github.io/userguide/) is the primary documentation for CodeIgniter 4. -The current **in-progress** User Guide can be found [here](https://codeigniter4.github.io/CodeIgniter4/). +The current **in-progress** User Guide can be found [here](https://codeigniter4.github.io/CodeIgniter4/). As with the rest of the framework, it is a work in progress, and will see changes over time to structure, explanations, etc. You might also be interested in the [API documentation](https://codeigniter4.github.io/api/) for the framework components. @@ -40,9 +43,9 @@ The user guide updating and deployment is a bit awkward at the moment, but we ar ## Repository Management -CodeIgniter is developed completely on a volunteer basis. As such, please give up to 7 days -for your issues to be reviewed. If you haven't heard from one of the team in that time period, -feel free to leave a comment on the issue so that it gets brought back to our attention. +CodeIgniter is developed completely on a volunteer basis. As such, please give up to 7 days +for your issues to be reviewed. If you haven't heard from one of the team in that time period, +feel free to leave a comment on the issue so that it gets brought back to our attention. 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 @@ -53,7 +56,7 @@ be closed! If you are not sure if you have found a bug, raise a thread on the fo someone else may have encountered the same thing. Before raising a new Github issue, please check that your bug hasn't already -been reported or fixed. +been reported or fixed. We use pull requests (PRs) for CONTRIBUTIONS to the repository. We are looking for contributions that address one of the reported bugs or @@ -69,8 +72,8 @@ to optional packages, with their own repository. We **are** accepting contributions from the community! -We will try to manage the process somewhat, by adding a ["help wanted" label](https://github.com/codeigniter4/CodeIgniter4/labels/help%20wanted) to those that we are -specifically interested in at any point in time. Join the discussion for those issues and let us know +We will try to manage the process somewhat, by adding a ["help wanted" label](https://github.com/codeigniter4/CodeIgniter4/labels/help%20wanted) 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 on one of them. At this time, we are not looking for out-of-scope contributions, only those that would be considered part of our controlled evolution! @@ -79,17 +82,17 @@ Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/ ## Server Requirements -PHP version 7.2 or higher is required, with the following extensions installed: +PHP version 7.2 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library +- [mbstring](http://php.net/manual/en/mbstring.installation.php) Additionally, make sure that the following extensions are enabled in your PHP: - json (enabled by default - don't turn it off) - xml (enabled by default - don't turn it off) -- [mbstring](http://php.net/manual/en/mbstring.installation.php) - [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) ## Running CodeIgniter Tests diff --git a/admin/README.md b/admin/README.md index dcb1b9ef0e31..5d9ab2d6d106 100644 --- a/admin/README.md +++ b/admin/README.md @@ -16,7 +16,7 @@ This folder contains tools or docs useful for project maintainers. It is meant to be downloaded by developers, or composer-installed. This is a read-only repository. - **appstarter** is the released application starter repository. - It is derived from the framework's `application` and `public` folders, with + It is derived from the framework's `app` and `public` folders, with a composer requirement dependency to pull in the framework itself. It is meant to be downloaded or composer-installed. This is a read-only repository. @@ -64,14 +64,10 @@ scripts used by the release manager: in it, and it will run the related scripts following, to revise the release distributions. Usage: `admin/release version qualifier` -- **release-framework** builds the distributable framework repo. - It could be used on its own, but is normally part of `release`. -- **release-appstarter** builds the distributable appstarter repo. - It could be used on its own, but is normally part of `release`. - **release-userguide** builds the distributable userguide repo. It could be used on its own, but is normally part of `release`. - **release-deploy** pushes the release changes to the appropriate github - repositories. Tag & create releases on github. This is not easily reversible! + repositories. Tag & create releases on GitHub. This is not easily reversible! Usage: `admin/release-deploy version qualifier` - **release-revert** can be used to restore your repositories to the state they were in before you started a release. **IF** you haven't deployed. diff --git a/admin/framework/README.md b/admin/framework/README.md index 03626c76a2df..275f01536660 100644 --- a/admin/framework/README.md +++ b/admin/framework/README.md @@ -9,8 +9,6 @@ This repository holds the distributable version of the framework, including the user guide. It has been built from the [development repository](https://github.com/codeigniter4/CodeIgniter4). -**This is pre-release code and should not be used in production sites.** - More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. The user guide corresponding to this version of the framework can be found diff --git a/admin/framework/composer.json b/admin/framework/composer.json index d8adb485fb25..a8045d379f79 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -9,6 +9,7 @@ "ext-curl": "*", "ext-intl": "*", "ext-json": "*", + "ext-mbstring": "*", "kint-php/kint": "^3.3", "psr/log": "^1.1", "laminas/laminas-escaper": "^2.6" diff --git a/admin/module/tests/database/ExampleDatabaseTest.php b/admin/module/tests/database/ExampleDatabaseTest.php index ccb6885f9fed..2de0b6ae01bf 100644 --- a/admin/module/tests/database/ExampleDatabaseTest.php +++ b/admin/module/tests/database/ExampleDatabaseTest.php @@ -26,6 +26,7 @@ public function testSoftDeleteLeavesRow() { $model = new ExampleModel(); $this->setPrivateProperty($model, 'useSoftDeletes', true); + $this->setPrivateProperty($model, 'tempUseSoftDeletes', true); $object = $model->first(); $model->delete($object->id); diff --git a/admin/pre-commit b/admin/pre-commit index d5553f77a50d..c655cd04fe25 100644 --- a/admin/pre-commit +++ b/admin/pre-commit @@ -17,13 +17,13 @@ SFILES=${SFILES:-$STAGED_FILES_CMD} echo "Checking PHP Lint..." for FILE in $SFILES do - php -l -d display_errors=0 $PROJECT/$FILE + php -l -d display_errors=0 "$PROJECT/$FILE" if [ $? != 0 ] then echo "Fix the error before commit." exit 1 fi - FILES="$FILES $PROJECT/$FILE" + FILES="$FILES $FILE" done if [ "$FILES" != "" ] diff --git a/admin/release b/admin/release index ea923ff3ea9f..d14167f0502e 100755 --- a/admin/release +++ b/admin/release @@ -89,9 +89,7 @@ if [ -d dist ]; then fi mkdir dist -setup_repo framework setup_repo userguide -setup_repo appstarter #--------------------------------------------------- # Housekeeping - make sure writable is flushed of test files @@ -139,9 +137,7 @@ git commit -S -m "Release ${RELEASE}" #--------------------------------------------------- # Build the distributables -. admin/release-framework . admin/release-userguide -. admin/release-appstarter #--------------------------------------------------- # Done for now diff --git a/admin/release-appstarter b/admin/release-appstarter deleted file mode 100644 index cd8d4e890e3f..000000000000 --- a/admin/release-appstarter +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -## Build app starter distributable - -# Setup variables -. admin/release-config -TARGET=dist/appstarter -cd $TARGET -git checkout $branch - -#--------------------------------------------------- -echo -e "${BOLD}Build the framework distributable${NORMAL}" - -echo -e "${BOLD}Copy the main files/folders...${NORMAL}" -releasable='app public writable README.md contributing.md env license.txt spark .gitignore' -for fff in $releasable ; do - if [ -d "$fff" ] ; then - rm -rf $fff - fi - cp -rf ${CI_DIR}/$fff . -done - -rm -rf tests -mkdir tests -cp -rf ${CI_DIR}/tests/_support tests/ - -echo -e "${BOLD}Override as needed...${NORMAL}" -cp -rf ${CI_DIR}/admin/starter/* . - -#--------------------------------------------------- -# And finally, get ready for merging -echo -e "${BOLD}Assemble the pieces...${NORMAL}" -git add . -git commit -S -m "Release ${RELEASE}" -git checkout master -git merge $branch - -cd $CI_DIR - -#--------------------------------------------------- -# Done for now -echo -e "${BOLD}Distributable app starter ready..${NORMAL}" diff --git a/admin/release-deploy b/admin/release-deploy index d7bb66f7c07a..fe70284024fc 100755 --- a/admin/release-deploy +++ b/admin/release-deploy @@ -23,14 +23,6 @@ echo -e "${BOLD}Pushing to the user guide repository${NORMAL}" cd ${CI_DIR}/dist/userguide git push origin master -echo -e "${BOLD}Pushing to the framework repository${NORMAL}" -cd ${CI_DIR}/dist/framework -git push origin master - -echo -e "${BOLD}Pushing to the app starter repository${NORMAL}" -cd ${CI_DIR}/dist/appstarter -git push origin master - cd ${CI_DIR} #--------------------------------------------------- diff --git a/admin/release-framework b/admin/release-framework deleted file mode 100644 index 593b6582bdb7..000000000000 --- a/admin/release-framework +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -## Build framework distributable - -# Setup variables -. admin/release-config -TARGET=dist/framework -cd $TARGET -git checkout $branch - -#--------------------------------------------------- -echo -e "${BOLD}Build the framework distributable${NORMAL}" - -echo -e "${BOLD}Copy the main files/folders...${NORMAL}" -releasable='app docs public system writable contributing.md env license.txt spark .gitignore' -for fff in $releasable ; do - if [ -d "$fff" ] ; then - rm -rf $fff - fi - cp -rf ${CI_DIR}/$fff . -done - -echo -e "${BOLD}Override as needed...${NORMAL}" -cprm -rf tests -mkdir tests -cp -rf ${CI_DIR}/tests/_support tests/ - -cp -rf ${CI_DIR}/admin/framework/* . - -#--------------------------------------------------- -# And finally, get ready for merging -echo -e "${BOLD}Assemble the pieces...${NORMAL}" -git add . -git commit -S -m "Release ${RELEASE}" -git checkout master -git merge $branch - -cd $CI_DIR - -#--------------------------------------------------- -# Done for now -echo -e "${BOLD}Distributable framework ready..${NORMAL}" diff --git a/admin/starter/.gitignore b/admin/starter/.gitignore deleted file mode 100644 index 6d0b5df9cae4..000000000000 --- a/admin/starter/.gitignore +++ /dev/null @@ -1,127 +0,0 @@ -#------------------------- -# 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 -Vagrantfile - -#------------------------- -# Temporary Files -#------------------------- -writable/cache/* -!writable/cache/index.html - -writable/logs/* -!writable/logs/index.html - -writable/session/* -!writable/session/index.html - -writable/uploads/* -!writable/uploads/index.html - -writable/debugbar/* - -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 -/.phpunit.*.cache \ No newline at end of file diff --git a/admin/starter/README.md b/admin/starter/README.md index 97b609055ac9..7c20eb17b64a 100644 --- a/admin/starter/README.md +++ b/admin/starter/README.md @@ -9,8 +9,6 @@ This repository holds a composer-installable app starter. It has been built from the [development repository](https://github.com/codeigniter4/CodeIgniter4). -**This is pre-release code and should not be used in production sites.** - More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. The user guide corresponding to this version of the framework can be found diff --git a/admin/starter/builds b/admin/starter/builds index 4e962ece8f2b..268e7a819c22 100755 --- a/admin/starter/builds +++ b/admin/starter/builds @@ -1,7 +1,7 @@ #!/usr/bin/env php =7.2", - "codeigniter4/framework": "^4.0.1" + "codeigniter4/framework": "^4" }, "require-dev": { "mikey179/vfsstream": "1.6.*", diff --git a/admin/starter/tests/database/ExampleDatabaseTest.php b/admin/starter/tests/database/ExampleDatabaseTest.php index ccb6885f9fed..2de0b6ae01bf 100644 --- a/admin/starter/tests/database/ExampleDatabaseTest.php +++ b/admin/starter/tests/database/ExampleDatabaseTest.php @@ -26,6 +26,7 @@ public function testSoftDeleteLeavesRow() { $model = new ExampleModel(); $this->setPrivateProperty($model, 'useSoftDeletes', true); + $this->setPrivateProperty($model, 'tempUseSoftDeletes', true); $object = $model->first(); $model->delete($object->id); diff --git a/admin/userguide/README.md b/admin/userguide/README.md index fdf2b76d9f27..67162bb9616d 100644 --- a/admin/userguide/README.md +++ b/admin/userguide/README.md @@ -9,8 +9,6 @@ This repository holds a composer-installable pre-built user guide for the framew It has been built from the [development repository](https://github.com/codeigniter4/CodeIgniter4). -**This is pre-release code and should not be used in production sites.** - More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. ## Installation & updates diff --git a/admin/workflow.md b/admin/workflow.md index 825a9c5d67f6..85e38e018ec2 100644 --- a/admin/workflow.md +++ b/admin/workflow.md @@ -12,12 +12,12 @@ release prep (`admin/release `)... - move or ignore stuff, distinguishing release from development - test that all is as it should be - merge the release branch into "master" & "develop" -- prepare the distribution repos After these have been vetted ... - push the release(s) to github (`admin/release-deploy `) -- **manually** create the releases & tag them on github, based on master +- **manually** create the release & tag it on GitHub, based on master Include any supplementary binaries as part of releases. +- Confirm the GitHub release action successfully deploys `appstarter` and `framework` - **manually** post a sticky announcement thread on the forum - **manually** tweet the release announcement @@ -37,6 +37,18 @@ 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. +## GitHub Action + +There is an action defined to run on any release publish event: +**.github/workflows/deploy.yml**. This will cascade any release made from +the main repo to the distribution repos, `appstarter` and `framework`. In order +for the action to authenticate you must create a Personal Access Token and add it +as a repo secret `ACCESS_TOKEN`. It is recommended that the PAT be to a secure bot +account with organization access that is dedicated for this purpose. + +If for some reason a release needs to be made that should not cascade the easiest +solution is to disable repo actions temporarily. + ## Usage Inside a shell prompt, in the project root: diff --git a/app/Config/Kint.php b/app/Config/Kint.php index 9562447ecac1..09db83dd5971 100644 --- a/app/Config/Kint.php +++ b/app/Config/Kint.php @@ -1,7 +1,7 @@ 0644, + + /* + * Logging Directory Path + * + * By default, logs are written to WRITEPATH . 'logs/' + * Specify a different destination here, if desired. + */ + 'path' => '', ], /** diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php index a570ef541680..41014d406bb3 100644 --- a/app/Config/Mimes.php +++ b/app/Config/Mimes.php @@ -516,11 +516,7 @@ public static function guessExtensionFromType(string $type, ?string $proposed_ex foreach (static::$mimes as $ext => $types) { - if (is_string($types) && $types === $type) - { - return $ext; - } - else if (is_array($types) && in_array($type, $types)) + if ((is_string($types) && $types === $type) || (is_array($types) && in_array($type, $types))) { return $ext; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 48327365e1b9..a2a9654bc526 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1,7 +1,7 @@ + } ?>

$

@@ -235,7 +235,7 @@ + } ?> @@ -287,15 +287,15 @@ - $value) : ?> + + } ?> + } ?> getName(), 'html') ?> diff --git a/composer.json b/composer.json index d8adb485fb25..a8045d379f79 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-curl": "*", "ext-intl": "*", "ext-json": "*", + "ext-mbstring": "*", "kint-php/kint": "^3.3", "psr/log": "^1.1", "laminas/laminas-escaper": "^2.6" diff --git a/contributing/documentation.rst b/contributing/documentation.rst index 3baaf7f001b2..bf7a56662f49 100644 --- a/contributing/documentation.rst +++ b/contributing/documentation.rst @@ -36,17 +36,10 @@ Tools Required ************** To see the rendered HTML, ePub, PDF, etc., you will need to install Sphinx -along with the PHP domain extension for Sphinx. The underlying requirement -is to have Python installed. Lastly, you will install the CI Lexer for -Pygments, so that code blocks can be properly highlighted. +along with the PHP domain extension for Sphinx. The underlying requirement +is to have Python installed. -.. code-block:: bash - - 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. +You can read more about installing all tools in /user_guide_src/README.rst ***************************************** Page and Section Headings and Subheadings diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8325f82a41a2..d12b2088a582 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -31,6 +31,7 @@ ./system/Commands/Sessions/Views/migration.tpl.php ./system/ComposerScripts.php ./system/Config/Routes.php + ./system/Test/bootstrap.php diff --git a/public/index.php b/public/index.php index 5b9e912f8270..3eaa592a1a4c 100644 --- a/public/index.php +++ b/public/index.php @@ -13,7 +13,7 @@ // Location of the Paths config file. // This is the line that might need to be changed, depending on your folder structure. -$pathsPath = FCPATH . '../app/Config/Paths.php'; +$pathsPath = realpath(FCPATH . '../app/Config/Paths.php'); // ^^^ Change this if you move your application folder /* diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index f16b2846d55e..49248eb198ed 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -39,8 +39,8 @@ namespace CodeIgniter\API; -use Config\Format; use CodeIgniter\HTTP\Response; +use Config\Format; /** * Response trait. @@ -56,7 +56,6 @@ */ trait ResponseTrait { - /** * Allows child classes to override the * status code that is used in their API. @@ -66,6 +65,7 @@ trait ResponseTrait protected $codes = [ 'created' => 201, 'deleted' => 200, + 'updated' => 200, 'no_content' => 204, 'invalid_request' => 400, 'unsupported_response_type' => 400, @@ -93,6 +93,15 @@ trait ResponseTrait 'not_implemented' => 501, ]; + /** + * How to format the response data. + * Either 'json' or 'xml'. If blank will be + * determine through content negotiation. + * + * @var string + */ + protected $format = 'json'; + //-------------------------------------------------------------------- /** @@ -190,6 +199,19 @@ public function respondDeleted($data = null, string $message = '') return $this->respond($data, $this->codes['deleted'], $message); } + /** + * Used after a resource has been successfully updated. + * + * @param mixed $data Data. + * @param string $message Message. + * + * @return mixed + */ + public function respondUpdated($data = null, string $message = '') + { + return $this->respond($data, $this->codes['updated'], $message); + } + //-------------------------------------------------------------------- /** @@ -364,9 +386,14 @@ protected function format($data = null) return $data; } - // Determine correct response type through content negotiation $config = new Format(); - $format = $this->request->negotiate('media', $config->supportedResponseFormats, false); + $format = "application/$this->format"; + + // Determine correct response type through content negotiation if not explicitly declared + if (empty($this->format) || ! in_array($this->format, ['json', 'xml'])) + { + $format = $this->request->negotiate('media', $config->supportedResponseFormats, false); + } $this->response->setContentType($format); @@ -387,4 +414,17 @@ protected function format($data = null) return $this->formatter->format($data); } + /** + * Sets the format the response should be in. + * + * @param string $format + * + * @return $this + */ + public function setResponseFormat(string $format = null) + { + $this->format = strtolower($format); + + return $this; + } } diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 46631074455d..fa5c9a2feb08 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -392,7 +392,7 @@ public function sanitizeFilename(string $filename): string // be a path. // http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_278 // Modified to allow backslash and colons for on Windows machines. - $filename = preg_replace('/[^a-zA-Z0-9\s\/\-\_\.\:\\\\]/', '', $filename); + $filename = preg_replace('/[^0-9\p{L}\s\/\-\_\.\:\\\\]/u', '', $filename); // Clean up our filename edges. $filename = trim($filename, '.-_'); diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 0b78e9c0ebfb..5aa217e11cce 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -353,7 +353,11 @@ public function findQualifiedNameFromPath(string $path) // Remove the file extension (.php) $className = mb_substr($className, 0, -4); - return $className; + // Check if this exists + if (class_exists($className)) + { + return $className; + } } } diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index e60036c79d0a..476c08d1e31b 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -437,7 +437,7 @@ 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 ++) { - static::write(''); + static::write(); } } @@ -504,9 +504,7 @@ public static function color(string $text, string $foreground, string $backgroun $string .= "\033[4m"; } - $string .= $text . "\033[0m"; - - return $string; + return $string . ($text . "\033[0m"); } //-------------------------------------------------------------------- @@ -702,24 +700,17 @@ public static function wrap(string $string = null, int $max = 0, int $pad_left = */ 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) + if (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; @@ -731,10 +722,6 @@ protected static function parseCommandLine() } static::$options[$arg] = $value; - - // Reset $optionsFound so it can collect segments - // past any options. - $optionsFound = false; } } @@ -958,7 +945,7 @@ public static function table(array $tbody, array $thead = []) } } - fwrite(STDOUT, $table); + static::write($table); } //-------------------------------------------------------------------- diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php index b1e6103ef1c2..2e1a8e116514 100644 --- a/system/CLI/CommandRunner.php +++ b/system/CLI/CommandRunner.php @@ -40,8 +40,8 @@ namespace CodeIgniter\CLI; -use Config\Services; use CodeIgniter\Controller; +use Config\Services; /** * Command runner @@ -104,7 +104,7 @@ public function index(array $params) if (is_null($command)) { - $command = 'help'; + $command = 'list'; } return $this->runCommand($command, $params); diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index 737db09e3dbc..86e70903e622 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -154,7 +154,7 @@ public function delete(string $key) { $key = $this->prefix . $key; - return is_file($this->path . $key) ? unlink($this->path . $key) : false; + return is_file($this->path . $key) && unlink($this->path . $key); } //-------------------------------------------------------------------- diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index f9a8f6cccc6f..c708a279e0c3 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -39,20 +39,20 @@ namespace CodeIgniter; use Closure; +use CodeIgniter\Debug\Timer; +use CodeIgniter\Events\Events; +use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Request; +use CodeIgniter\HTTP\Response; 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\Exceptions\RedirectException; use CodeIgniter\Router\RouteCollectionInterface; -use CodeIgniter\Exceptions\PageNotFoundException; +use Config\Cache; +use Config\Services; use Exception; /** @@ -66,7 +66,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - const CI_VERSION = '4.0.1'; + const CI_VERSION = '4.0.3'; /** * App startup time. @@ -194,7 +194,9 @@ public function initialize() if (! CI_DEBUG) { + // @codeCoverageIgnoreStart \Kint::$enabled_mode = false; + // @codeCoverageIgnoreEnd } } @@ -496,9 +498,11 @@ protected function bootstrapEnvironment() } else { + // @codeCoverageIgnoreStart header('HTTP/1.1 503 Service Unavailable.', true, 503); echo 'The application environment is not set correctly.'; exit(1); // EXIT_ERROR + // @codeCoverageIgnoreEnd } } @@ -550,9 +554,11 @@ protected function getRequestObject() return; } - if (is_cli() && ! (ENVIRONMENT === 'testing')) + if (is_cli() && ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart $this->request = Services::clirequest($this->config); + // @codeCoverageIgnoreEnd } else { @@ -747,9 +753,7 @@ public function displayPerformanceMetrics(string $output): string { $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); - $output = str_replace('{elapsed_time}', $this->totalTime, $output); - - return $output; + return str_replace('{elapsed_time}', $this->totalTime, $output); } //-------------------------------------------------------------------- @@ -958,10 +962,12 @@ protected function display404errors(PageNotFoundException $e) if (ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart if (ob_get_level() > 0) { ob_end_flush(); } + // @codeCoverageIgnoreEnd } else { @@ -972,7 +978,7 @@ protected function display404errors(PageNotFoundException $e) } } - throw PageNotFoundException::forPageNotFound($e->getMessage()); + throw PageNotFoundException::forPageNotFound(ENVIRONMENT !== 'production' || is_cli() ? $e->getMessage() : ''); } //-------------------------------------------------------------------- @@ -1110,7 +1116,9 @@ protected function sendResponse() */ protected function callExit($code) { + // @codeCoverageIgnoreStart exit($code); + // @codeCoverageIgnoreEnd } //-------------------------------------------------------------------- diff --git a/system/Commands/Database/CreateMigration.php b/system/Commands/Database/CreateMigration.php index 19d87a4458b9..3983d6775a28 100644 --- a/system/Commands/Database/CreateMigration.php +++ b/system/Commands/Database/CreateMigration.php @@ -40,8 +40,7 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; -use Config\Autoload; -use Config\Migrations; +use Config\Services; /** * Creates a new migration file. @@ -124,15 +123,14 @@ public function run(array $params = []) if (! empty($ns)) { - // Get all namespaces from PSR4 paths. - $config = new Autoload(); - $namespaces = $config->psr4; + // Get all namespaces + $namespaces = Services::autoloader()->getNamespace(); foreach ($namespaces as $namespace => $path) { if ($namespace === $ns) { - $homepath = realpath($path); + $homepath = realpath(reset($path)); break; } } @@ -178,7 +176,7 @@ public function down() helper('filesystem'); if (! write_file($path, $template)) { - CLI::error(lang('Migrations.writeError')); + CLI::error(lang('Migrations.writeError', [$path])); return; } diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index e0ec6696e8d9..bf6b4f8b716f 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -40,6 +40,7 @@ namespace CodeIgniter\Commands\Database; use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; /** * Does a rollback followed by a latest to refresh the current state @@ -95,6 +96,7 @@ class MigrateRefresh extends BaseCommand '-n' => 'Set migration namespace', '-g' => 'Set database group', '-all' => 'Set latest for all namespace, will ignore (-n) option', + '-f' => 'Force command - this option allows you to bypass the confirmation question when running this command in a production environment', ]; /** @@ -105,7 +107,20 @@ class MigrateRefresh extends BaseCommand */ public function run(array $params = []) { - $this->call('migrate:rollback', ['-b' => 0]); + $params = ['-b' => 0]; + + if (ENVIRONMENT === 'production') + { + $force = $params['-f'] ?? CLI::getOption('f'); + if (is_null($force) && CLI::prompt(lang('Migrations.refreshConfirm'), ['y', 'n']) === 'n') + { + return; + } + + $params['-f'] = ''; + } + + $this->call('migrate:rollback', $params); $this->call('migrate'); } diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index ba2598970e20..b2dca1c8ec56 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -42,7 +42,6 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use Config\Services; -use Config\Autoload; /** * Runs all of the migrations in reverse order, until they have @@ -97,6 +96,7 @@ class MigrateRollback extends BaseCommand protected $options = [ '-b' => 'Specify a batch to roll back to; e.g. "3" to return to batch #3 or "-2" to roll back twice', '-g' => 'Set database group', + '-f' => 'Force command - this option allows you to bypass the confirmation question when running this command in a production environment', ]; /** @@ -107,6 +107,15 @@ class MigrateRollback extends BaseCommand */ public function run(array $params = []) { + if (ENVIRONMENT === 'production') + { + $force = $params['-f'] ?? CLI::getOption('f'); + if (is_null($force) && CLI::prompt(lang('Migrations.rollBackConfirm'), ['y', 'n']) === 'n') + { + return; + } + } + $runner = Services::migrations(); $group = $params['-g'] ?? CLI::getOption('g'); diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 8e714369fc4c..94ad3ea984e6 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -42,7 +42,6 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use Config\Services; -use Config\Autoload; /** * Displays a list of all migrations and whether they've been run or not. @@ -106,6 +105,10 @@ class MigrateStatus extends BaseCommand 'CodeIgniter', 'Config', 'Tests\Support', + 'Kint', + 'Laminas\ZendFrameworkBridge', + 'Laminas\Escaper', + 'Psr\Log', ]; /** @@ -124,9 +127,11 @@ public function run(array $params = []) $runner->setGroup($group); } - // Get all namespaces from PSR4 paths. - $config = new Autoload(); - $namespaces = $config->psr4; + // Get all namespaces + $namespaces = Services::autoloader()->getNamespace(); + + // Determines whether any migrations were found + $found = false; // Loop for all $namespaces foreach ($namespaces as $namespace => $path) @@ -138,16 +143,17 @@ public function run(array $params = []) $runner->setNamespace($namespace); $migrations = $runner->findMigrations(); - $history = $runner->getHistory(); - - CLI::write($namespace); if (empty($migrations)) { - CLI::error(lang('Migrations.noneFound')); continue; } + $found = true; + $history = $runner->getHistory(); + + CLI::write($namespace); + ksort($migrations); $max = 0; @@ -176,6 +182,11 @@ public function run(array $params = []) CLI::write(str_pad(' ' . $migration->name, $max + 6) . ($date ? $date : '---')); } } + + if (! $found) + { + CLI::error(lang('Migrations.noneFound')); + } } } diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php index bae8e73dfff3..c20fe3220035 100644 --- a/system/Commands/ListCommands.php +++ b/system/Commands/ListCommands.php @@ -188,9 +188,8 @@ protected function padTitle(string $item, int $max, int $extra = 2, int $indent $max += $extra + $indent; $item = str_repeat(' ', $indent) . $item; - $item = str_pad($item, $max); - return $item; + return str_pad($item, $max); } //-------------------------------------------------------------------- diff --git a/system/Commands/Server/Serve.php b/system/Commands/Server/Serve.php index 9fb68fe80b5c..9e65862910b1 100644 --- a/system/Commands/Server/Serve.php +++ b/system/Commands/Server/Serve.php @@ -97,14 +97,14 @@ class Serve extends BaseCommand /** * The current port offset. * - * @var int + * @var integer */ protected $portOffset = 0; /** * The max number of ports to attempt to serve from * - * @var int + * @var integer */ protected $tries = 10; @@ -131,12 +131,14 @@ public function run(array $params) // Valid PHP Version? if (phpversion() < $this->minPHPVersion) { + // @codeCoverageIgnoreStart die('Your PHP version must be ' . $this->minPHPVersion . ' or higher to run CodeIgniter. Current version: ' . phpversion()); + // @codeCoverageIgnoreEnd } // Collect any user-supplied options and apply them. - $php = CLI::getOption('php') ?? PHP_BINARY; + $php = escapeshellarg(CLI::getOption('php') ?? PHP_BINARY); $host = CLI::getOption('host') ?? 'localhost'; $port = (int) (CLI::getOption('port') ?? '8080') + $this->portOffset; @@ -155,7 +157,8 @@ public function run(array $params) // to ensure our environment is set and it simulates basic mod_rewrite. passthru($php . ' -S ' . $host . ':' . $port . ' -t ' . $docroot . ' ' . $rewrite, $status); - if ($status && $this->portOffset < $this->tries) { + if ($status && $this->portOffset < $this->tries) + { $this->portOffset += 1; $this->run($params); diff --git a/system/Commands/Sessions/CreateMigration.php b/system/Commands/Sessions/CreateMigration.php index 15c7476a84c5..ca26bfaa856d 100644 --- a/system/Commands/Sessions/CreateMigration.php +++ b/system/Commands/Sessions/CreateMigration.php @@ -126,7 +126,7 @@ public function run(array $params = []) helper('filesystem'); if (! write_file($path, $template)) { - CLI::error(lang('Migrations.migWriteError')); + CLI::error(lang('Migrations.writeError', [$path])); return; } diff --git a/system/Common.php b/system/Common.php index a14bc66ec3b8..2e3fcd7d8884 100644 --- a/system/Common.php +++ b/system/Common.php @@ -37,19 +37,20 @@ * @filesource */ -use Config\App; -use Config\Logger; -use Config\Database; -use Config\Services; -use CodeIgniter\HTTP\URI; -use Laminas\Escaper\Escaper; use CodeIgniter\Config\Config; -use CodeIgniter\Test\TestLogger; +use CodeIgniter\Database\ConnectionInterface; +use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\Database\ConnectionInterface; -use CodeIgniter\Files\Exceptions\FileNotFoundException; +use CodeIgniter\HTTP\URI; +use CodeIgniter\Test\TestLogger; +use Config\App; +use Config\Database; +use Config\Logger; +use Config\Services; +use Config\View; +use Laminas\Escaper\Escaper; /** * Common Functions @@ -246,9 +247,11 @@ function db_connect($db = null, bool $getShared = true) */ function dd(...$vars) { + // @codeCoverageIgnoreStart Kint::$aliases[] = 'dd'; Kint::dump(...$vars); exit; + // @codeCoverageIgnoreEnd } } @@ -319,7 +322,7 @@ function esc($data, string $context = 'html', string $encoding = null) { if (is_array($data)) { - foreach ($data as $key => &$value) + foreach ($data as &$value) { $value = esc($value, $context); } @@ -384,10 +387,7 @@ function esc($data, string $context = 'html', string $encoding = null) * @param RequestInterface $request * @param ResponseInterface $response * - * Not testable, as it will exit! - * - * @throws \CodeIgniter\HTTP\Exceptions\HTTPException - * @codeCoverageIgnore + * @throws \CodeIgniter\HTTP\Exceptions\HTTPException */ function force_https(int $duration = 31536000, RequestInterface $request = null, ResponseInterface $response = null) { @@ -400,25 +400,33 @@ function force_https(int $duration = 31536000, RequestInterface $request = null, $response = Services::response(null, true); } - if (is_cli() || $request->isSecure()) + if (ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure())) { + // @codeCoverageIgnoreStart return; + // @codeCoverageIgnoreEnd } - // If the session library is loaded, we should regenerate + // If the session status is active, we should regenerate // the session ID for safety sake. - if (class_exists('Session', false)) + if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) { + // @codeCoverageIgnoreStart Services::session(null, true) ->regenerate(); + // @codeCoverageIgnoreEnd } - $uri = $request->uri; - $uri->setScheme('https'); + $baseURL = config(App::class)->baseURL; + + if (strpos($baseURL, 'http://') === 0) + { + $baseURL = (string) substr($baseURL, strlen('http://')); + } $uri = URI::createURIString( - $uri->getScheme(), $uri->getAuthority(true), $uri->getPath(), // Absolute URIs should use a "/" for an empty path - $uri->getQuery(), $uri->getFragment() + 'https', $baseURL, $request->uri->getPath(), // Absolute URIs should use a "/" for an empty path + $request->uri->getQuery(), $request->uri->getFragment() ); // Set an HSTS header @@ -426,7 +434,12 @@ function force_https(int $duration = 31536000, RequestInterface $request = null, $response->redirect($uri); $response->sendHeaders(); - exit(); + if (ENVIRONMENT !== 'testing') + { + // @codeCoverageIgnoreStart + exit(); + // @codeCoverageIgnoreEnd + } } } @@ -747,7 +760,9 @@ function old(string $key, $default = null, $escape = 'html') // Ensure the session is loaded if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') { + // @codeCoverageIgnoreStart session(); + // @codeCoverageIgnoreEnd } $request = Services::request(); @@ -1060,8 +1075,9 @@ function view(string $name, array $data = [], array $options = []): string */ $renderer = Services::renderer(); - $saveData = true; - if (array_key_exists('saveData', $options) && $options['saveData'] === true) + $saveData = config(View::class)->saveData; + + if (array_key_exists('saveData', $options)) { $saveData = (bool) $options['saveData']; unset($options['saveData']); diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index d53108f46a23..6ebb122705eb 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -90,7 +90,9 @@ protected static function moveFile(string $source, string $destination): bool if (empty($source)) { + // @codeCoverageIgnoreStart die('Cannot move file. Source path invalid.'); + // @codeCoverageIgnoreEnd } if (! is_file($source)) @@ -203,7 +205,9 @@ public static function moveEscaper() { if (! static::moveFile($source, $dest)) { + // @codeCoverageIgnoreStart die('Error moving: ' . $source); + // @codeCoverageIgnoreEnd } } } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 9a0100f10028..ceec8baa49b6 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -164,16 +164,12 @@ protected function getEnvValue(string $property, string $prefix, string $shortPr { 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; diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 541334a46496..4220d77f27be 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -128,7 +128,7 @@ public function parse(): ?array if (strpos($line, '=') !== false) { list($name, $value) = $this->normaliseVariable($line); - $vars[$name] = $value; + $vars[$name] = $value; } } @@ -314,10 +314,8 @@ protected function getVariable(string $name) { case array_key_exists($name, $_ENV): return $_ENV[$name]; - break; case array_key_exists($name, $_SERVER): return $_SERVER[$name]; - break; default: $value = getenv($name); diff --git a/system/Config/Services.php b/system/Config/Services.php index 6f166a64d61c..afb651232d11 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -39,6 +39,8 @@ namespace CodeIgniter\Config; use CodeIgniter\Cache\CacheFactory; +use CodeIgniter\Database\ConnectionInterface; +use CodeIgniter\Database\MigrationRunner; use CodeIgniter\Debug\Exceptions; use CodeIgniter\Debug\Iterator; use CodeIgniter\Debug\Timer; @@ -70,10 +72,8 @@ use CodeIgniter\Validation\Validation; use CodeIgniter\View\Cell; use CodeIgniter\View\Parser; -use Config\App; -use CodeIgniter\Database\ConnectionInterface; -use CodeIgniter\Database\MigrationRunner; use CodeIgniter\View\RendererInterface; +use Config\App; use Config\Cache; use Config\Images; use Config\Logger; @@ -207,8 +207,7 @@ public static function email($config = null, bool $getShared = true) { $config = new \Config\Email(); } - $email = new \CodeIgniter\Email\Email($config); - return $email; + return new \CodeIgniter\Email\Email($config); } /** @@ -232,8 +231,7 @@ public static function encrypter($config = null, $getShared = false) } $encryption = new Encryption($config); - $encrypter = $encryption->initialize($config); - return $encrypter; + return $encryption->initialize($config); } //-------------------------------------------------------------------- @@ -502,7 +500,7 @@ public static function pager($config = null, RendererInterface $view = null, boo if (empty($config)) { - $config = new \Config\Pager(); + $config = config('Pager'); } if (! $view instanceof RendererInterface) @@ -542,7 +540,7 @@ public static function parser(string $viewPath = null, $config = null, bool $get $viewPath = $paths->viewDirectory; } - return new Parser($config, $viewPath, static::locator(true), CI_DEBUG, static::logger(true)); + return new Parser($config, $viewPath, static::locator(), CI_DEBUG, static::logger()); } //-------------------------------------------------------------------- @@ -577,7 +575,7 @@ public static function renderer(string $viewPath = null, $config = null, bool $g $viewPath = $paths->viewDirectory; } - return new \CodeIgniter\View\View($config, $viewPath, static::locator(true), CI_DEBUG, static::logger(true)); + return new \CodeIgniter\View\View($config, $viewPath, static::locator(), CI_DEBUG, static::logger()); } //-------------------------------------------------------------------- @@ -705,7 +703,7 @@ public static function router(RouteCollectionInterface $routes = null, Request $ if (empty($routes)) { - $routes = static::routes(true); + $routes = static::routes(); } return new Router($routes, $request); @@ -759,7 +757,7 @@ public static function session(App $config = null, bool $getShared = true) $config = config(App::class); } - $logger = static::logger(true); + $logger = static::logger(); $driverName = $config->sessionDriver; $driver = new $driverName($config, static::request()->getIpAddress()); diff --git a/system/Controller.php b/system/Controller.php index 172104e0b2b5..985d828dd411 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -39,11 +39,11 @@ namespace CodeIgniter; -use Config\Services; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\Validation\Validation; use CodeIgniter\Validation\Exceptions\ValidationException; +use CodeIgniter\Validation\Validation; +use Config\Services; use Psr\Log\LoggerInterface; /** @@ -117,7 +117,6 @@ public function initController(RequestInterface $request, ResponseInterface $res $this->request = $request; $this->response = $response; $this->logger = $logger; - $this->logger->info('Controller "' . get_class($this) . '" loaded.'); if ($this->forceHTTPS > 0) { @@ -195,7 +194,7 @@ protected function validate($rules, array $messages = []): bool // If you replace the $rules array with the name of the group if (is_string($rules)) { - $validation = new \Config\Validation(); + $validation = config('Validation'); // If the rule wasn't found in the \Config\Validation, we // should throw an exception so the developer can find it. @@ -214,12 +213,10 @@ protected function validate($rules, array $messages = []): bool $rules = $validation->$rules; } - $success = $this->validator + return $this->validator ->withRequest($this->request) ->setRules($rules, $messages) ->run(); - - return $success; } //-------------------------------------------------------------------- diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 800247f855e7..33e700458048 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -39,9 +39,9 @@ namespace CodeIgniter\Database; +use Closure; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; -use Closure; /** * Class BaseBuilder @@ -103,7 +103,7 @@ class BaseBuilder * * @var array */ - protected $QBGroupBy = []; + public $QBGroupBy = []; /** * QB HAVING data @@ -213,6 +213,14 @@ class BaseBuilder */ protected $binds = []; + /** + * Collects the key count for named parameters + * in the Query object. + * + * @var array + */ + protected $bindsKeyCount = []; + /** * Some databases, like SQLite, do not by default * allow limiting of delete clauses. @@ -388,7 +396,7 @@ public function select($select = '*', bool $escape = null) */ public function selectMax(string $select = '', string $alias = '') { - return $this->maxMinAvgSum($select, $alias, 'MAX'); + return $this->maxMinAvgSum($select, $alias); } //-------------------------------------------------------------------- @@ -973,20 +981,38 @@ public function orHavingNotIn(string $key = null, $values = null, bool $escape = * @used-by whereNotIn() * @used-by orWhereNotIn() * - * @param string $key The field to search - * @param array|Closure $values The values searched on, or anonymous function with subquery - * @param boolean $not If the statement would be IN or NOT IN - * @param string $type - * @param boolean $escape - * @param string $clause (Internal use only) + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $not If the statement would be IN or NOT IN + * @param string $type + * @param boolean $escape + * @param string $clause (Internal use only) + * @throws InvalidArgumentException * * @return BaseBuilder */ protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null, string $clause = 'QBWhere') { - if ($key === null || $values === null || (! is_array($values) && ! ($values instanceof Closure))) + if (empty($key) || ! is_string($key)) { + if (CI_DEBUG) + { + throw new \InvalidArgumentException(sprintf('%s() expects $key to be a non-empty string', debug_backtrace(0, 2)[1]['function'])); + } + // @codeCoverageIgnoreStart return $this; + // @codeCoverageIgnoreEnd + } + + if ($values === null || (! is_array($values) && ! ($values instanceof Closure))) + { + if (CI_DEBUG) + { + throw new \InvalidArgumentException(sprintf('%s() expects $values to be of type array or closure', debug_backtrace(0, 2)[1]['function'])); + } + // @codeCoverageIgnoreStart + return $this; + // @codeCoverageIgnoreEnd } is_bool($escape) || $escape = $this->db->protectIdentifiers; @@ -1301,7 +1327,7 @@ protected function _like_statement(string $prefix = null, string $column, string */ public function groupStart() { - return $this->groupStartPrepare('', 'AND ', 'QBWhere'); + return $this->groupStartPrepare(); } //-------------------------------------------------------------------- @@ -1313,7 +1339,7 @@ public function groupStart() */ public function orGroupStart() { - return $this->groupStartPrepare('', 'OR ', 'QBWhere'); + return $this->groupStartPrepare('', 'OR '); } //-------------------------------------------------------------------- @@ -1325,7 +1351,7 @@ public function orGroupStart() */ public function notGroupStart() { - return $this->groupStartPrepare('NOT ', 'AND ', 'QBWhere'); + return $this->groupStartPrepare('NOT '); } //-------------------------------------------------------------------- @@ -1337,7 +1363,7 @@ public function notGroupStart() */ public function orNotGroupStart() { - return $this->groupStartPrepare('NOT ', 'OR ', 'QBWhere'); + return $this->groupStartPrepare('NOT ', 'OR '); } //-------------------------------------------------------------------- @@ -1349,7 +1375,7 @@ public function orNotGroupStart() */ public function groupEnd() { - return $this->groupEndPrepare('QBWhere'); + return $this->groupEndPrepare(); } // -------------------------------------------------------------------- @@ -1682,9 +1708,9 @@ public function offset(int $offset) * * @return string */ - protected function _limit(string $sql): string + protected function _limit(string $sql, bool $offsetIgnore = false): string { - return $sql . ' LIMIT ' . ($this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit; + return $sql . ' LIMIT ' . (false === $offsetIgnore && $this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit; } //-------------------------------------------------------------------- @@ -1897,7 +1923,7 @@ public function countAllResults(bool $reset = true) $limit = $this->QBLimit; $this->QBLimit = false; - $sql = ($this->QBDistinct === true) + $sql = ($this->QBDistinct === true || ! empty($this->QBGroupBy)) ? $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results" : @@ -2012,8 +2038,9 @@ public function insertBatch(array $set = null, bool $escape = null, int $batchSi { throw new DatabaseException('You must use the "set" method to update an entry.'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } } else @@ -2024,8 +2051,9 @@ public function insertBatch(array $set = null, bool $escape = null, int $batchSi { throw new DatabaseException('insertBatch() called with no data'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } $this->setInsertBatch($set, '', $escape); @@ -2234,8 +2262,9 @@ protected function validateInsert(): bool { throw new DatabaseException('You must use the "set" method to update an entry.'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } return true; @@ -2284,7 +2313,9 @@ public function replace(array $set = null) { throw new DatabaseException('You must use the "set" method to update an entry.'); } + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } $table = $this->QBFrom[0]; @@ -2445,7 +2476,7 @@ protected function _update(string $table, array $values): string return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) . $this->compileWhereHaving('QBWhere') . $this->compileOrderBy() - . ($this->QBLimit ? $this->_limit(' ') : ''); + . ($this->QBLimit ? $this->_limit(' ', true) : ''); } //-------------------------------------------------------------------- @@ -2468,8 +2499,9 @@ protected function validateUpdate(): bool { throw new DatabaseException('You must use the "set" method to update an entry.'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } return true; @@ -2497,8 +2529,9 @@ public function updateBatch(array $set = null, string $index = null, int $batchS { throw new DatabaseException('You must specify an index to match on for batch updates.'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } if ($set === null) @@ -2509,7 +2542,9 @@ public function updateBatch(array $set = null, string $index = null, int $batchS { throw new DatabaseException('You must use the "set" method to update an entry.'); } + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } } else @@ -2520,7 +2555,9 @@ public function updateBatch(array $set = null, string $index = null, int $batchS { throw new DatabaseException('updateBatch() called with no data'); } + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } $this->setUpdateBatch($set, $index); @@ -2573,7 +2610,7 @@ protected function _updateBatch(string $table, array $values, string $index): st $ids = []; $final = []; - foreach ($values as $key => $val) + foreach ($values as $val) { $ids[] = $val[$index]; @@ -2622,7 +2659,7 @@ public function setUpdateBatch($key, string $index = '', bool $escape = null) is_bool($escape) || $escape = $this->db->protectIdentifiers; - foreach ($key as $k => $v) + foreach ($key as $v) { $index_set = false; $clean = []; @@ -2769,8 +2806,9 @@ public function delete($where = '', int $limit = null, bool $reset_data = true) { throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.'); } - + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } $sql = $this->_delete($table); @@ -2787,7 +2825,7 @@ public function delete($where = '', int $limit = null, bool $reset_data = true) throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.'); } - $sql = $this->_limit($sql); + $sql = $this->_limit($sql, true); } if ($reset_data) @@ -2849,8 +2887,7 @@ public function decrement(string $column, int $value = 1) */ protected function _delete(string $table): string { - return 'DELETE ' . $this->compileIgnore('delete') . 'FROM ' . $table . $this->compileWhereHaving('QBWhere') - . ($this->QBLimit ? ' LIMIT ' . $this->QBLimit : ''); + return 'DELETE ' . $this->compileIgnore('delete') . 'FROM ' . $table . $this->compileWhereHaving('QBWhere'); } //-------------------------------------------------------------------- @@ -3304,6 +3341,12 @@ protected function resetSelect() { $this->db->setAliasedTables([]); } + + // Reset QBFrom part + if (! empty($this->QBFrom)) + { + $this->from(array_shift($this->QBFrom), true); + } } //-------------------------------------------------------------------- @@ -3402,12 +3445,11 @@ protected function setBind(string $key, $value = null, bool $escape = true): str return $key; } - $count = 0; - - while (array_key_exists($key . $count, $this->binds)) + if (! array_key_exists($key, $this->bindsKeyCount)) { - ++$count; + $this->bindsKeyCount[$key] = 0; } + $count = $this->bindsKeyCount[$key]++; $this->binds[$key . $count] = [ $value, diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index de9d510b088b..844cf7baf3bd 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Database; -use CodeIgniter\Events\Events; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Events\Events; /** * Class BaseConnection @@ -1002,7 +1002,7 @@ public function prepare(\Closure $func, array $options = []) $this->initialize(); } - $this->pretend(true); + $this->pretend(); $sql = $func($this); @@ -1381,9 +1381,7 @@ public function escape($str) { if (is_array($str)) { - $str = array_map([&$this, 'escape'], $str); - - return $str; + return array_map([&$this, 'escape'], $str); } else if (is_string($str) || ( is_object($str) && method_exists($str, '__toString'))) { diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index b9d586a36ec7..5e6abe357863 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -39,6 +39,8 @@ namespace CodeIgniter\Database; +use CodeIgniter\Entity; + /** * Class BaseResult */ @@ -187,12 +189,12 @@ public function getCustomResultObject(string $className) return $this->customResultObject[$className]; } - is_null($this->rowData) || $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(); $this->customResultObject[$className] = []; while ($row = $this->fetchObject($className)) { - if (method_exists($row, 'syncOriginal')) + if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) { $row->syncOriginal(); } @@ -237,7 +239,7 @@ public function getResultArray(): array return $this->resultArray; } - is_null($this->rowData) || $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(); while ($row = $this->fetchAssoc()) { $this->resultArray[] = $row; @@ -280,10 +282,10 @@ public function getResultObject(): array return $this->resultObject; } - is_null($this->rowData) || $this->dataSeek(0); + is_null($this->rowData) || $this->dataSeek(); while ($row = $this->fetchObject()) { - if (method_exists($row, 'syncOriginal')) + if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) { $row->syncOriginal(); } @@ -312,7 +314,7 @@ public function getRow($n = 0, string $type = 'object') if (! is_numeric($n)) { // We cache the row data for subsequent uses - is_array($this->rowData) || $this->rowData = $this->getRowArray(0); + is_array($this->rowData) || $this->rowData = $this->getRowArray(); // array_key_exists() instead of isset() to allow for NULL values if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) @@ -433,7 +435,7 @@ public function setRow($key, $value = null) // We cache the row data for subsequent uses if (! is_array($this->rowData)) { - $this->rowData = $this->getRowArray(0); + $this->rowData = $this->getRowArray(); } if (is_array($key)) diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index a2ba2b0df5c0..e9a4dd63a52a 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -356,7 +356,7 @@ public function backup($params = []) 'tables' => [], 'ignore' => [], 'filename' => '', - 'format' => 'gzip', // gzip, zip, txt + 'format' => 'gzip', // gzip, txt 'add_drop' => true, 'add_insert' => true, 'newline' => "\n", @@ -383,15 +383,14 @@ public function backup($params = []) } // Validate the format - if (! in_array($prefs['format'], ['gzip', 'zip', 'txt'], true)) + if (! in_array($prefs['format'], ['gzip', 'txt'], true)) { $prefs['format'] = 'txt'; } // 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')) - || ( $prefs['format'] === 'zip' && ! function_exists('gzcompress'))) + if ($prefs['format'] === 'gzip' && ! function_exists('gzencode')) { if ($this->db->DBDebug) { @@ -401,46 +400,12 @@ public function backup($params = []) $prefs['format'] = 'txt'; } - // Was a Zip file requested? - if ($prefs['format'] === 'zip') - { - // Set the filename if not provided (only needed with Zip files) - if ($prefs['filename'] === '') - { - $prefs['filename'] = (count($prefs['tables']) === 1 ? $prefs['tables'] : $this->db->database) - . date('Y-m-d_H-i', time()) . '.sql'; - } - else - { - // If they included the .zip file extension we'll remove it - if (preg_match('|.+?\.zip$|', $prefs['filename'])) - { - $prefs['filename'] = str_replace('.zip', '', $prefs['filename']); - } - - // Tack on the ".sql" file extension if needed - if (! preg_match('|.+?\.sql$|', $prefs['filename'])) - { - $prefs['filename'] .= '.sql'; - } - } - - // Load the Zip class and output it - // $CI =& get_instance(); - // $CI->load->library('zip'); - // $CI->zip->add_data($prefs['filename'], $this->_backup($prefs)); - // return $CI->zip->get_zip(); - } - elseif ($prefs['format'] === 'txt') // Was a text file requested? + if ($prefs['format'] === 'txt') // Was a text file requested? { return $this->_backup($prefs); } - elseif ($prefs['format'] === 'gzip') // Was a Gzip file requested? - { - return gzencode($this->_backup($prefs)); - } - return; + return gzencode($this->_backup($prefs)); } //-------------------------------------------------------------------- diff --git a/system/Database/Database.php b/system/Database/Database.php index a8d26b10032c..adb0063cd5f5 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -109,9 +109,7 @@ public function loadForge(ConnectionInterface $db) $db->initialize(); } - $class = new $className($db); - - return $class; + return new $className($db); } //-------------------------------------------------------------------- @@ -133,9 +131,7 @@ public function loadUtils(ConnectionInterface $db) $db->initialize(); } - $class = new $className($db); - - return $class; + return new $className($db); } //-------------------------------------------------------------------- diff --git a/system/Database/Forge.php b/system/Database/Forge.php index b40dc5ebf930..e38d505cfea9 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -110,14 +110,14 @@ class Forge * * @var string */ - protected $createDatabaseIfStr = null; + protected $createDatabaseIfStr; /** * CHECK DATABASE EXIST statement * * @var string */ - protected $checkDatabaseExistStr = null; + protected $checkDatabaseExistStr; /** * DROP DATABASE statement @@ -717,9 +717,7 @@ protected function _dropTable(string $table, bool $if_exists, bool $cascade): st } } - $sql = $sql . ' ' . $this->db->escapeIdentifiers($table); - - return $sql; + return $sql . ' ' . $this->db->escapeIdentifiers($table); } //-------------------------------------------------------------------- diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 6d4f0e2b6294..55350c86e8e5 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -38,10 +38,10 @@ namespace CodeIgniter\Database; -use Config\Services; use CodeIgniter\CLI\CLI; use CodeIgniter\Config\BaseConfig; use CodeIgniter\Exceptions\ConfigException; +use Config\Services; /** * Class MigrationRunner @@ -1027,6 +1027,15 @@ protected function migrate($direction, $migration): bool // Determine DBGroup to use $group = $instance->getDBGroup() ?? config('Database')->defaultGroup; + // Skip tests db group when not running in testing environment + if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') + { + // @codeCoverageIgnoreStart + $this->groupSkip = true; + return true; + // @codeCoverageIgnoreEnd + } + // Skip migration if group filtering was set if ($direction === 'up' && ! is_null($this->groupFilter) && $this->groupFilter !== $group) { diff --git a/system/Database/ModelFactory.php b/system/Database/ModelFactory.php index 6b7d514e8cc2..b2b43620ea0d 100644 --- a/system/Database/ModelFactory.php +++ b/system/Database/ModelFactory.php @@ -12,14 +12,6 @@ class ModelFactory */ static private $instances = []; - /** - * The Database connection to use, - * if other than default. - * - * @var ConnectionInterface - */ - static private $connection = null; - /** * Create new configuration instances or return * a shared instance diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 26b213ce9a20..9bda5b06f175 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -326,8 +326,19 @@ public function execute(string $sql) $res->free(); } } - - return $this->connID->query($this->prepQuery($sql)); + try + { + return $this->connID->query($this->prepQuery($sql)); + } + catch (\mysqli_sql_exception $e) + { + log_message('error', $e); + if ($this->DBDebug) + { + throw $e; + } + } + return false; } //-------------------------------------------------------------------- @@ -424,8 +435,6 @@ public function escapeLikeStringDirect($str) '\\' . '_', ], $str ); - - return $str; } //-------------------------------------------------------------------- diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 35d87b127a36..3ef41047f0da 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Database\MySQLi; -use CodeIgniter\Database\PreparedQueryInterface; use CodeIgniter\Database\BasePreparedQuery; +use CodeIgniter\Database\PreparedQueryInterface; /** * Prepared query for MySQLi @@ -116,9 +116,7 @@ public function _execute(array $data): bool // Bind it $this->statement->bind_param($bindTypes, ...$data); - $success = $this->statement->execute(); - - return $success; + return $this->statement->execute(); } //-------------------------------------------------------------------- diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index bf20a3d129fa..fa45c45db2dc 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -40,7 +40,6 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; -use http\Encoding\Stream\Inflate; /** * Builder for Postgre @@ -196,7 +195,9 @@ public function replace(array $set = null) { throw new DatabaseException('You must use the "set" method to update an entry.'); } + // @codeCoverageIgnoreStart return false; + // @codeCoverageIgnoreEnd } $table = $this->QBFrom[0]; @@ -269,7 +270,7 @@ public function delete($where = '', int $limit = null, bool $reset_data = true) * * @return string */ - protected function _limit(string $sql): string + protected function _limit(string $sql, bool $offsetIgnore = false): string { return $sql . ' LIMIT ' . $this->QBLimit . ($this->QBOffset ? " OFFSET {$this->QBOffset}" : ''); } @@ -316,7 +317,7 @@ protected function _update(string $table, array $values): string protected function _updateBatch(string $table, array $values, string $index): string { $ids = []; - foreach ($values as $key => $val) + foreach ($values as $val) { $ids[] = $val[$index]; diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index b239232b8c23..c3d6db6b4dda 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -187,11 +187,23 @@ public function getVersion(): string * * @param string $sql * - * @return resource + * @return mixed */ public function execute(string $sql) { - return pg_query($this->connID, $sql); + try + { + return pg_query($this->connID, $sql); + } + catch (\ErrorException $e) + { + log_message('error', $e); + if ($this->DBDebug) + { + throw $e; + } + } + return false; } //-------------------------------------------------------------------- diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 7b3bff3390f1..27e3da2c04dc 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Database\Postgre; -use CodeIgniter\Database\PreparedQueryInterface; use CodeIgniter\Database\BasePreparedQuery; +use CodeIgniter\Database\PreparedQueryInterface; /** * Prepared query for Postgre @@ -148,12 +148,10 @@ public function parameterize(string $sql): string // Track our current value $count = 0; - $sql = preg_replace_callback('/\?/', function ($matches) use (&$count) { + return preg_replace_callback('/\?/', function ($matches) use (&$count) { $count ++; return "\${$count}"; }, $sql); - - return $sql; } //-------------------------------------------------------------------- diff --git a/system/Database/Query.php b/system/Database/Query.php index 301185b425c7..dae2eeb11849 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -368,7 +368,7 @@ protected function compileBinds() { $sql = $this->finalQueryString; - $hasNamedBinds = strpos($sql, ':') !== false; + $hasNamedBinds = strpos($sql, ':') !== false && strpos($sql, ':=') === false; if (empty($this->binds) || empty($this->bindMarker) || (strpos($sql, $this->bindMarker) === false && @@ -440,9 +440,7 @@ protected function matchNamedBinds(string $sql, array $binds): string $replacers[":{$placeholder}:"] = $escapedValue; } - $sql = strtr($sql, $replacers); - - return $sql; + return strtr($sql, $replacers); } //-------------------------------------------------------------------- diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index ae83cfc1c03f..d3ac1cb622ca 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -165,9 +165,21 @@ public function getVersion(): string */ public function execute(string $sql) { - return $this->isWriteType($sql) - ? $this->connID->exec($sql) - : $this->connID->query($sql); + try + { + return $this->isWriteType($sql) + ? $this->connID->exec($sql) + : $this->connID->query($sql); + } + catch (\ErrorException $e) + { + log_message('error', $e); + if ($this->DBDebug) + { + throw $e; + } + } + return false; } //-------------------------------------------------------------------- @@ -323,8 +335,8 @@ public function _fieldData(string $table): array $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) ? (bool)$query[$i]->pk : false; - $retVal[$i]->nullable = isset($query[$i]->notnull) ? ! (bool)$query[$i]->notnull : false; + $retVal[$i]->primary_key = isset($query[$i]->pk) && (bool)$query[$i]->pk; + $retVal[$i]->nullable = isset($query[$i]->notnull) && ! (bool)$query[$i]->notnull; } return $retVal; diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 338c6e4ec3d6..bb6f48262a18 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -167,7 +167,6 @@ protected function _alterTable(string $alter_type, string $table, $field) ->run(); return ''; - break; case 'CHANGE': $sqlTable = new Table($this->db, $this); @@ -176,7 +175,6 @@ protected function _alterTable(string $alter_type, string $table, $field) ->run(); return null; - break; default: return parent::_alterTable($alter_type, $table, $field); } diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index fbd6feaca404..22202c3b5c4d 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Database\SQLite3; -use CodeIgniter\Database\PreparedQueryInterface; use CodeIgniter\Database\BasePreparedQuery; +use CodeIgniter\Database\PreparedQueryInterface; /** * Prepared query for SQLite3 diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php index 682b7cec859e..97eb414f982a 100644 --- a/system/Database/Seeder.php +++ b/system/Database/Seeder.php @@ -122,6 +122,8 @@ public function __construct(BaseConfig $config, BaseConnection $db = null) } $this->db = & $db; + + $this->forge = \Config\Database::forge($this->DBGroup); } //-------------------------------------------------------------------- diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 7cc3043b2396..7f7ca2cb2628 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -142,6 +142,7 @@ public function initialize() */ public function exceptionHandler(Throwable $exception) { + // @codeCoverageIgnoreStart $codes = $this->determineCodes($exception); $statusCode = $codes[0]; $exitCode = $codes[1]; @@ -171,6 +172,7 @@ public function exceptionHandler(Throwable $exception) $this->render($exception, $statusCode); exit($exitCode); + // @codeCoverageIgnoreEnd } //-------------------------------------------------------------------- @@ -186,11 +188,10 @@ public function exceptionHandler(Throwable $exception) * @param string $message * @param string|null $file * @param integer|null $line - * @param null $context * * @throws \ErrorException */ - public function errorHandler(int $severity, string $message, string $file = null, int $line = null, $context = null) + public function errorHandler(int $severity, string $message, string $file = null, int $line = null) { if (! (error_reporting() & $severity)) { @@ -371,17 +372,20 @@ protected function determineCodes(Throwable $exception): array */ public static function cleanPath(string $file): string { - if (strpos($file, APPPATH) === 0) + switch (true) { - $file = 'APPPATH/' . substr($file, strlen(APPPATH)); - } - elseif (strpos($file, SYSTEMPATH) === 0) - { - $file = 'SYSTEMPATH/' . substr($file, strlen(SYSTEMPATH)); - } - elseif (strpos($file, FCPATH) === 0) - { - $file = 'FCPATH/' . substr($file, strlen(FCPATH)); + case strpos($file, APPPATH) === 0: + $file = 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)); + break; + case strpos($file, SYSTEMPATH) === 0: + $file = 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)); + break; + case strpos($file, FCPATH) === 0: + $file = 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)); + break; + case defined('VENDORPATH') && strpos($file, VENDORPATH) === 0; + $file = 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)); + break; } return $file; diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 02e50fc85034..ff2be35cb617 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -118,7 +118,7 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques $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['segmentDuration'] = $this->roundTo($data['totalTime'] / 7); $data['segmentCount'] = (int) ceil($data['totalTime'] / $data['segmentDuration']); $data['CI_VERSION'] = \CodeIgniter\CodeIgniter::CI_VERSION; $data['collectors'] = []; @@ -167,7 +167,7 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques $data['vars']['post'][esc($name)] = is_array($value) ? '
' . esc(print_r($value, true)) . '
' : esc($value); } - foreach ($request->getHeaders() as $header => $value) + foreach ($request->getHeaders() as $value) { if (empty($value)) { @@ -415,6 +415,7 @@ public function respond() return; } + // @codeCoverageIgnoreStart $request = Services::request(); // If the request contains '?debugbar then we're @@ -459,6 +460,7 @@ public function respond() http_response_code(404); exit; // Exit here is needed to avoid load the index page } + // @codeCoverageIgnoreEnd } /** diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php index 1ae809cdfbcb..82ef63475af6 100644 --- a/system/Debug/Toolbar/Collectors/Config.php +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -39,9 +39,9 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; +use CodeIgniter\CodeIgniter; use Config\App; use Config\Services; -use CodeIgniter\CodeIgniter; /** * Debug toolbar configuration diff --git a/system/Debug/Toolbar/Collectors/Events.php b/system/Debug/Toolbar/Collectors/Events.php index 285d87d8e709..41c9f008c87d 100644 --- a/system/Debug/Toolbar/Collectors/Events.php +++ b/system/Debug/Toolbar/Collectors/Events.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; -use Config\Services; use CodeIgniter\View\RendererInterface; +use Config\Services; /** * Views collector @@ -111,7 +111,7 @@ protected function formatTimelineData(): array $rows = $this->viewer->getPerformanceData(); - foreach ($rows as $name => $info) + foreach ($rows as $info) { $data[] = [ 'name' => 'View: ' . $info['view'], diff --git a/system/Debug/Toolbar/Collectors/Views.php b/system/Debug/Toolbar/Collectors/Views.php index 8bcec5698340..bf0c1108cb07 100644 --- a/system/Debug/Toolbar/Collectors/Views.php +++ b/system/Debug/Toolbar/Collectors/Views.php @@ -39,8 +39,8 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; -use Config\Services; use CodeIgniter\View\RendererInterface; +use Config\Services; /** * Views collector @@ -126,7 +126,7 @@ protected function formatTimelineData(): array $rows = $this->viewer->getPerformanceData(); - foreach ($rows as $name => $info) + foreach ($rows as $info) { $data[] = [ 'name' => 'View: ' . $info['view'], diff --git a/system/Debug/Toolbar/Views/toolbarloader.js.php b/system/Debug/Toolbar/Views/toolbarloader.js.php index 4ecdeeb16968..af6933810063 100644 --- a/system/Debug/Toolbar/Views/toolbarloader.js.php +++ b/system/Debug/Toolbar/Views/toolbarloader.js.php @@ -75,9 +75,11 @@ function newXHR() { 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'; + if(h2) { + h2.innerHTML = 'History You have new debug data. '; + var badge = document.querySelector('a[data-tab="ci-history"] > span > .badge'); + badge.className += ' active'; + } } } }, false); diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 1f047f6e1ae4..4c2d3337ea66 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -38,10 +38,8 @@ namespace CodeIgniter\Encryption; -use Config\Encryption as EncryptionConfig; -use CodeIgniter\Encryption\Exceptions\EncryptionException; use CodeIgniter\Config\BaseConfig; -use Config\Services; +use CodeIgniter\Encryption\Exceptions\EncryptionException; /** * CodeIgniter Encryption Manager diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 72b6023656a1..50622d1f0d9f 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -64,7 +64,7 @@ class OpenSSLHandler extends BaseHandler * * @param BaseConfig $config * - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\Exceptions\EncryptionException */ public function __construct(BaseConfig $config = null) { @@ -77,7 +77,7 @@ public function __construct(BaseConfig $config = null) * @param string $data Input data * @param array $params Over-ridden parameters, specifically the key * @return string - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\Exceptions\EncryptionException */ public function encrypt($data, $params = null) { @@ -114,9 +114,8 @@ public function encrypt($data, $params = null) $result = $iv . $data; $hmacKey = \hash_hmac($this->digest, $result, $secret, true); - $result = $hmacKey . $result; - return $result; + return $hmacKey . $result; } // -------------------------------------------------------------------- @@ -127,7 +126,7 @@ public function encrypt($data, $params = null) * @param string $data Encrypted data * @param array $params Over-ridden parameters, specifically the key * @return string - * @throws \CodeIgniter\Encryption\EncryptionException + * @throws \CodeIgniter\Encryption\Exceptions\EncryptionException */ public function decrypt($data, $params = null) { @@ -145,7 +144,7 @@ public function decrypt($data, $params = null) } if (empty($this->key)) { - throw EncryptionException::forStarterKeyNeeded(); + throw EncryptionException::forNeedsStarterKey(); } // derive a secret key diff --git a/system/Entity.php b/system/Entity.php index 94153d0b9a15..1ccd6d298bde 100644 --- a/system/Entity.php +++ b/system/Entity.php @@ -39,14 +39,13 @@ namespace CodeIgniter; -use CodeIgniter\Exceptions\EntityException; -use CodeIgniter\I18n\Time; use CodeIgniter\Exceptions\CastException; +use CodeIgniter\I18n\Time; /** * Entity encapsulation, for use with CodeIgniter\Model */ -class Entity +class Entity implements \JsonSerializable { /** * Maps names used in sets and gets against unique @@ -165,7 +164,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true): array // allow our magic methods a chance to do their thing. foreach ($this->attributes as $key => $value) { - if (substr($key, 0, 1) === '_') + if (strpos($key, '_') === 0) { continue; } @@ -353,7 +352,7 @@ public function __set(string $key, $value = null) if (array_key_exists($key, $this->casts)) { - $isNullable = substr($this->casts[$key], 0, 1) === '?'; + $isNullable = strpos($this->casts[$key], '?') === 0; $castTo = $isNullable ? substr($this->casts[$key], 1) : $this->casts[$key]; } @@ -530,7 +529,7 @@ protected function mutateDate($value) protected function castAs($value, string $type) { - if (substr($type, 0, 1) === '?') + if (strpos($type, '?') === 0) { if ($value === null) { @@ -570,17 +569,15 @@ protected function castAs($value, string $type) $value = (array)$value; break; case 'json': - $value = $this->castAsJson($value, false); + $value = $this->castAsJson($value); break; case 'json-array': $value = $this->castAsJson($value, true); break; case 'datetime': - return new \DateTime($value); - break; + return $this->mutateDate($value); case 'timestamp': return strtotime($value); - break; } return $value; @@ -614,4 +611,15 @@ private function castAsJson($value, bool $asArray = false) } return $tmp; } + + /** + * Support for json_encode() + * + * @return array|mixed + * @throws \Exception + */ + public function jsonSerialize() + { + return $this->toArray(); + } } diff --git a/system/Exceptions/CastException.php b/system/Exceptions/CastException.php index a5bb06594e2e..ddcb7178e844 100644 --- a/system/Exceptions/CastException.php +++ b/system/Exceptions/CastException.php @@ -19,22 +19,17 @@ public static function forInvalidJsonFormatException(int $error) switch($error) { case JSON_ERROR_DEPTH: - throw new static(lang('Cast.jsonErrorDepth')); - break; + return new static(lang('Cast.jsonErrorDepth')); case JSON_ERROR_STATE_MISMATCH: - throw new static(lang('Cast.jsonErrorStateMismatch')); - break; + return new static(lang('Cast.jsonErrorStateMismatch')); case JSON_ERROR_CTRL_CHAR: - throw new static(lang('Cast.jsonErrorCtrlChar')); - break; + return new static(lang('Cast.jsonErrorCtrlChar')); case JSON_ERROR_SYNTAX: - throw new static(lang('Cast.jsonErrorSyntax')); - break; + return new static(lang('Cast.jsonErrorSyntax')); case JSON_ERROR_UTF8: - throw new static(lang('Cast.jsonErrorUtf8')); - break; + return new static(lang('Cast.jsonErrorUtf8')); default: - throw new static(lang('Cast.jsonErrorUnknown')); + return new static(lang('Cast.jsonErrorUnknown')); } } diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php index dc36377e59fa..964c52127579 100644 --- a/system/Exceptions/ConfigException.php +++ b/system/Exceptions/ConfigException.php @@ -16,6 +16,6 @@ class ConfigException extends CriticalError public static function forDisabledMigrations() { - throw new static(lang('Migrations.disabled')); + return new static(lang('Migrations.disabled')); } } diff --git a/system/Files/File.php b/system/Files/File.php index 8b5fc2ed2930..1f5979588eba 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -39,9 +39,9 @@ namespace CodeIgniter\Files; -use SplFileInfo; use CodeIgniter\Files\Exceptions\FileException; use CodeIgniter\Files\Exceptions\FileNotFoundException; +use SplFileInfo; /** * Wrapper for PHP's built-in SplFileInfo, with goodies. diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 853e135a5b53..451a2dabc787 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -39,9 +39,9 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Filters\Exceptions\FilterException; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\Filters\Exceptions\FilterException; /** * Filters @@ -150,47 +150,59 @@ public function run(string $uri, string $position = 'before') throw FilterException::forNoAlias($alias); } - $class = new $this->config->aliases[$alias](); - - if (! $class instanceof FilterInterface) + if (is_array($this->config->aliases[$alias])) { - throw FilterException::forIncorrectInterface(get_class($class)); + $classNames = $this->config->aliases[$alias]; + } + else + { + $classNames = [$this->config->aliases[$alias]]; } - if ($position === 'before') + foreach ($classNames as $className) { - $result = $class->before($this->request, $this->arguments[$alias] ?? null); + $class = new $className(); - if ($result instanceof RequestInterface) + if (! $class instanceof FilterInterface) { - $this->request = $result; - continue; + throw FilterException::forIncorrectInterface(get_class($class)); } - // If the response object was sent back, - // then send it and quit. - if ($result instanceof ResponseInterface) + if ($position === 'before') { - // short circuit - bypass any other filters - return $result; - } + $result = $class->before($this->request, $this->arguments[$alias] ?? null); - // Ignore an empty result - if (empty($result)) - { - continue; - } + if ($result instanceof RequestInterface) + { + $this->request = $result; + continue; + } - return $result; - } - elseif ($position === 'after') - { - $result = $class->after($this->request, $this->response); + // If the response object was sent back, + // then send it and quit. + if ($result instanceof ResponseInterface) + { + // short circuit - bypass any other filters + return $result; + } + + // Ignore an empty result + if (empty($result)) + { + continue; + } - if ($result instanceof ResponseInterface) + return $result; + } + elseif ($position === 'after') { - $this->response = $result; - continue; + $result = $class->after($this->request, $this->response); + + if ($result instanceof ResponseInterface) + { + $this->response = $result; + continue; + } } } } diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php index aea08c3c4a1d..a5807e42bcdd 100644 --- a/system/Filters/Honeypot.php +++ b/system/Filters/Honeypot.php @@ -38,10 +38,10 @@ namespace CodeIgniter\Filters; +use CodeIgniter\Honeypot\Exceptions\HoneypotException; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Config\Services; -use CodeIgniter\Honeypot\Exceptions\HoneypotException; /** * Honeypot filter diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index 50caf3930f9d..823f3ca240e6 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -56,13 +56,13 @@ class JSONFormatter implements FormatterInterface */ public function format($data) { - $options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + $options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR; $options = ENVIRONMENT === 'production' ? $options : $options | JSON_PRETTY_PRINT; $result = json_encode($data, $options, 512); - if (json_last_error() !== JSON_ERROR_NONE) + if ( ! in_array(json_last_error(), [JSON_ERROR_NONE, JSON_ERROR_RECURSION])) { throw FormatException::forInvalidJSON(json_last_error_msg()); } diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index c5506accbfeb..d1a8e94428d1 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -536,9 +536,7 @@ protected function applyMethod(string $method, array $curl_options): array // Have content? if ($size === null || $size > 0) { - $curl_options = $this->applyBody($curl_options); - - return $curl_options; + return $this->applyBody($curl_options); } if ($method === 'PUT' || $method === 'POST') diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 713312f9e5bf..910f700287cb 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -136,7 +136,7 @@ class ContentSecurityPolicy * * @var string */ - protected $reportURI = null; + protected $reportURI; /** * Used for security enforcement diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 0b363cdb5502..b7c04dfba89a 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -427,7 +427,7 @@ public function getPostGet($index = null, $filter = null, $flags = null) // Use $_POST directly here, since filter_has_var only // checks the initial POST data, not anything that might // have been added since. - return isset($_POST[$index]) ? $this->getPost($index, $filter, $flags) : $this->getGet($index, $filter, $flags); + return isset($_POST[$index]) ? $this->getPost($index, $filter, $flags) : (isset($_GET[$index]) ? $this->getGet($index, $filter, $flags) : $this->getPost()); } //-------------------------------------------------------------------- @@ -446,7 +446,7 @@ public function getGetPost($index = null, $filter = null, $flags = null) // Use $_GET directly here, since filter_has_var only // checks the initial GET data, not anything that might // have been added since. - return isset($_GET[$index]) ? $this->getGet($index, $filter, $flags) : $this->getPost($index, $filter, $flags); + return isset($_GET[$index]) ? $this->getGet($index, $filter, $flags) : (isset($_POST[$index]) ? $this->getPost($index, $filter, $flags) : $this->getGet()); } //-------------------------------------------------------------------- @@ -616,7 +616,6 @@ protected function detectURI(string $protocol, string $baseURL) $this->uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME)); $this->uri->setHost(parse_url($baseURL, PHP_URL_HOST)); $this->uri->setPort(parse_url($baseURL, PHP_URL_PORT)); - $this->uri->resolveRelativeURI(parse_url($baseURL, PHP_URL_PATH)); // Ensure we have any query vars $this->uri->setQuery($_SERVER['QUERY_STRING'] ?? ''); @@ -722,7 +721,7 @@ protected function parseRequestURI(): string $query = $parts['query'] ?? ''; $uri = $parts['path'] ?? ''; - if (isset($_SERVER['SCRIPT_NAME'][0])) + if (isset($_SERVER['SCRIPT_NAME'][0]) && pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php') { // strip the script name from the beginning of the URI if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0) diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 1afb1d5258d5..6d7a5cce8dbc 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -316,7 +316,9 @@ public function appendHeader(string $name, string $value) { $orig_name = $this->getHeaderName($name); - $this->headers[$orig_name]->appendValue($value); + array_key_exists($orig_name, $this->headers) + ? $this->headers[$orig_name]->appendValue($value) + : $this->setHeader($name, $value); return $this; } diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index 6ac2d06b7bc4..74aa4706227d 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -179,7 +179,7 @@ public function encoding(array $supported = []): string */ public function language(array $supported): string { - return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language')); + return $this->getBestMatch($supported, $this->request->getHeaderLine('accept-language'), false, false, true); } //-------------------------------------------------------------------- @@ -198,10 +198,11 @@ public function language(array $supported): string * @param boolean $enforceTypes If TRUE, will compare media types and sub-types. * @param boolean $strictMatch If TRUE, will return empty string on no match. * If FALSE, will return the first supported element. + * @param boolean $matchLocales If TRUE, will match locale sub-types to a broad type (fr-FR = fr) * * @return string Best match */ - protected function getBestMatch(array $supported, string $header = null, bool $enforceTypes = false, bool $strictMatch = false): string + protected function getBestMatch(array $supported, string $header = null, bool $enforceTypes = false, bool $strictMatch = false, bool $matchLocales = false): string { if (empty($supported)) { @@ -232,7 +233,7 @@ protected function getBestMatch(array $supported, string $header = null, bool $e // If an acceptable value is supported, return it foreach ($supported as $available) { - if ($this->match($accept, $available, $enforceTypes)) + if ($this->match($accept, $available, $enforceTypes, $matchLocales)) { return $available; } @@ -337,12 +338,14 @@ public function parseHeader(string $header): array /** * Match-maker * - * @param array $acceptable - * @param string $supported - * @param boolean $enforceTypes + * @param array $acceptable + * @param string $supported + * @param boolean $enforceTypes + * @param boolean $matchLocales + * * @return boolean */ - protected function match(array $acceptable, string $supported, bool $enforceTypes = false): bool + protected function match(array $acceptable, string $supported, bool $enforceTypes = false, $matchLocales = false): bool { $supported = $this->parseHeader($supported); if (is_array($supported) && count($supported) === 1) @@ -363,6 +366,12 @@ protected function match(array $acceptable, string $supported, bool $enforceType return $this->matchTypes($acceptable, $supported); } + // Do we need to match locales against broader locales? + if ($matchLocales) + { + return $this->matchLocales($acceptable, $supported); + } + return false; } @@ -409,8 +418,14 @@ protected function matchParameters(array $acceptable, array $supported): bool */ public function matchTypes(array $acceptable, array $supported): bool { - list($aType, $aSubType) = explode('/', $acceptable['value']); - list($sType, $sSubType) = explode('/', $supported['value']); + [ + $aType, + $aSubType, + ] = explode('/', $acceptable['value']); + [ + $sType, + $sSubType, + ] = explode('/', $supported['value']); // If the types don't match, we're done. if ($aType !== $sType) @@ -429,4 +444,25 @@ public function matchTypes(array $acceptable, array $supported): bool } //-------------------------------------------------------------------- + + /** + * Will match locales against their broader pairs, so that fr-FR would + * match a supported localed of fr + * + * @param array $acceptable + * @param array $supported + * + * @return boolean + */ + public function matchLocales(array $acceptable, array $supported): bool + { + $aBroad = mb_strpos($acceptable['value'], '-') > 0 + ? mb_substr($acceptable['value'], 0, mb_strpos($acceptable['value'], '-')) + : $acceptable['value']; + $sBroad = mb_strpos($supported['value'], '-') > 0 + ? mb_substr($supported['value'], 0, mb_strpos($supported['value'], '-')) + : $supported['value']; + + return strtolower($aBroad) === strtolower($sBroad); + } } diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index a6d3c5ec3cd5..0d8320bb72a9 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -40,10 +40,10 @@ namespace CodeIgniter\HTTP; -use Config\App; -use Config\Format; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\Pager\PagerInterface; +use Config\App; +use Config\Format; /** * Representation of an outgoing, getServer-side response. @@ -986,6 +986,7 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat $name = $prefix . $name; + $cookieHasFlag = false; foreach ($this->cookies as &$cookie) { if ($cookie['name'] === $name) @@ -1000,11 +1001,16 @@ public function deleteCookie(string $name = '', string $domain = '', string $pat } $cookie['value'] = ''; $cookie['expires'] = ''; - + $cookieHasFlag = true; break; } } + if (! $cookieHasFlag) + { + $this->setCookie($name, '', '', $domain, $path, $prefix); + } + return $this; } diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 001b0a96de68..09aa3cdc2eeb 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -566,7 +566,7 @@ public static function createURIString(string $scheme = null, string $authority $uri .= $authority; } - if ($path) + if ($path !== '') { $uri .= substr($uri, -1, 1) !== '/' ? '/' . ltrim($path, '/') : $path; } @@ -597,7 +597,12 @@ public function setAuthority(string $str) { $parts = parse_url($str); - if (empty($parts['host']) && ! empty($parts['path'])) + if (! isset($parts['path'])) + { + $parts['path'] = $this->getPath(); + } + + if (empty($parts['host']) && $parts['path'] !== '') { $parts['host'] = $parts['path']; unset($parts['path']); @@ -705,7 +710,9 @@ public function setPath(string $path) { $this->path = $this->filterPath($path); - $this->segments = explode('/', $this->path); + $tempPath = trim($this->path, '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); return $this; } @@ -721,7 +728,9 @@ public function refreshPath() { $this->path = $this->filterPath(implode('/', $this->segments)); - $this->segments = explode('/', $this->path); + $tempPath = trim($this->path, '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); return $this; } @@ -913,7 +922,7 @@ protected function applyParts(array $parts) { $this->user = $parts['user']; } - if (! empty($parts['path'])) + if (isset($parts['path']) && $parts['path'] !== '') { $this->path = $this->filterPath($parts['path']); } @@ -953,9 +962,11 @@ protected function applyParts(array $parts) } // Populate our segments array - if (! empty($parts['path'])) + if (isset($parts['path']) && $parts['path'] !== '') { - $this->segments = explode('/', trim($parts['path'], '/')); + $tempPath = trim($parts['path'], '/'); + + $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath); } } @@ -1048,14 +1059,14 @@ public function resolveRelativeURI(string $uri) */ protected function mergePaths(URI $base, URI $reference): string { - if (! empty($base->getAuthority()) && empty($base->getPath())) + if (! empty($base->getAuthority()) && $base->getPath() === '') { return '/' . ltrim($reference->getPath(), '/ '); } $path = explode('/', $base->getPath()); - if (empty($path[0])) + if ($path[0] === '') { unset($path[0]); } @@ -1082,7 +1093,7 @@ protected function mergePaths(URI $base, URI $reference): string */ public function removeDotSegments(string $path): string { - if (empty($path) || $path === '/') + if ($path === '' || $path === '/') { return $path; } @@ -1091,7 +1102,7 @@ public function removeDotSegments(string $path): string $input = explode('/', $path); - if (empty($input[0])) + if ($input[0] === '') { unset($input[0]); $input = array_values($input); diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index 6e5b5965a8a7..c6873fd5b45b 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -51,7 +51,7 @@ class UserAgent * * @var string */ - protected $agent = null; + protected $agent; /** * Flag for if the user-agent belongs to a browser diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 9c5d8f6e5120..c114df9fa97c 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -79,7 +79,7 @@ function _array_search_dot(array $indexes, array $array) ? array_shift($indexes) : null; - if (empty($currentIndex) || (! isset($array[$currentIndex]) && $currentIndex !== '*')) + if ((empty($currentIndex) && intval($currentIndex) !== 0) || (! isset($array[$currentIndex]) && $currentIndex !== '*')) { return null; } @@ -90,7 +90,7 @@ function _array_search_dot(array $indexes, array $array) // If $array has more than 1 item, we have to loop over each. if (is_array($array)) { - foreach ($array as $key => $value) + foreach ($array as $value) { $answer = _array_search_dot($indexes, $value); diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index d85f644d0994..50ab39272a2d 100755 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -96,9 +96,8 @@ function get_cookie($index, bool $xssClean = false) $request = \Config\Services::request(); $filter = true === $xssClean ? FILTER_SANITIZE_STRING : null; - $cookie = $request->getCookie($prefix . $index, $filter); - return $cookie; + return $request->getCookie($prefix . $index, $filter); } } diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php index 6cf362ad49b5..1951d2535350 100644 --- a/system/Helpers/date_helper.php +++ b/system/Helpers/date_helper.php @@ -96,8 +96,7 @@ function timezone_select(string $class = '', string $default = '', int $what = \ $selected = ($timezone === $default) ? 'selected' : ''; $buffer .= "" . PHP_EOL; } - $buffer .= '' . PHP_EOL; - return $buffer; + return $buffer . ('' . PHP_EOL); } } diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index 1ead233b47ff..2556e86ade2a 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -209,45 +209,54 @@ function delete_files(string $path, bool $del_dir = false, bool $htdocs = false, * Reads the specified directory and builds an array containing the filenames. * Any sub-folders contained within the specified path are read as well. * - * @param string $source_dir Path to source - * @param boolean $include_path Whether to include the path as part of the filename - * @param boolean $recursion Internal variable to determine recursion status - do not use in calls + * @param string $source_dir Path to source + * @param boolean|null $include_path Whether to include the path as part of the filename; false for no path, null for a relative path, true for full path + * @param boolean $hidden Whether to include hidden files (files beginning with a period) * * @return array */ - function get_filenames(string $source_dir, bool $include_path = false, bool $recursion = false): array + function get_filenames(string $source_dir, ?bool $include_path = false, bool $hidden = false): array { - static $fileData = []; + $files = []; + + $source_dir = realpath($source_dir) ?: $source_dir; + $source_dir = rtrim($source_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; try { - $fp = opendir($source_dir); - // reset the array and make sure $source_dir has a trailing slash on the initial call - if ($recursion === false) + foreach (new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($source_dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ) as $name => $object) { - $fileData = []; - $source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; - } + $basename = pathinfo($name, PATHINFO_BASENAME); - while (false !== ($file = readdir($fp))) - { - if (is_dir($source_dir . $file) && $file[0] !== '.') + if (! $hidden && $basename[0] === '.') { - get_filenames($source_dir . $file . DIRECTORY_SEPARATOR, $include_path, true); + continue; } - elseif ($file[0] !== '.') + elseif ($include_path === false) { - $fileData[] = ($include_path === true) ? $source_dir . $file : $file; + $files[] = $basename; + } + elseif (is_null($include_path)) + { + $files[] = str_replace($source_dir, '', $name); + } + else + { + $files[] = $name; } } - - closedir($fp); - return $fileData; } - catch (\Exception $fe) + catch (\Throwable $e) { return []; } + + sort($files); + + return $files; } } diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index 82c809ce3fa4..e505e65b70b8 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -69,6 +69,12 @@ function form_open(string $action = '', $attributes = [], array $hidden = []): s } // If an action is not a full URL then turn it into one elseif (strpos($action, '://') === false) { + // If an action has {locale} + if (strpos($action, '{locale}') !== false) + { + $action = str_replace('{locale}', Services::request()->getLocale(), $action); + } + $action = site_url($action); } @@ -180,7 +186,7 @@ function form_hidden($name, $value = '', bool $recursing = false): string if (! is_array($value)) { - $form .= '\n"; + $form .= '\n"; } else { @@ -408,7 +414,7 @@ function form_dropdown($data = '', $options = [], $selected = [], $extra = ''): { $sel = in_array($optgroup_key, $selected) ? ' selected="selected"' : ''; $form .= '\n"; + . $optgroup_val . "\n"; } $form .= "\n"; } @@ -416,7 +422,7 @@ function form_dropdown($data = '', $options = [], $selected = [], $extra = ''): { $form .= '\n"; + . $val . "\n"; } } @@ -645,9 +651,7 @@ function form_datalist(string $name, string $value, array $options): string $out .= "