diff --git a/.editorconfig b/.editorconfig index 53faf5c3beac..7382bb8eb572 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,15 @@ -; top-most EditorConfig file +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + root = true -; Unix-style newlines [*] -end_of_line = lf - -[*.php] -indent_style = space -indent_size = 4 charset = utf-8 -trim_trailing_whitespace = true +indent_size = 4 +indent_style = space +end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 79c65323dade..f1ed82db1653 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,17 +14,24 @@ contributing/ export-ignore .editorconfig export-ignore .nojekyll export-ignore export-ignore CODE_OF_CONDUCT.md export-ignore +CONTRIBUTING.md export-ignore PULL_REQUEST_TEMPLATE.md export-ignore stale.yml export-ignore Vagrantfile.dist export-ignore # They don't want our test files +tests/AutoReview/ export-ignore tests/system/ export-ignore utils/ export-ignore +depfile.yaml export-ignore rector.php export-ignore phpunit.xml.dist export-ignore +phpstan-baseline.neon.dist export-ignore phpstan.neon.dist export-ignore +phpstan-bootstrap.php export-ignore .php-cs-fixer.dist.php export-ignore +.php-cs-fixer.no-header.php export-ignore +.php-cs-fixer.user-guide.php export-ignore # The source user guide, either user_guide_src/ export-ignore diff --git a/.github/scripts/deploy-userguide b/.github/scripts/deploy-userguide new file mode 100755 index 000000000000..abbb768a5017 --- /dev/null +++ b/.github/scripts/deploy-userguide @@ -0,0 +1,36 @@ +#!/bin/bash + +## Deploy codeigniter4/userguide + +# Setup variables +SOURCE=$1 +TARGET=$2 +RELEASE=$3 +VERSION=`echo "$RELEASE" | cut -c 2-` + +echo "Preparing for version $3" +echo "Merging files from $1 to $2" + +# Prepare the source +cd $SOURCE +git checkout master +cd user_guide_src +make html +make epub + +# Prepare the target +cd $TARGET +git checkout master +rm -rf docs + +# Copy files +cp -Rf ${SOURCE}/user_guide_src/build/html ./docs +cp -Rf ${SOURCE}/user_guide_src/build/epub/CodeIgniter.epub ./CodeIgniter${VERSION}.epub + +# Ensure underscore prefixed files are published +touch ${TARGET}/docs/.nojekyll + +# Commit the changes +git add . +git commit -m "Release ${RELEASE}" +git push diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 4a7d20dd502b..55480f4fd5a4 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -24,12 +24,12 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: source - name: Checkout target - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: codeigniter4/api token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-framework.yml b/.github/workflows/deploy-framework.yml index cdd23f3c7242..9fdbc8f08127 100644 --- a/.github/workflows/deploy-framework.yml +++ b/.github/workflows/deploy-framework.yml @@ -18,12 +18,12 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: source - name: Checkout target - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: codeigniter4/framework token: ${{ secrets.ACCESS_TOKEN }} @@ -36,7 +36,7 @@ jobs: run: ./source/.github/scripts/deploy-framework ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/framework ${GITHUB_REF##*/} - name: Release - uses: actions/github-script@v5 + uses: actions/github-script@v6 with: github-token: ${{secrets.ACCESS_TOKEN}} script: | @@ -63,12 +63,12 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: source - name: Checkout target - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: codeigniter4/appstarter token: ${{ secrets.ACCESS_TOKEN }} @@ -81,7 +81,7 @@ jobs: run: ./source/.github/scripts/deploy-appstarter ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/appstarter ${GITHUB_REF##*/} - name: Release - uses: actions/github-script@v5 + uses: actions/github-script@v6 with: github-token: ${{secrets.ACCESS_TOKEN}} script: | diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 22e6eed4db4f..4ba79a8b341b 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -19,7 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + coverage: none # Build the latest User Guide - name: Build with Sphinx @@ -27,9 +33,16 @@ jobs: with: docs-folder: user_guide_src/ + - name: Add "Edit this page" links + run: | + cd user_guide_src + # Fix permissions + sudo chown -R runner:docker build/html/ + php add-edit-this-page build/html/ + # Create an artifact of the html output - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: HTML Documentation path: user_guide_src/build/html/ diff --git a/.github/workflows/deploy-userguide.yml b/.github/workflows/deploy-userguide.yml new file mode 100644 index 000000000000..384eeef5186f --- /dev/null +++ b/.github/workflows/deploy-userguide.yml @@ -0,0 +1,59 @@ +# When a new Release is created, deploy relevant +# files to each of the generated repos. +name: Deploy User Guide + +on: + release: + types: [published] + +jobs: + framework: + name: Deploy to userguide + if: (github.repository == 'codeigniter4/CodeIgniter4') + 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@v3 + with: + path: source + + - name: Checkout target + uses: actions/checkout@v3 + with: + repository: codeigniter4/userguide + token: ${{ secrets.ACCESS_TOKEN }} + path: userguide + + - name: Install Sphinx + run: | + sudo apt install python3-sphinx + sudo pip3 install sphinxcontrib-phpdomain + sudo pip3 install sphinx_rtd_theme + + - name: Chmod + run: chmod +x ./source/.github/scripts/deploy-userguide + + - name: Deploy + run: ./source/.github/scripts/deploy-userguide ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/userguide ${GITHUB_REF##*/} + + - name: Release + uses: actions/github-script@v6 + with: + github-token: ${{secrets.ACCESS_TOKEN}} + script: | + const release = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }) + github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: 'userguide', + tag_name: release.data.tag_name, + name: release.data.name, + body: release.data.body + }) diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml new file mode 100644 index 000000000000..7e6ff05c2734 --- /dev/null +++ b/.github/workflows/test-autoreview.yml @@ -0,0 +1,49 @@ +name: Automatic Code Review + +on: + pull_request: + paths: + - composer.json + - spark + - '**.php' + - .github/workflows/test-autoreview.yml + push: + paths: + - composer.json + - spark + - '**.php' + - .github/workflows/test-autoreview.yml + +jobs: + auto-review-tests: + name: Automatic Code Review + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + coverage: none + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer update --ansi + env: + COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} + + - name: Run AutoReview Tests + run: vendor/bin/phpunit --color=always --group=auto-review --no-coverage diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index cb7d9671e2cd..5fa13728b37d 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -21,13 +21,12 @@ jobs: fail-fast: false matrix: php-version: - - '7.3' - '7.4' - '8.0' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -41,7 +40,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} @@ -53,7 +52,10 @@ jobs: run: composer update --ansi --no-interaction - name: Run lint on `app/`, `admin/`, `public/` - run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.no-header.php-cs-fixer.dist.php --using-cache=no --diff + run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.php-cs-fixer.no-header.php --using-cache=no --diff - name: Run lint on `system/`, `tests`, `utils/`, and root PHP files run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --using-cache=no --diff + + - name: Run lint on `user_guide_src/source/` + run: vendor/bin/php-cs-fixer fix --verbose --ansi --dry-run --config=.php-cs-fixer.user-guide.php --using-cache=no --diff diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml index 0698809319fe..42b77998325c 100644 --- a/.github/workflows/test-deptrac.yml +++ b/.github/workflows/test-deptrac.yml @@ -6,20 +6,20 @@ on: pull_request: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' - 'composer.json' - 'depfile.yaml' - '.github/workflows/test-deptrac.yml' push: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' - 'composer.json' - 'depfile.yaml' - '.github/workflows/test-deptrac.yml' @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -50,7 +50,7 @@ jobs: run: mkdir -p ${{ steps.composer-cache.outputs.dir }} - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -60,7 +60,7 @@ jobs: run: mkdir -p build/ - name: Cache Deptrac results - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: build key: ${{ runner.os }}-deptrac-${{ github.sha }} diff --git a/.github/workflows/test-phpcpd.yml b/.github/workflows/test-phpcpd.yml index f4215df01c0e..e4d869c09d4a 100644 --- a/.github/workflows/test-phpcpd.yml +++ b/.github/workflows/test-phpcpd.yml @@ -6,20 +6,21 @@ on: pull_request: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'public/**' - - 'system/**' + - 'app/**.php' + - 'public/**.php' + - 'system/**.php' - '.github/workflows/test-phpcpd.yml' + push: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'public/**' - - 'system/**' + - 'app/**.php' + - 'public/**.php' + - 'system/**.php' - '.github/workflows/test-phpcpd.yml' jobs: @@ -28,7 +29,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index eceb97524605..805e34edcdde 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -6,21 +6,26 @@ on: pull_request: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' + - 'utils/**.php' - composer.json - - phpstan.neon.dist + - '**.neon.dist' + - '.github/workflows/test-phpstan.yml' + push: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' + - 'utils/**.php' - composer.json - - phpstan.neon.dist + - '**.neon.dist' + - '.github/workflows/test-phpstan.yml' jobs: build: @@ -32,7 +37,7 @@ jobs: php-versions: ['8.0', '8.1'] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -54,7 +59,7 @@ jobs: run: mkdir -p ${{ steps.composer-cache.outputs.dir }} - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -64,7 +69,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: build/phpstan key: ${{ runner.os }}-phpstan-${{ github.sha }} diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 61133437d3e8..9b7850ea03d5 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -5,19 +5,25 @@ on: branches: - develop paths: + - 'app/**.php' + - 'system/**.php' + - 'tests/**.php' + - 'spark' - composer.json - - spark - phpunit.xml.dist - - '**.php' - .github/workflows/test-phpunit.yml + pull_request: branches: - develop + - 'v4.*' paths: + - 'app/**.php' + - 'system/**.php' + - 'tests/**.php' + - 'spark' - composer.json - - spark - phpunit.xml.dist - - '**.php' - .github/workflows/test-phpunit.yml jobs: @@ -29,16 +35,13 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.3', '7.4', '8.0', '8.1'] - db-platforms: ['MySQLi', 'Postgre', 'SQLite3', 'SQLSRV'] + php-versions: ['7.4', '8.0', '8.1'] + db-platforms: ['MySQLi', 'Postgre', 'SQLite3', 'SQLSRV', 'OCI8'] mysql-versions: ['5.7'] include: - php-versions: '7.4' db-platforms: MySQLi mysql-versions: '8.0' - # @todo remove once 8.1 is stable enough - - php-versions: '8.1' - composer-flag: '--ignore-platform-req=php' services: mysql: @@ -70,6 +73,14 @@ jobs: - 1433:1433 options: --health-cmd="/opt/mssql-tools/bin/sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q 'SELECT @@VERSION'" --health-interval=10s --health-timeout=5s --health-retries=3 + oracle: + image: quillbuilduser/oracle-18-xe + env: + ORACLE_ALLOW_REMOTE: true + ports: + - 1521:1521 + options: --health-cmd="/opt/oracle/product/18c/dbhomeXE/bin/sqlplus -s sys/Oracle18@oracledbxe/XE as sysdba <<< 'SELECT 1 FROM DUAL'" --health-interval=10s --health-timeout=5s --health-retries=3 + redis: image: redis ports: @@ -86,15 +97,37 @@ jobs: if: matrix.db-platforms == 'SQLSRV' run: sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q "CREATE DATABASE test" + - name: Install Oracle InstantClient + if: matrix.db-platforms == 'OCI8' + run: | + sudo apt-get install wget libaio1 alien + sudo wget https://download.oracle.com/otn_software/linux/instantclient/185000/oracle-instantclient18.5-basic-18.5.0.0.0-3.x86_64.rpm + sudo wget https://download.oracle.com/otn_software/linux/instantclient/185000/oracle-instantclient18.5-devel-18.5.0.0.0-3.x86_64.rpm + sudo wget https://download.oracle.com/otn_software/linux/instantclient/185000/oracle-instantclient18.5-sqlplus-18.5.0.0.0-3.x86_64.rpm + sudo alien oracle-instantclient18.5-basic-18.5.0.0.0-3.x86_64.rpm + sudo alien oracle-instantclient18.5-devel-18.5.0.0.0-3.x86_64.rpm + sudo alien oracle-instantclient18.5-sqlplus-18.5.0.0.0-3.x86_64.rpm + sudo dpkg -i oracle-instantclient18.5-basic_18.5.0.0.0-4_amd64.deb oracle-instantclient18.5-devel_18.5.0.0.0-4_amd64.deb oracle-instantclient18.5-sqlplus_18.5.0.0.0-4_amd64.deb + echo "LD_LIBRARY_PATH=/lib/oracle/18.5/client64/lib/" >> $GITHUB_ENV + echo "NLS_LANG=AMERICAN_AMERICA.UTF8" >> $GITHUB_ENV + echo "C_INCLUDE_PATH=/usr/include/oracle/18.5/client64" >> $GITHUB_ENV + echo 'NLS_DATE_FORMAT=YYYY-MM-DD HH24:MI:SS' >> $GITHUB_ENV + echo 'NLS_TIMESTAMP_FORMAT=YYYY-MM-DD HH24:MI:SS' >> $GITHUB_ENV + echo 'NLS_TIMESTAMP_TZ_FORMAT=YYYY-MM-DD HH24:MI:SS' >> $GITHUB_ENV + + - name: Create database for Oracle Database + if: matrix.db-platforms == 'OCI8' + run: echo -e "ALTER SESSION SET CONTAINER = XEPDB1;\nCREATE BIGFILE TABLESPACE \"TEST\" DATAFILE '/opt/oracle/product/18c/dbhomeXE/dbs/TEST' SIZE 10M AUTOEXTEND ON MAXSIZE UNLIMITED SEGMENT SPACE MANAGEMENT AUTO EXTENT MANAGEMENT LOCAL AUTOALLOCATE;\nCREATE USER \"ORACLE\" IDENTIFIED BY \"ORACLE\" DEFAULT TABLESPACE \"TEST\" TEMPORARY TABLESPACE TEMP QUOTA UNLIMITED ON \"TEST\";\nGRANT CONNECT,RESOURCE TO \"ORACLE\";\nexit;" | /lib/oracle/18.5/client64/bin/sqlplus -s sys/Oracle18@localhost:1521/XE as sysdba + - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} tools: composer, pecl - extensions: imagick, sqlsrv, gd, sqlite3, redis, memcached, pgsql + extensions: imagick, sqlsrv, gd, sqlite3, redis, memcached, oci8, pgsql coverage: xdebug env: update: true @@ -111,7 +144,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -119,8 +152,8 @@ jobs: - name: Install dependencies run: | - composer update --ansi --no-interaction ${{ matrix.composer-flag }} - composer remove --ansi --dev --unused -W ${{ matrix.composer-flag }} -- rector/rector phpstan/phpstan friendsofphp/php-cs-fixer nexusphp/cs-config codeigniter/coding-standard + composer update --ansi --no-interaction + composer remove --ansi --dev --unused -W -- rector/rector phpstan/phpstan friendsofphp/php-cs-fixer nexusphp/cs-config codeigniter/coding-standard env: COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }} @@ -128,8 +161,15 @@ jobs: if: matrix.php-versions == '8.0' run: echo "TACHYCARDIA_MONITOR_GA=enabled" >> $GITHUB_ENV + - name: Compute coverage option + uses: actions/github-script@v6 + id: phpunit-coverage-option + with: + script: 'return "${{ matrix.php-versions }}" == "8.0" ? "" : "--no-coverage"' + result-encoding: string + - name: Test with PHPUnit - run: script -e -c "vendor/bin/phpunit --color=always" + run: script -e -c "vendor/bin/phpunit --color=always --exclude-group=auto-review ${{ steps.phpunit-coverage-option.outputs.result }}" env: DB: ${{ matrix.db-platforms }} TERM: xterm-256color @@ -137,7 +177,7 @@ jobs: - name: Run Coveralls if: github.repository_owner == 'codeigniter4' && matrix.php-versions == '8.0' run: | - composer global require --ansi php-coveralls/php-coveralls:^2.4 symfony/console:^5 + composer global require --ansi php-coveralls/php-coveralls:^2.4 php-coveralls --coverage_clover=build/logs/clover.xml -v env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 0d7262ceb4ea..f98d759f6edb 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -6,37 +6,43 @@ on: pull_request: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' + - 'tests/**.php' + - 'utils/**.php' - '.github/workflows/test-rector.yml' - composer.json - - 'utils/Rector/**' - - 'rector.php' + push: branches: - 'develop' - - '4.*' + - 'v4.*' paths: - - 'app/**' - - 'system/**' + - 'app/**.php' + - 'system/**.php' + - 'tests/**.php' + - 'utils/**.php' - '.github/workflows/test-rector.yml' - composer.json - - 'utils/Rector/**' - - 'rector.php' jobs: build: - name: PHP ${{ matrix.php-versions }} Analyze code (Rector) + name: PHP ${{ matrix.php-versions }} Analyze code (Rector) on ${{ matrix.paths }} runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: php-versions: ['7.4', '8.0'] + paths: + - app + - system + - tests + - utils/Rector steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -58,7 +64,7 @@ jobs: run: mkdir -p ${{ steps.composer-cache.outputs.dir }} - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -68,4 +74,4 @@ jobs: run: composer update --ansi --no-interaction - name: Run static analysis - run: vendor/bin/rector process --dry-run --no-progress-bar + run: vendor/bin/rector process ${{ matrix.paths }} --dry-run --no-progress-bar diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml new file mode 100644 index 000000000000..0d335a29552a --- /dev/null +++ b/.github/workflows/test-scss.yml @@ -0,0 +1,51 @@ +name: SCSS Compilation + +on: + pull_request: + branches: + - 'develop' + - 'v4.x' + paths: + - '**.scss' + - '**.css' + - '.github/workflows/test-scss.yml' + + push: + branches: + - 'develop' + - 'v4.x' + paths: + - '**.scss' + - '**.css' + - '.github/workflows/test-scss.yml' + +jobs: + build: + name: Compilation of SCSS (Dart Sass) + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3.0.0 + with: + # node version based on dart-sass test workflow + node-version: 16 + + - name: Install Dart Sass + run: | + npm install --global sass + sass --version + + - name: Run Dart Sass + run: sass --no-source-map admin/css/debug-toolbar/toolbar.scss system/Debug/Toolbar/Views/toolbar.css + + - name: Check for changed CSS files + run: | + if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then + echo "Your changes to the SCSS files did not match the expected CSS output." + git diff-files --patch + exit 1 + fi diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 44cfa52c8b17..b01c141a9c61 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Detect usage of tabs in RST files run: php utils/check_tabs_in_rst.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5df3c5e90a52..02c6549b2c03 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -29,22 +29,14 @@ ->notName('#Foobar.php$#') ->append([ __FILE__, - __DIR__ . '/.no-header.php-cs-fixer.dist.php', + __DIR__ . '/.php-cs-fixer.no-header.php', + __DIR__ . '/.php-cs-fixer.user-guide.php', __DIR__ . '/rector.php', __DIR__ . '/spark', + __DIR__ . '/user_guide_src/renumerate.php', ]); -$overrides = [ - 'ordered_class_elements' => [ - 'order' => [ - 'use_trait', - 'constant', - 'property', - 'method', - ], - 'sort_algorithm' => 'none', - ], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', diff --git a/.no-header.php-cs-fixer.dist.php b/.php-cs-fixer.no-header.php similarity index 80% rename from .no-header.php-cs-fixer.dist.php rename to .php-cs-fixer.no-header.php index 9417a859e07e..6c136cd4b164 100644 --- a/.no-header.php-cs-fixer.dist.php +++ b/.php-cs-fixer.no-header.php @@ -30,20 +30,10 @@ __DIR__ . '/admin/starter/builds', ]); -$overrides = [ - 'ordered_class_elements' => [ - 'order' => [ - 'use_trait', - 'constant', - 'property', - 'method', - ], - 'sort_algorithm' => 'none', - ], -]; +$overrides = []; $options = [ - 'cacheFile' => 'build/.no-header.php-cs-fixer.cache', + 'cacheFile' => 'build/.php-cs-fixer.no-header.cache', 'finder' => $finder, 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), 'customRules' => [ diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php new file mode 100644 index 000000000000..8081d73698ca --- /dev/null +++ b/.php-cs-fixer.user-guide.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\CodingStandard\CodeIgniter4; +use Nexus\CsConfig\Factory; +use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; +use Nexus\CsConfig\Fixer\Comment\SpaceAfterCommentStartFixer; +use Nexus\CsConfig\FixerGenerator; +use PhpCsFixer\Finder; + +$finder = Finder::create() + ->files() + ->in([ + __DIR__ . '/user_guide_src/source', + ]) + ->notPath([ + 'ci3sample/', + 'libraries/sessions/016.php', + 'database/query_builder/075.php', + ]); + +$overrides = [ + 'echo_tag_syntax' => false, + 'php_unit_internal_class' => false, + 'no_unused_imports' => false, + 'class_attributes_separation' => false, +]; + +$options = [ + 'cacheFile' => 'build/.php-cs-fixer.user-guide.cache', + 'finder' => $finder, + 'customFixers' => FixerGenerator::create('vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), + 'customRules' => [ + NoCodeSeparatorCommentFixer::name() => true, + SpaceAfterCommentStartFixer::name() => true, + ], +]; + +return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); diff --git a/CHANGELOG.md b/CHANGELOG.md index d2bf2ed7119c..03e7223b90cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,159 @@ # Changelog +## [v4.2.0](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.0) (2022-06-03) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.1.9...v4.2.0) + +### Breaking Changes +* Validation: support placeholders for anything by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5545 +* Fix: Validation. Error key for field with asterisk by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5609 +* Improve exception logging by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5684 +* fix: spark can't use options on PHP 7.4 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5836 +* fix: [Autoloader] Composer classmap usage by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5850 +* fix: using multiple CLI::color() in CLI::write() outputs strings with wrong color by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5893 +* refactor: [Router] extract a class for auto-routing by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5877 +* feat: Debugbar request microtime by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5958 +* refactor: `system/bootstrap.php` only loads files and registers autoloader by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5972 +* fix: `dot_array_search()` unexpected behavior by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5940 +* feat: QueryBuilder join() raw SQL string support by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5875 +* fix: change BaseService::reset() $initAutoloader to true by default by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6020 + +### Fixed Bugs +* chore: update admin/framework/composer.json Kint by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5531 +* fix: BaseConnection::getConnectDuration() number_format(): Passing null to parameter by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5536 +* Fix: Debug toolbar selectors by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5544 +* Fix: Toolbar. ciDebugBar.showTab() context. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5554 +* Refactor Database Collector display by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5553 +* fix: add missing Migration lang item by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5557 +* feat: add Validation Strict Rules by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5445 +* fix: `Time::createFromTimestamp()` sets incorrect time when specifying timezone by @totoprayogo1916 in https://github.com/codeigniter4/CodeIgniter4/pull/5588 +* fix: Entity's isset() and unset() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5497 +* Fix: Deletion timestamp of the Model is updated when a record that has been soft-deleted is deleted again by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5578 +* Fix: Added alias escaping in subquery by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5601 +* fix: spark migrate:status does not show status with different namespaces by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5605 +* BaseService - Use lowercase key in resetSingle by @najdanovicivan in https://github.com/codeigniter4/CodeIgniter4/pull/5596 +* Fix `array_flatten_with_dots` ignores empty array values by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5606 +* fix: debug toolbar Routes Params output by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5619 +* fix: DownloadResponse memory leak by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5623 +* fix: spark does not show Exception by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5638 +* fix: Config CSRF $redirect does not work by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5665 +* fix: do not call header() if headers have already been sent by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5680 +* fix: $routes->setDefaultMethod() does not work by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5682 +* fix: debug toolbar vars response headers includes request headers by @zl59503020 in https://github.com/codeigniter4/CodeIgniter4/pull/5701 +* fix: 404 override controller does not output Response object body by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5703 +* fix: auto routes incorrectly display route filters with GET method by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5712 +* fix: Model::paginate() missing argument $group by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5699 +* Fix options are not passed to Command $params by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5206 +* fix: forceGlobalSecureRequests break URI schemes other than HTTP by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5730 +* fix: TypeError when `$tokenRandomize = true` and no token posted by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5742 +* fix: $builder->ignore()->insertBatch() only ignores on first iteration by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5672 +* fix: app/Config/Routes.php is loaded twice on Windows by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5780 +* fix: table name is double prefixed when LIKE clause by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5778 +* fix: Publisher $restrictions regex to FCPATH by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5793 +* fix: Timer::getElapsedTime() returns incorrect value by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5798 +* bug: Publisher $restrictions regex typo by @MGatner in https://github.com/codeigniter4/CodeIgniter4/pull/5800 +* fix: [Validation] valid_date ErrorException when the field is not sent by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5804 +* fix: [Pager] can't get correct current page from segment by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5803 +* fix: bug that allows dynamic controllers to be used by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5814 +* config: remove App\ and Config\ in autoload.psr-4 in app starter composer.json by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5824 +* fix: failover's DBPrefix not working by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5816 +* fix: Validation returns incorrect errors after Redirect with Input by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5844 +* feat: [Parser] add configs to change conditional delimiters by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5842 +* fix: Commands::discoverCommands() loads incorrect classname by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5849 +* fix: Publisher::discover() loads incorrect classname by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5858 +* fix: validation errors in Model are not cleared when running validation again by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5861 +* fix: Parser fails with `({variable})` in loop by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5840 +* fix: [BaseConfig] string value is set from environment variable even if it should be int/float by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5779 +* fix: add Escaper Exception classes in $coreClassmap by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5891 +* fix: Composer PSR-4 overwrites Config\Autoload::$psr4 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5902 +* fix: Reverse Routing does not take into account the default namespace by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5936 +* fix: [Validation] Fields with an asterisk throws exception by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5938 +* fix: GDHandler::convert() does not work by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5969 +* fix: Images\Handlers\GDHandler Implicit conversion from float to int loses precision by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5965 +* fix: GDHandler::save() removes transparency by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5971 +* fix: route limit to subdomains does not work by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5961 +* fix: Model::_call() static analysis by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5970 +* fix: invalid css in error_404.php by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5978 +* Fix: Route placeholder (:any) with {locale} by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/6003 +* Changing the subquery builder for the Oracle by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5999 +* fix: CURLRequest request body is not reset on the next request by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6014 +* Bug: The SQLSRV driver ignores the port value from the config. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/6036 +* fix: `set_radio()` not working as expected by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6037 +* fix: add config for SQLite3 Foreign Keys by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6050 +* fix: Ignore non-HTML responses in storePreviousURL by @tearoom6 in https://github.com/codeigniter4/CodeIgniter4/pull/6012 +* fix: SQLite3\Table::copyData() does not escape column names by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6055 +* Fix `slash_item()` erroring when property fetched does not exist on `Config\App` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6058 + +### New Features +* Feature Add Oracle driver by @ytetsuro in https://github.com/codeigniter4/CodeIgniter4/pull/2487 +* feat: new improved auto router by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5889 +* feat: new improved auto router `spark routes` command by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5953 +* feat: `db:table` command by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5979 + +### Enhancements +* feat: CSP enhancements by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5516 +* Feature: Subqueries in the FROM section by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5510 +* Added new View Decorators. by @lonnieezell in https://github.com/codeigniter4/CodeIgniter4/pull/5567 +* feat: auto routes listing by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5590 +* Feature: "spark routes" command shows routes with closure. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5651 +* feat: `spark routes` shows filters by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5628 +* Allow calling getQuery() multiple times, and other improvements by @vlakoff in https://github.com/codeigniter4/CodeIgniter4/pull/5127 +* feat: add Controller::validateData() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5639 +* feat: can add route handler as callable by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5713 +* Checking if the subquery uses the same object as the main query by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5743 +* Feature: Subquery for SELECT by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5736 +* Extend Validation from BaseConfig so Registrars can add rules. by @lonnieezell in https://github.com/codeigniter4/CodeIgniter4/pull/5789 +* config: add mime type for webp by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5838 +* feat: add `$includeDir` option to `get_filenames()` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5862 +* feat: throws exception when controller name in routes contains `/` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5885 +* [PHPStan] Prepare for PHPStan 1.6.x-dev by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/5876 +* [Rector] Add back SimplifyUselessVariableRector by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/5911 +* Redirecting Routes. Placeholders. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5916 +* script_tag(): cosmetic for value-less attributes by @xlii-chl in https://github.com/codeigniter4/CodeIgniter4/pull/5884 +* feat: QueryBuilder raw SQL string support by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5817 +* improve Router Exception message by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5984 +* feat: DBForge::addField() `default` value raw SQL string support by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5957 +* Add sample file for preloading by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5974 +* Feature. QueryBuilder. Query union. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/6015 +* feat: `getFieldData()` returns nullable data on PostgreSQL by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5981 + +### Refactoring +* refactor: add Factories::models() to suppress PHPStan error by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5358 +* Fixed style for PHP7.4 by @ytetsuro in https://github.com/codeigniter4/CodeIgniter4/pull/5581 +* Fix Autoloader::initialize() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5592 +* refactor: CURLRequest and the slow tests by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5593 +* Refactor `if_exist` validation with dot notation by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5607 +* refactor: small changes in Filters and Router by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5627 +* refactor: replace deprecated `getFilterForRoute()` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5624 +* refactor: make BaseController abstract by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5647 +* refactor: move logic to prevent access to initController by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5648 +* refactor: remove migrations routes by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5652 +* refactor: update Kint CSP nonce by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5657 +* Deprecate object implementations of `clean_path()` function by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5681 +* refactor: Session does not use cookies() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5656 +* refactor: replace deprecated Response::getReason() with getReasonPhrase() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5700 +* refactor: isCLI() in CLIRequest and IncomingRequest by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5653 +* refactor: CodeIgniter has context by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5650 +* Forge use statement by @mostafakhudair in https://github.com/codeigniter4/CodeIgniter4/pull/5729 +* refactor: remove `&` before $db by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5726 +* refactor: remove unneeded `&` references in ContentSecurityPolicy.php by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5734 +* Nonce replacement optimization. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5733 +* [Rector] Clean up skip config and re-run Rector by @samsonasik in https://github.com/codeigniter4/CodeIgniter4/pull/5813 +* refactor: DB Session Handler by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5696 +* Rename `Abstact` to `Abstract` by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/5833 +* refactor: extract RedirectResponse::withErrors() method by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5860 +* Optimizing the RouteCollection::getRoutes() method by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/5918 +* refactor: add strtolower() to Request::getMethod() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5963 +* refactor: remove `$_SERVER['HTTP_HOST']` in RouteCollection by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/5962 +* refactor: deprecate const `EVENT_PRIORITY_*` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6000 +* fix: replace EVENT_PRIORITY_NORMAL with Events::PRIORITY_NORMAL by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6005 +* Router class optimization. by @iRedds in https://github.com/codeigniter4/CodeIgniter4/pull/6004 +* Prefer `is_file()` by @MGatner in https://github.com/codeigniter4/CodeIgniter4/pull/6025 +* refactor: use get_filenames() 4th param in FileLocator by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6026 +* refactor: use get_filenames() 4th param by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6031 +* refactor: CodeIgniter $context check by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6047 +* Small change to improve code reading by @valmorflores in https://github.com/codeigniter4/CodeIgniter4/pull/6051 +* refactor: remove `CodeIgniter\Services` by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6053 + ## [v4.1.9](https://github.com/codeigniter4/CodeIgniter4/tree/v4.1.9) (2022-02-25) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.1.8...v4.1.9) @@ -3178,7 +3332,3 @@ These changes increase security when handling uploaded files as the client can n - Fix debugbar loading while csp is enabled [\#1129](https://github.com/codeigniter4/CodeIgniter4/pull/1129) ([puschie286](https://github.com/puschie286)) - Run session tests in separate processes - fix for \#1106 [\#1128](https://github.com/codeigniter4/CodeIgniter4/pull/1128) ([andreif23](https://github.com/andreif23)) - Feature/sqlite [\#793](https://github.com/codeigniter4/CodeIgniter4/pull/793) ([lonnieezell](https://github.com/lonnieezell)) - - - -\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/README.md b/README.md index fa5194457cbc..d8525f299037 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,22 @@ to optional packages, with their own repository. ## Contributing -We **are** accepting contributions from the community! +We **are** accepting contributions from the community! It doesn't matter whether you can code, write documentation, or help find bugs, +all contributions are welcome. Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/README.md). +CodeIgniter has had thousands on contributions from people since its creation. This project would not be what it is without them. + + + + + +Made with [contrib.rocks](https://contrib.rocks). + ## Server Requirements -PHP version 7.3 or higher is required, with the following extensions installed: +PHP version 7.4 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) diff --git a/Vagrantfile.dist b/Vagrantfile.dist index 857f92314a40..e85393ff8588 100644 --- a/Vagrantfile.dist +++ b/Vagrantfile.dist @@ -50,7 +50,7 @@ Vagrant.configure("2") do |config| PGSQL_ROOT_PASS="password" VIRTUALHOST="localhost" CODEIGNITER_PATH="/var/www/codeigniter" - PHP_VERSION=7.3 + PHP_VERSION=7.4 PGSQL_VERSION=10 #APT_PROXY="192.168.10.1:3142" @@ -166,8 +166,8 @@ Vagrant.configure("2") do |config| sed -i "s/APACHE_RUN_USER=www-data/APACHE_RUN_USER=vagrant/" /etc/apache2/envvars sed -i "s/APACHE_RUN_GROUP=www-data/APACHE_RUN_GROUP=vagrant/" /etc/apache2/envvars grep -q "Listen 81" /etc/apache2/ports.conf || sed -i "s/^Listen 80/Listen 80\\nListen 81\\nListen 82/" /etc/apache2/ports.conf - sed -i "s/^display_errors = Off/display_errors = On/" /etc/php/7.3/apache2/php.ini - sed -i "s/^display_startup_errors = Off/display_startup_errors = On/" /etc/php/7.3/apache2/php.ini + sed -i "s/^display_errors = Off/display_errors = On/" /etc/php/7.4/apache2/php.ini + sed -i "s/^display_startup_errors = Off/display_startup_errors = On/" /etc/php/7.4/apache2/php.ini echo "ServerName ${VIRTUALHOST} diff --git a/admin/RELEASE.md b/admin/RELEASE.md index eb4e621e1e40..b7ec0ced1905 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -37,7 +37,7 @@ Copy the resulting content into **CHANGELOG.md** and adjust the format to match * Replace **CHANGELOG.md** with the new version generated above * Set the date in **user_guide_src/source/changelogs/{version}.rst** to format `Release Date: January 31, 2021` * Create a new changelog for the next version at **user_guide_src/source/changelogs/{next_version}.rst** and add it to **index.rst** -* If there are additional upgrade steps, create **user_guide_src/source/installation/upgrade_{ver}.rst** and add it to **upgrading.rst** +* Create **user_guide_src/source/installation/upgrade_{ver}.rst**, fill in the "All Changes" section, and add it to **upgrading.rst** * Commit the changes with "Prep for 4.x.x release" and push to origin * Create a new PR from `release-4.x.x` to `develop`: * Title: "Prep for 4.x.x release" @@ -57,6 +57,12 @@ CodeIgniter 4.x.x release. See the changelog: https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md ``` * Watch for the "Deploy Framework" Action to make sure **framework** and **appstarter** get updated +* Run the following commands to install and test AppStarter and verify the new version: +```bash +composer create-project codeigniter4/appstarter release-test +cd release-test +composer test && composer info codeigniter4/framework +``` ## User Guide @@ -94,6 +100,13 @@ the User Guide repo to **public/userguide4** and browse to the website to make s * Make a new topic in the "News & Discussion" forums: https://forum.codeigniter.com/forum-2.html * The content is somewhat organic, but should include any major features and changes as well as a link to the User Guide's changelog +## After Publishing Security Advisory + +* Send a PR to [PHP Security Advisories Database](https://github.com/FriendsOfPHP/security-advisories). + * E.g. https://github.com/FriendsOfPHP/security-advisories/pull/606 + * See https://github.com/FriendsOfPHP/security-advisories#contributing + * Don't forget to run `php -d memory_limit=-1 validator.php`, before submitting the PR + ## Appendix ### Sphinx Installation diff --git a/admin/css/debug-toolbar/_theme-dark.scss b/admin/css/debug-toolbar/_theme-dark.scss index 801b75664f22..ead7d02a58e3 100644 --- a/admin/css/debug-toolbar/_theme-dark.scss +++ b/admin/css/debug-toolbar/_theme-dark.scss @@ -129,8 +129,8 @@ } .badge { - background-color: $g-blue; - color: $m-gray; + background-color: $g-red; + color: $t-light; } } diff --git a/admin/css/debug-toolbar/_theme-light.scss b/admin/css/debug-toolbar/_theme-light.scss index 41dbb9175fde..4e4295ccd131 100644 --- a/admin/css/debug-toolbar/_theme-light.scss +++ b/admin/css/debug-toolbar/_theme-light.scss @@ -125,7 +125,7 @@ } .badge { - background-color: $g-blue; + background-color: $g-red; color: $t-light; } } diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index c73510182ac7..23e0807fabfc 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -77,16 +77,9 @@ // General elements h1 { - bottom: 0; - display: inline-block; - font-size: $base-size - 2; + display: flex; font-weight: normal; - margin: 0 16px 0 0; - padding: 0; - position: absolute; - right: 30px; - text-align: left; - top: 0; + margin: 0 0 0 auto; svg { width: 16px; @@ -238,15 +231,8 @@ // The "Open/Close" toggle #debug-bar-link { - bottom: 0; - display: inline-block; - font-size: $base-size; - line-height: 36px; + display: flex; padding: 6px; - position: absolute; - right: 10px; - top: 0; - width: 24px; } // The toolbar menus @@ -488,8 +474,8 @@ width: 70px; } -.debug-bar-width140p { - width: 140px; +.debug-bar-width190p { + width: 190px; } .debug-bar-width20e { diff --git a/admin/framework/README.md b/admin/framework/README.md index 6b7c6673e33a..ebc758ed530e 100644 --- a/admin/framework/README.md +++ b/admin/framework/README.md @@ -43,7 +43,7 @@ Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/ ## Server Requirements -PHP version 7.3 or higher is required, with the following extensions installed: +PHP version 7.4 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 25ac1f07ecc3..03d3965770ba 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -5,19 +5,19 @@ "homepage": "https://codeigniter.com", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "ext-curl": "*", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.0", + "kint-php/kint": "^4.1.1", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { "codeigniter/coding-standard": "^1.1", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "^3.1", + "friendsofphp/php-cs-fixer": "3.6.*", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.3", "phpunit/phpunit": "^9.1", diff --git a/admin/module/composer.json b/admin/module/composer.json index 70197a43010c..172533e9e5f2 100644 --- a/admin/module/composer.json +++ b/admin/module/composer.json @@ -4,7 +4,7 @@ "homepage": "https://codeigniter.com", "license": "MIT", "require": { - "php": "^7.3 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "codeigniter4/codeigniter4": "dev-develop", diff --git a/admin/pre-commit b/admin/pre-commit index 4f1e7d11f544..1fa6b15e5984 100644 --- a/admin/pre-commit +++ b/admin/pre-commit @@ -42,9 +42,9 @@ if [ "$FILES" != "" ]; then # Run on whole codebase to skip on unnecessary filtering # Run first on app, admin, public if [ -d /proc/cygdrive ]; then - ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php + ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php else - php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php + php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php fi if [ $? != 0 ]; then @@ -63,6 +63,18 @@ if [ "$FILES" != "" ]; then echo "Files in system, tests, utils, or root are not following the coding standards. Please fix them before commit." exit 1 fi + + # Next, run on user_guide_src/source PHP files + if [ -d /proc/cygdrive ]; then + ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php + else + php ./vendor/bin/php-cs-fixer fix --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php + fi + + if [ $? != 0 ]; then + echo "Files in user_guide_src/source are not following the coding standards. Please fix them before commit." + exit 1 + fi fi if [ "$STAGED_RST_FILES" != "" ]; then diff --git a/admin/starter/.github/workflows/phpunit.yml b/admin/starter/.github/workflows/phpunit.yml index 5e37bd91e504..73d413cad92f 100644 --- a/admin/starter/.github/workflows/phpunit.yml +++ b/admin/starter/.github/workflows/phpunit.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - php-versions: ['7.3', '7.4'] + php-versions: ['7.4', '8.0'] runs-on: ubuntu-latest diff --git a/admin/starter/README.md b/admin/starter/README.md index 363e7c89f304..2e17d9c98888 100644 --- a/admin/starter/README.md +++ b/admin/starter/README.md @@ -50,7 +50,7 @@ Problems with it can be raised on our forum, or as issues in the main repository ## Server Requirements -PHP version 7.3 or higher is required, with the following extensions installed: +PHP version 7.4 or higher is required, with the following extensions installed: - [intl](http://php.net/manual/en/intl.requirements.php) - [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/admin/starter/builds b/admin/starter/builds index 0b10a150ac59..cc2ca0851ac7 100755 --- a/admin/starter/builds +++ b/admin/starter/builds @@ -39,7 +39,7 @@ if (is_file($file)) { if ($dev) { $array['minimum-stability'] = 'dev'; $array['prefer-stable'] = true; - $array['repositories'] = $array['repositories'] ?? []; + $array['repositories'] ??= []; $found = false; diff --git a/admin/starter/composer.json b/admin/starter/composer.json index 9f7dc492cbc8..026329ca9a24 100644 --- a/admin/starter/composer.json +++ b/admin/starter/composer.json @@ -5,8 +5,8 @@ "homepage": "https://codeigniter.com", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", - "codeigniter4/framework": "^4" + "php": "^7.4 || ^8.0", + "codeigniter4/framework": "^4.0" }, "require-dev": { "fakerphp/faker": "^1.9", @@ -17,10 +17,6 @@ "ext-fileinfo": "Improves mime type detection for files" }, "autoload": { - "psr-4": { - "App\\": "app", - "Config\\": "app/Config" - }, "exclude-from-classmap": [ "**/Database/Migrations/**" ] diff --git a/admin/starter/tests/README.md b/admin/starter/tests/README.md index 1edb10176e21..9d20661afecd 100644 --- a/admin/starter/tests/README.md +++ b/admin/starter/tests/README.md @@ -6,13 +6,14 @@ It is not intended to be a full description of the test features that you can use to test your application. Those details can be found in the documentation. ## Resources + * [CodeIgniter 4 User Guide on Testing](https://codeigniter4.github.io/userguide/testing/index.html) -* [PHPUnit docs](https://phpunit.readthedocs.io/en/8.5/index.html) +* [PHPUnit docs](https://phpunit.de/documentation.html) ## Requirements It is recommended to use the latest version of PHPUnit. At the time of this -writing we are running version 8.5.13. Support for this has been built into the +writing we are running version 9.x. Support for this has been built into the **composer.json** file that ships with CodeIgniter and can easily be installed via [Composer](https://getcomposer.org/) if you don't already have it installed globally. @@ -30,8 +31,8 @@ for code coverage to be calculated successfully. A number of the tests use a running database. In order to set up the database edit the details for the `tests` group in **app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine -that is currently running on your machine. More details on a test database setup are in the -*Docs>>Testing>>Testing Your Database* section of the documentation. +that is currently running on your machine. More details on a test database setup are in the +[Testing Your Database](https://codeigniter4.github.io/userguide/testing/database.html) section of the documentation. If you want to run the tests without using live database you can exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** - @@ -44,6 +45,10 @@ The entire test suite can be run by simply typing one command-line command from > ./phpunit +If you are using Windows, use the following command. + + > vendor\bin\phpunit + You can limit tests to those within a single test directory by specifying the directory name after phpunit. diff --git a/admin/userguide/composer.json b/admin/userguide/composer.json index a8fc259202fd..61428fa50ff1 100644 --- a/admin/userguide/composer.json +++ b/admin/userguide/composer.json @@ -5,7 +5,7 @@ "homepage": "https://codeigniter.com", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "codeigniter4/framework": "^4" }, "support": { diff --git a/app/Config/App.php b/app/Config/App.php index 88b295e9a010..c6c716822756 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -3,6 +3,7 @@ namespace Config; use CodeIgniter\Config\BaseConfig; +use CodeIgniter\Session\Handlers\FileHandler; class App extends BaseConfig { @@ -151,7 +152,7 @@ class App extends BaseConfig * * @var string */ - public $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler'; + public $sessionDriver = FileHandler::class; /** * -------------------------------------------------------------------------- @@ -318,7 +319,7 @@ class App extends BaseConfig * (empty string) means default SameSite attribute set by browsers (`Lax`) * will be set on cookies. If set to `None`, `$cookieSecure` must also be set. * - * @var string + * @var string|null * * @deprecated use Config\Cookie::$samesite property instead. */ @@ -436,7 +437,7 @@ class App extends BaseConfig * Defaults to `Lax` as recommended in this link: * * @see https://portswigger.net/web-security/csrf/samesite-cookies - * @deprecated Use `Config\Security` $samesite property instead of using this property. + * @deprecated `Config\Cookie` $samesite property is used. * * @var string */ diff --git a/app/Config/Constants.php b/app/Config/Constants.php index 8f8498a58a0a..0c1b53487cd9 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -38,9 +38,9 @@ defined('HOUR') || define('HOUR', 3600); defined('DAY') || define('DAY', 86400); defined('WEEK') || define('WEEK', 604800); -defined('MONTH') || define('MONTH', 2592000); -defined('YEAR') || define('YEAR', 31536000); -defined('DECADE') || define('DECADE', 315360000); +defined('MONTH') || define('MONTH', 2_592_000); +defined('YEAR') || define('YEAR', 31_536_000); +defined('DECADE') || define('DECADE', 315_360_000); /* | -------------------------------------------------------------------------- @@ -77,3 +77,18 @@ defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead. + */ +define('EVENT_PRIORITY_LOW', 200); + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead. + */ +define('EVENT_PRIORITY_NORMAL', 100); + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead. + */ +define('EVENT_PRIORITY_HIGH', 10); diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php index 6fa5bd7b4cc7..aa18ba9f1060 100644 --- a/app/Config/ContentSecurityPolicy.php +++ b/app/Config/ContentSecurityPolicy.php @@ -164,4 +164,25 @@ class ContentSecurityPolicy extends BaseConfig * @var string|string[]|null */ public $sandbox; + + /** + * Nonce tag for style + * + * @var string + */ + public $styleNonceTag = '{csp-style-nonce}'; + + /** + * Nonce tag for script + * + * @var string + */ + public $scriptNonceTag = '{csp-script-nonce}'; + + /** + * Replace nonce tag automatically + * + * @var bool + */ + public $autoNonce = true; } diff --git a/app/Config/Database.php b/app/Config/Database.php index 1e6340899bf4..87d73b13ad44 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -57,23 +57,24 @@ class Database extends Config * @var array */ public $tests = [ - 'DSN' => '', - 'hostname' => '127.0.0.1', - 'username' => '', - 'password' => '', - 'database' => ':memory:', - 'DBDriver' => 'SQLite3', - 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS - 'pConnect' => false, - 'DBDebug' => (ENVIRONMENT !== 'production'), - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', - 'swapPre' => '', - 'encrypt' => false, - 'compress' => false, - 'strictOn' => false, - 'failover' => [], - 'port' => 3306, + 'DSN' => '', + 'hostname' => '127.0.0.1', + 'username' => '', + 'password' => '', + 'database' => ':memory:', + 'DBDriver' => 'SQLite3', + 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS + 'pConnect' => false, + 'DBDebug' => (ENVIRONMENT !== 'production'), + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + 'port' => 3306, + 'foreignKeys' => true, ]; public function __construct() diff --git a/app/Config/Events.php b/app/Config/Events.php index 183280ebda31..5219f4ac3f68 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -32,9 +32,7 @@ ob_end_flush(); } - ob_start(static function ($buffer) { - return $buffer; - }); + ob_start(static fn ($buffer) => $buffer); } /* diff --git a/app/Config/Feature.php b/app/Config/Feature.php index af42534ac423..4c5ec90cd3ff 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -10,7 +10,7 @@ class Feature extends BaseConfig { /** - * Enable multiple filters for a route or not + * Enable multiple filters for a route or not. * * If you enable this: * - CodeIgniter\CodeIgniter::handleRequest() uses: @@ -24,4 +24,9 @@ class Feature extends BaseConfig * @var bool */ public $multipleFilters = false; + + /** + * Use improved new auto routing instead of the default legacy version. + */ + public bool $autoRoutesImproved = false; } diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 14685207f71b..d0a97238b1d1 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -49,7 +49,11 @@ class Filters extends BaseConfig * particular HTTP method (GET, POST, etc.). * * Example: - * 'post' => ['csrf', 'throttle'] + * 'post' => ['foo', 'bar'] + * + * If you use this, you should disable auto-routing because auto-routing + * permits any HTTP method to access a controller. Accessing the controller + * with a method you don’t expect could bypass the filter. * * @var array */ diff --git a/app/Config/Format.php b/app/Config/Format.php index 533540e27918..d89e40842c22 100644 --- a/app/Config/Format.php +++ b/app/Config/Format.php @@ -4,6 +4,8 @@ use CodeIgniter\Config\BaseConfig; use CodeIgniter\Format\FormatterInterface; +use CodeIgniter\Format\JSONFormatter; +use CodeIgniter\Format\XMLFormatter; class Format extends BaseConfig { @@ -40,9 +42,9 @@ class Format extends BaseConfig * @var array */ public $formatters = [ - 'application/json' => 'CodeIgniter\Format\JSONFormatter', - 'application/xml' => 'CodeIgniter\Format\XMLFormatter', - 'text/xml' => 'CodeIgniter\Format\XMLFormatter', + 'application/json' => JSONFormatter::class, + 'application/xml' => XMLFormatter::class, + 'text/xml' => XMLFormatter::class, ]; /** diff --git a/app/Config/Logger.php b/app/Config/Logger.php index a4eaeb648955..406d9aaccc85 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -2,6 +2,7 @@ namespace Config; +use CodeIgniter\Log\Handlers\FileHandler; use CodeIgniter\Config\BaseConfig; class Logger extends BaseConfig @@ -83,7 +84,7 @@ class Logger extends BaseConfig * File Handler * -------------------------------------------------------------------- */ - 'CodeIgniter\Log\Handlers\FileHandler' => [ + FileHandler::class => [ // The log levels that this handler will handle. 'handles' => [ diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php index 786bc6a1e5c7..a1bb458a2804 100644 --- a/app/Config/Mimes.php +++ b/app/Config/Mimes.php @@ -260,6 +260,7 @@ class Mimes 'image/png', 'image/x-png', ], + 'webp' => 'image/webp', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'css' => [ diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php index f3768bc577b4..47475112c080 100644 --- a/app/Config/Publisher.php +++ b/app/Config/Publisher.php @@ -23,6 +23,6 @@ class Publisher extends BasePublisher */ public $restrictions = [ ROOTPATH => '*', - FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', ]; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 0060a6f67dff..ff2ac645cb9a 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -7,7 +7,7 @@ // Load the system's routing file first, so that the app and ENVIRONMENT // can override as needed. -if (file_exists(SYSTEMPATH . 'Config/Routes.php')) { +if (is_file(SYSTEMPATH . 'Config/Routes.php')) { require SYSTEMPATH . 'Config/Routes.php'; } @@ -21,7 +21,11 @@ $routes->setDefaultMethod('index'); $routes->setTranslateURIDashes(false); $routes->set404Override(); -$routes->setAutoRoute(true); +// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps +// where controller filters or CSRF protection are bypassed. +// If you don't want to define all routes, please use the Auto Routing (Improved). +// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true. +//$routes->setAutoRoute(false); /* * -------------------------------------------------------------------- @@ -46,6 +50,6 @@ * You will have access to the $routes object within that file without * needing to reload it. */ -if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) { +if (is_file(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) { require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php'; } diff --git a/app/Config/Security.php b/app/Config/Security.php index 05083f8b1db1..107bd9549dc5 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -111,7 +111,7 @@ class Security extends BaseConfig * * @var string * - * @deprecated + * @deprecated `Config\Cookie` $samesite property is used. */ public $samesite = 'Lax'; } diff --git a/app/Config/Validation.php b/app/Config/Validation.php index 1cff0424f4de..a254c1850015 100644 --- a/app/Config/Validation.php +++ b/app/Config/Validation.php @@ -2,12 +2,13 @@ namespace Config; +use CodeIgniter\Config\BaseConfig; use CodeIgniter\Validation\CreditCardRules; use CodeIgniter\Validation\FileRules; use CodeIgniter\Validation\FormatRules; use CodeIgniter\Validation\Rules; -class Validation +class Validation extends BaseConfig { //-------------------------------------------------------------------- // Setup diff --git a/app/Config/View.php b/app/Config/View.php index 024e8302a01b..78cd547e3b8f 100644 --- a/app/Config/View.php +++ b/app/Config/View.php @@ -3,6 +3,7 @@ namespace Config; use CodeIgniter\Config\View as BaseView; +use CodeIgniter\View\ViewDecoratorInterface; class View extends BaseView { @@ -41,4 +42,15 @@ class View extends BaseView * @var array */ public $plugins = []; + + /** + * View Decorators are class methods that will be run in sequence to + * have a chance to alter the generated output just prior to caching + * the results. + * + * All classes must implement CodeIgniter\View\ViewDecoratorInterface + * + * @var class-string[] + */ + public array $decorators = []; } diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 0328f140bfdf..122db5f9a9f2 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -19,7 +19,7 @@ * * For security be sure to declare any new methods as protected or private. */ -class BaseController extends Controller +abstract class BaseController extends Controller { /** * Instance of the main Request object. diff --git a/app/Views/errors/html/debug.css b/app/Views/errors/html/debug.css index 384d66d95f87..98f54dbc8a01 100644 --- a/app/Views/errors/html/debug.css +++ b/app/Views/errors/html/debug.css @@ -1,197 +1,197 @@ :root { - --main-bg-color: #fff; - --main-text-color: #555; - --dark-text-color: #222; - --light-text-color: #c7c7c7; - --brand-primary-color: #E06E3F; - --light-bg-color: #ededee; - --dark-bg-color: #404040; + --main-bg-color: #fff; + --main-text-color: #555; + --dark-text-color: #222; + --light-text-color: #c7c7c7; + --brand-primary-color: #E06E3F; + --light-bg-color: #ededee; + --dark-bg-color: #404040; } body { - height: 100%; - background: var(--main-bg-color); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - color: var(--main-text-color); - font-weight: 300; - margin: 0; - padding: 0; + height: 100%; + background: var(--main-bg-color); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + color: var(--main-text-color); + font-weight: 300; + margin: 0; + padding: 0; } h1 { - font-weight: lighter; - letter-spacing: 0.8; - font-size: 3rem; - color: var(--dark-text-color); - margin: 0; + font-weight: lighter; + letter-spacing: 0.8; + font-size: 3rem; + color: var(--dark-text-color); + margin: 0; } h1.headline { - margin-top: 20%; - font-size: 5rem; + margin-top: 20%; + font-size: 5rem; } .text-center { - text-align: center; + text-align: center; } p.lead { - font-size: 1.6rem; + font-size: 1.6rem; } .container { - max-width: 75rem; - margin: 0 auto; - padding: 1rem; + max-width: 75rem; + margin: 0 auto; + padding: 1rem; } .header { - background: var(--light-bg-color); - color: var(--dark-text-color); + background: var(--light-bg-color); + color: var(--dark-text-color); } .header .container { - padding: 1rem 1.75rem 1.75rem 1.75rem; + padding: 1rem 1.75rem 1.75rem 1.75rem; } .header h1 { - font-size: 2.5rem; - font-weight: 500; + font-size: 2.5rem; + font-weight: 500; } .header p { - font-size: 1.2rem; - margin: 0; - line-height: 2.5; + font-size: 1.2rem; + margin: 0; + line-height: 2.5; } .header a { - color: var(--brand-primary-color); - margin-left: 2rem; - display: none; - text-decoration: none; + color: var(--brand-primary-color); + margin-left: 2rem; + display: none; + text-decoration: none; } .header:hover a { - display: inline; + display: inline; } .footer { - background: var(--dark-bg-color); - color: var(--light-text-color); + background: var(--dark-bg-color); + color: var(--light-text-color); } .footer .container { - border-top: 1px solid #e7e7e7; - margin-top: 1rem; - text-align: center; + border-top: 1px solid #e7e7e7; + margin-top: 1rem; + text-align: center; } .source { - background: #343434; - color: var(--light-text-color); - padding: 0.5em 1em; - border-radius: 5px; - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; - font-size: 0.85rem; - margin: 0; - overflow-x: scroll; + background: #343434; + color: var(--light-text-color); + padding: 0.5em 1em; + border-radius: 5px; + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + font-size: 0.85rem; + margin: 0; + overflow-x: scroll; } .source span.line { - line-height: 1.4; + line-height: 1.4; } .source span.line .number { - color: #666; + color: #666; } .source .line .highlight { - display: block; - background: var(--dark-text-color); - color: var(--light-text-color); + display: block; + background: var(--dark-text-color); + color: var(--light-text-color); } .source span.highlight .number { - color: #fff; + color: #fff; } .tabs { - list-style: none; - list-style-position: inside; - margin: 0; - padding: 0; - margin-bottom: -1px; + list-style: none; + list-style-position: inside; + margin: 0; + padding: 0; + margin-bottom: -1px; } .tabs li { - display: inline; + display: inline; } .tabs a:link, .tabs a:visited { - padding: 0rem 1rem; - line-height: 2.7; - text-decoration: none; - color: var(--dark-text-color); - background: var(--light-bg-color); - border: 1px solid rgba(0,0,0,0.15); - border-bottom: 0; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - display: inline-block; + padding: 0rem 1rem; + line-height: 2.7; + text-decoration: none; + color: var(--dark-text-color); + background: var(--light-bg-color); + border: 1px solid rgba(0,0,0,0.15); + border-bottom: 0; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + display: inline-block; } .tabs a:hover { - background: var(--light-bg-color); - border-color: rgba(0,0,0,0.15); + background: var(--light-bg-color); + border-color: rgba(0,0,0,0.15); } .tabs a.active { - background: var(--main-bg-color); - color: var(--main-text-color); + background: var(--main-bg-color); + color: var(--main-text-color); } .tab-content { - background: var(--main-bg-color); - border: 1px solid rgba(0,0,0,0.15); + background: var(--main-bg-color); + border: 1px solid rgba(0,0,0,0.15); } .content { - padding: 1rem; + padding: 1rem; } .hide { - display: none; + display: none; } .alert { - margin-top: 2rem; - display: block; - text-align: center; - line-height: 3.0; - background: #d9edf7; - border: 1px solid #bcdff1; - border-radius: 5px; - color: #31708f; + margin-top: 2rem; + display: block; + text-align: center; + line-height: 3.0; + background: #d9edf7; + border: 1px solid #bcdff1; + border-radius: 5px; + color: #31708f; } ul, ol { - line-height: 1.8; + line-height: 1.8; } table { - width: 100%; - overflow: hidden; + width: 100%; + overflow: hidden; } th { - text-align: left; - border-bottom: 1px solid #e7e7e7; - padding-bottom: 0.5rem; + text-align: left; + border-bottom: 1px solid #e7e7e7; + padding-bottom: 0.5rem; } td { - padding: 0.2rem 0.5rem 0.2rem 0; + padding: 0.2rem 0.5rem 0.2rem 0; } tr:hover td { - background: #f1f1f1; + background: #f1f1f1; } td pre { - white-space: pre-wrap; + white-space: pre-wrap; } .trace a { - color: inherit; + color: inherit; } .trace table { - width: auto; + width: auto; } .trace tr td:first-child { - min-width: 5em; - font-weight: bold; + min-width: 5em; + font-weight: bold; } .trace td { - background: var(--light-bg-color); - padding: 0 1rem; + background: var(--light-bg-color); + padding: 0 1rem; } .trace td pre { - margin: 0; + margin: 0; } .args { - display: none; + display: none; } diff --git a/app/Views/errors/html/debug.js b/app/Views/errors/html/debug.js index 2b4d0638153a..99199cac872c 100644 --- a/app/Views/errors/html/debug.js +++ b/app/Views/errors/html/debug.js @@ -1,118 +1,116 @@ -// Tabs - var tabLinks = new Array(); var contentDivs = new Array(); function init() { - // Grab the tab links and content divs from the page - var tabListItems = document.getElementById('tabs').childNodes; - console.log(tabListItems); - for (var i = 0; i < tabListItems.length; i ++) - { - if (tabListItems[i].nodeName == "LI") - { - var tabLink = getFirstChildWithTagName(tabListItems[i], 'A'); - var id = getHash(tabLink.getAttribute('href')); - tabLinks[id] = tabLink; - contentDivs[id] = document.getElementById(id); - } - } + // Grab the tab links and content divs from the page + var tabListItems = document.getElementById('tabs').childNodes; + console.log(tabListItems); + for (var i = 0; i < tabListItems.length; i ++) + { + if (tabListItems[i].nodeName == "LI") + { + var tabLink = getFirstChildWithTagName(tabListItems[i], 'A'); + var id = getHash(tabLink.getAttribute('href')); + tabLinks[id] = tabLink; + contentDivs[id] = document.getElementById(id); + } + } - // Assign onclick events to the tab links, and - // highlight the first tab - var i = 0; + // Assign onclick events to the tab links, and + // highlight the first tab + var i = 0; - for (var id in tabLinks) - { - tabLinks[id].onclick = showTab; - tabLinks[id].onfocus = function () { - this.blur() - }; - if (i == 0) - { - tabLinks[id].className = 'active'; - } - i ++; - } + for (var id in tabLinks) + { + tabLinks[id].onclick = showTab; + tabLinks[id].onfocus = function () { + this.blur() + }; + if (i == 0) + { + tabLinks[id].className = 'active'; + } + i ++; + } - // Hide all content divs except the first - var i = 0; + // Hide all content divs except the first + var i = 0; - for (var id in contentDivs) - { - if (i != 0) - { - console.log(contentDivs[id]); - contentDivs[id].className = 'content hide'; - } - i ++; - } + for (var id in contentDivs) + { + if (i != 0) + { + console.log(contentDivs[id]); + contentDivs[id].className = 'content hide'; + } + i ++; + } } function showTab() { - var selectedId = getHash(this.getAttribute('href')); + var selectedId = getHash(this.getAttribute('href')); - // Highlight the selected tab, and dim all others. - // Also show the selected content div, and hide all others. - for (var id in contentDivs) - { - if (id == selectedId) - { - tabLinks[id].className = 'active'; - contentDivs[id].className = 'content'; - } - else - { - tabLinks[id].className = ''; - contentDivs[id].className = 'content hide'; - } - } + // Highlight the selected tab, and dim all others. + // Also show the selected content div, and hide all others. + for (var id in contentDivs) + { + if (id == selectedId) + { + tabLinks[id].className = 'active'; + contentDivs[id].className = 'content'; + } + else + { + tabLinks[id].className = ''; + contentDivs[id].className = 'content hide'; + } + } - // Stop the browser following the link - return false; + // Stop the browser following the link + return false; } function getFirstChildWithTagName(element, tagName) { - for (var i = 0; i < element.childNodes.length; i ++) - { - if (element.childNodes[i].nodeName == tagName) - { - return element.childNodes[i]; - } - } + for (var i = 0; i < element.childNodes.length; i ++) + { + if (element.childNodes[i].nodeName == tagName) + { + return element.childNodes[i]; + } + } } function getHash(url) { - var hashPos = url.lastIndexOf('#'); - return url.substring(hashPos + 1); + var hashPos = url.lastIndexOf('#'); + return url.substring(hashPos + 1); } function toggle(elem) { - elem = document.getElementById(elem); + elem = document.getElementById(elem); - if (elem.style && elem.style['display']) - { - // Only works with the "style" attr - var disp = elem.style['display']; - } - else if (elem.currentStyle) - { - // For MSIE, naturally - var disp = elem.currentStyle['display']; - } - else if (window.getComputedStyle) - { - // For most other browsers - var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display'); - } + if (elem.style && elem.style['display']) + { + // Only works with the "style" attr + var disp = elem.style['display']; + } + else if (elem.currentStyle) + { + // For MSIE, naturally + var disp = elem.currentStyle['display']; + } + else if (window.getComputedStyle) + { + // For most other browsers + var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display'); + } - // Toggle the state of the "display" style - elem.style.display = disp == 'block' ? 'none' : 'block'; + // Toggle the state of the "display" style + elem.style.display = disp == 'block' ? 'none' : 'block'; - return false; + return false; } diff --git a/app/Views/errors/html/error_404.php b/app/Views/errors/html/error_404.php index 1cca20c485a9..f81717fdd0f5 100644 --- a/app/Views/errors/html/error_404.php +++ b/app/Views/errors/html/error_404.php @@ -1,84 +1,84 @@ - - 404 Page Not Found + + 404 Page Not Found - + -
-

404 - File Not Found

+
+

404 - File Not Found

-

- - - - Sorry! Cannot seem to find the page you were looking for. - -

-
+

+ + + + Sorry! Cannot seem to find the page you were looking for. + +

+
diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php index 693afed491f5..77e963b2920a 100644 --- a/app/Views/errors/html/error_exception.php +++ b/app/Views/errors/html/error_exception.php @@ -2,87 +2,87 @@ - - + + - <?= esc($title) ?> - + <?= esc($title) ?> + - + - -
-
-

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

-

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

-
-
- - -
-

at line

- - -
- -
- -
- -
- - - -
- - -
- -
    - $row) : ?> - -
  1. -

    - - - +

    +
    +

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

    +

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

    +
    +
    + + +
    +

    at line

    + + +
    + +
    + +
    + +
    + + + +
    + + +
    + +
      + $row) : ?> + +
    1. +

      + + + - - {PHP internal code} - - - - -   —   - - - ( arguments ) -

      - - - + {PHP internal code} + + + + +   —   + + + ( arguments ) +
      +
      + + $value) : ?> - - - - - - -
      name : "#{$key}") ?>
      -
      - - () - - - - -   —   () - -

      - - - -
      - -
      - -
    2. - - -
    - -
    - - -
    - - + name : "#{$key}") ?> +
    + + + + +
    + + () + + + + +   —   () + +

    + + + +
    + +
    + +
  2. + + +
+ +
+ + +
+ + -

$

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

Constants

- - - - - - - - - - $value) : ?> - - - - - - -
KeyValue
- - - -
- -
- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathgetUri()) ?>
HTTP MethodgetMethod(true)) ?>
IP AddressgetIPAddress()) ?>
Is AJAX Request?isAJAX() ? 'yes' : 'no' ?>
Is CLI Request?isCLI() ? 'yes' : 'no' ?>
Is Secure Request?isSecure() ? 'yes' : 'no' ?>
User AgentgetUserAgent()->getAgentString()) ?>
- - - - - $ + + + + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ + + + + + +

Constants

+ + + + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PathgetUri()) ?>
HTTP MethodgetMethod())) ?>
IP AddressgetIPAddress()) ?>
Is AJAX Request?isAJAX() ? 'yes' : 'no' ?>
Is CLI Request?isCLI() ? 'yes' : 'no' ?>
Is Secure Request?isSecure() ? 'yes' : 'no' ?>
User AgentgetUserAgent()->getAgentString()) ?>
+ + + + + - - -

$

- - - - - - - - - - $value) : ?> - - - - - - -
KeyValue
- - - -
- -
- - - - - -
- No $_GET, $_POST, or $_COOKIE Information to show. -
- - - - getHeaders(); ?> - - -

Headers

- - - - - - - - - - - + +

$

+ +
HeaderValue
+ + + + + + + + $value) : ?> + + + + + + +
KeyValue
+ + + +
+ +
+ + + + + +
+ No $_GET, $_POST, or $_COOKIE Information to show. +
+ + + + getHeaders(); ?> + + +

Headers

+ + + + + + + + + + + - - - - - - - - -
HeaderValue
getName(), 'html') ?>getValueLine(), 'html') ?>
- - -
- - - + + getName(), 'html') ?> + getValueLine(), 'html') ?> + + + + + + + +
+ + + setStatusCode(http_response_code()); ?> -
- - - - - -
Response StatusgetStatusCode() . ' - ' . $response->getReason()) ?>
- - getHeaders(); ?> - - - -

Headers

- - - - - - - - - - $value) : ?> - - - - - - -
HeaderValue
getHeaderLine($name), 'html') ?>
- - -
- - -
- - -
    - -
  1. - -
-
- - -
- - - - - - - - - - - - - - - - -
Memory Usage
Peak Memory Usage:
Memory Limit:
- -
- -
- - - - +
+ + + + + +
Response StatusgetStatusCode() . ' - ' . $response->getReasonPhrase()) ?>
+ + getHeaders(); ?> + + + +

Headers

+ + + + + + + + + + $value) : ?> + + + + + + +
HeaderValue
getHeaderLine($name), 'html') ?>
+ + +
+ + +
+ + +
    + +
  1. + +
+
+ + +
+ + + + + + + + + + + + + + + + +
Memory Usage
Peak Memory Usage:
Memory Limit:
+ +
+ + + + + + diff --git a/app/Views/errors/html/production.php b/app/Views/errors/html/production.php index cca49c2ed9c3..9faa4a15b783 100644 --- a/app/Views/errors/html/production.php +++ b/app/Views/errors/html/production.php @@ -1,24 +1,24 @@ - - + + - Whoops! + Whoops! - + -
+
-

Whoops!

+

Whoops!

-

We seem to have hit a snag. Please try again later...

+

We seem to have hit a snag. Please try again later...

-
+
diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php index 9ee2e427c308..c66a9615c6be 100644 --- a/app/Views/welcome_message.php +++ b/app/Views/welcome_message.php @@ -1,229 +1,229 @@ - - Welcome to CodeIgniter 4! - - - - - - - + + Welcome to CodeIgniter 4! + + + + + + +
- - -
- -

Welcome to CodeIgniter

- -

The small framework with powerful features

- -
+ + +
+ +

Welcome to CodeIgniter

+ +

The small framework with powerful features

+ +
@@ -231,91 +231,91 @@
-

About this page

+

About this page

-

The page you are looking at is being generated dynamically by CodeIgniter.

+

The page you are looking at is being generated dynamically by CodeIgniter.

-

If you would like to edit this page you will find it located at:

+

If you would like to edit this page you will find it located at:

-
app/Views/welcome_message.php
+
app/Views/welcome_message.php
-

The corresponding controller for this page can be found at:

+

The corresponding controller for this page can be found at:

-
app/Controllers/Home.php
+
app/Controllers/Home.php
-
+
-

Go further

+

Go further

-

- - Learn -

+

+ + Learn +

-

The User Guide contains an introduction, tutorial, a number of "how to" - guides, and then reference documentation for the components that make up - the framework. Check the User Guide !

+

The User Guide contains an introduction, tutorial, a number of "how to" + guides, and then reference documentation for the components that make up + the framework. Check the User Guide !

-

- - Discuss -

+

+ + Discuss +

-

CodeIgniter is a community-developed open source project, with several - venues for the community members to gather and exchange ideas. View all - the threads on CodeIgniter's forum, or chat on Slack !

+

CodeIgniter is a community-developed open source project, with several + venues for the community members to gather and exchange ideas. View all + the threads on CodeIgniter's forum, or chat on Slack !

-

- - Contribute -

+

+ + Contribute +

-

CodeIgniter is a community driven project and accepts contributions - of code and documentation from the community. Why not - - join us ?

+

CodeIgniter is a community driven project and accepts contributions + of code and documentation from the community. Why not + + join us ?

-
+
-
+
-

Page rendered in {elapsed_time} seconds

+

Page rendered in {elapsed_time} seconds

-

Environment:

+

Environment:

-
+
-
+
-

© CodeIgniter Foundation. CodeIgniter is open source project released under the MIT - open source licence.

+

© CodeIgniter Foundation. CodeIgniter is open source project released under the MIT + open source licence.

-
+
diff --git a/app/index.html b/app/index.html index b702fbc3967b..69df4e1dff68 100644 --- a/app/index.html +++ b/app/index.html @@ -1,7 +1,7 @@ - 403 Forbidden + 403 Forbidden diff --git a/composer.json b/composer.json index 376e2cdff38f..2c93e30b7d72 100644 --- a/composer.json +++ b/composer.json @@ -5,26 +5,26 @@ "homepage": "https://codeigniter.com", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "ext-curl": "*", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.0", + "kint-php/kint": "^4.1.1", "laminas/laminas-escaper": "^2.9", "psr/log": "^1.1" }, "require-dev": { - "codeigniter/coding-standard": "1.2.*", + "codeigniter/coding-standard": "^1.1", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "3.2.*", + "friendsofphp/php-cs-fixer": "3.6.*", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.3", "nexusphp/tachycardia": "^1.0", - "phpstan/phpstan": "1.4.3", + "phpstan/phpstan": "^1.7.1", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1", - "rector/rector": "0.12.10" + "rector/rector": "0.13.3" }, "suggest": { "ext-fileinfo": "Improves mime type detection for files" @@ -50,6 +50,7 @@ "autoload-dev": { "psr-4": { "CodeIgniter\\": "tests/system/", + "CodeIgniter\\AutoReview\\": "tests/AutoReview/", "Utils\\": "utils/" } }, @@ -61,12 +62,14 @@ "analyze": "phpstan analyse", "test": "phpunit", "cs": [ - "php-cs-fixer fix --verbose --dry-run --diff --config=.no-header.php-cs-fixer.dist.php", - "php-cs-fixer fix --verbose --dry-run --diff" + "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.user-guide.php", + "php-cs-fixer fix --ansi --verbose --dry-run --diff --config=.php-cs-fixer.no-header.php", + "php-cs-fixer fix --ansi --verbose --dry-run --diff" ], "cs-fix": [ - "php-cs-fixer fix --verbose --diff --config=.no-header.php-cs-fixer.dist.php", - "php-cs-fixer fix --verbose --diff" + "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.user-guide.php", + "php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", + "php-cs-fixer fix --ansi --verbose --diff" ] }, "scripts-descriptions": { diff --git a/contributing/bug_report.md b/contributing/bug_report.md index c564a9a734f4..43767cdd1766 100644 --- a/contributing/bug_report.md +++ b/contributing/bug_report.md @@ -27,15 +27,7 @@ have found a bug, again - please ask on the forums first. ## Security -Did you find a security issue in CodeIgniter? - -Please *don't* disclose it publicly, but e-mail us at -, or report it via our page on -[HackerOne](https://hackerone.com/codeigniter). - -If you've found a critical vulnerability, we'd be happy to credit you in -our -[ChangeLog](https://codeigniter4.github.io/CodeIgniter4/changelogs/index.html). +See [SECURITY.md](../SECURITY.md). ## Tips for a Good Issue Report diff --git a/contributing/css.md b/contributing/css.md index 3108319a0e1b..7accf5e83801 100644 --- a/contributing/css.md +++ b/contributing/css.md @@ -1,6 +1,6 @@ -# Contribution CSS +# Contribution to Debug Toolbar CSS -CodeIgniter uses SASS to generate the debug toolbar's CSS. Therefore, +CodeIgniter uses Dart Sass to generate the debug toolbar's CSS. Therefore, you will need to install it first. You can find further instructions on the official website: @@ -9,34 +9,32 @@ the official website: Open your terminal, and navigate to CodeIgniter's root folder. To generate the CSS file, use the following command: -`sass --no-cache --sourcemap=none admin/css/debug-toolbar/toolbar.scss system/Debug/Toolbar/Views/toolbar.css` +`sass --no-source-map admin/css/debug-toolbar/toolbar.scss system/Debug/Toolbar/Views/toolbar.css` -Details: -- `--no-cache` is a parameter defined to disable SASS cache, -this prevents a "cache" folder from being created -- `--sourcemap=none` is a parameter which prevents soucemap files from being generated -- `admin/css/debug-toolbar/toolbar.scss` is the SASS source +Details: +- `--no-source-map` is an option which prevents sourcemap files from being generated +- `admin/css/debug-toolbar/toolbar.scss` is the SASS source - `system/Debug/Toolbar/Views/toolbar.css` is he CSS destination ## Color scheme **Themes** -Dark: `#252525` / `rgb(37, 37, 37)` -Light: `#FFFFFF` / `rgb(255, 255, 255)` +Dark: `#252525` / `rgb(37, 37, 37)` +Light: `#FFFFFF` / `rgb(255, 255, 255)` **Glossy colors** -Blue: `#5BC0DE` / `rgb(91, 192, 222)` -Gray: `#434343` / `rgb(67, 67, 67)` -Green: `#9ACE25` / `rgb(154, 206, 37)` -Orange: `#DD8615` / `rgb(221, 134, 21)` -Red: `#DD4814` / `rgb(221, 72, 20)` +Blue: `#5BC0DE` / `rgb(91, 192, 222)` +Gray: `#434343` / `rgb(67, 67, 67)` +Green: `#9ACE25` / `rgb(154, 206, 37)` +Orange: `#DD8615` / `rgb(221, 134, 21)` +Red: `#DD4814` / `rgb(221, 72, 20)` **Matt colors** -Blue: `#D8EAF0` / `rgb(216, 234, 240)` -Gray: `#DFDFDF` / `rgb(223, 223, 223)` -Green: `#DFF0D8` / `rgb(223, 240, 216)` -Orange: `#FDC894` / `rgb(253, 200, 148)` -Red: `#EF9090` / `rgb(239, 144, 144)` +Blue: `#D8EAF0` / `rgb(216, 234, 240)` +Gray: `#DFDFDF` / `rgb(223, 223, 223)` +Green: `#DFF0D8` / `rgb(223, 240, 216)` +Orange: `#FDC894` / `rgb(253, 200, 148)` +Red: `#EF9090` / `rgb(239, 144, 144)` diff --git a/contributing/documentation.rst b/contributing/documentation.rst index eba3eb99437e..405cb127902a 100644 --- a/contributing/documentation.rst +++ b/contributing/documentation.rst @@ -9,9 +9,7 @@ on readability and user friendliness. While they can be quite technical, we always write for humans! A local table of contents should always be included, like the one below. -It is created automatically by inserting the following: - -:: +It is created automatically by inserting the following:: .. contents:: :local: @@ -86,8 +84,8 @@ create these with the following tab triggers:: References ********** -References to a Section -======================= +To a Section +============ If you need to link to a specific section, the first you add the label before a header:: @@ -102,11 +100,30 @@ And then you can reference it like this:: See :ref:`curlrequest-request-options-headers` for how to add. -References to a Page -==================== +To a Section in the Page +======================== + +You can reference a section in the current page like the following:: + + See `Result Rows`_ + +To a Page +========= You can reference a page like the following:: - :doc:`Session <../libraries/sessions>` library + See :doc:`Session <../libraries/sessions>` library + + See :doc:`../libraries/sessions` library + +To a URL +======== + + `CodeIgniter 4 framework `_ + +To a Function +============= + + :php:func:`dot_array_search` - :doc:`../libraries/sessions` library + :php:func:`Response::setCookie() ` diff --git a/contributing/internals.md b/contributing/internals.md index 6ced5f56dc48..e2d387028fe1 100644 --- a/contributing/internals.md +++ b/contributing/internals.md @@ -17,11 +17,9 @@ other core packages, you can create that in the constructor using the override that: ```php - public function __construct(Foo $foo=null) + public function __construct(?Foo $foo = null) { - $this->foo = $foo instanceOf Foo - ? $foo - : \Config\Services::foo(); + $this->foo = $foo ?? \Config\Services::foo(); } ``` @@ -75,7 +73,7 @@ package itself will need its own sub-namespace that collects all related files into one grouping, like `CodeIgniter\HTTP`. Files MUST be named the same as the class they hold, and they must match -the Style Guide <./styleguide.md>, meaning CamelCase class and +the [Style Guide](styleguide.md), meaning CamelCase class and file names. They should be in their own directory that matches the sub-namespace under the **system** directory. @@ -122,17 +120,9 @@ scans and keep performance high. ## Command-Line Support -CodeIgniter has never been known for it's strong CLI support. However, +CodeIgniter has never been known for its strong CLI support. However, if your package could benefit from it, create a new file under -**system/Commands**. The class contained within is simply a controller -that is intended for CLI usage only. The `index()` method should provide -a list of available commands provided by that package. - -Routes must be added to **system/Config/Routes.php** using the `cli()` -method to ensure it is not accessible through the browser, but is -restricted to the CLI only. - -See the **MigrationsCommand** file for an example. +**system/Commands**. ## Documentation diff --git a/contributing/pull_request.md b/contributing/pull_request.md index e788b76fede3..51f976232987 100644 --- a/contributing/pull_request.md +++ b/contributing/pull_request.md @@ -119,7 +119,7 @@ See [Contribution CSS](./css.md). ### Compatibility -CodeIgniter4 requires [PHP 7.3](https://php.net/releases/7_3_0.php). +CodeIgniter4 requires [PHP 7.4](https://php.net/releases/7_4_0.php). ### Backwards Compatibility diff --git a/contributing/workflow.md b/contributing/workflow.md index b2309df9477a..c4c4bb073865 100644 --- a/contributing/workflow.md +++ b/contributing/workflow.md @@ -78,7 +78,7 @@ Then synchronizing is done by pulling from us and pushing to you. This is normally done locally, so that you can resolve any merge conflicts. For instance, to synchronize **develop** branches: - git checkout develop + git switch develop git fetch upstream git merge upstream/develop git push origin develop @@ -109,8 +109,8 @@ For instance, make sure you are in the *develop* branch, and create a new feature branch, based on *develop*, for a new feature you are creating: - git checkout develop - git checkout -b new/mind-reader + git switch develop + git switch -c new/mind-reader Saving changes only updates your local working area. @@ -131,15 +131,15 @@ Just make sure that your commits in a feature branch are all related. If you are working on two features at a time, then you will want to switch between them to keep the contributions separate. For instance: - git checkout new/mind-reader + git switch new/mind-reader // work away git add . git commit -S -m "Added adapter for abc" - git checkout fix/issue-123 + git switch fix/issue-123 // work away git add . git commit -S -m "Fixed problem in DEF\Something" - git checkout develop + git switch develop The last checkout makes sure that you end up in your *develop* branch as a starting point for your next session working with your repository. @@ -155,17 +155,16 @@ that it could benefit from a review by fellow developers. > Remember to sync your local repo with the shared one before pushing! It is a lot easier to resolve conflicts at this stage. - Synchronize your repository: - git checkout develop + git switch develop git fetch upstream git merge upstream/develop git push origin develop Bring your feature branch up to date: - git checkout new/mind-reader + git switch new/mind-reader git rebase upstream/develop And finally push your local branch to your GitHub repository: @@ -215,6 +214,33 @@ Label your PRs with the one of the following [labels](https://github.com/codeign And if your PRs have the breaking changes, label the following label: - **breaking change** ... PRs that may break existing functionalities +## Updating Your Branch + +If you are asked for changes in the review, commit the fix in your branch and push it to GitHub again. + +If the `develop` branch progresses and conflicts arise that prevent merging, or if you are asked to *rebase*, +do the following: + +Synchronize your repository: + + git switch develop + git fetch upstream + git merge upstream/develop + git push origin develop + +Bring your feature branch up to date: + + git switch new/mind-reader + git rebase upstream/develop + +You might get conflicts when you rebase. It is your +responsibility to resolve those locally, so that you can continue +collaborating with the shared repository. + +And finally push your local branch to your GitHub repository: + + git push --force-with-lease origin new/mind-reader + ## Cleanup If your PR is accepted and merged into the shared repository, you can diff --git a/depfile.yaml b/depfile.yaml deleted file mode 100644 index 301f17076f82..000000000000 --- a/depfile.yaml +++ /dev/null @@ -1,231 +0,0 @@ -# Defines the layers for each framework -# component and their allowed interactions. -# The following components are exempt -# due to their global nature: -# - CLI & Commands -# - Config -# - Debug -# - Exception -# - Service -# - Validation\FormatRules -paths: - - ./app - - ./system -exclude_files: - - '#.*test.*#i' -layers: - - name: API - collectors: - - type: className - regex: ^Codeigniter\\API\\.* - - name: Cache - collectors: - - type: className - regex: ^Codeigniter\\Cache\\.* - - name: Controller - collectors: - - type: className - regex: ^CodeIgniter\\Controller$ - - name: Cookie - collectors: - - type: className - regex: ^Codeigniter\\Cookie\\.* - - name: Database - collectors: - - type: className - regex: ^Codeigniter\\Database\\.* - - name: Email - collectors: - - type: className - regex: ^Codeigniter\\Email\\.* - - name: Encryption - collectors: - - type: className - regex: ^Codeigniter\\Encryption\\.* - - name: Entity - collectors: - - type: className - regex: ^Codeigniter\\Entity\\.* - - name: Events - collectors: - - type: className - regex: ^Codeigniter\\Events\\.* - - name: Files - collectors: - - type: className - regex: ^Codeigniter\\Files\\.* - - name: Filters - collectors: - - type: bool - must: - - type: className - regex: ^Codeigniter\\Filters\\Filter.* - - name: Format - collectors: - - type: className - regex: ^Codeigniter\\Format\\.* - - name: Honeypot - collectors: - - type: className - regex: ^Codeigniter\\.*Honeypot.* # includes the Filter - - name: HTTP - collectors: - - type: bool - must: - - type: className - regex: ^Codeigniter\\HTTP\\.* - must_not: - - type: className - regex: (Exception|URI) - - name: I18n - collectors: - - type: className - regex: ^Codeigniter\\I18n\\.* - - name: Images - collectors: - - type: className - regex: ^Codeigniter\\Images\\.* - - name: Language - collectors: - - type: className - regex: ^Codeigniter\\Language\\.* - - name: Log - collectors: - - type: className - regex: ^Codeigniter\\Log\\.* - - name: Model - collectors: - - type: className - regex: ^Codeigniter\\.*Model$ - - name: Modules - collectors: - - type: className - regex: ^Codeigniter\\Modules\\.* - - name: Pager - collectors: - - type: className - regex: ^Codeigniter\\Pager\\.* - - name: Publisher - collectors: - - type: className - regex: ^Codeigniter\\Publisher\\.* - - name: RESTful - collectors: - - type: className - regex: ^Codeigniter\\RESTful\\.* - - name: Router - collectors: - - type: className - regex: ^Codeigniter\\Router\\.* - - name: Security - collectors: - - type: className - regex: ^Codeigniter\\Security\\.* - - name: Session - collectors: - - type: className - regex: ^Codeigniter\\Session\\.* - - name: Throttle - collectors: - - type: className - regex: ^Codeigniter\\Throttle\\.* - - name: Typography - collectors: - - type: className - regex: ^Codeigniter\\Typography\\.* - - name: URI - collectors: - - type: className - regex: ^CodeIgniter\\HTTP\\URI$ - - name: Validation - collectors: - - type: bool - must: - - type: className - regex: ^Codeigniter\\Validation\\.* - must_not: - - type: className - regex: ^Codeigniter\\Validation\\FormatRules$ - - name: View - collectors: - - type: className - regex: ^Codeigniter\\View\\.* -ruleset: - API: - - Format - - HTTP - Controller: - - HTTP - - Validation - Database: - - Entity - - Events - Email: - - Events - Entity: - - I18n - Filters: - - HTTP - Honeypot: - - Filters - - HTTP - HTTP: - - Cookie - - Files - - Security - - URI - Images: - - Files - Model: - - Database - - I18n - - Pager - - Validation - Pager: - - URI - - View - Publisher: - - Files - - URI - RESTful: - - +API - - +Controller - Router: - - HTTP - Security: - - Cookie - - Session - - HTTP - Session: - - Cookie - - Database - Throttle: - - Cache - Validation: - - HTTP - View: - - Cache -skip_violations: - # Individual class exemptions - CodeIgniter\Entity\Cast\URICast: - - CodeIgniter\HTTP\URI - CodeIgniter\Log\Handlers\ChromeLoggerHandler: - - CodeIgniter\HTTP\ResponseInterface - CodeIgniter\View\Table: - - CodeIgniter\Database\BaseResult - CodeIgniter\View\Plugins: - - CodeIgniter\HTTP\URI - - # BC changes that should be fixed - CodeIgniter\HTTP\ResponseTrait: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\ResponseInterface: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\Response: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\RedirectResponse: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\DownloadResponse: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\Validation\Validation: - - CodeIgniter\View\RendererInterface diff --git a/deptrac.yaml b/deptrac.yaml new file mode 100644 index 000000000000..8d43af6117ae --- /dev/null +++ b/deptrac.yaml @@ -0,0 +1,233 @@ +# Defines the layers for each framework +# component and their allowed interactions. +# The following components are exempt +# due to their global nature: +# - CLI & Commands +# - Config +# - Debug +# - Exception +# - Service +# - Validation\FormatRules +parameters: + paths: + - ./app + - ./system + exclude_files: + - '#.*test.*#i' + layers: + - name: API + collectors: + - type: className + regex: ^Codeigniter\\API\\.* + - name: Cache + collectors: + - type: className + regex: ^Codeigniter\\Cache\\.* + - name: Controller + collectors: + - type: className + regex: ^CodeIgniter\\Controller$ + - name: Cookie + collectors: + - type: className + regex: ^Codeigniter\\Cookie\\.* + - name: Database + collectors: + - type: className + regex: ^Codeigniter\\Database\\.* + - name: Email + collectors: + - type: className + regex: ^Codeigniter\\Email\\.* + - name: Encryption + collectors: + - type: className + regex: ^Codeigniter\\Encryption\\.* + - name: Entity + collectors: + - type: className + regex: ^Codeigniter\\Entity\\.* + - name: Events + collectors: + - type: className + regex: ^Codeigniter\\Events\\.* + - name: Files + collectors: + - type: className + regex: ^Codeigniter\\Files\\.* + - name: Filters + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\Filters\\Filter.* + - name: Format + collectors: + - type: className + regex: ^Codeigniter\\Format\\.* + - name: Honeypot + collectors: + - type: className + regex: ^Codeigniter\\.*Honeypot.* # includes the Filter + - name: HTTP + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\HTTP\\.* + must_not: + - type: className + regex: (Exception|URI) + - name: I18n + collectors: + - type: className + regex: ^Codeigniter\\I18n\\.* + - name: Images + collectors: + - type: className + regex: ^Codeigniter\\Images\\.* + - name: Language + collectors: + - type: className + regex: ^Codeigniter\\Language\\.* + - name: Log + collectors: + - type: className + regex: ^Codeigniter\\Log\\.* + - name: Model + collectors: + - type: className + regex: ^Codeigniter\\.*Model$ + - name: Modules + collectors: + - type: className + regex: ^Codeigniter\\Modules\\.* + - name: Pager + collectors: + - type: className + regex: ^Codeigniter\\Pager\\.* + - name: Publisher + collectors: + - type: className + regex: ^Codeigniter\\Publisher\\.* + - name: RESTful + collectors: + - type: className + regex: ^Codeigniter\\RESTful\\.* + - name: Router + collectors: + - type: className + regex: ^Codeigniter\\Router\\.* + - name: Security + collectors: + - type: className + regex: ^Codeigniter\\Security\\.* + - name: Session + collectors: + - type: className + regex: ^Codeigniter\\Session\\.* + - name: Throttle + collectors: + - type: className + regex: ^Codeigniter\\Throttle\\.* + - name: Typography + collectors: + - type: className + regex: ^Codeigniter\\Typography\\.* + - name: URI + collectors: + - type: className + regex: ^CodeIgniter\\HTTP\\URI$ + - name: Validation + collectors: + - type: bool + must: + - type: className + regex: ^Codeigniter\\Validation\\.* + must_not: + - type: className + regex: ^Codeigniter\\Validation\\FormatRules$ + - name: View + collectors: + - type: className + regex: ^Codeigniter\\View\\.* + ruleset: + API: + - Format + - HTTP + Controller: + - HTTP + - Validation + Database: + - Entity + - Events + Email: + - Events + Entity: + - I18n + Filters: + - HTTP + Honeypot: + - Filters + - HTTP + HTTP: + - Cookie + - Files + - Security + - URI + Images: + - Files + Model: + - Database + - I18n + - Pager + - Validation + Pager: + - URI + - View + Publisher: + - Files + - URI + RESTful: + - +API + - +Controller + Router: + - HTTP + Security: + - Cookie + - Session + - HTTP + Session: + - Cookie + - HTTP + - Database + Throttle: + - Cache + Validation: + - HTTP + View: + - Cache + skip_violations: + # Individual class exemptions + CodeIgniter\Entity\Cast\URICast: + - CodeIgniter\HTTP\URI + CodeIgniter\Log\Handlers\ChromeLoggerHandler: + - CodeIgniter\HTTP\ResponseInterface + CodeIgniter\View\Table: + - CodeIgniter\Database\BaseResult + CodeIgniter\View\Plugins: + - CodeIgniter\HTTP\URI + + # BC changes that should be fixed + CodeIgniter\HTTP\ResponseTrait: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\ResponseInterface: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\Response: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\RedirectResponse: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\HTTP\DownloadResponse: + - CodeIgniter\Pager\PagerInterface + CodeIgniter\Validation\Validation: + - CodeIgniter\View\RendererInterface diff --git a/env b/env index c60b367265e7..67faaee5b57a 100644 --- a/env +++ b/env @@ -21,6 +21,8 @@ #-------------------------------------------------------------------- # app.baseURL = '' +# If you have trouble with `.`, you could also use `_`. +# app_baseURL = '' # app.forceGlobalSecureRequests = false # app.sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler' @@ -60,7 +62,7 @@ # contentsecuritypolicy.scriptSrc = 'self' # contentsecuritypolicy.styleSrc = 'self' # contentsecuritypolicy.imageSrc = 'self' -# contentsecuritypolicy.base_uri = null +# contentsecuritypolicy.baseURI = null # contentsecuritypolicy.childSrc = null # contentsecuritypolicy.connectSrc = 'self' # contentsecuritypolicy.fontSrc = null @@ -73,6 +75,9 @@ # contentsecuritypolicy.reportURI = null # contentsecuritypolicy.sandbox = false # contentsecuritypolicy.upgradeInsecureRequests = false +# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}' +# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}' +# contentsecuritypolicy.autoNonce = true #-------------------------------------------------------------------- # COOKIE diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index d25e5e23a409..17f71d164f29 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -25,11 +25,6 @@ parameters: count: 1 path: system/Autoloader/Autoloader.php - - - message: "#^Method CodeIgniter\\\\Validation\\\\ValidationInterface\\:\\:run\\(\\) invoked with 3 parameters, 0\\-2 required\\.$#" - count: 1 - path: system/BaseModel.php - - message: "#^Property Config\\\\Cache\\:\\:\\$backupHandler \\(string\\) in isset\\(\\) is not nullable\\.$#" count: 1 @@ -105,11 +100,6 @@ parameters: count: 1 path: system/CodeIgniter.php - - - message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:getSegments\\(\\)\\.$#" - count: 1 - path: system/CodeIgniter.php - - message: "#^Call to an undefined method CodeIgniter\\\\HTTP\\\\Request\\:\\:setLocale\\(\\)\\.$#" count: 1 @@ -125,21 +115,6 @@ parameters: count: 1 path: system/CodeIgniter.php - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: system/CodeIgniter.php - - - - message: "#^Binary operation \"\\+\" between array\\\\|false and non\\-empty\\-array\\ results in an error\\.$#" - count: 1 - path: system/Common.php - - - - message: "#^Variable \\$params on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: system/Common.php - - message: "#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$db \\(CodeIgniter\\\\Database\\\\BaseConnection\\) in empty\\(\\) is not falsy\\.$#" count: 1 @@ -205,11 +180,6 @@ parameters: count: 1 path: system/Database/MigrationRunner.php - - - message: "#^Cannot access property \\$affected_rows on bool\\|object\\|resource\\.$#" - count: 1 - path: system/Database/MySQLi/Connection.php - - message: "#^Cannot access property \\$errno on bool\\|object\\|resource\\.$#" count: 1 @@ -485,16 +455,6 @@ parameters: count: 1 path: system/Debug/Exceptions.php - - - message: "#^Parameter \\#4 \\$replacement of function array_splice expects array\\|string, true given\\.$#" - count: 1 - path: system/Debug/Exceptions.php - - - - message: "#^Property CodeIgniter\\\\Debug\\\\Exceptions\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Debug/Exceptions.php - - message: "#^Property Config\\\\Exceptions\\:\\:\\$sensitiveDataInTrace \\(array\\) in isset\\(\\) is not nullable\\.$#" count: 1 @@ -505,16 +465,6 @@ parameters: count: 1 path: system/Debug/Toolbar.php - - - message: "#^Variable \\$request on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: system/Debug/Toolbar.php - - - - message: "#^Variable \\$response on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: system/Debug/Toolbar.php - - message: "#^Call to an undefined method CodeIgniter\\\\View\\\\RendererInterface\\:\\:getPerformanceData\\(\\)\\.$#" count: 1 @@ -570,11 +520,6 @@ parameters: count: 1 path: system/Filters/Filters.php - - - message: "#^Parameter \\#1 \\$seconds of function sleep expects int, float given\\.$#" - count: 1 - path: system/HTTP/CURLRequest.php - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -621,12 +566,7 @@ parameters: path: system/HTTP/Request.php - - message: "#^Property Config\\\\App\\:\\:\\$cookieSameSite \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 3 - path: system/HTTP/Response.php - - - - message: "#^Cannot unset offset 'path' on array\\{host\\: mixed\\}\\.$#" + message: "#^Cannot unset offset 'path' on array{host: non-empty-string}\\.$#" count: 1 path: system/HTTP/URI.php @@ -660,16 +600,6 @@ parameters: count: 1 path: system/Helpers/number_helper.php - - - message: "#^Variable \\$mockService in empty\\(\\) always exists and is always falsy\\.$#" - count: 1 - path: system/Helpers/test_helper.php - - - - message: "#^Parameter \\#2 \\$times of function str_repeat expects int, float given\\.$#" - count: 1 - path: system/Helpers/text_helper.php - - message: "#^Variable \\$pool might not be defined\\.$#" count: 2 @@ -725,19 +655,9 @@ parameters: count: 1 path: system/Log/Logger.php - - - message: "#^Call to an undefined method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:asArray\\(\\)\\.$#" - count: 1 - path: system/Model.php - - - - message: "#^Property CodeIgniter\\\\RESTful\\\\ResourceController\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/RESTful/ResourceController.php - - message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getDefaultNamespace\\(\\)\\.$#" - count: 2 + count: 3 path: system/Router/Router.php - @@ -752,7 +672,7 @@ parameters: - message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutesOptions\\(\\)\\.$#" - count: 2 + count: 1 path: system/Router/Router.php - @@ -766,100 +686,30 @@ parameters: path: system/Router/Router.php - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Router/Router.php - - - - message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#" + message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(.*\\)\\.$#" count: 2 path: system/Router/Router.php - - message: "#^Property Config\\\\App\\:\\:\\$CSRFCookieName \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\App\\:\\:\\$CSRFExpire \\(int\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\App\\:\\:\\$CSRFHeaderName \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\App\\:\\:\\$CSRFRegenerate \\(bool\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\App\\:\\:\\$CSRFTokenName \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\Security\\:\\:\\$cookieName \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\Security\\:\\:\\$csrfProtection \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\Security\\:\\:\\$expires \\(int\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/Security/Security.php - - - - message: "#^Property Config\\\\Security\\:\\:\\$headerName \\(string\\) on left side of \\?\\? is not nullable\\.$#" + message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 - path: system/Security/Security.php + path: system/Router/Router.php - - message: "#^Property Config\\\\Security\\:\\:\\$regenerate \\(bool\\) on left side of \\?\\? is not nullable\\.$#" + message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#" count: 1 - path: system/Security/Security.php + path: system/Router/Router.php - - message: "#^Property Config\\\\Security\\:\\:\\$tokenName \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 + message: "#^Property Config\\\\App\\:\\:\\$CSRF[a-zA-Z]+ \\([a-zA-Z]+\\) on left side of \\?\\? is not nullable\\.$#" + count: 6 path: system/Security/Security.php - - message: "#^Property Config\\\\Security\\:\\:\\$tokenRandomize \\(bool\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 + message: "#^Property Config\\\\Security\\:\\:\\$[a-zA-Z]+ \\([a-zA-Z]+\\) on left side of \\?\\? is not nullable\\.$#" + count: 8 path: system/Security/Security.php - - - message: "#^Access to an undefined property Config\\\\App\\:\\:\\$sessionDBGroup\\.$#" - count: 1 - path: system/Session/Handlers/DatabaseHandler.php - - - - message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Session/Handlers/DatabaseHandler.php - - - - message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Session/Handlers/FileHandler.php - - - - message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Session/Handlers/MemcachedHandler.php - - - - message: "#^Property CodeIgniter\\\\Session\\\\Handlers\\\\BaseHandler\\:\\:\\$sessionID \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Session/Handlers/RedisHandler.php - - message: "#^Strict comparison using \\=\\=\\= between string and true will always evaluate to false\\.$#" count: 1 @@ -880,11 +730,6 @@ parameters: count: 1 path: system/Session/Session.php - - - message: "#^Property Config\\\\App\\:\\:\\$cookieSameSite \\(string\\) on left side of \\?\\? is not nullable\\.$#" - count: 2 - path: system/Session/Session.php - - message: "#^Property Config\\\\App\\:\\:\\$cookieSecure \\(bool\\) on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -960,11 +805,6 @@ parameters: count: 1 path: system/Test/Fabricator.php - - - message: "#^Access to protected property CodeIgniter\\\\HTTP\\\\Request\\:\\:\\$uri\\.$#" - count: 1 - path: system/Test/FeatureTestCase.php - - message: "#^Property CodeIgniter\\\\Test\\\\CIUnitTestCase\\:\\:\\$bodyFormat \\(string\\) in isset\\(\\) is not nullable\\.$#" count: 1 @@ -985,11 +825,6 @@ parameters: count: 1 path: system/Test/Mock/MockConnection.php - - - message: "#^Property CodeIgniter\\\\Test\\\\Mock\\\\MockResourcePresenter\\:\\:\\$formatter \\(CodeIgniter\\\\Format\\\\FormatterInterface\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Test/Mock/MockResourcePresenter.php - - message: "#^Property CodeIgniter\\\\Throttle\\\\Throttler\\:\\:\\$testTime \\(int\\) on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -1015,3 +850,11 @@ parameters: count: 1 path: system/View/Parser.php + - + message: "#^Result of \\|\\| is always false\\.$#" + paths: + - system/Cache/CacheFactory.php + + - + message: "#^Binary operation \"/\" between string and 8 results in an error\\.$#" + path: system/Encryption/Handlers/OpenSSLHandler.php diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 000000000000..99acf96227fe --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,7 @@ + + + ./tests/AutoReview + ./tests/system ./tests/system/Database diff --git a/preload.php b/preload.php new file mode 100644 index 000000000000..7e1a04956fa9 --- /dev/null +++ b/preload.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +/* + *--------------------------------------------------------------- + * Sample file for Preloading + *--------------------------------------------------------------- + * See https://www.php.net/manual/en/opcache.preloading.php + * + * How to Use: + * 1. Set Preload::$paths. + * 2. Set opcache.preload in php.ini. + * php.ini: + * opcache.preload=/path/to/preload.php + */ + +// Load the paths config file +require __DIR__ . '/app/Config/Paths.php'; + +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +/** + * See https://www.php.net/manual/en/function.str-contains.php#126277 + */ +if (! function_exists('str_contains')) { + /** + * Polyfill of str_contains() + */ + function str_contains(string $haystack, string $needle): bool + { + return empty($needle) || strpos($haystack, $needle) !== false; + } +} + +class Preload +{ + /** + * @var array Paths to preload. + */ + private array $paths = [ + [ + 'include' => // __DIR__ . '/vendor/codeigniter4/framework/system', + __DIR__ . '/system', + 'exclude' => [ + // Not needed if you don't use them. + '/system/Database/OCI8/', + '/system/Database/Postgre/', + '/system/Database/SQLSRV/', + // Not needed. + '/system/Database/Seeder.php', + '/system/Test/', + '/system/Language/', + '/system/CLI/', + '/system/Commands/', + '/system/Publisher/', + '/system/ComposerScripts.php', + '/Views/', + // Errors occur. + '/system/Config/Routes.php', + '/system/ThirdParty/', + ], + ], + ]; + + public function __construct() + { + $this->loadAutoloader(); + } + + private function loadAutoloader() + { + $paths = new Config\Paths(); + require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; + } + + /** + * Load PHP files. + */ + public function load() + { + foreach ($this->paths as $path) { + $directory = new RecursiveDirectoryIterator($path['include']); + $fullTree = new RecursiveIteratorIterator($directory); + $phpFiles = new RegexIterator( + $fullTree, + '/.+((? $file) { + foreach ($path['exclude'] as $exclude) { + if (str_contains($file[0], $exclude)) { + continue 2; + } + } + + require_once $file[0]; + echo 'Loaded: ' . $file[0] . "\n"; + } + } + } +} + +(new Preload())->load(); diff --git a/public/index.php b/public/index.php index 77373025f96d..51f4be81e731 100644 --- a/public/index.php +++ b/public/index.php @@ -3,6 +3,9 @@ // Path to the front controller (this file) define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR); +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + /* *--------------------------------------------------------------- * BOOTSTRAP THE APPLICATION @@ -12,20 +15,34 @@ * and fires up an environment-specific bootstrapping. */ -// Ensure the current directory is pointing to the front controller's directory -chdir(__DIR__); - // Load our paths config file // This is the line that might need to be changed, depending on your folder structure. -$pathsConfig = FCPATH . '../app/Config/Paths.php'; -// ^^^ Change this if you move your application folder -require realpath($pathsConfig) ?: $pathsConfig; +require FCPATH . '../app/Config/Paths.php'; +// ^^^ Change this line if you move your application folder $paths = new Config\Paths(); // Location of the framework bootstrap file. -$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; -$app = require realpath($bootstrap) ?: $bootstrap; +require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; + +// Load environment settings from .env files into $_SERVER and $_ENV +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +/* + * --------------------------------------------------------------- + * GRAB OUR CODEIGNITER INSTANCE + * --------------------------------------------------------------- + * + * The CodeIgniter class contains the core functionality to make + * the application run, and does all of the dirty work to get + * the pieces all working together. + */ + +$app = Config\Services::codeigniter(); +$app->initialize(); +$context = is_cli() ? 'php-cli' : 'web'; +$app->setContext($context); /* *--------------------------------------------------------------- @@ -34,4 +51,5 @@ * Now that everything is setup, it's time to actually fire * up the engines and make this app do its thang. */ + $app->run(); diff --git a/rector.php b/rector.php index 21f63c0eca1b..e83bfcc18c63 100644 --- a/rector.php +++ b/rector.php @@ -17,19 +17,17 @@ use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector; use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector; use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector; +use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\If_\CombineIfRector; use Rector\CodeQuality\Rector\If_\ShortenElseIfRector; use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector; use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector; -use Rector\CodeQuality\Rector\Return_\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; -use Rector\Core\Configuration\Option; -use Rector\Core\ValueObject\PhpVersion; +use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; -use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; use Rector\DeadCode\Rector\MethodCall\RemoveEmptyMethodCallRector; use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; @@ -43,48 +41,46 @@ use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector; use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; use Rector\PSR4\Rector\FileWithoutNamespace\NormalizeNamespaceByPSR4ComposerAutoloadRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; -use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Utils\Rector\PassStrictParameterToFunctionParameterRector; use Utils\Rector\RemoveErrorSuppressInTryCatchStmtsRector; use Utils\Rector\RemoveVarTagFromClassConstantRector; use Utils\Rector\UnderscoreToCamelCaseVariableNameRector; -return static function (ContainerConfigurator $containerConfigurator): void { - $containerConfigurator->import(SetList::DEAD_CODE); - $containerConfigurator->import(LevelSetList::UP_TO_PHP_73); - $containerConfigurator->import(PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD); - $containerConfigurator->import(PHPUnitSetList::PHPUNIT_80); +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->sets([ + SetList::DEAD_CODE, + LevelSetList::UP_TO_PHP_74, + PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, + PHPUnitSetList::PHPUNIT_80, + PHPUnitSetList::REMOVE_MOCKS, + ]); - $parameters = $containerConfigurator->parameters(); + $rectorConfig->parallel(); - $parameters->set(Option::PARALLEL, true); // paths to refactor; solid alternative to CLI arguments - $parameters->set(Option::PATHS, [__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils/Rector']); + $rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils/Rector']); // do you need to include constants, class aliases or custom autoloader? files listed will be executed - $parameters->set(Option::BOOTSTRAP_FILES, [ + $rectorConfig->bootstrapFiles([ __DIR__ . '/system/Test/bootstrap.php', ]); // is there a file you need to skip? - $parameters->set(Option::SKIP, [ + $rectorConfig->skip([ __DIR__ . '/app/Views', __DIR__ . '/system/Debug/Toolbar/Views/toolbar.tpl.php', - __DIR__ . '/system/Debug/Kint/RichRenderer.php', __DIR__ . '/system/ThirdParty', __DIR__ . '/tests/system/Config/fixtures', __DIR__ . '/tests/_support', JsonThrowOnErrorRector::class, StringifyStrNeedlesRector::class, - // requires php 8 - RemoveUnusedPromotedPropertyRector::class, - - // private method called via getPrivateMethodInvoker RemoveUnusedPrivateMethodRector::class => [ + // private method called via getPrivateMethodInvoker __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', ], @@ -103,9 +99,18 @@ __DIR__ . '/system/Session/Handlers', ], - // may cause load view files directly when detecting class that - // make warning - StringClassNameToClassConstantRector::class, + StringClassNameToClassConstantRector::class => [ + // may cause load view files directly when detecting namespaced string + // due to internal PHPStan issue + __DIR__ . '/app/Config/Pager.php', + __DIR__ . '/app/Config/Validation.php', + __DIR__ . '/tests/system/Validation/StrictRules/ValidationTest.php', + __DIR__ . '/tests/system/Validation/ValidationTest.php', + + // expected Qualified name + __DIR__ . '/tests/system/Autoloader/FileLocatorTest.php', + __DIR__ . '/tests/system/Router/RouteCollectionTest.php', + ], // sometime too detail CountOnNullRector::class, @@ -118,34 +123,41 @@ ]); // auto import fully qualified class names - $parameters->set(Option::AUTO_IMPORT_NAMES, true); - $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_73); - - $services = $containerConfigurator->services(); - $services->set(UnderscoreToCamelCaseVariableNameRector::class); - $services->set(SimplifyUselessVariableRector::class); - $services->set(RemoveAlwaysElseRector::class); - $services->set(PassStrictParameterToFunctionParameterRector::class); - $services->set(CountArrayToEmptyArrayComparisonRector::class); - $services->set(ForToForeachRector::class); - $services->set(ChangeNestedForeachIfsToEarlyContinueRector::class); - $services->set(ChangeIfElseValueAssignToEarlyReturnRector::class); - $services->set(SimplifyStrposLowerRector::class); - $services->set(CombineIfRector::class); - $services->set(SimplifyIfReturnBoolRector::class); - $services->set(InlineIfToExplicitIfRector::class); - $services->set(PreparedValueToEarlyReturnRector::class); - $services->set(ShortenElseIfRector::class); - $services->set(SimplifyIfElseToTernaryRector::class); - $services->set(UnusedForeachValueToArrayKeysRector::class); - $services->set(ChangeArrayPushToArrayAssignRector::class); - $services->set(UnnecessaryTernaryExpressionRector::class); - $services->set(RemoveErrorSuppressInTryCatchStmtsRector::class); - $services->set(RemoveVarTagFromClassConstantRector::class); - $services->set(AddPregQuoteDelimiterRector::class); - $services->set(SimplifyRegexPatternRector::class); - $services->set(FuncGetArgsToVariadicParamRector::class); - $services->set(MakeInheritedMethodVisibilitySameAsParentRector::class); - $services->set(SimplifyEmptyArrayCheckRector::class); - $services->set(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); + $rectorConfig->importNames(); + + $rectorConfig->rule(UnderscoreToCamelCaseVariableNameRector::class); + $rectorConfig->rule(SimplifyUselessVariableRector::class); + $rectorConfig->rule(RemoveAlwaysElseRector::class); + $rectorConfig->rule(PassStrictParameterToFunctionParameterRector::class); + $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); + $rectorConfig->rule(ForToForeachRector::class); + $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); + $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); + $rectorConfig->rule(SimplifyStrposLowerRector::class); + $rectorConfig->rule(CombineIfRector::class); + $rectorConfig->rule(SimplifyIfReturnBoolRector::class); + $rectorConfig->rule(InlineIfToExplicitIfRector::class); + $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); + $rectorConfig->rule(ShortenElseIfRector::class); + $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); + $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); + $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); + $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); + $rectorConfig->rule(RemoveErrorSuppressInTryCatchStmtsRector::class); + $rectorConfig->rule(RemoveVarTagFromClassConstantRector::class); + $rectorConfig->rule(AddPregQuoteDelimiterRector::class); + $rectorConfig->rule(SimplifyRegexPatternRector::class); + $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); + $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); + $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); + $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); + $rectorConfig->ruleWithConfiguration(StringClassNameToClassConstantRector::class, [ + 'Error', + 'Exception', + 'InvalidArgumentException', + 'Closure', + 'stdClass', + 'SQLite3', + ]); + $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); }; diff --git a/spark b/spark index 9a5a5db90284..225422aace74 100755 --- a/spark +++ b/spark @@ -1,6 +1,15 @@ #!/usr/bin/env php + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + /* * -------------------------------------------------------------------- * CodeIgniter command-line tools @@ -12,8 +21,28 @@ * this class mainly acts as a passthru to the framework itself. */ +// Refuse to run when called from php-cgi +if (strpos(PHP_SAPI, 'cgi') === 0) { + exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); +} + +// We want errors to be shown when using it from the CLI. +error_reporting(-1); +ini_set('display_errors', '1'); + +/** + * @var bool + * + * @deprecated No longer in use. `CodeIgniter` has `$context` property. + */ define('SPARKED', true); +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + /* *--------------------------------------------------------------- * BOOTSTRAP THE APPLICATION @@ -23,34 +52,28 @@ define('SPARKED', true); * and fires up an environment-specific bootstrapping. */ -// Refuse to run when called from php-cgi -if (strpos(PHP_SAPI, 'cgi') === 0) { - exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); -} - -// Path to the front controller -define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); - // Load our paths config file -$pathsConfig = 'app/Config/Paths.php'; +// This is the line that might need to be changed, depending on your folder structure. +require FCPATH . '../app/Config/Paths.php'; // ^^^ Change this line if you move your application folder -require realpath($pathsConfig) ?: $pathsConfig; $paths = new Config\Paths(); -// Ensure the current directory is pointing to the front controller's directory -chdir(FCPATH); +// Location of the framework bootstrap file. +require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; + +// Load environment settings from .env files into $_SERVER and $_ENV +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); -$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; -$app = require realpath($bootstrap) ?: $bootstrap; +// Grab our CodeIgniter +$app = Config\Services::codeigniter(); +$app->initialize(); +$app->setContext('spark'); // Grab our Console $console = new CodeIgniter\CLI\Console($app); -// We want errors to be shown when using it from the CLI. -error_reporting(-1); -ini_set('display_errors', '1'); - // Show basic information before we do anything else. if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore diff --git a/stale.yml b/stale.yml deleted file mode 100644 index 897cc082a10e..000000000000 --- a/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security -# Label to use when marking an issue as stale -staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be automatically closed in a week if no further activity occurs. - Thank you for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 9ee722b45235..f20affaffaf5 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -75,7 +75,7 @@ trait ResponseTrait /** * Current Formatter instance. This is usually set by ResponseTrait::format * - * @var FormatterInterface + * @var FormatterInterface|null */ protected $formatter; diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 492fb09930f0..311428fe4165 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -82,6 +82,10 @@ class Autoloader */ public function initialize(Autoload $config, Modules $modules) { + $this->prefixes = []; + $this->classmap = []; + $this->files = []; + // We have to have one or the other, though we don't enforce the need // to have both present in order to work. if (empty($config->psr4) && empty($config->classmap)) { @@ -100,12 +104,28 @@ public function initialize(Autoload $config, Modules $modules) $this->files = $config->files; } + if (is_file(COMPOSER_PATH)) { + $this->loadComposerInfo($modules); + } + + return $this; + } + + private function loadComposerInfo(Modules $modules): void + { + /** + * @var ClassLoader $composer + */ + $composer = include COMPOSER_PATH; + + $this->loadComposerClassmap($composer); + // Should we load through Composer's namespaces, also? if ($modules->discoverInComposer) { - $this->discoverComposerNamespaces(); + $this->loadComposerNamespaces($composer); } - return $this; + unset($composer); } /** @@ -292,8 +312,36 @@ public function sanitizeFilename(string $filename): string return trim($filename, '.-_'); } + private function loadComposerNamespaces(ClassLoader $composer): void + { + $paths = $composer->getPrefixesPsr4(); + + // Get rid of CodeIgniter so we don't have duplicates + if (isset($paths['CodeIgniter\\'])) { + unset($paths['CodeIgniter\\']); + } + + $newPaths = []; + + foreach ($paths as $key => $value) { + // Composer stores namespaces with trailing slash. We don't. + $newPaths[rtrim($key, '\\ ')] = $value; + } + + $this->addNamespace($newPaths); + } + + private function loadComposerClassmap(ClassLoader $composer): void + { + $classes = $composer->getClassMap(); + + $this->classmap = array_merge($this->classmap, $classes); + } + /** * Locates autoload information from Composer, if available. + * + * @deprecated No longer used. */ protected function discoverComposerNamespaces() { diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index b8bfdf6df217..14d7982c49b4 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -108,7 +108,7 @@ public function locateFile(string $file, ?string $folder = null, string $ext = ' } /** - * Examines a file and returns the fully qualified domain name. + * Examines a file and returns the fully qualified class name. */ public function getClassname(string $file): string { @@ -186,7 +186,7 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = } if (! $prioritizeApp && ! empty($appPaths)) { - $foundPaths = array_merge($foundPaths, $appPaths); + $foundPaths = [...$foundPaths, ...$appPaths]; } // Remove any duplicates @@ -212,7 +212,7 @@ protected function ensureExt(string $path, string $ext): string /** * Return the namespace mappings we know about. * - * @return array|string + * @return array> */ protected function getNamespaces() { @@ -289,6 +289,8 @@ public function findQualifiedNameFromPath(string $path) /** * Scans the defined namespaces, returning a list of all files * that are contained within the subpath specified by $path. + * + * @return string[] List of file paths */ public function listFiles(string $path): array { @@ -307,7 +309,7 @@ public function listFiles(string $path): array continue; } - $tempFiles = get_filenames($fullPath, true); + $tempFiles = get_filenames($fullPath, true, false, false); if (! empty($tempFiles)) { $files = array_merge($files, $tempFiles); @@ -319,7 +321,9 @@ public function listFiles(string $path): array /** * Scans the provided namespace, returning a list of all files - * that are contained within the subpath specified by $path. + * that are contained within the sub path specified by $path. + * + * @return string[] List of file paths */ public function listNamespaceFiles(string $prefix, string $path): array { @@ -339,7 +343,7 @@ public function listNamespaceFiles(string $prefix, string $path): array continue; } - $tempFiles = get_filenames($fullPath, true); + $tempFiles = get_filenames($fullPath, true, false, false); if (! empty($tempFiles)) { $files = array_merge($files, $tempFiles); diff --git a/system/BaseModel.php b/system/BaseModel.php index 971303664a8e..39bd9ce2cb4c 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -293,9 +293,9 @@ public function __construct(?ValidationInterface $validation = null) $this->tempAllowCallbacks = $this->allowCallbacks; /** - * @var Validation $validation + * @var Validation|null $validation */ - $validation = $validation ?? Services::validation(null, false); + $validation ??= Services::validation(null, false); $this->validation = $validation; $this->initialize(); @@ -1076,7 +1076,7 @@ public function paginate(?int $perPage = null, string $group = 'default', ?int $ $pager = Services::pager(null, null, false); if ($segment) { - $pager->setSegment($segment); + $pager->setSegment($segment, $group); } $page = $page >= 1 ? $page : $pager->getCurrentPage($group); @@ -1344,7 +1344,9 @@ public function validate($data): bool return true; } - return $this->validation->setRules($rules, $this->validationMessages)->run($data, null, $this->DBGroup); + $this->validation->reset()->setRules($rules, $this->validationMessages); + + return $this->validation->run($data, null, $this->DBGroup); } /** @@ -1489,9 +1491,9 @@ public function asObject(string $class = 'object') } /** - * Takes a class an returns an array of it's public and protected + * Takes a class and returns an array of it's public and protected * properties as an array suitable for use in creates and updates. - * This method use objectToRawArray internally and does conversion + * This method uses objectToRawArray() internally and does conversion * to string on all Time instances * * @param object|string $data Data @@ -1521,7 +1523,7 @@ protected function objectToArray($data, bool $onlyChanged = true, bool $recursiv } /** - * Takes a class an returns an array of it's public and protected + * Takes a class and returns an array of its public and protected * properties as an array with raw values. * * @param object|string $data Data diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index 8f843c4f5c73..f5d0d370e0ea 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -96,7 +96,7 @@ public function __construct(LoggerInterface $logger, Commands $commands) /** * Actually execute a command. * - * @param array $params + * @param array $params */ abstract public function run(array $params); diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 347a894f7578..d88374e80944 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -469,7 +469,7 @@ public static function clearScreen() */ public static function color(string $text, string $foreground, ?string $background = null, ?string $format = null): string { - if (! static::$isColored) { + if (! static::$isColored || $text === '') { return $text; } @@ -481,6 +481,48 @@ public static function color(string $text, string $foreground, ?string $backgrou throw CLIException::forInvalidColor('background', $background); } + $newText = ''; + + // Detect if color method was already in use with this text + if (strpos($text, "\033[0m") !== false) { + $pattern = '/\\033\\[0;.+?\\033\\[0m/u'; + + preg_match_all($pattern, $text, $matches); + $coloredStrings = $matches[0]; + + // No colored string found. Invalid strings with no `\033[0;??`. + if ($coloredStrings === []) { + return $newText . self::getColoredText($text, $foreground, $background, $format); + } + + $nonColoredText = preg_replace( + $pattern, + '<<__colored_string__>>', + $text + ); + $nonColoredChunks = preg_split( + '/<<__colored_string__>>/u', + $nonColoredText + ); + + foreach ($nonColoredChunks as $i => $chunk) { + if ($chunk !== '') { + $newText .= self::getColoredText($chunk, $foreground, $background, $format); + } + + if (isset($coloredStrings[$i])) { + $newText .= $coloredStrings[$i]; + } + } + } else { + $newText .= self::getColoredText($text, $foreground, $background, $format); + } + + return $newText; + } + + private static function getColoredText(string $text, string $foreground, ?string $background, ?string $format): string + { $string = "\033[" . static::$foreground_colors[$foreground] . 'm'; if ($background !== null) { @@ -491,30 +533,6 @@ public static function color(string $text, string $foreground, ?string $backgrou $string .= "\033[4m"; } - // Detect if color method was already in use with this text - if (strpos($text, "\033[0m") !== false) { - // Split the text into parts so that we can see - // if any part missing the color definition - $chunks = mb_split('\\033\\[0m', $text); - // Reset text - $text = ''; - - foreach ($chunks as $chunk) { - if ($chunk === '') { - continue; - } - - // If chunk doesn't have colors defined we need to add them - if (strpos($chunk, "\033[") === false) { - $chunk = static::color($chunk, $foreground, $background, $format); - // Add color reset before chunk and clear end of the string - $text .= rtrim("\033[0m" . $chunk, "\033[0m"); - } else { - $text .= $chunk; - } - } - } - return $string . $text . "\033[0m"; } diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php index a6985d0db931..ef4ed057b606 100644 --- a/system/CLI/CommandRunner.php +++ b/system/CLI/CommandRunner.php @@ -40,19 +40,14 @@ public function __construct() * so we have the chance to look for a Command first. * * @param string $method - * @param array ...$params + * @param array $params * * @throws ReflectionException * * @return mixed */ - public function _remap($method, ...$params) + public function _remap($method, $params) { - // The first param is usually empty, so scrap it. - if (empty($params[0])) { - array_shift($params); - } - return $this->index($params); } diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 3f84f33d918f..2714db30bcf9 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -96,9 +96,9 @@ public function discoverCommands() // Loop over each file checking to see if a command with that // alias exists in the class. foreach ($files as $file) { - $className = $locator->findQualifiedNameFromPath($file); + $className = $locator->getClassname($file); - if (empty($className) || ! class_exists($className)) { + if ($className === '' || ! class_exists($className)) { continue; } diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index 5d1f37cdfc24..87ab4e333dca 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -222,6 +222,6 @@ public function getMetaData(string $key) */ public function isSupported(): bool { - return class_exists('Predis\Client'); + return class_exists(Client::class); } } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 5704e57be92e..82b08e318879 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -12,7 +12,6 @@ namespace CodeIgniter; use Closure; -use CodeIgniter\Debug\Kint\RichRenderer; use CodeIgniter\Debug\Timer; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; @@ -30,10 +29,13 @@ use CodeIgniter\Router\Router; use Config\App; use Config\Cache; +use Config\Kint as KintConfig; use Config\Services; use Exception; use Kint; use Kint\Renderer\CliRenderer; +use Kint\Renderer\RichRenderer; +use LogicException; /** * This class is the core of the framework, and will analyse the @@ -45,9 +47,9 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.1.9'; + public const CI_VERSION = '4.2.0'; - private const MIN_PHP_VERSION = '7.3'; + private const MIN_PHP_VERSION = '7.4'; /** * App startup time. @@ -80,7 +82,7 @@ class CodeIgniter /** * Current request. * - * @var CLIRequest|IncomingRequest|Request + * @var CLIRequest|IncomingRequest|Request|null */ protected $request; @@ -141,6 +143,16 @@ class CodeIgniter */ protected $useSafeOutput = false; + /** + * Context + * web: Invoked by HTTP request + * php-cli: Invoked by CLI via `php public/index.php` + * spark: Invoked by CLI via the `spark` command + * + * @phpstan-var 'php-cli'|'spark'|'web' + */ + protected ?string $context = null; + /** * Constructor. */ @@ -236,7 +248,7 @@ protected function initializeKint() $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php'; - if (file_exists($file)) { + if (is_file($file)) { require_once $file; } }); @@ -247,7 +259,7 @@ protected function initializeKint() /** * Config\Kint */ - $config = config('Config\Kint'); + $config = config(KintConfig::class); Kint::$depth_limit = $config->maxDepth; Kint::$display_called_from = $config->displayCalledFrom; @@ -257,7 +269,11 @@ protected function initializeKint() Kint::$plugins = $config->plugins; } - Kint::$renderers[Kint::MODE_RICH] = RichRenderer::class; + $csp = Services::csp(); + if ($csp->enabled()) { + RichRenderer::$js_nonce = $csp->getScriptNonce(); + RichRenderer::$css_nonce = $csp->getStyleNonce(); + } RichRenderer::$theme = $config->richTheme; RichRenderer::$folder = $config->richFolder; @@ -286,10 +302,14 @@ protected function initializeKint() * @throws Exception * @throws RedirectException * - * @return bool|mixed|RequestInterface|ResponseInterface + * @return bool|mixed|RequestInterface|ResponseInterface|void */ public function run(?RouteCollectionInterface $routes = null, bool $returnResponse = false) { + if ($this->context === null) { + throw new LogicException('Context must be set before run() is called. If you are upgrading from 4.1.x, you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.'); + } + $this->startBenchmark(); $this->getRequestObject(); @@ -299,7 +319,7 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->spoofRequestMethod(); - if ($this->request instanceof IncomingRequest && $this->request->getMethod() === 'cli') { + if ($this->request instanceof IncomingRequest && strtolower($this->request->getMethod()) === 'cli') { $this->response->setStatusCode(405)->setBody('Method Not Allowed'); return $this->sendResponse(); @@ -322,6 +342,11 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon return; } + // spark command has nothing to do with HTTP redirect and 404 + if ($this->isSparked()) { + return $this->handleRequest($routes, $cacheConfig, $returnResponse); + } + try { return $this->handleRequest($routes, $cacheConfig, $returnResponse); } catch (RedirectException $e) { @@ -355,6 +380,30 @@ public function useSafeOutput(bool $safe = true) return $this; } + /** + * Invoked via spark command? + */ + private function isSparked(): bool + { + return $this->context === 'spark'; + } + + /** + * Invoked via php-cli command? + */ + private function isPhpCli(): bool + { + return $this->context === 'php-cli'; + } + + /** + * Web access? + */ + private function isWeb(): bool + { + return $this->context === 'web'; + } + /** * Handles the main request logic and fires the controller. * @@ -387,7 +436,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache } // Never run filters when running through Spark cli - if (! defined('SPARKED')) { + if (! $this->isSparked()) { // Run "before" filters $this->benchmark->start('before_filters'); $possibleResponse = $filters->run($uri, 'before'); @@ -428,7 +477,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache $this->gatherOutput($cacheConfig, $returned); // Never run filters when running through Spark cli - if (! defined('SPARKED')) { + if (! $this->isSparked()) { $filters->setResponse($this->response); // Run "after" filters @@ -546,10 +595,8 @@ protected function getRequestObject() return; } - if (is_cli() && ENVIRONMENT !== 'testing') { - // @codeCoverageIgnoreStart + if ($this->isSparked() || $this->isPhpCli()) { $this->request = Services::clirequest($this->config); - // @codeCoverageIgnoreEnd } else { $this->request = Services::request($this->config); // guess at protocol if needed @@ -565,7 +612,7 @@ protected function getResponseObject() { $this->response = Services::response($this->config); - if (! is_cli() || ENVIRONMENT === 'testing') { + if ($this->isWeb()) { $this->response->setProtocolVersion($this->request->getProtocolVersion()); } @@ -583,7 +630,7 @@ protected function getResponseObject() * @param int $duration How long the Strict Transport Security * should be enforced for this URL. */ - protected function forceSecureAccess($duration = 31536000) + protected function forceSecureAccess($duration = 31_536_000) { if ($this->config->forceGlobalSecureRequests !== true) { return; @@ -705,7 +752,7 @@ public function displayPerformanceMetrics(string $output): string * * @throws RedirectException * - * @return string|string[]|null + * @return string|string[]|null Route filters, that is, the filters specified in the routes file */ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) { @@ -746,6 +793,8 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) /** * Determines the path to use for us to try to route to, based * on user input (setPath), or the CLI/IncomingRequest path. + * + * @return string */ protected function determinePath() { @@ -775,6 +824,8 @@ public function setPath(string $path) * Now that everything has been setup, this method attempts to run the * controller method and make the script go. If it's not able to, will * show the appropriate Page Not Found error. + * + * @return ResponseInterface|string|void */ protected function startController() { @@ -802,7 +853,7 @@ protected function startController() /** * Instantiates the controller class. * - * @return mixed + * @return Controller */ protected function createController() { @@ -817,19 +868,34 @@ protected function createController() /** * Runs the controller, allowing for _remap methods to function. * + * CI4 supports three types of requests: + * 1. Web: URI segments become parameters, sent to Controllers via Routes, + * output controlled by Headers to browser + * 2. Spark: accessed by CLI via the spark command, arguments are Command arguments, + * sent to Commands by CommandRunner, output controlled by CLI class + * 3. PHP CLI: accessed by CLI via php public/index.php, arguments become URI segments, + * sent to Controllers via Routes, output varies + * * @param mixed $class * - * @return mixed + * @return false|ResponseInterface|string|void */ protected function runController($class) { - // If this is a console request then use the input segments as parameters - $params = defined('SPARKED') ? $this->request->getSegments() : $this->router->params(); + if ($this->isSparked()) { + // This is a Spark request + /** @var CLIRequest $request */ + $request = $this->request; + $params = $request->getArgs(); - if (method_exists($class, '_remap')) { - $output = $class->_remap($this->method, ...$params); + $output = $class->_remap($this->method, $params); } else { - $output = $class->{$this->method}(...$params); + // This is a Web request or PHP CLI request + $params = $this->router->params(); + + $output = method_exists($class, '_remap') + ? $class->_remap($this->method, ...$params) + : $class->{$this->method}(...$params); } $this->benchmark->stop('controller'); @@ -845,6 +911,8 @@ protected function display404errors(PageNotFoundException $e) { // Is there a 404 Override available? if ($override = $this->router->get404Override()) { + $returned = null; + if ($override instanceof Closure) { echo $override($e->getMessage()); } elseif (is_array($override)) { @@ -855,13 +923,13 @@ protected function display404errors(PageNotFoundException $e) $this->method = $override[1]; $controller = $this->createController(); - $this->runController($controller); + $returned = $this->runController($controller); } unset($override); $cacheConfig = new Cache(); - $this->gatherOutput($cacheConfig); + $this->gatherOutput($cacheConfig, $returned); $this->sendResponse(); return; @@ -882,14 +950,16 @@ protected function display404errors(PageNotFoundException $e) ob_end_flush(); // @codeCoverageIgnore } - throw PageNotFoundException::forPageNotFound(ENVIRONMENT !== 'production' || is_cli() ? $e->getMessage() : ''); + throw PageNotFoundException::forPageNotFound( + (ENVIRONMENT !== 'production' || ! $this->isWeb()) ? $e->getMessage() : '' + ); } /** * Gathers the script output from the buffer, replaces some execution * time tag in the output and displays the debug toolbar, if required. * - * @param mixed|null $returned + * @param ResponseInterface|string|null $returned */ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) { @@ -901,6 +971,11 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) } if ($returned instanceof DownloadResponse) { + // Turn off output buffering completely, even if php.ini output_buffering is not off + while (ob_get_level() > 0) { + ob_end_clean(); + } + $this->response = $returned; return; @@ -943,8 +1018,8 @@ protected function gatherOutput(?Cache $cacheConfig = null, $returned = null) public function storePreviousURL($uri) { // Ignore CLI requests - if (is_cli() && ENVIRONMENT !== 'testing') { - return; // @codeCoverageIgnore + if (! $this->isWeb()) { + return; } // Ignore AJAX requests if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX()) { @@ -956,6 +1031,11 @@ public function storePreviousURL($uri) return; } + // Ignore non-HTML responses + if (strpos($this->response->getHeaderLine('Content-Type'), 'text/html') === false) { + return; + } + // This is mainly needed during testing... if (is_string($uri)) { $uri = new URI($uri); @@ -973,7 +1053,7 @@ public function storePreviousURL($uri) public function spoofRequestMethod() { // Only works with POSTED forms - if ($this->request->getMethod() !== 'post') { + if (strtolower($this->request->getMethod()) !== 'post') { return; } @@ -1011,4 +1091,18 @@ protected function callExit($code) { exit($code); // @codeCoverageIgnore } + + /** + * Sets the app context. + * + * @phpstan-param 'php-cli'|'spark'|'web' $context + * + * @return $this + */ + public function setContext(string $context) + { + $this->context = $context; + + return $this; + } } diff --git a/system/Commands/Database/Migrate.php b/system/Commands/Database/Migrate.php index 91a699b346d6..bcf71cade337 100644 --- a/system/Commands/Database/Migrate.php +++ b/system/Commands/Database/Migrate.php @@ -91,7 +91,7 @@ public function run(array $params) CLI::write($message); } - CLI::write('Done migrations.', 'green'); + CLI::write(lang('Migrations.migrated'), 'green'); // @codeCoverageIgnoreStart } catch (Throwable $e) { diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 91b52950b2b4..8b0885a659b1 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -79,8 +79,8 @@ class MigrateStatus extends BaseCommand */ public function run(array $params) { - $runner = Services::migrations(); - $group = $params['g'] ?? CLI::getOption('g'); + $runner = Services::migrations(); + $paramGroup = $params['g'] ?? CLI::getOption('g'); // Get all namespaces $namespaces = Services::autoloader()->getNamespace(); @@ -108,7 +108,8 @@ public function run(array $params) continue; } - $history = $runner->getHistory((string) $group); + $runner->setNamespace($namespace); + $history = $runner->getHistory((string) $paramGroup); ksort($migrations); foreach ($migrations as $uid => $migration) { diff --git a/system/Commands/Database/ShowTableInfo.php b/system/Commands/Database/ShowTableInfo.php new file mode 100644 index 000000000000..7e3d6a4807ff --- /dev/null +++ b/system/Commands/Database/ShowTableInfo.php @@ -0,0 +1,284 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Database; + +use CodeIgniter\CLI\BaseCommand; +use CodeIgniter\CLI\CLI; +use CodeIgniter\Database\BaseConnection; +use Config\Database; + +/** + * Get table data if it exists in the database. + */ +class ShowTableInfo extends BaseCommand +{ + /** + * The group the command is lumped under + * when listing commands. + * + * @var string + */ + protected $group = 'Database'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'db:table'; + + /** + * the Command's short description + * + * @var string + */ + protected $description = 'Retrieves information on the selected table.'; + + /** + * the Command's usage + * + * @var string + */ + protected $usage = <<<'EOL' + db:table [] [options] + + Examples: + db:table --show + db:table --metadata + db:table my_table --metadata + db:table my_table + db:table my_table --limit-rows 5 --limit-field-value 10 --desc + EOL; + + /** + * The Command's arguments + * + * @var array + */ + protected $arguments = [ + 'table_name' => 'The table name to show info', + ]; + + /** + * The Command's options + * + * @var array + */ + protected $options = [ + '--show' => 'Lists the names of all database tables.', + '--metadata' => 'Retrieves list containing field information.', + '--desc' => 'Sorts the table rows in DESC order.', + '--limit-rows' => 'Limits the number of rows. Default: 10.', + '--limit-field-value' => 'Limits the length of field values. Default: 15.', + ]; + + /** + * @phpstan-var list> Table Data. + */ + private array $tbody; + + private BaseConnection $db; + + /** + * @var bool Sort the table rows in DESC order or not. + */ + private bool $sortDesc = false; + + private string $DBPrefix; + + public function run(array $params) + { + $this->db = Database::connect(); + $this->DBPrefix = $this->db->getPrefix(); + + $tables = $this->db->listTables(); + + if (array_key_exists('desc', $params)) { + $this->sortDesc = true; + } + + if ($tables === []) { + CLI::error('Database has no tables!', 'light_gray', 'red'); + CLI::newLine(); + + return; + } + + if (array_key_exists('show', $params)) { + $this->showAllTables($tables); + + return; + } + + $tableName = $params[0] ?? null; + $limitRows = (int) ($params['limit-rows'] ?? 10); + $limitFieldValue = (int) ($params['limit-field-value'] ?? 15); + + if (! in_array($tableName, $tables, true)) { + $tableNameNo = CLI::promptByKey( + ['Here is the list of your database tables:', 'Which table do you want to see?'], + $tables, + 'required' + ); + + $tableName = $tables[$tableNameNo]; + } + + if (array_key_exists('metadata', $params)) { + $this->showFieldMetaData($tableName); + + return; + } + + $this->showDataOfTable($tableName, $limitRows, $limitFieldValue); + } + + private function removeDBPrefix(): void + { + $this->db->setPrefix(''); + } + + private function restoreDBPrefix(): void + { + $this->db->setPrefix($this->DBPrefix); + } + + private function showDataOfTable(string $tableName, int $limitRows, int $limitFieldValue) + { + CLI::newLine(); + CLI::write("Data of Table \"{$tableName}\":", 'black', 'yellow'); + CLI::newLine(); + + $this->removeDBPrefix(); + $thead = $this->db->getFieldNames($tableName); + $this->restoreDBPrefix(); + + // If there is a field named `id`, sort by it. + $sortField = null; + if (in_array('id', $thead, true)) { + $sortField = 'id'; + } + + $this->tbody = $this->makeTableRows($tableName, $limitRows, $limitFieldValue, $sortField); + CLI::table($this->tbody, $thead); + } + + private function showAllTables(array $tables) + { + CLI::write('The following is a list of the names of all database tables:', 'black', 'yellow'); + CLI::newLine(); + + $thead = ['ID', 'Table Name', 'Num of Rows', 'Num of Fields']; + $this->tbody = $this->makeTbodyForShowAllTables($tables); + + CLI::table($this->tbody, $thead); + CLI::newLine(); + } + + private function makeTbodyForShowAllTables(array $tables): array + { + $this->removeDBPrefix(); + + foreach ($tables as $id => $tableName) { + $table = $this->db->protectIdentifiers($tableName); + $db = $this->db->query("SELECT * FROM {$table}"); + + $this->tbody[] = [ + $id + 1, + $tableName, + $db->getNumRows(), + $db->getFieldCount(), + ]; + } + + $this->restoreDBPrefix(); + + if ($this->sortDesc) { + krsort($this->tbody); + } + + return $this->tbody; + } + + private function makeTableRows( + string $tableName, + int $limitRows, + int $limitFieldValue, + ?string $sortField = null + ): array { + $this->tbody = []; + + $this->removeDBPrefix(); + $builder = $this->db->table($tableName); + $builder->limit($limitRows); + if ($sortField !== null) { + $builder->orderBy($sortField, $this->sortDesc ? 'DESC' : 'ASC'); + } + $rows = $builder->get()->getResultArray(); + $this->restoreDBPrefix(); + + foreach ($rows as $row) { + $row = array_map( + static fn ($item): string => mb_strlen((string) $item) > $limitFieldValue + ? mb_substr((string) $item, 0, $limitFieldValue) . '...' + : (string) $item, + $row + ); + $this->tbody[] = $row; + } + + if ($sortField === null && $this->sortDesc) { + krsort($this->tbody); + } + + return $this->tbody; + } + + private function showFieldMetaData(string $tableName): void + { + CLI::newLine(); + CLI::write("List of Metadata Information in Table \"{$tableName}\":", 'black', 'yellow'); + CLI::newLine(); + + $thead = ['Field Name', 'Type', 'Max Length', 'Nullable', 'Default', 'Primary Key']; + + $this->removeDBPrefix(); + $fields = $this->db->getFieldData($tableName); + $this->restoreDBPrefix(); + + foreach ($fields as $row) { + $this->tbody[] = [ + $row->name, + $row->type, + $row->max_length, + isset($row->nullable) ? $this->setYesOrNo($row->nullable) : 'n/a', + $row->default, + isset($row->primary_key) ? $this->setYesOrNo($row->primary_key) : 'n/a', + ]; + } + + if ($this->sortDesc) { + krsort($this->tbody); + } + + CLI::table($this->tbody, $thead); + } + + private function setYesOrNo(bool $fieldValue): string + { + if ($fieldValue) { + return CLI::color('Yes', 'green'); + } + + return CLI::color('No', 'red'); + } +} diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index 810f2dc92d39..b9d1794ff1fb 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -151,8 +151,8 @@ protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): $baseEnv = ROOTPATH . 'env'; $envFile = ROOTPATH . '.env'; - if (! file_exists($envFile)) { - if (! file_exists($baseEnv)) { + if (! is_file($envFile)) { + if (! is_file($baseEnv)) { CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow'); CLI::write('Here\'s your new key instead: ' . CLI::color($newKey, 'yellow')); CLI::newLine(); diff --git a/system/Commands/Generators/ControllerGenerator.php b/system/Commands/Generators/ControllerGenerator.php index 36a951cf0df0..f27c77ee0fe5 100644 --- a/system/Commands/Generators/ControllerGenerator.php +++ b/system/Commands/Generators/ControllerGenerator.php @@ -14,6 +14,9 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use CodeIgniter\CLI\GeneratorTrait; +use CodeIgniter\Controller; +use CodeIgniter\RESTful\ResourceController; +use CodeIgniter\RESTful\ResourcePresenter; /** * Generates a skeleton controller file. @@ -99,7 +102,7 @@ protected function prepare(string $class): string // Gets the appropriate parent class to extend. if ($bare || $rest) { if ($bare) { - $useStatement = 'CodeIgniter\Controller'; + $useStatement = Controller::class; $extends = 'Controller'; } elseif ($rest) { $rest = is_string($rest) ? $rest : 'controller'; @@ -112,10 +115,10 @@ protected function prepare(string $class): string } if ($rest === 'controller') { - $useStatement = 'CodeIgniter\RESTful\ResourceController'; + $useStatement = ResourceController::class; $extends = 'ResourceController'; } elseif ($rest === 'presenter') { - $useStatement = 'CodeIgniter\RESTful\ResourcePresenter'; + $useStatement = ResourcePresenter::class; $extends = 'ResourcePresenter'; } } diff --git a/system/Commands/Generators/MigrateCreate.php b/system/Commands/Generators/MigrateCreate.php index 4803dc96db25..0b4935342831 100644 --- a/system/Commands/Generators/MigrateCreate.php +++ b/system/Commands/Generators/MigrateCreate.php @@ -77,9 +77,9 @@ class MigrateCreate extends BaseCommand public function run(array $params) { // Resolve arguments before passing to make:migration - $params[0] = $params[0] ?? CLI::getSegment(2); + $params[0] ??= CLI::getSegment(2); - $params['namespace'] = $params['namespace'] ?? CLI::getOption('namespace') ?? APP_NAMESPACE; + $params['namespace'] ??= CLI::getOption('namespace') ?? APP_NAMESPACE; if (array_key_exists('force', $params) || CLI::getOption('force')) { $params['force'] = null; diff --git a/system/Commands/Help.php b/system/Commands/Help.php index 7562333a4f09..4dbc2df6d34d 100644 --- a/system/Commands/Help.php +++ b/system/Commands/Help.php @@ -71,8 +71,8 @@ class Help extends BaseCommand */ public function run(array $params) { - $command = array_shift($params); - $command = $command ?? 'help'; + $command = array_shift($params); + $command ??= 'help'; $commands = $this->commands->getCommands(); if (! $this->commands->verifyCommand($command, $commands)) { diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php index 5586de575dbd..b844e7a260f2 100644 --- a/system/Commands/Utilities/Environment.php +++ b/system/Commands/Utilities/Environment.php @@ -72,7 +72,7 @@ final class Environment extends BaseCommand * * @var array */ - private static $knownTypes = [ + private static array $knownTypes = [ 'production', 'development', ]; diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 210d1e11c5f7..c8fc46bf3876 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -11,14 +11,19 @@ namespace CodeIgniter\Commands\Utilities; +use Closure; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector; +use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved; +use CodeIgniter\Commands\Utilities\Routes\FilterCollector; +use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator; use Config\Services; /** - * Lists all of the user-defined routes. This will include any Routes files - * that can be discovered, but will NOT include any routes that are not defined - * in a routes file, but are instead discovered through auto-routing. + * Lists all the routes. This will include any Routes files + * that can be discovered, and will include routes that are not defined + * in routes files, but are instead discovered through auto-routing. */ class Routes extends BaseCommand { @@ -42,7 +47,7 @@ class Routes extends BaseCommand * * @var string */ - protected $description = 'Displays all of user-defined routes. Does NOT display auto-detected routes.'; + protected $description = 'Displays all routes.'; /** * the Command's usage @@ -84,27 +89,69 @@ public function run(array $params) 'cli', ]; - $tbody = []; + $tbody = []; + $uriGenerator = new SampleURIGenerator(); + $filterCollector = new FilterCollector(); foreach ($methods as $method) { $routes = $collection->getRoutes($method); foreach ($routes as $route => $handler) { - // filter for strings, as callbacks aren't displayable - if (is_string($handler)) { + if (is_string($handler) || $handler instanceof Closure) { + $sampleUri = $uriGenerator->get($route); + $filters = $filterCollector->get($method, $sampleUri); + $tbody[] = [ strtoupper($method), $route, - $handler, + is_string($handler) ? $handler : '(Closure)', + implode(' ', array_map('class_basename', $filters['before'])), + implode(' ', array_map('class_basename', $filters['after'])), ]; } } } + if ($collection->shouldAutoRoute()) { + $autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false; + + if ($autoRoutesImproved) { + $autoRouteCollector = new AutoRouteCollectorImproved( + $collection->getDefaultNamespace(), + $collection->getDefaultController(), + $collection->getDefaultMethod(), + $methods, + $collection->getRegisteredControllers('*') + ); + + $autoRoutes = $autoRouteCollector->get(); + } else { + $autoRouteCollector = new AutoRouteCollector( + $collection->getDefaultNamespace(), + $collection->getDefaultController(), + $collection->getDefaultMethod() + ); + + $autoRoutes = $autoRouteCollector->get(); + + foreach ($autoRoutes as &$routes) { + // There is no `auto` method, but it is intentional not to get route filters. + $filters = $filterCollector->get('auto', $uriGenerator->get($routes[1])); + + $routes[] = implode(' ', array_map('class_basename', $filters['before'])); + $routes[] = implode(' ', array_map('class_basename', $filters['after'])); + } + } + + $tbody = [...$tbody, ...$autoRoutes]; + } + $thead = [ 'Method', 'Route', 'Handler', + 'Before Filters', + 'After Filters', ]; CLI::table($tbody, $thead); diff --git a/system/Commands/Utilities/Routes/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouteCollector.php new file mode 100644 index 000000000000..30c8eecfee9d --- /dev/null +++ b/system/Commands/Utilities/Routes/AutoRouteCollector.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +/** + * Collects data for auto route listing. + */ +final class AutoRouteCollector +{ + /** + * @var string namespace to search + */ + private string $namespace; + + private string $defaultController; + private string $defaultMethod; + + /** + * @param string $namespace namespace to search + */ + public function __construct(string $namespace, string $defaultController, string $defaultMethod) + { + $this->namespace = $namespace; + $this->defaultController = $defaultController; + $this->defaultMethod = $defaultMethod; + } + + /** + * @return array> + * @phpstan-return list> + */ + public function get(): array + { + $finder = new ControllerFinder($this->namespace); + $reader = new ControllerMethodReader($this->namespace); + + $tbody = []; + + foreach ($finder->find() as $class) { + $output = $reader->read( + $class, + $this->defaultController, + $this->defaultMethod + ); + + foreach ($output as $item) { + $tbody[] = [ + 'auto', + $item['route'], + $item['handler'], + ]; + } + } + + return $tbody; + } +} diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php new file mode 100644 index 000000000000..0df2e6bff490 --- /dev/null +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved; + +use CodeIgniter\Commands\Utilities\Routes\ControllerFinder; +use CodeIgniter\Commands\Utilities\Routes\FilterCollector; + +/** + * Collects data for Auto Routing Improved. + */ +final class AutoRouteCollector +{ + /** + * @var string namespace to search + */ + private string $namespace; + + private string $defaultController; + private string $defaultMethod; + private array $httpMethods; + + /** + * List of controllers in Defined Routes that should not be accessed via Auto-Routing. + * + * @var class-string[] + */ + private array $protectedControllers; + + /** + * @param string $namespace namespace to search + */ + public function __construct( + string $namespace, + string $defaultController, + string $defaultMethod, + array $httpMethods, + array $protectedControllers + ) { + $this->namespace = $namespace; + $this->defaultController = $defaultController; + $this->defaultMethod = $defaultMethod; + $this->httpMethods = $httpMethods; + $this->protectedControllers = $protectedControllers; + } + + /** + * @return array> + * @phpstan-return list> + */ + public function get(): array + { + $finder = new ControllerFinder($this->namespace); + $reader = new ControllerMethodReader($this->namespace, $this->httpMethods); + + $tbody = []; + + foreach ($finder->find() as $class) { + // Exclude controllers in Defined Routes. + if (in_array($class, $this->protectedControllers, true)) { + continue; + } + + $routes = $reader->read( + $class, + $this->defaultController, + $this->defaultMethod + ); + + if ($routes === []) { + continue; + } + + $routes = $this->addFilters($routes); + + foreach ($routes as $item) { + $tbody[] = [ + strtoupper($item['method']) . '(auto)', + $item['route'] . $item['route_params'], + $item['handler'], + $item['before'], + $item['after'], + ]; + } + } + + return $tbody; + } + + private function addFilters($routes) + { + $filterCollector = new FilterCollector(true); + + foreach ($routes as &$route) { + // Search filters for the URI with all params + $sampleUri = $this->generateSampleUri($route); + $filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); + + // Search filters for the URI without optional params + $sampleUri = $this->generateSampleUri($route, false); + $filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); + + // Get common array elements + $filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']); + $filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']); + + $route['before'] = implode(' ', array_map('class_basename', $filters['before'])); + $route['after'] = implode(' ', array_map('class_basename', $filters['after'])); + } + + return $routes; + } + + private function generateSampleUri(array $route, bool $longest = true): string + { + $sampleUri = ''; + + if (isset($route['params'])) { + $i = 1; + + foreach ($route['params'] as $required) { + if ($longest && ! $required) { + $sampleUri .= '/' . $i++; + } + } + } + + return $sampleUri; + } +} diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php new file mode 100644 index 000000000000..3f373c433f1b --- /dev/null +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved; + +use ReflectionClass; +use ReflectionMethod; + +/** + * Reads a controller and returns a list of auto route listing. + */ +final class ControllerMethodReader +{ + /** + * @var string the default namespace + */ + private string $namespace; + + private array $httpMethods; + + /** + * @param string $namespace the default namespace + */ + public function __construct(string $namespace, array $httpMethods) + { + $this->namespace = $namespace; + $this->httpMethods = $httpMethods; + } + + /** + * Returns found route info in the controller. + * + * @phpstan-param class-string $class + * + * @return array> + * @phpstan-return list> + */ + public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array + { + $reflection = new ReflectionClass($class); + + if ($reflection->isAbstract()) { + return []; + } + + $classname = $reflection->getName(); + $classShortname = $reflection->getShortName(); + + $output = []; + $classInUri = $this->getUriByClass($classname); + + foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + $methodName = $method->getName(); + + foreach ($this->httpMethods as $httpVerb) { + if (strpos($methodName, $httpVerb) === 0) { + // Remove HTTP verb prefix. + $methodInUri = lcfirst(substr($methodName, strlen($httpVerb))); + + if ($methodInUri === $defaultMethod) { + $routeWithoutController = $this->getRouteWithoutController( + $classShortname, + $defaultController, + $classInUri, + $classname, + $methodName, + $httpVerb + ); + + if ($routeWithoutController !== []) { + $output = [...$output, ...$routeWithoutController]; + + continue; + } + + // Route for the default method. + $output[] = [ + 'method' => $httpVerb, + 'route' => $classInUri, + 'route_params' => '', + 'handler' => '\\' . $classname . '::' . $methodName, + 'params' => [], + ]; + + continue; + } + + $route = $classInUri . '/' . $methodInUri; + + $params = []; + $routeParams = ''; + $refParams = $method->getParameters(); + + foreach ($refParams as $param) { + $required = true; + if ($param->isOptional()) { + $required = false; + + $routeParams .= '[/..]'; + } else { + $routeParams .= '/..'; + } + + // [variable_name => required?] + $params[$param->getName()] = $required; + } + + $output[] = [ + 'method' => $httpVerb, + 'route' => $route, + 'route_params' => $routeParams, + 'handler' => '\\' . $classname . '::' . $methodName, + 'params' => $params, + ]; + } + } + } + + return $output; + } + + /** + * @phpstan-param class-string $classname + * + * @return string URI path part from the folder(s) and controller + */ + private function getUriByClass(string $classname): string + { + // remove the namespace + $pattern = '/' . preg_quote($this->namespace, '/') . '/'; + $class = ltrim(preg_replace($pattern, '', $classname), '\\'); + + $classParts = explode('\\', $class); + $classPath = ''; + + foreach ($classParts as $part) { + // make the first letter lowercase, because auto routing makes + // the URI path's first letter uppercase and search the controller + $classPath .= lcfirst($part) . '/'; + } + + return rtrim($classPath, '/'); + } + + /** + * Gets a route without default controller. + */ + private function getRouteWithoutController( + string $classShortname, + string $defaultController, + string $uriByClass, + string $classname, + string $methodName, + string $httpVerb + ): array { + $output = []; + + if ($classShortname === $defaultController) { + $pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#'; + $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/'); + $routeWithoutController = $routeWithoutController ?: '/'; + + $output[] = [ + 'method' => $httpVerb, + 'route' => $routeWithoutController, + 'route_params' => '', + 'handler' => '\\' . $classname . '::' . $methodName, + 'params' => [], + ]; + } + + return $output; + } +} diff --git a/system/Commands/Utilities/Routes/ControllerFinder.php b/system/Commands/Utilities/Routes/ControllerFinder.php new file mode 100644 index 000000000000..7e7865876750 --- /dev/null +++ b/system/Commands/Utilities/Routes/ControllerFinder.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Config\Services; + +/** + * Finds all controllers in a namespace for auto route listing. + */ +final class ControllerFinder +{ + /** + * @var string namespace to search + */ + private string $namespace; + + private FileLocator $locator; + + /** + * @param string $namespace namespace to search + */ + public function __construct(string $namespace) + { + $this->namespace = $namespace; + $this->locator = Services::locator(); + } + + /** + * @return string[] + * @phpstan-return class-string[] + */ + public function find(): array + { + $nsArray = explode('\\', trim($this->namespace, '\\')); + $count = count($nsArray); + $ns = ''; + + for ($i = 0; $i < $count; $i++) { + $ns .= '\\' . array_shift($nsArray); + $path = implode('\\', $nsArray); + + $files = $this->locator->listNamespaceFiles($ns, $path); + + if ($files !== []) { + break; + } + } + + $classes = []; + + foreach ($files as $file) { + if (\is_file($file)) { + $classnameOrEmpty = $this->locator->getClassname($file); + + if ($classnameOrEmpty !== '') { + /** @phpstan-var class-string $classname */ + $classname = $classnameOrEmpty; + + $classes[] = $classname; + } + } + } + + return $classes; + } +} diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php new file mode 100644 index 000000000000..27bdad046a99 --- /dev/null +++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +use ReflectionClass; +use ReflectionMethod; + +/** + * Reads a controller and returns a list of auto route listing. + */ +final class ControllerMethodReader +{ + /** + * @var string the default namespace + */ + private string $namespace; + + /** + * @param string $namespace the default namespace + */ + public function __construct(string $namespace) + { + $this->namespace = $namespace; + } + + /** + * @phpstan-param class-string $class + * + * @return array + * @phpstan-return list + */ + public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array + { + $reflection = new ReflectionClass($class); + + if ($reflection->isAbstract()) { + return []; + } + + $classname = $reflection->getName(); + $classShortname = $reflection->getShortName(); + + $output = []; + $uriByClass = $this->getUriByClass($classname); + + if ($this->hasRemap($reflection)) { + $methodName = '_remap'; + + $routeWithoutController = $this->getRouteWithoutController( + $classShortname, + $defaultController, + $uriByClass, + $classname, + $methodName + ); + $output = [...$output, ...$routeWithoutController]; + + $output[] = [ + 'route' => $uriByClass . '[/...]', + 'handler' => '\\' . $classname . '::' . $methodName, + ]; + + return $output; + } + + foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + $methodName = $method->getName(); + + $route = $uriByClass . '/' . $methodName; + + // Exclude BaseController and initController + // See system/Config/Routes.php + if (preg_match('#\AbaseController.*#', $route) === 1) { + continue; + } + if (preg_match('#.*/initController\z#', $route) === 1) { + continue; + } + + if ($methodName === $defaultMethod) { + $routeWithoutController = $this->getRouteWithoutController( + $classShortname, + $defaultController, + $uriByClass, + $classname, + $methodName + ); + $output = [...$output, ...$routeWithoutController]; + + $output[] = [ + 'route' => $uriByClass, + 'handler' => '\\' . $classname . '::' . $methodName, + ]; + } + + $output[] = [ + 'route' => $route . '[/...]', + 'handler' => '\\' . $classname . '::' . $methodName, + ]; + } + + return $output; + } + + /** + * Whether the class has a _remap() method. + */ + private function hasRemap(ReflectionClass $class): bool + { + if ($class->hasMethod('_remap')) { + $remap = $class->getMethod('_remap'); + + return $remap->isPublic(); + } + + return false; + } + + /** + * @phpstan-param class-string $classname + * + * @return string URI path part from the folder(s) and controller + */ + private function getUriByClass(string $classname): string + { + // remove the namespace + $pattern = '/' . preg_quote($this->namespace, '/') . '/'; + $class = ltrim(preg_replace($pattern, '', $classname), '\\'); + + $classParts = explode('\\', $class); + $classPath = ''; + + foreach ($classParts as $part) { + // make the first letter lowercase, because auto routing makes + // the URI path's first letter uppercase and search the controller + $classPath .= lcfirst($part) . '/'; + } + + return rtrim($classPath, '/'); + } + + /** + * Gets a route without default controller. + */ + private function getRouteWithoutController( + string $classShortname, + string $defaultController, + string $uriByClass, + string $classname, + string $methodName + ): array { + $output = []; + + if ($classShortname === $defaultController) { + $pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#'; + $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/'); + $routeWithoutController = $routeWithoutController ?: '/'; + + $output[] = [ + 'route' => $routeWithoutController, + 'handler' => '\\' . $classname . '::' . $methodName, + ]; + } + + return $output; + } +} diff --git a/system/Commands/Utilities/Routes/FilterCollector.php b/system/Commands/Utilities/Routes/FilterCollector.php new file mode 100644 index 000000000000..630416749115 --- /dev/null +++ b/system/Commands/Utilities/Routes/FilterCollector.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +use CodeIgniter\Config\Services; +use CodeIgniter\Filters\Filters; +use CodeIgniter\HTTP\Request; +use CodeIgniter\Router\Router; + +/** + * Collects filters for a route. + */ +final class FilterCollector +{ + /** + * Whether to reset Defined Routes. + * + * If set to true, route filters are not found. + */ + private bool $resetRoutes; + + public function __construct(bool $resetRoutes = false) + { + $this->resetRoutes = $resetRoutes; + } + + /** + * @param string $method HTTP method + * @param string $uri URI path to find filters for + * + * @return array{before: list, after: list} array of filter alias or classname + */ + public function get(string $method, string $uri): array + { + if ($method === 'cli') { + return [ + 'before' => [], + 'after' => [], + ]; + } + + $request = Services::request(null, false); + $request->setMethod($method); + + $router = $this->createRouter($request); + $filters = $this->createFilters($request); + + $finder = new FilterFinder($router, $filters); + + return $finder->find($uri); + } + + private function createRouter(Request $request): Router + { + $routes = Services::routes(); + + if ($this->resetRoutes) { + $routes->resetRoutes(); + } + + return new Router($routes, $request); + } + + private function createFilters(Request $request): Filters + { + $config = config('Filters'); + + return new Filters($config, $request, Services::response()); + } +} diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php new file mode 100644 index 000000000000..f36ddd9e362d --- /dev/null +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +use CodeIgniter\Filters\Filters; +use CodeIgniter\Router\Exceptions\RedirectException; +use CodeIgniter\Router\Router; +use Config\Services; + +/** + * Finds filters. + */ +final class FilterFinder +{ + private Router $router; + private Filters $filters; + + public function __construct(?Router $router = null, ?Filters $filters = null) + { + $this->router = $router ?? Services::router(); + $this->filters = $filters ?? Services::filters(); + } + + private function getRouteFilters(string $uri): array + { + $this->router->handle($uri); + + $multipleFiltersEnabled = config('Feature')->multipleFilters ?? false; + if (! $multipleFiltersEnabled) { + $filter = $this->router->getFilter(); + + return $filter === null ? [] : [$filter]; + } + + return $this->router->getFilters(); + } + + /** + * @param string $uri URI path to find filters for + * + * @return array{before: list, after: list} array of filter alias or classname + */ + public function find(string $uri): array + { + $this->filters->reset(); + + // Add route filters + try { + $routeFilters = $this->getRouteFilters($uri); + $this->filters->enableFilters($routeFilters, 'before'); + $this->filters->enableFilters($routeFilters, 'after'); + + $this->filters->initialize($uri); + + return $this->filters->getFilters(); + } catch (RedirectException $e) { + return [ + 'before' => [], + 'after' => [], + ]; + } + } +} diff --git a/system/Commands/Utilities/Routes/SampleURIGenerator.php b/system/Commands/Utilities/Routes/SampleURIGenerator.php new file mode 100644 index 000000000000..984e50abd0e2 --- /dev/null +++ b/system/Commands/Utilities/Routes/SampleURIGenerator.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities\Routes; + +use CodeIgniter\Config\Services; +use CodeIgniter\Router\RouteCollection; + +/** + * Generate a sample URI path from route key regex. + */ +final class SampleURIGenerator +{ + private RouteCollection $routes; + + /** + * Sample URI path for placeholder. + * + * @var array + */ + private array $samples = [ + 'any' => '123/abc', + 'segment' => 'abc_123', + 'alphanum' => 'abc123', + 'num' => '123', + 'alpha' => 'abc', + 'hash' => 'abc_123', + ]; + + public function __construct(?RouteCollection $routes = null) + { + $this->routes = $routes ?? Services::routes(); + } + + /** + * @param string $routeKey route key regex + * + * @return string sample URI path + */ + public function get(string $routeKey): string + { + $sampleUri = $routeKey; + + foreach ($this->routes->getPlaceholders() as $placeholder => $regex) { + $sample = $this->samples[$placeholder] ?? '::unknown::'; + + $sampleUri = str_replace('(' . $regex . ')', $sample, $sampleUri); + } + + // auto route + return str_replace('[/...]', '/1/2/3/4/5', $sampleUri); + } +} diff --git a/system/Common.php b/system/Common.php index e132d982b7d4..30e28a1dc840 100644 --- a/system/Common.php +++ b/system/Common.php @@ -288,6 +288,38 @@ function csrf_meta(?string $id = null): string } } +if (! function_exists('csp_style_nonce')) { + /** + * Generates a nonce attribute for style tag. + */ + function csp_style_nonce(): string + { + $csp = Services::csp(); + + if (! $csp->enabled()) { + return ''; + } + + return 'nonce="' . $csp->getStyleNonce() . '"'; + } +} + +if (! function_exists('csp_script_nonce')) { + /** + * Generates a nonce attribute for script tag. + */ + function csp_script_nonce(): string + { + $csp = Services::csp(); + + if (! $csp->enabled()) { + return ''; + } + + return 'nonce="' . $csp->getScriptNonce() . '"'; + } +} + if (! function_exists('db_connect')) { /** * Grabs a database connection and returns it to the user. @@ -445,7 +477,7 @@ function esc($data, string $context = 'html', ?string $encoding = null) * * @throws HTTPException */ - function force_https(int $duration = 31536000, ?RequestInterface $request = null, ?ResponseInterface $response = null) + function force_https(int $duration = 31_536_000, ?RequestInterface $request = null, ?ResponseInterface $response = null) { if ($request === null) { $request = Services::request(null, true); @@ -618,7 +650,7 @@ function helper($filenames) } // All namespaced files get added in next - $includes = array_merge($includes, $localIncludes); + $includes = [...$includes, ...$localIncludes]; // And the system default one should be added in last. if (! empty($systemHelper)) { @@ -773,7 +805,6 @@ function log_message(string $level, string $message, array $context = []) * @param class-string $name * * @return T - * @phpstan-return Model */ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null) { @@ -950,7 +981,6 @@ function single_service(string $name, ...$params) $method = new ReflectionMethod($service, $name); $count = $method->getNumberOfParameters(); $mParam = $method->getParameters(); - $params = $params ?? []; if ($count === 1) { // This service needs only one argument, which is the shared @@ -983,10 +1013,26 @@ function single_service(string $name, ...$params) */ function slash_item(string $item): ?string { - $config = config(App::class); + $config = config(App::class); + + if (! property_exists($config, $item)) { + return null; + } + $configItem = $config->{$item}; - if (! isset($configItem) || empty(trim($configItem))) { + if (! is_scalar($configItem)) { + throw new RuntimeException(sprintf( + 'Cannot convert "%s::$%s" of type "%s" to type "string".', + App::class, + $item, + gettype($configItem) + )); + } + + $configItem = trim((string) $configItem); + + if ($configItem === '') { return $configItem; } @@ -1095,7 +1141,7 @@ function view(string $name, array $data = [], array $options = []): string * View cells are used within views to insert HTML chunks that are managed * by other classes. * - * @param null $params + * @param array|string|null $params * * @throws ReflectionException */ diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index ef4dfe1a801b..62e5d828e1ce 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -30,10 +30,8 @@ final class ComposerScripts { /** * Path to the ThirdParty directory. - * - * @var string */ - private static $path = __DIR__ . '/ThirdParty/'; + private static string $path = __DIR__ . '/ThirdParty/'; /** * Direct dependencies of CodeIgniter to copy @@ -41,7 +39,7 @@ final class ComposerScripts * * @var array> */ - private static $dependencies = [ + private static array $dependencies = [ 'kint-src' => [ 'license' => __DIR__ . '/../vendor/kint-php/kint/LICENSE', 'from' => __DIR__ . '/../vendor/kint-php/kint/src/', diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 0818271e002f..79cad2ab8d25 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -11,6 +11,19 @@ namespace CodeIgniter\Config; +use Laminas\Escaper\Escaper; +use Laminas\Escaper\Exception\ExceptionInterface; +use Laminas\Escaper\Exception\InvalidArgumentException as EscaperInvalidArgumentException; +use Laminas\Escaper\Exception\RuntimeException; +use Psr\Log\AbstractLogger; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; +use Psr\Log\LoggerTrait; +use Psr\Log\LogLevel; +use Psr\Log\NullLogger; + /** * AUTOLOADER CONFIGURATION * @@ -93,15 +106,18 @@ class AutoloadConfig * @var array */ protected $coreClassmap = [ - 'Psr\Log\AbstractLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php', - 'Psr\Log\InvalidArgumentException' => SYSTEMPATH . 'ThirdParty/PSR/Log/InvalidArgumentException.php', - 'Psr\Log\LoggerAwareInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php', - 'Psr\Log\LoggerAwareTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php', - 'Psr\Log\LoggerInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerInterface.php', - 'Psr\Log\LoggerTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerTrait.php', - 'Psr\Log\LogLevel' => SYSTEMPATH . 'ThirdParty/PSR/Log/LogLevel.php', - 'Psr\Log\NullLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/NullLogger.php', - 'Laminas\Escaper\Escaper' => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php', + AbstractLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php', + InvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/PSR/Log/InvalidArgumentException.php', + LoggerAwareInterface::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php', + LoggerAwareTrait::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php', + LoggerInterface::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerInterface.php', + LoggerTrait::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerTrait.php', + LogLevel::class => SYSTEMPATH . 'ThirdParty/PSR/Log/LogLevel.php', + NullLogger::class => SYSTEMPATH . 'ThirdParty/PSR/Log/NullLogger.php', + ExceptionInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/ExceptionInterface.php', + EscaperInvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/InvalidArgumentException.php', + RuntimeException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/RuntimeException.php', + Escaper::class => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php', ]; /** @@ -130,6 +146,6 @@ public function __construct() $this->psr4 = array_merge($this->corePsr4, $this->psr4); $this->classmap = array_merge($this->coreClassmap, $this->classmap); - $this->files = array_merge($this->coreFiles, $this->files); + $this->files = [...$this->coreFiles, ...$this->files]; } } diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 9b71a4963bab..359fcc1488ba 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -88,7 +88,7 @@ public function __construct() * * @param mixed $property * - * @return mixed + * @return void */ protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix) { @@ -102,16 +102,28 @@ protected function initEnvValue(&$property, string $name, string $prefix, string } elseif ($value === 'true') { $value = true; } - $property = is_bool($value) ? $value : trim($value, '\'"'); - } + if (is_bool($value)) { + $property = $value; + + return; + } + + $value = trim($value, '\'"'); - return $property; + if (is_int($property)) { + $value = (int) $value; + } elseif (is_float($property)) { + $value = (float) $value; + } + + $property = $value; + } } /** * Retrieve an environment-specific configuration setting * - * @return mixed + * @return string|null */ protected function getEnvValue(string $property, string $prefix, string $shortPrefix) { diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index d390807fc51f..d77cfb1bbc51 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -28,6 +28,7 @@ use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\ContentSecurityPolicy; use CodeIgniter\HTTP\CURLRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Negotiate; @@ -56,6 +57,7 @@ use Config\App; use Config\Autoload; use Config\Cache; +use Config\ContentSecurityPolicy as CSPConfig; use Config\Encryption; use Config\Exceptions as ConfigExceptions; use Config\Filters as ConfigFilters; @@ -94,6 +96,7 @@ * @method static CLIRequest clirequest(App $config = null, $getShared = true) * @method static CodeIgniter codeigniter(App $config = null, $getShared = true) * @method static Commands commands($getShared = true) + * @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true) * @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true) * @method static Email email($config = null, $getShared = true) * @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false) @@ -162,7 +165,7 @@ class BaseService * * @var array */ - private static $serviceNames = []; + private static array $serviceNames = []; /** * Returns a shared instance of any of the class' services. @@ -270,7 +273,7 @@ public static function serviceExists(string $name): ?string /** * Reset shared instances and mocks for testing. */ - public static function reset(bool $initAutoloader = false) + public static function reset(bool $initAutoloader = true) { static::$mocks = []; static::$instances = []; @@ -285,6 +288,7 @@ public static function reset(bool $initAutoloader = false) */ public static function resetSingle(string $name) { + $name = strtolower($name); unset(static::$mocks[$name], static::$instances[$name]); } @@ -328,7 +332,7 @@ protected static function discoverServices(string $name, array $arguments) foreach ($files as $file) { $classname = $locator->getClassname($file); - if (! in_array($classname, ['CodeIgniter\\Config\\Services'], true)) { + if (! in_array($classname, [Services::class], true)) { static::$services[] = new $classname(); } } @@ -365,7 +369,7 @@ protected static function buildServicesCache(): void foreach ($files as $file) { $classname = $locator->getClassname($file); - if ($classname !== 'CodeIgniter\\Config\\Services') { + if ($classname !== Services::class) { self::$serviceNames[] = $classname; static::$services[] = new $classname(); } diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 8bcef3e09610..31188c1b03a1 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Config; +use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Model; use Config\Services; @@ -23,7 +24,6 @@ * instantiation checks. * * @method static BaseConfig config(...$arguments) - * @method static Model models(...$arguments) */ class Factories { @@ -41,7 +41,7 @@ class Factories * * @var array */ - private static $configOptions = [ + private static array $configOptions = [ 'component' => 'config', 'path' => 'Config', 'instanceOf' => null, @@ -67,6 +67,22 @@ class Factories */ protected static $instances = []; + /** + * This method is only to prevent PHPStan error. + * If we have a solution, we can remove this method. + * See https://github.com/codeigniter4/CodeIgniter4/pull/5358 + * + * @template T of Model + * + * @param class-string $name + * + * @return T + */ + public static function models(string $name, array $options = [], ?ConnectionInterface &$conn = null) + { + return self::__callStatic('models', [$name, $options, $conn]); + } + /** * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. @@ -263,7 +279,7 @@ public static function setOptions(string $component, array $values): array /** * Resets the static arrays, optionally just for one component * - * @param string $component Lowercase, plural component name + * @param string|null $component Lowercase, plural component name */ public static function reset(?string $component = null) { diff --git a/system/Config/Routes.php b/system/Config/Routes.php index 4ace952bc3c8..0f16b675a8ad 100644 --- a/system/Config/Routes.php +++ b/system/Config/Routes.php @@ -9,8 +9,6 @@ * the LICENSE file that was distributed with this source code. */ -use CodeIgniter\Exceptions\PageNotFoundException; - /* * System URI Routing * @@ -21,20 +19,5 @@ * already loaded up and ready for us to use. */ -// Prevent access to BaseController -$routes->add('BaseController(:any)', static function () { - throw PageNotFoundException::forPageNotFound(); -}); - -// Prevent access to initController method -$routes->add('(:any)/initController', static function () { - throw PageNotFoundException::forPageNotFound(); -}); - -// Migrations -$routes->cli('migrations/(:segment)/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1/$2'); -$routes->cli('migrations/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1'); -$routes->cli('migrations', '\CodeIgniter\Commands\MigrationsCommand::index'); - // CLI Catchall - uses a _remap to call Commands $routes->cli('ci(:any)', '\CodeIgniter\CLI\CommandRunner::index/$1'); diff --git a/system/Config/Services.php b/system/Config/Services.php index 0b681f17ce17..9dca50115f51 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -28,6 +28,7 @@ use CodeIgniter\Format\Format; use CodeIgniter\Honeypot\Honeypot; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\ContentSecurityPolicy; use CodeIgniter\HTTP\CURLRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Negotiate; @@ -46,6 +47,9 @@ use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\Router; use CodeIgniter\Security\Security; +use CodeIgniter\Session\Handlers\Database\MySQLiHandler; +use CodeIgniter\Session\Handlers\Database\PostgreHandler; +use CodeIgniter\Session\Handlers\DatabaseHandler; use CodeIgniter\Session\Session; use CodeIgniter\Throttle\Throttler; use CodeIgniter\Typography\Typography; @@ -56,6 +60,8 @@ use CodeIgniter\View\View; use Config\App; use Config\Cache; +use Config\ContentSecurityPolicy as CSPConfig; +use Config\Database; use Config\Email as EmailConfig; use Config\Encryption as EncryptionConfig; use Config\Exceptions as ExceptionsConfig; @@ -101,7 +107,7 @@ public static function cache(?Cache $config = null, bool $getShared = true) return static::getSharedInstance('cache', $config); } - $config = $config ?? new Cache(); + $config ??= new Cache(); return CacheFactory::getHandler($config); } @@ -118,7 +124,7 @@ public static function clirequest(?App $config = null, bool $getShared = true) return static::getSharedInstance('clirequest', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); return new CLIRequest($config); } @@ -134,7 +140,7 @@ public static function codeigniter(?App $config = null, bool $getShared = true) return static::getSharedInstance('codeigniter', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); return new CodeIgniter($config); } @@ -153,6 +159,22 @@ public static function commands(bool $getShared = true) return new Commands(); } + /** + * Content Security Policy + * + * @return ContentSecurityPolicy + */ + public static function csp(?CSPConfig $config = null, bool $getShared = true) + { + if ($getShared) { + return static::getSharedInstance('csp', $config); + } + + $config ??= config('ContentSecurityPolicy'); + + return new ContentSecurityPolicy($config); + } + /** * The CURL Request class acts as a simple HTTP client for interacting * with other servers, typically through APIs. @@ -165,8 +187,8 @@ public static function curlrequest(array $options = [], ?ResponseInterface $resp return static::getSharedInstance('curlrequest', $options, $response, $config); } - $config = $config ?? config('App'); - $response = $response ?? new Response($config); + $config ??= config('App'); + $response ??= new Response($config); return new CURLRequest( $config, @@ -209,7 +231,7 @@ public static function encrypter(?EncryptionConfig $config = null, $getShared = return static::getSharedInstance('encrypter', $config); } - $config = $config ?? config('Encryption'); + $config ??= config('Encryption'); $encryption = new Encryption($config); return $encryption->initialize($config); @@ -234,9 +256,9 @@ public static function exceptions( return static::getSharedInstance('exceptions', $config, $request, $response); } - $config = $config ?? config('Exceptions'); - $request = $request ?? AppServices::request(); - $response = $response ?? AppServices::response(); + $config ??= config('Exceptions'); + $request ??= AppServices::request(); + $response ??= AppServices::response(); return new Exceptions($config, $request, $response); } @@ -255,7 +277,7 @@ public static function filters(?FiltersConfig $config = null, bool $getShared = return static::getSharedInstance('filters', $config); } - $config = $config ?? config('Filters'); + $config ??= config('Filters'); return new Filters($config, AppServices::request(), AppServices::response()); } @@ -271,7 +293,7 @@ public static function format(?FormatConfig $config = null, bool $getShared = tr return static::getSharedInstance('format', $config); } - $config = $config ?? config('Format'); + $config ??= config('Format'); return new Format($config); } @@ -288,7 +310,7 @@ public static function honeypot(?HoneypotConfig $config = null, bool $getShared return static::getSharedInstance('honeypot', $config); } - $config = $config ?? config('Honeypot'); + $config ??= config('Honeypot'); return new Honeypot($config); } @@ -305,7 +327,7 @@ public static function image(?string $handler = null, ?Images $config = null, bo return static::getSharedInstance('image', $handler, $config); } - $config = $config ?? config('Images'); + $config ??= config('Images'); $handler = $handler ?: $config->defaultHandler; $class = $config->handlers[$handler]; @@ -371,7 +393,7 @@ public static function migrations(?Migrations $config = null, ?ConnectionInterfa return static::getSharedInstance('migrations', $config, $db); } - $config = $config ?? config('Migrations'); + $config ??= config('Migrations'); return new MigrationRunner($config, $db); } @@ -389,7 +411,7 @@ public static function negotiator(?RequestInterface $request = null, bool $getSh return static::getSharedInstance('negotiator', $request); } - $request = $request ?? AppServices::request(); + $request ??= AppServices::request(); return new Negotiate($request); } @@ -405,8 +427,8 @@ public static function pager(?PagerConfig $config = null, ?RendererInterface $vi return static::getSharedInstance('pager', $config, $view); } - $config = $config ?? config('Pager'); - $view = $view ?? AppServices::renderer(); + $config ??= config('Pager'); + $view ??= AppServices::renderer(); return new Pager($config, $view); } @@ -423,7 +445,7 @@ public static function parser(?string $viewPath = null, ?ViewConfig $config = nu } $viewPath = $viewPath ?: config('Paths')->viewDirectory; - $config = $config ?? config('View'); + $config ??= config('View'); return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger()); } @@ -442,7 +464,7 @@ public static function renderer(?string $viewPath = null, ?ViewConfig $config = } $viewPath = $viewPath ?: config('Paths')->viewDirectory; - $config = $config ?? config('View'); + $config ??= config('View'); return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger()); } @@ -458,7 +480,7 @@ public static function request(?App $config = null, bool $getShared = true) return static::getSharedInstance('request', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); return new IncomingRequest( $config, @@ -479,7 +501,7 @@ public static function response(?App $config = null, bool $getShared = true) return static::getSharedInstance('response', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); return new Response($config); } @@ -495,7 +517,7 @@ public static function redirectresponse(?App $config = null, bool $getShared = t return static::getSharedInstance('redirectresponse', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); $response = new RedirectResponse($config); $response->setProtocolVersion(AppServices::request()->getProtocolVersion()); @@ -529,8 +551,8 @@ public static function router(?RouteCollectionInterface $routes = null, ?Request return static::getSharedInstance('router', $routes, $request); } - $routes = $routes ?? AppServices::routes(); - $request = $request ?? AppServices::request(); + $routes ??= AppServices::routes(); + $request ??= AppServices::request(); return new Router($routes, $request); } @@ -547,7 +569,7 @@ public static function security(?App $config = null, bool $getShared = true) return static::getSharedInstance('security', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); return new Security($config); } @@ -563,11 +585,25 @@ public static function session(?App $config = null, bool $getShared = true) return static::getSharedInstance('session', $config); } - $config = $config ?? config('App'); + $config ??= config('App'); $logger = AppServices::logger(); $driverName = $config->sessionDriver; - $driver = new $driverName($config, AppServices::request()->getIPAddress()); + + if ($driverName === DatabaseHandler::class) { + $DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup; + $db = Database::connect($DBGroup); + + $driver = $db->getPlatform(); + + if ($driver === 'MySQLi') { + $driverName = MySQLiHandler::class; + } elseif ($driver === 'Postgre') { + $driverName = PostgreHandler::class; + } + } + + $driver = new $driverName($config, AppServices::request()->getIPAddress()); $driver->setLogger($logger); $session = new Session($driver, $config); @@ -621,7 +657,7 @@ public static function toolbar(?ToolbarConfig $config = null, bool $getShared = return static::getSharedInstance('toolbar', $config); } - $config = $config ?? config('Toolbar'); + $config ??= config('Toolbar'); return new Toolbar($config); } @@ -653,7 +689,7 @@ public static function validation(?ValidationConfig $config = null, bool $getSha return static::getSharedInstance('validation', $config); } - $config = $config ?? config('Validation'); + $config ??= config('Validation'); return new Validation($config, AppServices::renderer()); } diff --git a/system/Config/View.php b/system/Config/View.php index 5a8baeac2d6a..42d40faf879e 100644 --- a/system/Config/View.php +++ b/system/Config/View.php @@ -11,6 +11,8 @@ namespace CodeIgniter\Config; +use CodeIgniter\View\ViewDecoratorInterface; + /** * View configuration */ @@ -76,6 +78,8 @@ class View extends BaseConfig * @var array */ protected $corePlugins = [ + 'csp_script_nonce' => '\CodeIgniter\View\Plugins::cspScriptNonce', + 'csp_style_nonce' => '\CodeIgniter\View\Plugins::cspStyleNonce', 'current_url' => '\CodeIgniter\View\Plugins::currentURL', 'previous_url' => '\CodeIgniter\View\Plugins::previousURL', 'mailto' => '\CodeIgniter\View\Plugins::mailto', @@ -86,6 +90,17 @@ class View extends BaseConfig 'siteURL' => '\CodeIgniter\View\Plugins::siteURL', ]; + /** + * View Decorators are class methods that will be run in sequence to + * have a chance to alter the generated output just prior to caching + * the results. + * + * All classes must implement CodeIgniter\View\ViewDecoratorInterface + * + * @var class-string[] + */ + public array $decorators = []; + /** * Merge the built-in and developer-configured filters and plugins, * with preference to the developer ones. diff --git a/system/Controller.php b/system/Controller.php index b50c8bcf3470..6116160c0fcb 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -97,7 +97,7 @@ public function initController(RequestInterface $request, ResponseInterface $res * * @throws HTTPException */ - protected function forceHTTPS(int $duration = 31536000) + protected function forceHTTPS(int $duration = 31_536_000) { force_https($duration, $this->request, $this->response); } @@ -128,13 +128,37 @@ protected function loadHelpers() } /** - * A shortcut to performing validation on input data. If validation - * is not successful, a $errors property will be set on this class. + * A shortcut to performing validation on Request data. * * @param array|string $rules * @param array $messages An array of custom error messages */ protected function validate($rules, array $messages = []): bool + { + $this->setValidator($rules, $messages); + + return $this->validator->withRequest($this->request)->run(); + } + + /** + * A shortcut to performing validation on any input data. + * + * @param array $data The data to validate + * @param array|string $rules + * @param array $messages An array of custom error messages + * @param string|null $dbGroup The database group to use + */ + protected function validateData(array $data, $rules, array $messages = [], ?string $dbGroup = null): bool + { + $this->setValidator($rules, $messages); + + return $this->validator->run($data, null, $dbGroup); + } + + /** + * @param array|string $rules + */ + private function setValidator($rules, array $messages): void { $this->validator = Services::validation(); @@ -157,6 +181,6 @@ protected function validate($rules, array $messages = []): bool $rules = $validation->{$rules}; } - return $this->validator->withRequest($this->request)->setRules($rules, $messages)->run(); + $this->validator->setRules($rules, $messages); } } diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 150d42069949..e40d0a1201d5 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -95,7 +95,7 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * * @var array */ - private static $defaults = [ + private static array $defaults = [ 'prefix' => '', 'expires' => 0, 'path' => '/', @@ -110,12 +110,10 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * A cookie name can be any US-ASCII characters, except control characters, * spaces, tabs, or separator characters. * - * @var string - * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes * @see https://tools.ietf.org/html/rfc2616#section-2.2 */ - private static $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}"; + private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}"; /** * Set the default attributes to a Cookie instance by injecting @@ -491,7 +489,7 @@ public function withPath(?string $path) */ public function withDomain(?string $domain) { - $domain = $domain ?? self::$defaults['domain']; + $domain ??= self::$defaults['domain']; $this->validatePrefix($this->prefix, $this->secure, $this->path, $domain); $cookie = clone $this; diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 2a431accd2f5..ca983691ea0c 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -109,6 +109,13 @@ class BaseBuilder */ public $QBOrderBy = []; + /** + * QB UNION data + * + * @var array + */ + protected array $QBUnion = []; + /** * QB NO ESCAPE data * @@ -270,7 +277,7 @@ class BaseBuilder * * @throws DatabaseException */ - public function __construct($tableName, ConnectionInterface &$db, ?array $options = null) + public function __construct($tableName, ConnectionInterface $db, ?array $options = null) { if (empty($tableName)) { throw new DatabaseException('A table must be specified when creating a new Query Builder.'); @@ -356,7 +363,7 @@ public function ignore(bool $ignore = true) /** * Generates the SELECT portion of the query * - * @param array|string $select + * @param array|RawSql|string $select * * @return $this */ @@ -371,6 +378,12 @@ public function select($select = '*', ?bool $escape = null) $escape = $this->db->protectIdentifiers; } + if ($select instanceof RawSql) { + $this->QBSelect[] = $select; + + return $this; + } + foreach ($select as $val) { $val = trim($val); @@ -445,6 +458,16 @@ public function selectCount(string $select = '', string $alias = '') return $this->maxMinAvgSum($select, $alias, 'COUNT'); } + /** + * Adds a subquery to the selection + */ + public function selectSubquery(BaseBuilder $subquery, string $as): self + { + $this->QBSelect[] = $this->buildSubquery($subquery, true, $as); + + return $this; + } + /** * SELECT [MAX|MIN|AVG|SUM|COUNT]() * @@ -519,41 +542,55 @@ public function distinct(bool $val = true) * * @return $this */ - public function from($from, bool $overwrite = false) + public function from($from, bool $overwrite = false): self { if ($overwrite === true) { $this->QBFrom = []; $this->db->setAliasedTables([]); } - foreach ((array) $from as $val) { - if (strpos($val, ',') !== false) { - foreach (explode(',', $val) as $v) { - $v = trim($v); - $this->trackAliases($v); - - $this->QBFrom[] = $this->db->protectIdentifiers($v, true, null, false); - } + foreach ((array) $from as $table) { + if (strpos($table, ',') !== false) { + $this->from(explode(',', $table)); } else { - $val = trim($val); + $table = trim($table); - // Extract any aliases that might exist. We use this information - // in the protectIdentifiers to know whether to add a table prefix - $this->trackAliases($val); + if ($table === '') { + continue; + } - $this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false); + $this->trackAliases($table); + $this->QBFrom[] = $this->db->protectIdentifiers($table, true, null, false); } } return $this; } + /** + * @param BaseBuilder $from Expected subquery + * @param string $alias Subquery alias + * + * @return $this + */ + public function fromSubquery(BaseBuilder $from, string $alias): self + { + $table = $this->buildSubquery($from, true, $alias); + + $this->trackAliases($table); + $this->QBFrom[] = $table; + + return $this; + } + /** * Generates the JOIN portion of the query * + * @param RawSql|string $cond + * * @return $this */ - public function join(string $table, string $cond, string $type = '', ?bool $escape = null) + public function join(string $table, $cond, string $type = '', ?bool $escape = null) { if ($type !== '') { $type = strtoupper(trim($type)); @@ -573,6 +610,17 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca $escape = $this->db->protectIdentifiers; } + // Do we want to escape the table name? + if ($escape === true) { + $table = $this->db->protectIdentifiers($table, true, null, false); + } + + if ($cond instanceof RawSql) { + $this->QBJoin[] = $type . 'JOIN ' . $table . ' ON ' . $cond; + + return $this; + } + if (! $this->hasOperator($cond)) { $cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')'; } elseif ($escape === false) { @@ -606,11 +654,6 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca } } - // Do we want to escape the table name? - if ($escape === true) { - $table = $this->db->protectIdentifiers($table, true, null, false); - } - // Assemble the JOIN statement $this->QBJoin[] = $type . 'JOIN ' . $table . $cond; @@ -621,8 +664,8 @@ public function join(string $table, string $cond, string $type = '', ?bool $esca * Generates the WHERE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -637,9 +680,9 @@ public function where($key, $value = null, ?bool $escape = null) * Generates the WHERE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $key - * @param mixed $value - * @param bool $escape + * @param array|RawSql|string $key + * @param mixed $value + * @param bool $escape * * @return $this */ @@ -654,15 +697,20 @@ public function orWhere($key, $value = null, ?bool $escape = null) * @used-by having() * @used-by orHaving() * - * @param mixed $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ protected function whereHaving(string $qbKey, $key, $value = null, string $type = 'AND ', ?bool $escape = null) { - if (! is_array($key)) { - $key = [$key => $value]; + if ($key instanceof RawSql) { + $keyValue = [(string) $key => $key]; + $escape = false; + } elseif (! is_array($key)) { + $keyValue = [$key => $value]; + } else { + $keyValue = $key; } // If the escape value was not set will base it on the global setting @@ -670,10 +718,13 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type $escape = $this->db->protectIdentifiers; } - foreach ($key as $k => $v) { + foreach ($keyValue as $k => $v) { $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type); - if ($v !== null) { + if ($v instanceof RawSql) { + $k = ''; + $op = ''; + } elseif ($v !== null) { $op = $this->getOperator($k, true); if (! empty($op)) { @@ -709,10 +760,17 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type $op = ''; } - $this->{$qbKey}[] = [ - 'condition' => $prefix . $k . $op . $v, - 'escape' => $escape, - ]; + if ($v instanceof RawSql) { + $this->{$qbKey}[] = [ + 'condition' => $v->with($prefix . $k . $op . $v), + 'escape' => $escape, + ]; + } else { + $this->{$qbKey}[] = [ + 'condition' => $prefix . $k . $op . $v, + 'escape' => $escape, + ]; + } } return $this; @@ -889,7 +947,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -902,7 +960,7 @@ public function like($field, string $match = '', string $side = 'both', ?bool $e * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -915,7 +973,7 @@ public function notLike($field, string $match = '', string $side = 'both', ?bool * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -928,7 +986,7 @@ public function orLike($field, string $match = '', string $side = 'both', ?bool * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -941,7 +999,7 @@ public function orNotLike($field, string $match = '', string $side = 'both', ?bo * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -954,7 +1012,7 @@ public function havingLike($field, string $match = '', string $side = 'both', ?b * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -967,7 +1025,7 @@ public function notHavingLike($field, string $match = '', string $side = 'both', * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -980,7 +1038,7 @@ public function orHavingLike($field, string $match = '', string $side = 'both', * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -999,20 +1057,50 @@ public function orNotHavingLike($field, string $match = '', string $side = 'both * @used-by notHavingLike() * @used-by orNotHavingLike() * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', ?bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere') { - if (! is_array($field)) { - $field = [$field => $match]; - } - $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers; $side = strtolower($side); - foreach ($field as $k => $v) { + if ($field instanceof RawSql) { + $k = (string) $field; + $v = $match; + $insensitiveSearch = false; + + $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type); + + if ($side === 'none') { + $bind = $this->setBind($field->getBindingKey(), $v, $escape); + } elseif ($side === 'before') { + $bind = $this->setBind($field->getBindingKey(), "%{$v}", $escape); + } elseif ($side === 'after') { + $bind = $this->setBind($field->getBindingKey(), "{$v}%", $escape); + } else { + $bind = $this->setBind($field->getBindingKey(), "%{$v}%", $escape); + } + + $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch); + + // some platforms require an escape sequence definition for LIKE wildcards + if ($escape === true && $this->db->likeEscapeStr !== '') { + $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar); + } + + $this->{$clause}[] = [ + 'condition' => $field->with($likeStatement), + 'escape' => $escape, + ]; + + return $this; + } + + $keyValue = ! is_array($field) ? [$field => $match] : $field; + + foreach ($keyValue as $k => $v) { if ($insensitiveSearch === true) { $v = strtolower($v); } @@ -1029,7 +1117,7 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri $bind = $this->setBind($k, "%{$v}%", $escape); } - $likeStatement = $this->_like_statement($prefix, $this->db->protectIdentifiers($k, false, $escape), $not, $bind, $insensitiveSearch); + $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch); // some platforms require an escape sequence definition for LIKE wildcards if ($escape === true && $this->db->likeEscapeStr !== '') { @@ -1051,12 +1139,54 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string { if ($insensitiveSearch === true) { - return "{$prefix} LOWER({$column}) {$not} LIKE :{$bind}:"; + return "{$prefix} LOWER(" . $this->db->escapeIdentifiers($column) . ") {$not} LIKE :{$bind}:"; } return "{$prefix} {$column} {$not} LIKE :{$bind}:"; } + /** + * Add UNION statement + * + * @param BaseBuilder|Closure $union + * + * @return $this + */ + public function union($union) + { + return $this->addUnionStatement($union); + } + + /** + * Add UNION ALL statement + * + * @param BaseBuilder|Closure $union + * + * @return $this + */ + public function unionAll($union) + { + return $this->addUnionStatement($union, true); + } + + /** + * @used-by union() + * @used-by unionAll() + * + * @param BaseBuilder|Closure $union + * + * @return $this + */ + protected function addUnionStatement($union, bool $all = false) + { + $this->QBUnion[] = "\n" . 'UNION ' + . ($all ? 'ALL ' : '') + . 'SELECT * FROM ' + . $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1)); + + return $this; + } + /** * Starts a query group. * @@ -1247,8 +1377,8 @@ public function groupBy($by, ?bool $escape = null) /** * Separates multiple calls with 'AND'. * - * @param array|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -1260,8 +1390,8 @@ public function having($key, $value = null, ?bool $escape = null) /** * Separates multiple calls with 'OR'. * - * @param array|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -1650,7 +1780,10 @@ public function insertBatch(?array $set = null, ?bool $escape = null, int $batch } if (! $hasQBSet) { - $this->resetWrite(); + $this->resetRun([ + 'QBSet' => [], + 'QBKeys' => [], + ]); } } @@ -2314,6 +2447,8 @@ protected function compileSelect($selectOverride = false): string if (empty($this->QBSelect)) { $sql .= '*'; + } elseif ($this->QBSelect[0] instanceof RawSql) { + $sql .= (string) $this->QBSelect[0]; } else { // Cycle through the "select" portion of the query and prep each column name. // The reason we protect identifiers here rather than in the select() function @@ -2341,10 +2476,10 @@ protected function compileSelect($selectOverride = false): string . $this->compileOrderBy(); if ($this->QBLimit) { - return $this->_limit($sql . "\n"); + $sql = $this->_limit($sql . "\n"); } - return $sql; + return $this->unionInjection($sql); } /** @@ -2382,6 +2517,12 @@ protected function compileWhereHaving(string $qbKey): string continue; } + if ($qbkey['condition'] instanceof RawSql) { + $qbkey = $qbkey['condition']; + + continue; + } + if ($qbkey['escape'] === false) { $qbkey = $qbkey['condition']; @@ -2493,6 +2634,17 @@ protected function compileOrderBy(): string return ''; } + protected function unionInjection(string $sql): string + { + if ($this->QBUnion === []) { + return $sql; + } + + return 'SELECT * FROM (' . $sql . ') ' + . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0') + . implode("\n", $this->QBUnion); + } + /** * Takes an object as input and converts the class variables to array key/vals * @@ -2612,6 +2764,7 @@ protected function resetSelect() 'QBDistinct' => false, 'QBLimit' => false, 'QBOffset' => false, + 'QBUnion' => [], ]); if (! empty($this->db)) { @@ -2743,16 +2896,29 @@ protected function isSubquery($value): bool /** * @param BaseBuilder|Closure $builder * @param bool $wrapped Wrap the subquery in brackets + * @param string $alias Subquery alias */ - protected function buildSubquery($builder, bool $wrapped = false): string + protected function buildSubquery($builder, bool $wrapped = false, string $alias = ''): string { if ($builder instanceof Closure) { - $instance = (clone $this)->from([], true)->resetQuery(); - $builder = $builder($instance); + $builder($builder = $this->db->newQuery()); + } + + if ($builder === $this) { + throw new DatabaseException('The subquery cannot be the same object as the main query object.'); } $subquery = strtr($builder->getCompiledSelect(), "\n", ' '); - return $wrapped ? '(' . $subquery . ')' : $subquery; + if ($wrapped) { + $subquery = '(' . $subquery . ')'; + $alias = trim($alias); + + if ($alias !== '') { + $subquery .= ' ' . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers($alias) : $alias); + } + } + + return $subquery; } } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 53e5dad1b351..9d7e69bf5256 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -14,6 +14,7 @@ use Closure; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Events\Events; +use stdClass; use Throwable; /** @@ -326,7 +327,7 @@ abstract class BaseConnection implements ConnectionInterface * * @var string */ - protected $queryClass = 'CodeIgniter\\Database\\Query'; + protected $queryClass = Query::class; /** * Saves our connection settings. @@ -342,6 +343,13 @@ public function __construct(array $params) if (class_exists($queryClass)) { $this->queryClass = $queryClass; } + + if ($this->failover !== []) { + // If there is a failover database, connect now to do failover. + // Otherwise, Query Builder creates SQL statement with the main database config + // (DBPrefix) even when the main database is down. + $this->initialize(); + } } /** @@ -508,7 +516,7 @@ public function getPrefix(): string } /** - * The name of the platform in use (MySQLi, mssql, etc) + * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc) */ public function getPlatform(): string { @@ -854,6 +862,14 @@ public function table($tableName) return new $className($tableName, $this); } + /** + * Returns a new instance of the BaseBuilder class with a cleared FROM clause. + */ + public function newQuery(): BaseBuilder + { + return $this->table(',')->from([], true); + } + /** * Creates a prepared statement with the database that can then * be used to execute multiple statements against. Within the @@ -954,10 +970,12 @@ public function getConnectDuration(int $decimals = 6): string * the correct identifiers. * * @param array|string $item - * @param bool $prefixSingle Prefix an item with no segments? - * @param bool $fieldExists Supplied $item contains a field name? + * @param bool $prefixSingle Prefix a table name with no segments? + * @param bool $protectIdentifiers Protect table or column names? + * @param bool $fieldExists Supplied $item contains a column name? * * @return array|string + * @phpstan-return ($item is array ? array : string) */ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true) { @@ -1013,8 +1031,7 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro // // NOTE: The ! empty() condition prevents this method // from breaking when QB isn't enabled. - $firstSegment = trim($parts[0], $this->escapeChar); - if (! empty($this->aliasedTables) && in_array($firstSegment, $this->aliasedTables, true)) { + if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) { if ($protectIdentifiers === true) { foreach ($parts as $key => $val) { if (! in_array($val, $this->reservedIdentifiers, true)) { @@ -1429,7 +1446,7 @@ public function fieldExists(string $fieldName, string $tableName): bool /** * Returns an object with field data * - * @return array + * @return stdClass[] */ public function getFieldData(string $table) { @@ -1518,7 +1535,8 @@ public function isWriteType($sql): bool * * Must return an array with keys 'code' and 'message': * - * return ['code' => null, 'message' => null); + * @return array + * @phpstan-return array{code: int|string|null, message: string|null} */ abstract public function error(): array; diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index ba36915c79fe..ce5a208a1ef3 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -57,7 +57,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface public function __construct(BaseConnection $db) { - $this->db = &$db; + $this->db = $db; } /** @@ -69,7 +69,7 @@ public function __construct(BaseConnection $db) * * @return mixed */ - public function prepare(string $sql, array $options = [], string $queryClass = 'CodeIgniter\\Database\\Query') + public function prepare(string $sql, array $options = [], string $queryClass = Query::class) { // We only supports positional placeholders (?) // in order to work with the execute method below, so we diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index 7848ae75ecf0..9ba48c927619 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -49,9 +49,9 @@ abstract class BaseUtils /** * Class constructor */ - public function __construct(ConnectionInterface &$db) + public function __construct(ConnectionInterface $db) { - $this->db = &$db; + $this->db = $db; } /** diff --git a/system/Database/Config.php b/system/Database/Config.php index 6d2e82bf1109..f73c93a1169a 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -55,7 +55,7 @@ public static function connect($group = null, bool $getShared = true) $group = 'custom-' . md5(json_encode($config)); } - $config = $config ?? config('Database'); + $config ??= config('Database'); if (empty($group)) { $group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup; diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 406f75b47030..0ffd245fa7a0 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -179,7 +179,7 @@ class Forge */ public function __construct(BaseConnection $db) { - $this->db = &$db; + $this->db = $db; } /** @@ -804,9 +804,7 @@ protected function _alterTable(string $alterType, string $table, $fields) $fields = explode(',', $fields); } - $fields = array_map(function ($field) { - return 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)); - }, $fields); + $fields = array_map(fn ($field) => 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)), $fields); return $sql . implode(', ', $fields); } @@ -982,6 +980,8 @@ protected function _attributeDefault(array &$attributes, array &$field) // Override the NULL attribute if that's our default $attributes['NULL'] = true; $field['null'] = empty($this->null) ? '' : ' ' . $this->null; + } elseif ($attributes['DEFAULT'] instanceof RawSql) { + $field['default'] = $this->default . $attributes['DEFAULT']; } else { $field['default'] = $this->default . $this->db->escape($attributes['DEFAULT']); } diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 517688889ec6..e92d31b02450 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -40,7 +40,8 @@ class MigrationRunner protected $table; /** - * The Namespace where migrations can be found. + * The Namespace where migrations can be found. + * `null` is all namespaces. * * @var string|null */ @@ -423,7 +424,7 @@ public function findNamespaceMigrations(string $namespace): array if (! empty($this->path)) { helper('filesystem'); $dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/'; - $files = get_filenames($dir, true); + $files = get_filenames($dir, true, false, false); } else { $files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/'); } diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php new file mode 100644 index 000000000000..a954ec2337ad --- /dev/null +++ b/system/Database/OCI8/Builder.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\Exceptions\DatabaseException; + +/** + * Builder for OCI8 + */ +class Builder extends BaseBuilder +{ + /** + * Identifier escape character + * + * @var string + */ + protected $escapeChar = '"'; + + /** + * ORDER BY random keyword + * + * @var array + */ + protected $randomKeyword = [ + '"DBMS_RANDOM"."RANDOM"', + ]; + + /** + * COUNT string + * + * @used-by CI_DB_driver::count_all() + * @used-by BaseBuilder::count_all_results() + * + * @var string + */ + protected $countString = 'SELECT COUNT(1) '; + + /** + * Limit used flag + * + * If we use LIMIT, we'll add a field that will + * throw off num_fields later. + * + * @var bool + */ + protected $limitUsed = false; + + /** + * A reference to the database connection. + * + * @var Connection + */ + protected $db; + + /** + * Generates a platform-specific insert string from the supplied data. + */ + protected function _insertBatch(string $table, array $keys, array $values): string + { + $insertKeys = implode(', ', $keys); + $hasPrimaryKey = in_array('PRIMARY', array_column($this->db->getIndexData($table), 'type'), true); + + // ORA-00001 measures + if ($hasPrimaryKey) { + $sql = 'INSERT INTO ' . $table . ' (' . $insertKeys . ") \n SELECT * FROM (\n"; + $selectQueryValues = []; + + foreach ($values as $value) { + $selectValues = implode(',', array_map(static fn ($value, $key) => $value . ' as ' . $key, explode(',', substr(substr($value, 1), 0, -1)), $keys)); + $selectQueryValues[] = 'SELECT ' . $selectValues . ' FROM DUAL'; + } + + return $sql . implode("\n UNION ALL \n", $selectQueryValues) . "\n)"; + } + + $sql = "INSERT ALL\n"; + + foreach ($values as $value) { + $sql .= ' INTO ' . $table . ' (' . $insertKeys . ') VALUES ' . $value . "\n"; + } + + return $sql . 'SELECT * FROM DUAL'; + } + + /** + * Generates a platform-specific replace string from the supplied data + */ + protected function _replace(string $table, array $keys, array $values): string + { + $fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys); + + $uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) { + $hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields); + + return ($index->type === 'PRIMARY') && $hasAllFields; + }); + $replaceableFields = array_filter($keys, static function ($columnName) use ($uniqueIndexes) { + foreach ($uniqueIndexes as $index) { + if (in_array(trim($columnName, '"'), $index->fields, true)) { + return false; + } + } + + return true; + }); + + $sql = 'MERGE INTO ' . $table . "\n USING (SELECT "; + + $sql .= implode(', ', array_map(static fn ($columnName, $value) => $value . ' ' . $columnName, $keys, $values)); + + $sql .= ' FROM DUAL) "_replace" ON ( '; + + $onList = []; + $onList[] = '1 != 1'; + + foreach ($uniqueIndexes as $index) { + $onList[] = '(' . implode(' AND ', array_map(static fn ($columnName) => $table . '."' . $columnName . '" = "_replace"."' . $columnName . '"', $index->fields)) . ')'; + } + + $sql .= implode(' OR ', $onList) . ') WHEN MATCHED THEN UPDATE SET '; + + $sql .= implode(', ', array_map(static fn ($columnName) => $columnName . ' = "_replace".' . $columnName, $replaceableFields)); + + $sql .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $replaceableFields) . ') VALUES '; + + return $sql . (' (' . implode(', ', array_map(static fn ($columnName) => '"_replace".' . $columnName, $replaceableFields)) . ')'); + } + + /** + * Generates a platform-specific truncate string from the supplied data + * + * If the database does not support the truncate() command, + * then this method maps to 'DELETE FROM table' + */ + protected function _truncate(string $table): string + { + return 'TRUNCATE TABLE ' . $table; + } + + /** + * Compiles a delete string and runs the query + * + * @param mixed $where + * + * @throws DatabaseException + * + * @return mixed + */ + public function delete($where = '', ?int $limit = null, bool $resetData = true) + { + if (! empty($limit)) { + $this->QBLimit = $limit; + } + + return parent::delete($where, null, $resetData); + } + + /** + * Generates a platform-specific delete string from the supplied data + */ + protected function _delete(string $table): string + { + if ($this->QBLimit) { + $this->where('rownum <= ', $this->QBLimit, false); + $this->QBLimit = false; + } + + return parent::_delete($table); + } + + /** + * Generates a platform-specific update string from the supplied data + */ + protected function _update(string $table, array $values): string + { + $valStr = []; + + foreach ($values as $key => $val) { + $valStr[] = $key . ' = ' . $val; + } + + if ($this->QBLimit) { + $this->where('rownum <= ', $this->QBLimit, false); + } + + return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) + . $this->compileWhereHaving('QBWhere') + . $this->compileOrderBy(); + } + + /** + * Generates a platform-specific LIMIT clause. + */ + protected function _limit(string $sql, bool $offsetIgnore = false): string + { + $offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0); + if (version_compare($this->db->getVersion(), '12.1', '>=')) { + // OFFSET-FETCH can be used only with the ORDER BY clause + if (empty($this->QBOrderBy)) { + $sql .= ' ORDER BY 1'; + } + + return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY'; + } + + $this->limitUsed = true; + $limitTemplateQuery = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (%s) INNER_QUERY WHERE ROWNUM < %d)' . ($offset ? ' WHERE RNUM >= %d' : ''); + + return sprintf($limitTemplateQuery, $sql, $offset + $this->QBLimit + 1, $offset); + } + + /** + * Resets the query builder values. Called by the get() function + */ + protected function resetSelect() + { + $this->limitUsed = false; + parent::resetSelect(); + } +} diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php new file mode 100644 index 000000000000..dc0b26f57c0e --- /dev/null +++ b/system/Database/OCI8/Connection.php @@ -0,0 +1,720 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\ConnectionInterface; +use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\Query; +use ErrorException; +use stdClass; + +/** + * Connection for OCI8 + */ +class Connection extends BaseConnection implements ConnectionInterface +{ + /** + * Database driver + * + * @var string + */ + protected $DBDriver = 'OCI8'; + + /** + * Identifier escape character + * + * @var string + */ + public $escapeChar = '"'; + + /** + * List of reserved identifiers + * + * Identifiers that must NOT be escaped. + * + * @var array + */ + protected $reservedIdentifiers = [ + '*', + 'rownum', + ]; + + protected $validDSNs = [ + 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS + // Easy Connect string (Oracle 10g+) + 'ec' => '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i', + 'in' => '/^[a-z0-9$_]+$/i', // Instance name (defined in tnsnames.ora) + ]; + + /** + * Reset $stmtId flag + * + * Used by storedProcedure() to prevent execute() from + * re-setting the statement ID. + */ + protected $resetStmtId = true; + + /** + * Statement ID + * + * @var resource + */ + protected $stmtId; + + /** + * Commit mode flag + * + * @used-by PreparedQuery::_execute() + * + * @var int + */ + public $commitMode = OCI_COMMIT_ON_SUCCESS; + + /** + * Cursor ID + * + * @var resource + */ + protected $cursorId; + + /** + * Latest inserted table name. + * + * @used-by PreparedQuery::_execute() + * + * @var string|null + */ + public $lastInsertedTableName; + + /** + * confirm DNS format. + */ + private function isValidDSN(): bool + { + foreach ($this->validDSNs as $regexp) { + if (preg_match($regexp, $this->DSN)) { + return true; + } + } + + return false; + } + + /** + * Connect to the database. + * + * @return mixed + */ + public function connect(bool $persistent = false) + { + if (empty($this->DSN) && ! $this->isValidDSN()) { + $this->buildDSN(); + } + + $func = $persistent ? 'oci_pconnect' : 'oci_connect'; + + return empty($this->charset) + ? $func($this->username, $this->password, $this->DSN) + : $func($this->username, $this->password, $this->DSN, $this->charset); + } + + /** + * Keep or establish the connection if no queries have been sent for + * a length of time exceeding the server's idle timeout. + * + * @return void + */ + public function reconnect() + { + } + + /** + * Close the database connection. + * + * @return void + */ + protected function _close() + { + if (is_resource($this->cursorId)) { + oci_free_statement($this->cursorId); + } + if (is_resource($this->stmtId)) { + oci_free_statement($this->stmtId); + } + oci_close($this->connID); + } + + /** + * Select a specific database table to use. + */ + public function setDatabase(string $databaseName): bool + { + return false; + } + + /** + * Returns a string containing the version of the database being used. + */ + public function getVersion(): string + { + if (isset($this->dataCache['version'])) { + return $this->dataCache['version']; + } + + if (! $this->connID || ($versionString = oci_server_version($this->connID)) === false) { + return ''; + } + if (preg_match('#Release\s(\d+(?:\.\d+)+)#', $versionString, $match)) { + return $this->dataCache['version'] = $match[1]; + } + + return ''; + } + + /** + * Executes the query against the database. + * + * @return false|resource + */ + protected function execute(string $sql) + { + try { + if ($this->resetStmtId === true) { + $this->stmtId = oci_parse($this->connID, $sql); + } + + oci_set_prefetch($this->stmtId, 1000); + + $result = oci_execute($this->stmtId, $this->commitMode) ? $this->stmtId : false; + $insertTableName = $this->parseInsertTableName($sql); + + if ($result && $insertTableName !== '') { + $this->lastInsertedTableName = $insertTableName; + } + + return $result; + } catch (ErrorException $e) { + log_message('error', $e->getMessage()); + + if ($this->DBDebug) { + throw $e; + } + } + + return false; + } + + /** + * Get the table name for the insert statement from sql. + */ + public function parseInsertTableName(string $sql): string + { + $commentStrippedSql = preg_replace(['/\/\*(.|\n)*?\*\//m', '/--.+/'], '', $sql); + $isInsertQuery = strpos(strtoupper(ltrim($commentStrippedSql)), 'INSERT') === 0; + + if (! $isInsertQuery) { + return ''; + } + + preg_match('/(?is)\b(?:into)\s+("?\w+"?)/', $commentStrippedSql, $match); + $tableName = $match[1] ?? ''; + + return strpos($tableName, '"') === 0 ? trim($tableName, '"') : strtoupper($tableName); + } + + /** + * Returns the total number of rows affected by this query. + */ + public function affectedRows(): int + { + return oci_num_rows($this->stmtId); + } + + /** + * Generates the SQL for listing tables in a platform-dependent manner. + */ + protected function _listTables(bool $prefixLimit = false): string + { + $sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"'; + + if ($prefixLimit !== false && $this->DBPrefix !== '') { + return $sql . ' WHERE "TABLE_NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . "%' " + . sprintf($this->likeEscapeStr, $this->likeEscapeChar); + } + + return $sql; + } + + /** + * Generates a platform-specific query string so that the column names can be fetched. + */ + protected function _listColumns(string $table = ''): string + { + if (strpos($table, '.') !== false) { + sscanf($table, '%[^.].%s', $owner, $table); + } else { + $owner = $this->username; + } + + return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . ' + AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($this->DBPrefix . $table)); + } + + /** + * Returns an array of objects with field data + * + * @throws DatabaseException + * + * @return stdClass[] + */ + protected function _fieldData(string $table): array + { + if (strpos($table, '.') !== false) { + sscanf($table, '%[^.].%s', $owner, $table); + } else { + $owner = $this->username; + } + + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE + FROM ALL_TAB_COLUMNS + WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . ' + AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($table)); + + if (($query = $this->query($sql)) === false) { + throw new DatabaseException(lang('Database.failGetFieldData')); + } + $query = $query->getResultObject(); + + $retval = []; + + for ($i = 0, $c = count($query); $i < $c; $i++) { + $retval[$i] = new stdClass(); + $retval[$i]->name = $query[$i]->COLUMN_NAME; + $retval[$i]->type = $query[$i]->DATA_TYPE; + + $length = $query[$i]->CHAR_LENGTH > 0 ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION; + $length ??= $query[$i]->DATA_LENGTH; + + $retval[$i]->max_length = $length; + + $default = $query[$i]->DATA_DEFAULT; + if ($default === null && $query[$i]->NULLABLE === 'N') { + $default = ''; + } + $retval[$i]->default = $default; + $retval[$i]->nullable = $query[$i]->NULLABLE === 'Y'; + } + + return $retval; + } + + /** + * Returns an array of objects with index data + * + * @throws DatabaseException + * + * @return stdClass[] + */ + protected function _indexData(string $table): array + { + if (strpos($table, '.') !== false) { + sscanf($table, '%[^.].%s', $owner, $table); + } else { + $owner = $this->username; + } + + $sql = 'SELECT AIC.INDEX_NAME, UC.CONSTRAINT_TYPE, AIC.COLUMN_NAME ' + . ' FROM ALL_IND_COLUMNS AIC ' + . ' LEFT JOIN USER_CONSTRAINTS UC ON AIC.INDEX_NAME = UC.CONSTRAINT_NAME AND AIC.TABLE_NAME = UC.TABLE_NAME ' + . 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtolower($table)) . ' ' + . 'AND AIC.TABLE_OWNER = ' . $this->escape(strtoupper($owner)) . ' ' + . ' ORDER BY UC.CONSTRAINT_TYPE, AIC.COLUMN_POSITION'; + + if (($query = $this->query($sql)) === false) { + throw new DatabaseException(lang('Database.failGetIndexData')); + } + $query = $query->getResultObject(); + + $retVal = []; + $constraintTypes = [ + 'P' => 'PRIMARY', + 'U' => 'UNIQUE', + ]; + + foreach ($query as $row) { + if (isset($retVal[$row->INDEX_NAME])) { + $retVal[$row->INDEX_NAME]->fields[] = $row->COLUMN_NAME; + + continue; + } + + $retVal[$row->INDEX_NAME] = new stdClass(); + $retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME; + $retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME]; + $retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX'; + } + + return $retVal; + } + + /** + * Returns an array of objects with Foreign key data + * + * @throws DatabaseException + * + * @return stdClass[] + */ + protected function _foreignKeyData(string $table): array + { + $sql = 'SELECT + acc.constraint_name, + acc.table_name, + acc.column_name, + ccu.table_name foreign_table_name, + accu.column_name foreign_column_name + FROM all_cons_columns acc + JOIN all_constraints ac + ON acc.owner = ac.owner + AND acc.constraint_name = ac.constraint_name + JOIN all_constraints ccu + ON ac.r_owner = ccu.owner + AND ac.r_constraint_name = ccu.constraint_name + JOIN all_cons_columns accu + ON accu.constraint_name = ccu.constraint_name + AND accu.table_name = ccu.table_name + WHERE ac.constraint_type = ' . $this->escape('R') . ' + AND acc.table_name = ' . $this->escape($table); + $query = $this->query($sql); + + if ($query === false) { + throw new DatabaseException(lang('Database.failGetForeignKeyData')); + } + $query = $query->getResultObject(); + + $retVal = []; + + foreach ($query as $row) { + $obj = new stdClass(); + $obj->constraint_name = $row->CONSTRAINT_NAME; + $obj->table_name = $row->TABLE_NAME; + $obj->column_name = $row->COLUMN_NAME; + $obj->foreign_table_name = $row->FOREIGN_TABLE_NAME; + $obj->foreign_column_name = $row->FOREIGN_COLUMN_NAME; + $retVal[] = $obj; + } + + return $retVal; + } + + /** + * Returns platform-specific SQL to disable foreign key checks. + * + * @return string + */ + protected function _disableForeignKeyChecks() + { + return <<<'SQL' + BEGIN + FOR c IN + (SELECT c.owner, c.table_name, c.constraint_name + FROM user_constraints c, user_tables t + WHERE c.table_name = t.table_name + AND c.status = 'ENABLED' + AND c.constraint_type = 'R' + AND t.iot_type IS NULL + ORDER BY c.constraint_type DESC) + LOOP + dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" disable constraint "' || c.constraint_name || '"'); + END LOOP; + END; + SQL; + } + + /** + * Returns platform-specific SQL to enable foreign key checks. + * + * @return string + */ + protected function _enableForeignKeyChecks() + { + return <<<'SQL' + BEGIN + FOR c IN + (SELECT c.owner, c.table_name, c.constraint_name + FROM user_constraints c, user_tables t + WHERE c.table_name = t.table_name + AND c.status = 'DISABLED' + AND c.constraint_type = 'R' + AND t.iot_type IS NULL + ORDER BY c.constraint_type DESC) + LOOP + dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" enable constraint "' || c.constraint_name || '"'); + END LOOP; + END; + SQL; + } + + /** + * Get cursor. Returns a cursor from the database + * + * @return resource + */ + public function getCursor() + { + return $this->cursorId = oci_new_cursor($this->connID); + } + + /** + * Executes a stored procedure + * + * @param string $procedureName procedure name to execute + * @param array $params params array keys + * KEY OPTIONAL NOTES + * name no the name of the parameter should be in : format + * value no the value of the parameter. If this is an OUT or IN OUT parameter, + * this should be a reference to a variable + * type yes the type of the parameter + * length yes the max size of the parameter + * + * @return bool|Query|Result + */ + public function storedProcedure(string $procedureName, array $params) + { + if ($procedureName === '') { + throw new DatabaseException(lang('Database.invalidArgument', [$procedureName])); + } + + // Build the query string + $sql = sprintf( + 'BEGIN %s (' . substr(str_repeat(',%s', count($params)), 1) . '); END;', + $procedureName, + ...array_map(static fn ($row) => $row['name'], $params) + ); + + $this->resetStmtId = false; + $this->stmtId = oci_parse($this->connID, $sql); + $this->bindParams($params); + $result = $this->query($sql); + $this->resetStmtId = true; + + return $result; + } + + /** + * Bind parameters + * + * @param array $params + * + * @return void + */ + protected function bindParams($params) + { + if (! is_array($params) || ! is_resource($this->stmtId)) { + return; + } + + foreach ($params as $param) { + oci_bind_by_name( + $this->stmtId, + $param['name'], + $param['value'], + $param['length'] ?? -1, + $param['type'] ?? SQLT_CHR + ); + } + } + + /** + * Returns the last error code and message. + * + * Must return an array with keys 'code' and 'message': + * + * return ['code' => null, 'message' => null); + */ + public function error(): array + { + // oci_error() returns an array that already contains + // 'code' and 'message' keys, but it can return false + // if there was no error .... + $error = oci_error(); + $resources = [$this->cursorId, $this->stmtId, $this->connID]; + + foreach ($resources as $resource) { + if (is_resource($resource)) { + $error = oci_error($resource); + break; + } + } + + return is_array($error) + ? $error + : [ + 'code' => '', + 'message' => '', + ]; + } + + public function insertID(): int + { + if (empty($this->lastInsertedTableName)) { + return 0; + } + + $indexs = $this->getIndexData($this->lastInsertedTableName); + $fieldDatas = $this->getFieldData($this->lastInsertedTableName); + + if (! $indexs || ! $fieldDatas) { + return 0; + } + + $columnTypeList = array_column($fieldDatas, 'type', 'name'); + $primaryColumnName = ''; + + foreach ($indexs as $index) { + if ($index->type !== 'PRIMARY' || count($index->fields) !== 1) { + continue; + } + + $primaryColumnName = $this->protectIdentifiers($index->fields[0], false, false); + $primaryColumnType = $columnTypeList[$primaryColumnName]; + + if ($primaryColumnType !== 'NUMBER') { + $primaryColumnName = ''; + } + } + + if (! $primaryColumnName) { + return 0; + } + + $query = $this->query('SELECT DATA_DEFAULT FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?', [$this->lastInsertedTableName, $primaryColumnName])->getRow(); + $lastInsertValue = str_replace('nextval', 'currval', $query->DATA_DEFAULT ?? '0'); + $query = $this->query(sprintf('SELECT %s SEQ FROM DUAL', $lastInsertValue))->getRow(); + + return (int) ($query->SEQ ?? 0); + } + + /** + * Build a DSN from the provided parameters + * + * @return void + */ + protected function buildDSN() + { + if ($this->DSN !== '') { + $this->DSN = ''; + } + + // Legacy support for TNS in the hostname configuration field + $this->hostname = str_replace(["\n", "\r", "\t", ' '], '', $this->hostname); + + if (preg_match($this->validDSNs['tns'], $this->hostname)) { + $this->DSN = $this->hostname; + + return; + } + + $isEasyConnectableHostName = $this->hostname !== '' && strpos($this->hostname, '/') === false && strpos($this->hostname, ':') === false; + $easyConnectablePort = ! empty($this->port) && ctype_digit($this->port) ? ':' . $this->port : ''; + $easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : ''; + + if ($isEasyConnectableHostName && ($easyConnectablePort !== '' || $easyConnectableDatabase !== '')) { + /* If the hostname field isn't empty, doesn't contain + * ':' and/or '/' and if port and/or database aren't + * empty, then the hostname field is most likely indeed + * just a hostname. Therefore we'll try and build an + * Easy Connect string from these 3 settings, assuming + * that the database field is a service name. + */ + $this->DSN = $this->hostname . $easyConnectablePort . $easyConnectableDatabase; + + if (preg_match($this->validDSNs['ec'], $this->DSN)) { + return; + } + } + + /* At this point, we can only try and validate the hostname and + * database fields separately as DSNs. + */ + if (preg_match($this->validDSNs['ec'], $this->hostname) || preg_match($this->validDSNs['in'], $this->hostname)) { + $this->DSN = $this->hostname; + + return; + } + + $this->database = str_replace(["\n", "\r", "\t", ' '], '', $this->database); + + foreach ($this->validDSNs as $regexp) { + if (preg_match($regexp, $this->database)) { + return; + } + } + + /* Well - OK, an empty string should work as well. + * PHP will try to use environment variables to + * determine which Oracle instance to connect to. + */ + $this->DSN = ''; + } + + /** + * Begin Transaction + */ + protected function _transBegin(): bool + { + $this->commitMode = OCI_NO_AUTO_COMMIT; + + return true; + } + + /** + * Commit Transaction + */ + protected function _transCommit(): bool + { + $this->commitMode = OCI_COMMIT_ON_SUCCESS; + + return oci_commit($this->connID); + } + + /** + * Rollback Transaction + */ + protected function _transRollback(): bool + { + $this->commitMode = OCI_COMMIT_ON_SUCCESS; + + return oci_rollback($this->connID); + } + + /** + * Returns the name of the current database being used. + */ + public function getDatabase(): string + { + if (! empty($this->database)) { + return $this->database; + } + + return $this->query('SELECT DEFAULT_TABLESPACE FROM USER_USERS')->getRow()->DEFAULT_TABLESPACE ?? ''; + } + + /** + * Get the prefix of the function to access the DB. + */ + protected function getDriverFunctionPrefix(): string + { + return 'oci_'; + } +} diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php new file mode 100644 index 000000000000..9cd5bad945cd --- /dev/null +++ b/system/Database/OCI8/Forge.php @@ -0,0 +1,301 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use CodeIgniter\Database\Forge as BaseForge; + +/** + * Forge for OCI8 + */ +class Forge extends BaseForge +{ + /** + * DROP INDEX statement + * + * @var string + */ + protected $dropIndexStr = 'DROP INDEX %s'; + + /** + * CREATE DATABASE statement + * + * @var false + */ + protected $createDatabaseStr = false; + + /** + * CREATE TABLE IF statement + * + * @var false + */ + protected $createTableIfStr = false; + + /** + * DROP TABLE IF EXISTS statement + * + * @var false + */ + protected $dropTableIfStr = false; + + /** + * DROP DATABASE statement + * + * @var false + */ + protected $dropDatabaseStr = false; + + /** + * UNSIGNED support + * + * @var array|bool + */ + protected $unsigned = false; + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $null = 'NULL'; + + /** + * RENAME TABLE statement + * + * @var string + */ + protected $renameTableStr = 'ALTER TABLE %s RENAME TO %s'; + + /** + * DROP CONSTRAINT statement + * + * @var string + */ + protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; + + /** + * ALTER TABLE + * + * @param string $alterType ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * + * @return string|string[] + */ + protected function _alterTable(string $alterType, string $table, $field) + { + $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); + + if ($alterType === 'DROP') { + $fields = array_map(fn ($field) => $this->db->escapeIdentifiers(trim($field)), is_string($field) ? explode(',', $field) : $field); + + return $sql . ' DROP (' . implode(',', $fields) . ') CASCADE CONSTRAINT INVALIDATE'; + } + if ($alterType === 'CHANGE') { + $alterType = 'MODIFY'; + } + + $nullableMap = array_column($this->db->getFieldData($table), 'nullable', 'name'); + $sqls = []; + + for ($i = 0, $c = count($field); $i < $c; $i++) { + if ($alterType === 'MODIFY') { + // If a null constraint is added to a column with a null constraint, + // ORA-01451 will occur, + // so add null constraint is used only when it is different from the current null constraint. + $isWantToAddNull = strpos($field[$i]['null'], ' NOT') === false; + $currentNullAddable = $nullableMap[$field[$i]['name']]; + + if ($isWantToAddNull === $currentNullAddable) { + $field[$i]['null'] = ''; + } + } + + if ($field[$i]['_literal'] !== false) { + $field[$i] = "\n\t" . $field[$i]['_literal']; + } else { + $field[$i]['_literal'] = "\n\t" . $this->_processColumn($field[$i]); + + if (! empty($field[$i]['comment'])) { + $sqls[] = 'COMMENT ON COLUMN ' + . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($field[$i]['name']) + . ' IS ' . $field[$i]['comment']; + } + + if ($alterType === 'MODIFY' && ! empty($field[$i]['new_name'])) { + $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field[$i]['name']) + . ' TO ' . $this->db->escapeIdentifiers($field[$i]['new_name']); + } + + $field[$i] = "\n\t" . $field[$i]['_literal']; + } + } + + $sql .= ' ' . $alterType . ' '; + $sql .= count($field) === 1 + ? $field[0] + : '(' . implode(',', $field) . ')'; + + // RENAME COLUMN must be executed after MODIFY + array_unshift($sqls, $sql); + + return $sqls; + } + + /** + * Field attribute AUTO_INCREMENT + * + * @return void + */ + protected function _attributeAutoIncrement(array &$attributes, array &$field) + { + if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + && stripos($field['type'], 'NUMBER') !== false + && version_compare($this->db->getVersion(), '12.1', '>=') + ) { + $field['auto_increment'] = ' GENERATED BY DEFAULT AS IDENTITY'; + } + } + + /** + * Process column + */ + protected function _processColumn(array $field): string + { + $constraint = ''; + // @todo: can’t cover multi pattern when set type. + if ($field['type'] === 'VARCHAR2' && strpos($field['length'], "('") === 0) { + $constraint = ' CHECK(' . $this->db->escapeIdentifiers($field['name']) + . ' IN ' . $field['length'] . ')'; + + $field['length'] = '(' . max(array_map('mb_strlen', explode("','", mb_substr($field['length'], 2, -2)))) . ')' . $constraint; + } elseif (count($this->primaryKeys) === 1 && $field['name'] === $this->primaryKeys[0]) { + $field['unique'] = ''; + } + + return $this->db->escapeIdentifiers($field['name']) + . ' ' . $field['type'] . $field['length'] + . $field['unsigned'] + . $field['default'] + . $field['auto_increment'] + . $field['null'] + . $field['unique']; + } + + /** + * Performs a data type mapping between different databases. + * + * @return void + */ + protected function _attributeType(array &$attributes) + { + // Reset field lengths for data types that don't support it + // Usually overridden by drivers + switch (strtoupper($attributes['TYPE'])) { + case 'TINYINT': + $attributes['CONSTRAINT'] ??= 3; + // no break + case 'SMALLINT': + $attributes['CONSTRAINT'] ??= 5; + // no break + case 'MEDIUMINT': + $attributes['CONSTRAINT'] ??= 7; + // no break + case 'INT': + case 'INTEGER': + $attributes['CONSTRAINT'] ??= 11; + // no break + case 'BIGINT': + $attributes['CONSTRAINT'] ??= 19; + // no break + case 'NUMERIC': + $attributes['TYPE'] = 'NUMBER'; + + return; + + case 'BOOLEAN': + $attributes['TYPE'] = 'NUMBER'; + $attributes['CONSTRAINT'] = 1; + $attributes['UNSIGNED'] = true; + $attributes['NULL'] = false; + + return; + + case 'DOUBLE': + $attributes['TYPE'] = 'FLOAT'; + $attributes['CONSTRAINT'] ??= 126; + + return; + + case 'DATETIME': + case 'TIME': + $attributes['TYPE'] = 'DATE'; + + return; + + case 'SET': + case 'ENUM': + case 'VARCHAR': + $attributes['CONSTRAINT'] ??= 255; + // no break + case 'TEXT': + case 'MEDIUMTEXT': + $attributes['CONSTRAINT'] ??= 4000; + $attributes['TYPE'] = 'VARCHAR2'; + } + } + + /** + * Generates a platform-specific DROP TABLE string + * + * @return bool|string + */ + protected function _dropTable(string $table, bool $ifExists, bool $cascade) + { + $sql = parent::_dropTable($table, $ifExists, $cascade); + + if ($sql !== true && $cascade === true) { + $sql .= ' CASCADE CONSTRAINTS PURGE'; + } elseif ($sql !== true) { + $sql .= ' PURGE'; + } + + return $sql; + } + + protected function _processForeignKeys(string $table): string + { + $sql = ''; + + $allowActions = [ + 'CASCADE', + 'SET NULL', + 'NO ACTION', + ]; + + foreach ($this->foreignKeys as $fkey) { + $nameIndex = $table . '_' . implode('_', $fkey['field']) . '_fk'; + $nameIndexFilled = $this->db->escapeIdentifiers($nameIndex); + $foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field'])); + $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']); + $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField'])); + + $formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)"; + $sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled); + + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) { + $sql .= ' ON DELETE ' . $fkey['onDelete']; + } + } + + return $sql; + } +} diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php new file mode 100644 index 000000000000..311dacc4045f --- /dev/null +++ b/system/Database/OCI8/PreparedQuery.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use BadMethodCallException; +use CodeIgniter\Database\BasePreparedQuery; +use CodeIgniter\Database\PreparedQueryInterface; + +/** + * Prepared query for OCI8 + */ +class PreparedQuery extends BasePreparedQuery implements PreparedQueryInterface +{ + /** + * A reference to the db connection to use. + * + * @var Connection + */ + protected $db; + + /** + * Latest inserted table name. + */ + private ?string $lastInsertTableName = null; + + /** + * Prepares the query against the database, and saves the connection + * info necessary to execute the query later. + * + * NOTE: This version is based on SQL code. Child classes should + * override this method. + * + * @param array $options Passed to the connection's prepare statement. + * Unused in the OCI8 driver. + * + * @return mixed + */ + public function _prepare(string $sql, array $options = []) + { + if (! $this->statement = oci_parse($this->db->connID, $this->parameterize($sql))) { + $error = oci_error($this->db->connID); + $this->errorCode = $error['code'] ?? 0; + $this->errorString = $error['message'] ?? ''; + } + + $this->lastInsertTableName = $this->db->parseInsertTableName($sql); + + return $this; + } + + /** + * Takes a new set of data and runs it against the currently + * prepared query. Upon success, will return a Results object. + */ + public function _execute(array $data): bool + { + if (null === $this->statement) { + throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.'); + } + + $lastKey = 0; + + foreach (array_keys($data) as $key) { + oci_bind_by_name($this->statement, ':' . $key, $data[$key]); + $lastKey = $key; + } + + $result = oci_execute($this->statement, $this->db->commitMode); + + if ($result && $this->lastInsertTableName !== '') { + $this->db->lastInsertedTableName = $this->lastInsertTableName; + } + + return $result; + } + + /** + * Returns the result object for the prepared query. + * + * @return mixed + */ + public function _getResult() + { + return $this->statement; + } + + /** + * Replaces the ? placeholders with :0, :1, etc parameters for use + * within the prepared query. + */ + public function parameterize(string $sql): string + { + // Track our current value + $count = 0; + + return preg_replace_callback('/\?/', static function ($matches) use (&$count) { + return ':' . ($count++); + }, $sql); + } +} diff --git a/system/Database/OCI8/Result.php b/system/Database/OCI8/Result.php new file mode 100644 index 000000000000..72a1b0980a28 --- /dev/null +++ b/system/Database/OCI8/Result.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use CodeIgniter\Database\BaseResult; +use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Entity; + +/** + * Result for OCI8 + */ +class Result extends BaseResult implements ResultInterface +{ + /** + * Gets the number of fields in the result set. + */ + public function getFieldCount(): int + { + return oci_num_fields($this->resultID); + } + + /** + * Generates an array of column names in the result set. + */ + public function getFieldNames(): array + { + return array_map(fn ($fieldIndex) => oci_field_name($this->resultID, $fieldIndex), range(1, $this->getFieldCount())); + } + + /** + * Generates an array of objects representing field meta-data. + */ + public function getFieldData(): array + { + return array_map(fn ($fieldIndex) => (object) [ + 'name' => oci_field_name($this->resultID, $fieldIndex), + 'type' => oci_field_type($this->resultID, $fieldIndex), + 'max_length' => oci_field_size($this->resultID, $fieldIndex), + ], range(1, $this->getFieldCount())); + } + + /** + * Frees the current result. + * + * @return void + */ + public function freeResult() + { + if (is_resource($this->resultID)) { + oci_free_statement($this->resultID); + $this->resultID = false; + } + } + + /** + * Moves the internal pointer to the desired offset. This is called + * internally before fetching results to make sure the result set + * starts at zero. + * + * @return false + */ + public function dataSeek(int $n = 0) + { + // We can't support data seek by oci + return false; + } + + /** + * Returns the result set as an array. + * + * Overridden by driver classes. + * + * @return mixed + */ + protected function fetchAssoc() + { + return oci_fetch_assoc($this->resultID); + } + + /** + * Returns the result set as an object. + * + * Overridden by child classes. + * + * @return bool|Entity|object + */ + protected function fetchObject(string $className = 'stdClass') + { + $row = oci_fetch_object($this->resultID); + + if ($className === 'stdClass' || ! $row) { + return $row; + } + if (is_subclass_of($className, Entity::class)) { + return (new $className())->setAttributes((array) $row); + } + + $instance = new $className(); + + foreach (get_object_vars($row) as $key => $value) { + $instance->{$key} = $value; + } + + return $instance; + } +} diff --git a/system/Database/OCI8/Utils.php b/system/Database/OCI8/Utils.php new file mode 100644 index 000000000000..870306d8b8b1 --- /dev/null +++ b/system/Database/OCI8/Utils.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\OCI8; + +use CodeIgniter\Database\BaseUtils; +use CodeIgniter\Database\Exceptions\DatabaseException; + +/** + * Utils for OCI8 + */ +class Utils extends BaseUtils +{ + /** + * List databases statement + * + * @var string + */ + protected $listDatabases = 'SELECT TABLESPACE_NAME FROM USER_TABLESPACES'; + + /** + * Platform dependent version of the backup function. + * + * @return mixed + */ + public function _backup(?array $prefs = null) + { + throw new DatabaseException('Unsupported feature of the database platform you are using.'); + } +} diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 1006ab20012e..1d80f687568c 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -13,6 +13,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\RawSql; /** * Builder for Postgre @@ -241,7 +242,7 @@ protected function _updateBatch(string $table, array $values, string $index): st foreach (array_keys($val) as $field) { if ($field !== $index) { - $final[$field] = $final[$field] ?? []; + $final[$field] ??= []; $final[$field][] = "WHEN {$val[$index]} THEN {$val[$field]}"; } @@ -300,9 +301,11 @@ protected function _like_statement(?string $prefix, string $column, ?string $not /** * Generates the JOIN portion of the query * + * @param RawSql|string $cond + * * @return BaseBuilder */ - public function join(string $table, string $cond, string $type = '', ?bool $escape = null) + public function join(string $table, $cond, string $type = '', ?bool $escape = null) { if (! in_array('FULL OUTER', $this->joinTypes, true)) { $this->joinTypes = array_merge($this->joinTypes, ['FULL OUTER']); diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 57b4c78fd749..6827ba70b54f 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -234,9 +234,9 @@ protected function _listColumns(string $table = ''): string */ protected function _fieldData(string $table): array { - $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default" - FROM "information_schema"."columns" - WHERE LOWER("table_name") = ' + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default", "is_nullable" + FROM "information_schema"."columns" + WHERE LOWER("table_name") = ' . $this->escape(strtolower($table)) . ' ORDER BY "ordinal_position"'; @@ -252,6 +252,7 @@ protected function _fieldData(string $table): array $retVal[$i]->name = $query[$i]->column_name; $retVal[$i]->type = $query[$i]->data_type; + $retVal[$i]->nullable = $query[$i]->is_nullable === 'YES'; $retVal[$i]->default = $query[$i]->column_default; $retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision; } @@ -284,9 +285,7 @@ protected function _indexData(string $table): array $obj = new stdClass(); $obj->name = $row->indexname; $_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef))); - $obj->fields = array_map(static function ($v) { - return trim($v); - }, $_fields); + $obj->fields = array_map(static fn ($v) => trim($v), $_fields); if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) { $obj->type = 'PRIMARY'; diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 53e42b867f0f..890534663256 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -52,7 +52,7 @@ class PreparedQuery extends BasePreparedQuery */ public function _prepare(string $sql, array $options = []) { - $this->name = (string) random_int(1, 10000000000000000); + $this->name = (string) random_int(1, 10_000_000_000_000_000); $sql = $this->parameterize($sql); diff --git a/system/Database/Query.php b/system/Database/Query.php index 91b98c77d2ef..702575ca857c 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -23,10 +23,17 @@ class Query implements QueryInterface */ protected $originalQueryString; + /** + * The query string if table prefix has been swapped. + * + * @var string|null + */ + protected $swappedQueryString; + /** * The final query string after binding, etc. * - * @var string + * @var string|null */ protected $finalQueryString; @@ -84,7 +91,7 @@ class Query implements QueryInterface */ public $db; - public function __construct(ConnectionInterface &$db) + public function __construct(ConnectionInterface $db) { $this->db = $db; } @@ -99,6 +106,7 @@ public function __construct(ConnectionInterface &$db) public function setQuery(string $sql, $binds = null, bool $setEscape = true) { $this->originalQueryString = $sql; + unset($this->swappedQueryString); if ($binds !== null) { if (! is_array($binds)) { @@ -116,6 +124,8 @@ public function setQuery(string $sql, $binds = null, bool $setEscape = true) $this->binds = $binds; } + unset($this->finalQueryString); + return $this; } @@ -134,6 +144,8 @@ public function setBinds(array $binds, bool $setEscape = true) $this->binds = $binds; + unset($this->finalQueryString); + return $this; } @@ -144,11 +156,9 @@ public function setBinds(array $binds, bool $setEscape = true) public function getQuery(): string { if (empty($this->finalQueryString)) { - $this->finalQueryString = $this->originalQueryString; + $this->compileBinds(); } - $this->compileBinds(); - return $this->finalQueryString; } @@ -251,9 +261,14 @@ public function isWriteType(): bool */ public function swapPrefix(string $orig, string $swap) { - $sql = empty($this->finalQueryString) ? $this->originalQueryString : $this->finalQueryString; + $sql = $this->swappedQueryString ?? $this->originalQueryString; + + $from = '/(\W)' . $orig . '(\S)/'; + $to = '\\1' . $swap . '\\2'; - $this->finalQueryString = preg_replace('/(\W)' . $orig . '(\S+?)/', '\\1' . $swap . '\\2', $sql); + $this->swappedQueryString = preg_replace($from, $to, $sql); + + unset($this->finalQueryString); return $this; } @@ -267,16 +282,18 @@ public function getOriginalQuery(): string } /** - * Escapes and inserts any binds into the finalQueryString object. + * Escapes and inserts any binds into the finalQueryString property. * * @see https://regex101.com/r/EUEhay/5 */ protected function compileBinds() { - $sql = $this->finalQueryString; + $sql = $this->swappedQueryString ?? $this->originalQueryString; $binds = $this->binds; if (empty($binds)) { + $this->finalQueryString = $sql; + return; } @@ -391,11 +408,7 @@ public function debugToolbarDisplay(): string 'WHERE', ]; - if (empty($this->finalQueryString)) { - $this->compileBinds(); // @codeCoverageIgnore - } - - $sql = esc($this->finalQueryString); + $sql = esc($this->getQuery()); /** * @see https://stackoverflow.com/a/20767160 @@ -403,9 +416,7 @@ public function debugToolbarDisplay(): string */ $search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/'; - return preg_replace_callback($search, static function ($matches) { - return '' . str_replace(' ', ' ', $matches[0]) . ''; - }, $sql); + return preg_replace_callback($search, static fn ($matches) => '' . str_replace(' ', ' ', $matches[0]) . '', $sql); } /** diff --git a/system/Database/RawSql.php b/system/Database/RawSql.php new file mode 100644 index 000000000000..7ecb7fd378ae --- /dev/null +++ b/system/Database/RawSql.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +class RawSql +{ + /** + * @var string Raw SQL string + */ + private string $string; + + public function __construct(string $sqlString) + { + $this->string = $sqlString; + } + + public function __toString(): string + { + return $this->string; + } + + /** + * Create new instance with new SQL string + */ + public function with(string $newSqlString): self + { + $new = clone $this; + $new->string = $newSqlString; + + return $new; + } + + /** + * Returns unique id for binding key + */ + public function getBindingKey(): string + { + return 'RawSql' . spl_object_id($this); + } +} diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 17d8bd576c9f..a5bf829a6b74 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -14,6 +14,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Database\RawSql; use CodeIgniter\Database\ResultInterface; /** @@ -68,7 +69,7 @@ protected function _fromTables(): string $from = []; foreach ($this->QBFrom as $value) { - $from[] = $this->getFullName($value); + $from[] = strpos($value, '(SELECT') === 0 ? $value : $this->getFullName($value); } return implode(', ', $from); @@ -88,9 +89,11 @@ protected function _truncate(string $table): string /** * Generates the JOIN portion of the query * + * @param RawSql|string $cond + * * @return $this */ - public function join(string $table, string $cond, string $type = '', ?bool $escape = null) + public function join(string $table, $cond, string $type = '', ?bool $escape = null) { if ($type !== '') { $type = strtoupper(trim($type)); @@ -382,9 +385,7 @@ protected function _replace(string $table, array $keys, array $values): string } // Get the unique field names - $escKeyFields = array_map(function (string $field): string { - return $this->db->protectIdentifiers($field); - }, array_values(array_unique($keyFields))); + $escKeyFields = array_map(fn (string $field): string => $this->db->protectIdentifiers($field), array_values(array_unique($keyFields))); // Get the binds $binds = $this->binds; @@ -596,7 +597,7 @@ protected function compileSelect($selectOverride = false): string $sql = $this->_limit($sql . "\n"); } - return $sql; + return $this->unionInjection($sql); } /** diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 3c86839d2e88..f180ca4f6b3b 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -120,6 +120,10 @@ public function connect(bool $persistent = false) unset($connection['UID'], $connection['PWD']); } + if (strpos($this->hostname, ',') === false && $this->port !== '') { + $this->hostname .= ', ' . $this->port; + } + sqlsrv_configure('WarningsReturnAsErrors', 0); $this->connID = sqlsrv_connect($this->hostname, $connection); @@ -229,9 +233,7 @@ protected function _indexData(string $table): array $obj->name = $row->index_name; $_fields = explode(',', trim($row->index_keys)); - $obj->fields = array_map(static function ($v) { - return trim($v); - }, $_fields); + $obj->fields = array_map(static fn ($v) => trim($v), $_fields); if (strpos($row->index_description, 'primary key located on') !== false) { $obj->type = 'PRIMARY'; @@ -467,6 +469,8 @@ protected function execute(string $sql) * Returns the last error encountered by this connection. * * @return mixed + * + * @deprecated Use `error()` instead. */ public function getError() { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 14d297604cbe..67ef057e12de 100755 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -152,9 +152,7 @@ protected function _alterTable(string $alterType, string $table, $field) $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP '; - $fields = array_map(static function ($item) { - return 'COLUMN [' . trim($item) . ']'; - }, (array) $field); + $fields = array_map(static fn ($item) => 'COLUMN [' . trim($item) . ']', (array) $field); return $sql . implode(',', $fields); } diff --git a/system/Database/SQLSRV/Utils.php b/system/Database/SQLSRV/Utils.php index cf94d3dad783..22a12bcdf02a 100755 --- a/system/Database/SQLSRV/Utils.php +++ b/system/Database/SQLSRV/Utils.php @@ -34,7 +34,7 @@ class Utils extends BaseUtils */ protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE'; - public function __construct(ConnectionInterface &$db) + public function __construct(ConnectionInterface $db) { parent::__construct($db); diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 39fbca8bcfea..35ede04c2c9c 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -37,6 +37,20 @@ class Connection extends BaseConnection */ public $escapeChar = '`'; + /** + * @var bool Enable Foreign Key constraint or not + */ + protected $foreignKeys = false; + + public function initialize() + { + parent::initialize(); + + if ($this->foreignKeys) { + $this->enableForeignKeyChecks(); + } + } + /** * Connect to the database. * diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 8f04c0f645f1..1ecb28fdf8b7 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -28,6 +28,7 @@ class Table * All of the fields this table represents. * * @var array + * @phpstan-var array> */ protected $fields = []; @@ -276,10 +277,18 @@ protected function copyData() $exFields[] = $name; } - $exFields = implode(', ', $exFields); - $newFields = implode(', ', $newFields); - - $this->db->query("INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}"); + $exFields = implode( + ', ', + array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields) + ); + $newFields = implode( + ', ', + array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields) + ); + + $this->db->query( + "INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}" + ); } /** @@ -289,6 +298,7 @@ protected function copyData() * @param array|bool $fields * * @return mixed + * @phpstan-return ($fields is array ? array : mixed) */ protected function formatFields($fields) { diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php index 92d281965c7e..893de7d56ec4 100644 --- a/system/Database/Seeder.php +++ b/system/Database/Seeder.php @@ -67,11 +67,9 @@ class Seeder /** * Faker Generator instance. * - * @var Generator|null - * * @deprecated */ - private static $faker; + private static ?Generator $faker = null; /** * Seeder constructor. @@ -92,9 +90,9 @@ public function __construct(Database $config, ?BaseConnection $db = null) $this->config = &$config; - $db = $db ?? Database::connect($this->DBGroup); + $db ??= Database::connect($this->DBGroup); - $this->db = &$db; + $this->db = $db; $this->forge = Database::forge($this->DBGroup); } diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index d319c2f9746e..879dab718a16 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -102,14 +102,19 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { - log_message('critical', $exception->getMessage() . "\n{trace}", [ - 'trace' => $exception->getTraceAsString(), + log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ + 'message' => $exception->getMessage(), + 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file + 'exLine' => $exception->getLine(), // {line} refers to THIS line + 'trace' => self::renderBacktrace($exception->getTrace()), ]); } if (! is_cli()) { $this->response->setStatusCode($statusCode); - header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode); + if (! headers_sent()) { + header(sprintf('HTTP/%s %s %s', $this->request->getProtocolVersion(), $this->response->getStatusCode(), $this->response->getReasonPhrase()), true, $statusCode); + } if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) { $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send(); @@ -318,6 +323,8 @@ protected function determineCodes(Throwable $exception): array /** * This makes nicer looking paths for the error output. + * + * @deprecated Use dedicated `clean_path()` function. */ public static function cleanPath(string $file): string { @@ -352,11 +359,11 @@ public static function describeMemory(int $bytes): string return $bytes . 'B'; } - if ($bytes < 1048576) { + if ($bytes < 1_048_576) { return round($bytes / 1024, 2) . 'KB'; } - return round($bytes / 1048576, 2) . 'MB'; + return round($bytes / 1_048_576, 2) . 'MB'; } /** @@ -430,4 +437,55 @@ public static function highlightFile(string $file, int $lineNumber, int $lines = return '
' . $out . '
'; } + + private static function renderBacktrace(array $backtrace): string + { + $backtraces = []; + + foreach ($backtrace as $index => $trace) { + $frame = $trace + ['file' => '[internal function]', 'line' => '', 'class' => '', 'type' => '', 'args' => []]; + + if ($frame['file'] !== '[internal function]') { + $frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']); + } + + unset($frame['line']); + $idx = $index; + $idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT); + + $args = implode(', ', array_map(static function ($value): string { + switch (true) { + case is_object($value): + return sprintf('Object(%s)', get_class($value)); + + case is_array($value): + return $value !== [] ? '[...]' : '[]'; + + case $value === null: + return 'null'; + + case is_resource($value): + return sprintf('resource (%s)', get_resource_type($value)); + + case is_string($value): + return var_export(clean_path($value), true); + + default: + return var_export($value, true); + } + }, $frame['args'])); + + $backtraces[] = sprintf( + '%s %s: %s%s%s(%s)', + $idx, + clean_path($frame['file']), + $frame['class'], + $frame['type'], + $frame['function'], + $args + ); + } + + return implode("\n", $backtraces); + } } diff --git a/system/Debug/Kint/RichRenderer.php b/system/Debug/Kint/RichRenderer.php deleted file mode 100644 index 756cac75e144..000000000000 --- a/system/Debug/Kint/RichRenderer.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Debug\Kint; - -use Kint\Renderer\RichRenderer as KintRichRenderer; - -/** - * Overrides RichRenderer::preRender() for CSP - */ -class RichRenderer extends KintRichRenderer -{ - public function preRender() - { - $output = ''; - - if ($this->pre_render) { - foreach (self::$pre_render_sources as $type => $values) { - $contents = ''; - - foreach ($values as $v) { - $contents .= $v($this); - } - - if (! \strlen($contents)) { - continue; - } - - switch ($type) { - case 'script': - $output .= ''; - break; - - case 'style': - $output .= ''; - break; - - default: - $output .= $contents; - } - } - - // Don't pre-render on every dump - if (! $this->force_pre_render) { - self::$needs_pre_render = false; - } - } - - $output .= '
'; - - return $output; - } -} diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 5783c77aae72..9ca51d1c9b08 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -78,7 +78,7 @@ public function stop(string $name) * @param string $name The name of the timer. * @param int $decimals Number of decimal places. * - * @return float|null Returns null if timer exists by that name. + * @return float|null Returns null if timer does not exist by that name. * Returns a float representing the number of * seconds elapsed while that timer was running. */ @@ -96,7 +96,7 @@ public function getElapsedTime(string $name, int $decimals = 4) $timer['end'] = microtime(true); } - return (float) number_format($timer['end'] - $timer['start'], $decimals); + return (float) number_format($timer['end'] - $timer['start'], $decimals, '.', ''); } /** diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 3502ae877caf..b91fb16023b0 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -53,8 +53,11 @@ public function __construct(ToolbarConfig $config) foreach ($config->collectors as $collector) { if (! class_exists($collector)) { - log_message('critical', 'Toolbar collector does not exists(' . $collector . ').' . - 'please check $collectors in the Config\Toolbar.php file.'); + log_message( + 'critical', + 'Toolbar collector does not exist (' . $collector . ').' + . ' Please check $collectors in the app/Config/Toolbar.php file.' + ); continue; } @@ -76,7 +79,7 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques { // Data items used within the view. $data['url'] = current_url(); - $data['method'] = $request->getMethod(true); + $data['method'] = strtoupper($request->getMethod()); $data['isAJAX'] = $request->isAJAX(); $data['startTime'] = $startTime; $data['totalTime'] = $totalTime * 1000; @@ -151,8 +154,13 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques 'statusCode' => $response->getStatusCode(), 'reason' => esc($response->getReasonPhrase()), 'contentType' => esc($response->getHeaderLine('content-type')), + 'headers' => [], ]; + foreach ($response->headers() as $header) { + $data['vars']['response']['headers'][esc($header->getName())] = esc($header->getValueLine()); + } + $data['config'] = Config::display(); if ($response->CSP !== null) { @@ -348,14 +356,14 @@ protected function roundTo(float $number, int $increments = 5): float public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null) { /** - * @var IncomingRequest $request - * @var Response $response + * @var IncomingRequest|null $request + * @var Response|null $response */ if (CI_DEBUG && ! is_cli()) { global $app; - $request = $request ?? Services::request(); - $response = $response ?? Services::response(); + $request ??= Services::request(); + $response ??= Services::response(); // Disable the toolbar for downloads if ($response instanceof DownloadResponse) { @@ -373,8 +381,8 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r helper('filesystem'); - // Updated to time() so we can get history - $time = time(); + // Updated to microtime() so we can get history + $time = sprintf('%.6f', microtime(true)); if (! is_dir(WRITEPATH . 'debugbar')) { mkdir(WRITEPATH . 'debugbar', 0777); @@ -402,11 +410,11 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r $kintScript = substr($kintScript, 0, strpos($kintScript, '') + 8); $script = PHP_EOL - . '' - . '' - . '' + . '' + . '' . $kintScript . PHP_EOL; @@ -480,10 +488,10 @@ protected function format(string $data, string $format = 'html'): string { $data = json_decode($data, true); - if ($this->config->maxHistory !== 0) { + if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) Services::request()->getGet('debugbar_time'), $debugbarTime)) { $history = new History(); $history->setFiles( - (int) Services::request()->getGet('debugbar_time'), + $debugbarTime[0], $this->config->maxHistory ); diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php index 5d8d39ff169f..e8aaad9ba115 100644 --- a/system/Debug/Toolbar/Collectors/BaseCollector.php +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -11,8 +11,6 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; -use CodeIgniter\Debug\Exceptions; - /** * Base Toolbar collector */ @@ -174,13 +172,13 @@ public function display() } /** - * Clean Path - * * This makes nicer looking paths for the error output. + * + * @deprecated Use the dedicated `clean_path()` function. */ public function cleanPath(string $file): string { - return Exceptions::cleanPath($file); + return clean_path($file); } /** diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 26cb02ddfeb0..6a845ae51bf3 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -208,10 +208,8 @@ public function getTitleDetails(): string { $this->getConnections(); - $queryCount = count(static::$queries); - $uniqueCount = count(array_filter(static::$queries, static function ($query) { - return $query['duplicate'] === false; - })); + $queryCount = count(static::$queries); + $uniqueCount = count(array_filter(static::$queries, static fn ($query) => $query['duplicate'] === false)); $connectionCount = count($this->connections); return sprintf( diff --git a/system/Debug/Toolbar/Collectors/Files.php b/system/Debug/Toolbar/Collectors/Files.php index d6aefd2c3fe9..ea14dcfd1ec3 100644 --- a/system/Debug/Toolbar/Collectors/Files.php +++ b/system/Debug/Toolbar/Collectors/Files.php @@ -58,7 +58,7 @@ public function display(): array $userFiles = []; foreach ($rawFiles as $file) { - $path = $this->cleanPath($file); + $path = clean_path($file); if (strpos($path, 'SYSTEMPATH') !== false) { $coreFiles[] = [ diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php index 3c0f4268f65e..afc68ee85f2e 100644 --- a/system/Debug/Toolbar/Collectors/History.php +++ b/system/Debug/Toolbar/Collectors/History.php @@ -11,6 +11,8 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; +use DateTime; + /** * History collector */ @@ -56,10 +58,10 @@ class History extends BaseCollector /** * Specify time limit & file count for debug history. * - * @param int $current Current history time - * @param int $limit Max history files + * @param string $current Current history time + * @param int $limit Max history files */ - public function setFiles(int $current, int $limit = 20) + public function setFiles(string $current, int $limit = 20) { $filenames = glob(WRITEPATH . 'debugbar/debugbar_*.json'); @@ -81,13 +83,13 @@ public function setFiles(int $current, int $limit = 20) $contents = @json_decode($contents); if (json_last_error() === JSON_ERROR_NONE) { - preg_match_all('/\d+/', $filename, $time); - $time = (int) end($time[0]); + preg_match('/debugbar_(.*)\.json$/s', $filename, $time); + $time = sprintf('%.6f', $time[1] ?? 0); // Debugbar files shown in History Collector $files[] = [ 'time' => $time, - 'datetime' => date('Y-m-d H:i:s', $time), + 'datetime' => DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u'), 'active' => $time === $current, 'status' => $contents->vars->response->statusCode, 'method' => $contents->method, diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 060d8b995872..5ea88c0411c8 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -78,9 +78,13 @@ public function display(): array foreach ($rawParams as $key => $param) { $params[] = [ - 'name' => $param->getName(), + 'name' => '$' . $param->getName() . ' = ', 'value' => $router->params()[$key] ?? - ('<empty> | default: ' . var_export($param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, true)), + ' | default: ' + . var_export( + $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, + true + ), ]; } diff --git a/system/Debug/Toolbar/Views/_config.tpl b/system/Debug/Toolbar/Views/_config.tpl index 3934cf62ebee..b6c7e0c3d86f 100644 --- a/system/Debug/Toolbar/Views/_config.tpl +++ b/system/Debug/Toolbar/Views/_config.tpl @@ -1,48 +1,48 @@

- Read the CodeIgniter docs... + Read the CodeIgniter docs...

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeIgniter Version:{ ciVersion }
PHP Version:{ phpVersion }
PHP SAPI:{ phpSAPI }
Environment:{ environment }
Base URL: - { if $baseURL == '' } -
- The $baseURL should always be set manually to prevent possible URL personification from external parties. -
- { else } - { baseURL } - { endif } -
Timezone:{ timezone }
Locale:{ locale }
Content Security Policy Enabled:{ if $cspEnabled } Yes { else } No { endif }
CodeIgniter Version:{ ciVersion }
PHP Version:{ phpVersion }
PHP SAPI:{ phpSAPI }
Environment:{ environment }
Base URL: + { if $baseURL == '' } +
+ The $baseURL should always be set manually to prevent possible URL personification from external parties. +
+ { else } + { baseURL } + { endif } +
Timezone:{ timezone }
Locale:{ locale }
Content Security Policy Enabled:{ if $cspEnabled } Yes { else } No { endif }
diff --git a/system/Debug/Toolbar/Views/_history.tpl b/system/Debug/Toolbar/Views/_history.tpl index 9db00ecc4679..7f22f560f936 100644 --- a/system/Debug/Toolbar/Views/_history.tpl +++ b/system/Debug/Toolbar/Views/_history.tpl @@ -1,22 +1,22 @@ - - - - - - - - - + + + + + + + + + {files} - - + diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index ed4230be0988..11a3301a954c 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -16,16 +16,20 @@ margin: 0px; padding: 0px; clear: both; - text-align: center; } - #debug-icon a svg { - margin: 8px; - max-width: 20px; - max-height: 20px; } - #debug-icon.fixed-top { - bottom: auto; - top: 0; } - #debug-icon .debug-bar-ndisplay { - display: none; } + text-align: center; +} +#debug-icon a svg { + margin: 8px; + max-width: 20px; + max-height: 20px; +} +#debug-icon.fixed-top { + bottom: auto; + top: 0; +} +#debug-icon .debug-bar-ndisplay { + display: none; +} #debug-bar { bottom: 0; @@ -37,213 +41,250 @@ line-height: 36px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 16px; - font-weight: 400; } - #debug-bar h1 { - bottom: 0; - display: inline-block; - font-size: 14px; - font-weight: normal; - margin: 0 16px 0 0; - padding: 0; - position: absolute; - right: 30px; - text-align: left; - top: 0; } - #debug-bar h1 svg { - width: 16px; - margin-right: 5px; } - #debug-bar h2 { - font-size: 16px; - margin: 0; - padding: 5px 0 10px 0; } - #debug-bar h2 span { - font-size: 13px; } - #debug-bar h3 { - font-size: 12px; - font-weight: 200; - margin: 0 0 0 10px; - padding: 0; - text-transform: uppercase; } - #debug-bar p { - font-size: 12px; - margin: 0 0 0 15px; - padding: 0; } - #debug-bar a { - text-decoration: none; } - #debug-bar a:hover { - text-decoration: underline; } - #debug-bar button { - border: 1px solid; - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - cursor: pointer; - line-height: 15px; } - #debug-bar button:hover { - text-decoration: underline; } - #debug-bar table { - border-collapse: collapse; - font-size: 14px; - line-height: normal; - margin: 5px 10px 15px 10px; - width: calc(100% - 10px); } - #debug-bar table strong { - font-weight: 500; } - #debug-bar table th { - display: table-cell; - font-weight: 600; - padding-bottom: 0.7em; - text-align: left; } - #debug-bar table tr { - border: none; } - #debug-bar table td { - border: none; - display: table-cell; - margin: 0; - text-align: left; } - #debug-bar table td:first-child { - max-width: 20%; } - #debug-bar table td:first-child.narrow { - width: 7em; } - #debug-bar td[data-debugbar-route] form { - display: none; } - #debug-bar td[data-debugbar-route]:hover form { - display: block; } - #debug-bar td[data-debugbar-route]:hover > div { - display: none; } - #debug-bar td[data-debugbar-route] input[type=text] { - padding: 2px; } - #debug-bar .toolbar { - display: flex; - overflow: hidden; - overflow-y: auto; - padding: 0 12px 0 12px; - white-space: nowrap; - z-index: 10000; } - #debug-bar.fixed-top { - bottom: auto; - top: 0; } - #debug-bar.fixed-top .tab { - bottom: auto; - top: 36px; } - #debug-bar #toolbar-position a, - #debug-bar #toolbar-theme a { - padding: 0 6px; - display: inline-flex; - vertical-align: top; } - #debug-bar #toolbar-position a:hover, - #debug-bar #toolbar-theme a:hover { - text-decoration: none; } - #debug-bar #debug-bar-link { - bottom: 0; - display: inline-block; - font-size: 16px; - line-height: 36px; - padding: 6px; - position: absolute; - right: 10px; - top: 0; - width: 24px; } - #debug-bar .ci-label { - display: inline-flex; - font-size: 14px; } - #debug-bar .ci-label:hover { - cursor: pointer; } - #debug-bar .ci-label a { - color: inherit; - display: flex; - letter-spacing: normal; - padding: 0 10px; - text-decoration: none; - align-items: center; } - #debug-bar .ci-label img { - margin: 6px 3px 6px 0; - width: 16px !important; } - #debug-bar .ci-label .badge { - border-radius: 12px; - -moz-border-radius: 12px; - -webkit-border-radius: 12px; - display: inline-block; - font-size: 75%; - font-weight: bold; - line-height: 12px; - margin-left: 5px; - padding: 2px 5px; - text-align: center; - vertical-align: baseline; - white-space: nowrap; } - #debug-bar .tab { - bottom: 35px; - display: none; - left: 0; - max-height: 62%; - overflow: hidden; - overflow-y: auto; - padding: 1em 2em; - position: fixed; - right: 0; - z-index: 9999; } - #debug-bar .timeline { - margin-left: 0; - width: 100%; } - #debug-bar .timeline th { - border-left: 1px solid; - font-size: 12px; - font-weight: 200; - padding: 5px 5px 10px 5px; - position: relative; - text-align: left; } - #debug-bar .timeline th:first-child { - border-left: 0; } - #debug-bar .timeline td { - border-left: 1px solid; - padding: 5px; - position: relative; } - #debug-bar .timeline td:first-child { - border-left: 0; - max-width: none; } - #debug-bar .timeline td.child-container { - padding: 0px; } - #debug-bar .timeline td.child-container .timeline { - margin: 0px; } - #debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { - padding-left: calc(5px + 10px * var(--level)); } - #debug-bar .timeline .timer { - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - display: inline-block; - padding: 5px; - position: absolute; - top: 30%; } - #debug-bar .timeline .timeline-parent { - cursor: pointer; } - #debug-bar .timeline .timeline-parent td:first-child nav { - background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; - background-position: 0 25%; - display: inline-block; - height: 15px; - width: 15px; - margin-right: 3px; - vertical-align: middle; } - #debug-bar .timeline .timeline-parent-open { - background-color: #DFDFDF; } - #debug-bar .timeline .timeline-parent-open td:first-child nav { - background-position: 0 75%; } - #debug-bar .timeline .child-row:hover { - background: transparent; } - #debug-bar .route-params, - #debug-bar .route-params-item { - vertical-align: top; } - #debug-bar .route-params td:first-child, - #debug-bar .route-params-item td:first-child { - font-style: italic; - padding-left: 1em; - text-align: right; } + font-weight: 400; +} +#debug-bar h1 { + display: flex; + font-weight: normal; + margin: 0 0 0 auto; +} +#debug-bar h1 svg { + width: 16px; + margin-right: 5px; +} +#debug-bar h2 { + font-size: 16px; + margin: 0; + padding: 5px 0 10px 0; +} +#debug-bar h2 span { + font-size: 13px; +} +#debug-bar h3 { + font-size: 12px; + font-weight: 200; + margin: 0 0 0 10px; + padding: 0; + text-transform: uppercase; +} +#debug-bar p { + font-size: 12px; + margin: 0 0 0 15px; + padding: 0; +} +#debug-bar a { + text-decoration: none; +} +#debug-bar a:hover { + text-decoration: underline; +} +#debug-bar button { + border: 1px solid; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + cursor: pointer; + line-height: 15px; +} +#debug-bar button:hover { + text-decoration: underline; +} +#debug-bar table { + border-collapse: collapse; + font-size: 14px; + line-height: normal; + margin: 5px 10px 15px 10px; + width: calc(100% - 10px); +} +#debug-bar table strong { + font-weight: 500; +} +#debug-bar table th { + display: table-cell; + font-weight: 600; + padding-bottom: 0.7em; + text-align: left; +} +#debug-bar table tr { + border: none; +} +#debug-bar table td { + border: none; + display: table-cell; + margin: 0; + text-align: left; +} +#debug-bar table td:first-child { + max-width: 20%; +} +#debug-bar table td:first-child.narrow { + width: 7em; +} +#debug-bar td[data-debugbar-route] form { + display: none; +} +#debug-bar td[data-debugbar-route]:hover form { + display: block; +} +#debug-bar td[data-debugbar-route]:hover > div { + display: none; +} +#debug-bar td[data-debugbar-route] input[type=text] { + padding: 2px; +} +#debug-bar .toolbar { + display: flex; + overflow: hidden; + overflow-y: auto; + padding: 0 12px 0 12px; + white-space: nowrap; + z-index: 10000; +} +#debug-bar.fixed-top { + bottom: auto; + top: 0; +} +#debug-bar.fixed-top .tab { + bottom: auto; + top: 36px; +} +#debug-bar #toolbar-position a, +#debug-bar #toolbar-theme a { + padding: 0 6px; + display: inline-flex; + vertical-align: top; +} +#debug-bar #toolbar-position a:hover, +#debug-bar #toolbar-theme a:hover { + text-decoration: none; +} +#debug-bar #debug-bar-link { + display: flex; + padding: 6px; +} +#debug-bar .ci-label { + display: inline-flex; + font-size: 14px; +} +#debug-bar .ci-label:hover { + cursor: pointer; +} +#debug-bar .ci-label a { + color: inherit; + display: flex; + letter-spacing: normal; + padding: 0 10px; + text-decoration: none; + align-items: center; +} +#debug-bar .ci-label img { + margin: 6px 3px 6px 0; + width: 16px !important; +} +#debug-bar .ci-label .badge { + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-border-radius: 12px; + display: inline-block; + font-size: 75%; + font-weight: bold; + line-height: 12px; + margin-left: 5px; + padding: 2px 5px; + text-align: center; + vertical-align: baseline; + white-space: nowrap; +} +#debug-bar .tab { + bottom: 35px; + display: none; + left: 0; + max-height: 62%; + overflow: hidden; + overflow-y: auto; + padding: 1em 2em; + position: fixed; + right: 0; + z-index: 9999; +} +#debug-bar .timeline { + margin-left: 0; + width: 100%; +} +#debug-bar .timeline th { + border-left: 1px solid; + font-size: 12px; + font-weight: 200; + padding: 5px 5px 10px 5px; + position: relative; + text-align: left; +} +#debug-bar .timeline th:first-child { + border-left: 0; +} +#debug-bar .timeline td { + border-left: 1px solid; + padding: 5px; + position: relative; +} +#debug-bar .timeline td:first-child { + border-left: 0; + max-width: none; +} +#debug-bar .timeline td.child-container { + padding: 0px; +} +#debug-bar .timeline td.child-container .timeline { + margin: 0px; +} +#debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { + padding-left: calc(5px + 10px * var(--level)); +} +#debug-bar .timeline .timer { + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + display: inline-block; + padding: 5px; + position: absolute; + top: 30%; +} +#debug-bar .timeline .timeline-parent { + cursor: pointer; +} +#debug-bar .timeline .timeline-parent td:first-child nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; +} +#debug-bar .timeline .timeline-parent-open { + background-color: #DFDFDF; +} +#debug-bar .timeline .timeline-parent-open td:first-child nav { + background-position: 0 75%; +} +#debug-bar .timeline .child-row:hover { + background: transparent; +} +#debug-bar .route-params, +#debug-bar .route-params-item { + vertical-align: top; +} +#debug-bar .route-params td:first-child, +#debug-bar .route-params-item td:first-child { + font-style: italic; + padding-left: 1em; + text-align: right; +} .debug-view.show-view { border: 1px solid; - margin: 4px; } + margin: 4px; +} .debug-view-path { font-family: monospace; @@ -251,403 +292,511 @@ letter-spacing: normal; min-height: 16px; padding: 2px; - text-align: left; } + text-align: left; +} .show-view .debug-view-path { - display: block !important; } + display: block !important; +} @media screen and (max-width: 1024px) { #debug-bar .ci-label img { - margin: unset; } - .hide-sm { - display: none !important; } } + margin: unset; + } + .hide-sm { + display: none !important; + } +} #debug-icon { background-color: #FFFFFF; box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, - #debug-icon a:link, - #debug-icon a:visited { - color: #DD8615; } + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-icon a:active, +#debug-icon a:link, +#debug-icon a:visited { + color: #DD8615; +} #debug-bar { background-color: #FFFFFF; - color: #434343; } + color: #434343; +} +#debug-bar h1, +#debug-bar h2, +#debug-bar h3, +#debug-bar p, +#debug-bar a, +#debug-bar button, +#debug-bar table, +#debug-bar thead, +#debug-bar tr, +#debug-bar td, +#debug-bar button, +#debug-bar .toolbar { + background-color: transparent; + color: #434343; +} +#debug-bar button { + background-color: #FFFFFF; +} +#debug-bar table strong { + color: #DD8615; +} +#debug-bar table tbody tr:hover { + background-color: #DFDFDF; +} +#debug-bar table tbody tr.current { + background-color: #FDC894; +} +#debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#debug-bar .toolbar { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-bar .toolbar img { + filter: brightness(0) invert(0.4); +} +#debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; +} +#debug-bar .muted { + color: #434343; +} +#debug-bar .muted td { + color: #DFDFDF; +} +#debug-bar .muted:hover td { + color: #434343; +} +#debug-bar #toolbar-position, +#debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#debug-bar .ci-label.active { + background-color: #DFDFDF; +} +#debug-bar .ci-label:hover { + background-color: #DFDFDF; +} +#debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#debug-bar .tab { + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; +} +#debug-bar .timeline th, +#debug-bar .timeline td { + border-color: #DFDFDF; +} +#debug-bar .timeline .timer { + background-color: #DD8615; +} + +.debug-view.show-view { + border-color: #DD8615; +} + +.debug-view-path { + background-color: #FDC894; + color: #434343; +} + +@media (prefers-color-scheme: dark) { + #debug-icon { + background-color: #252525; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; + } + #debug-icon a:active, +#debug-icon a:link, +#debug-icon a:visited { + color: #DD8615; + } + + #debug-bar { + background-color: #252525; + color: #DFDFDF; + } #debug-bar h1, - #debug-bar h2, - #debug-bar h3, - #debug-bar p, - #debug-bar a, - #debug-bar button, - #debug-bar table, - #debug-bar thead, - #debug-bar tr, - #debug-bar td, - #debug-bar button, - #debug-bar .toolbar { +#debug-bar h2, +#debug-bar h3, +#debug-bar p, +#debug-bar a, +#debug-bar button, +#debug-bar table, +#debug-bar thead, +#debug-bar tr, +#debug-bar td, +#debug-bar button, +#debug-bar .toolbar { background-color: transparent; - color: #434343; } + color: #DFDFDF; + } #debug-bar button { - background-color: #FFFFFF; } + background-color: #252525; + } #debug-bar table strong { - color: #DD8615; } + color: #DD8615; + } #debug-bar table tbody tr:hover { - background-color: #DFDFDF; } + background-color: #434343; + } #debug-bar table tbody tr.current { - background-color: #FDC894; } - #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; } + background-color: #FDC894; + } + #debug-bar table tbody tr.current td { + color: #252525; + } + #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; + } #debug-bar .toolbar { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); } + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar .toolbar img { + filter: brightness(0) invert(1); + } #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #DFDFDF; - -moz-box-shadow: 0 1px 4px #DFDFDF; - -webkit-box-shadow: 0 1px 4px #DFDFDF; } + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; + } #debug-bar .muted { - color: #434343; } - #debug-bar .muted td { - color: #DFDFDF; } - #debug-bar .muted:hover td { - color: #434343; } + color: #DFDFDF; + } + #debug-bar .muted td { + color: #434343; + } + #debug-bar .muted:hover td { + color: #DFDFDF; + } #debug-bar #toolbar-position, - #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); } +#debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); + } #debug-bar .ci-label.active { - background-color: #DFDFDF; } + background-color: #252525; + } #debug-bar .ci-label:hover { - background-color: #DFDFDF; } + background-color: #252525; + } #debug-bar .ci-label .badge { - background-color: #5BC0DE; - color: #FFFFFF; } + background-color: #DD4814; + color: #FFFFFF; + } #debug-bar .tab { - background-color: #FFFFFF; - box-shadow: 0 -1px 4px #DFDFDF; - -moz-box-shadow: 0 -1px 4px #DFDFDF; - -webkit-box-shadow: 0 -1px 4px #DFDFDF; } + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; + } #debug-bar .timeline th, - #debug-bar .timeline td { - border-color: #DFDFDF; } +#debug-bar .timeline td { + border-color: #434343; + } #debug-bar .timeline .timer { - background-color: #DD8615; } - -.debug-view.show-view { - border-color: #DD8615; } -#debug-bar tr[data-toggle] { - cursor: pointer; } - -.debug-view-path { - background-color: #FDC894; - color: #434343; } + background-color: #DD8615; + } -@media (prefers-color-scheme: dark) { - #debug-icon { - background-color: #252525; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #debug-icon a:active, - #debug-icon a:link, - #debug-icon a:visited { - color: #DD8615; } - #debug-bar { - background-color: #252525; - color: #DFDFDF; } - #debug-bar h1, - #debug-bar h2, - #debug-bar h3, - #debug-bar p, - #debug-bar a, - #debug-bar button, - #debug-bar table, - #debug-bar thead, - #debug-bar tr, - #debug-bar td, - #debug-bar button, - #debug-bar .toolbar { - background-color: transparent; - color: #DFDFDF; } - #debug-bar button { - background-color: #252525; } - #debug-bar table strong { - color: #DD8615; } - #debug-bar table tbody tr:hover { - background-color: #434343; } - #debug-bar table tbody tr.current { - background-color: #FDC894; } - #debug-bar table tbody tr.current td { - color: #252525; } - #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; } - #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; } - #debug-bar .toolbar img { - filter: brightness(0) invert(1); } - #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; } - #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; } - #debug-bar .muted { - color: #DFDFDF; } - #debug-bar .muted td { - color: #797979; } - #debug-bar .muted:hover td { - color: #DFDFDF; } - #debug-bar #toolbar-position, - #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); } - #debug-bar .ci-label.active { - background-color: #252525; } - #debug-bar .ci-label:hover { - background-color: #252525; } - #debug-bar .ci-label .badge { - background-color: #5BC0DE; - color: #DFDFDF; } - #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; } - #debug-bar .timeline th, - #debug-bar .timeline td { - border-color: #434343; } - #debug-bar .timeline .timer { - background-color: #DD8615; } .debug-view.show-view { - border-color: #DD8615; } + border-color: #DD8615; + } + .debug-view-path { background-color: #FDC894; - color: #434343; } } - + color: #434343; + } +} #toolbarContainer.dark #debug-icon { background-color: #252525; box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.dark #debug-icon a:active, - #toolbarContainer.dark #debug-icon a:link, - #toolbarContainer.dark #debug-icon a:visited { - color: #DD8615; } - + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.dark #debug-icon a:active, +#toolbarContainer.dark #debug-icon a:link, +#toolbarContainer.dark #debug-icon a:visited { + color: #DD8615; +} #toolbarContainer.dark #debug-bar { background-color: #252525; - color: #DFDFDF; } - #toolbarContainer.dark #debug-bar h1, - #toolbarContainer.dark #debug-bar h2, - #toolbarContainer.dark #debug-bar h3, - #toolbarContainer.dark #debug-bar p, - #toolbarContainer.dark #debug-bar a, - #toolbarContainer.dark #debug-bar button, - #toolbarContainer.dark #debug-bar table, - #toolbarContainer.dark #debug-bar thead, - #toolbarContainer.dark #debug-bar tr, - #toolbarContainer.dark #debug-bar td, - #toolbarContainer.dark #debug-bar button, - #toolbarContainer.dark #debug-bar .toolbar { - background-color: transparent; - color: #DFDFDF; } - #toolbarContainer.dark #debug-bar button { - background-color: #252525; } - #toolbarContainer.dark #debug-bar table strong { - color: #DD8615; } - #toolbarContainer.dark #debug-bar table tbody tr:hover { - background-color: #434343; } - #toolbarContainer.dark #debug-bar table tbody tr.current { - background-color: #FDC894; } - #toolbarContainer.dark #ci-database table tbody tr.duplicate { - background-color: #434343;} - #toolbarContainer.dark #debug-bar table tbody tr.current td { - color: #252525; } - #toolbarContainer.dark #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; } - #toolbarContainer.dark #debug-bar .toolbar { - background-color: #434343; - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; } - #toolbarContainer.dark #debug-bar .toolbar img { - filter: brightness(0) invert(1); } - #toolbarContainer.dark #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #434343; - -moz-box-shadow: 0 0 4px #434343; - -webkit-box-shadow: 0 0 4px #434343; } - #toolbarContainer.dark #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #434343; - -moz-box-shadow: 0 1px 4px #434343; - -webkit-box-shadow: 0 1px 4px #434343; } - #toolbarContainer.dark #debug-bar .muted { - color: #DFDFDF; } - #toolbarContainer.dark #debug-bar .muted td { - color: #797979; } - #toolbarContainer.dark #debug-bar .muted:hover td { - color: #DFDFDF; } - #toolbarContainer.dark #debug-bar #toolbar-position, - #toolbarContainer.dark #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); } - #toolbarContainer.dark #debug-bar .ci-label.active { - background-color: #252525; } - #toolbarContainer.dark #debug-bar .ci-label:hover { - background-color: #252525; } - #toolbarContainer.dark #debug-bar .ci-label .badge { - background-color: #5BC0DE; - color: #DFDFDF; } - #toolbarContainer.dark #debug-bar .tab { - background-color: #252525; - box-shadow: 0 -1px 4px #434343; - -moz-box-shadow: 0 -1px 4px #434343; - -webkit-box-shadow: 0 -1px 4px #434343; } - #toolbarContainer.dark #debug-bar .timeline th, - #toolbarContainer.dark #debug-bar .timeline td { - border-color: #434343; } - #toolbarContainer.dark #debug-bar .timeline .timer { - background-color: #DD8615; } - + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar h1, +#toolbarContainer.dark #debug-bar h2, +#toolbarContainer.dark #debug-bar h3, +#toolbarContainer.dark #debug-bar p, +#toolbarContainer.dark #debug-bar a, +#toolbarContainer.dark #debug-bar button, +#toolbarContainer.dark #debug-bar table, +#toolbarContainer.dark #debug-bar thead, +#toolbarContainer.dark #debug-bar tr, +#toolbarContainer.dark #debug-bar td, +#toolbarContainer.dark #debug-bar button, +#toolbarContainer.dark #debug-bar .toolbar { + background-color: transparent; + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar button { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar table strong { + color: #DD8615; +} +#toolbarContainer.dark #debug-bar table tbody tr:hover { + background-color: #434343; +} +#toolbarContainer.dark #debug-bar table tbody tr.current { + background-color: #FDC894; +} +#toolbarContainer.dark #debug-bar table tbody tr.current td { + color: #252525; +} +#toolbarContainer.dark #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.dark #debug-bar .toolbar { + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; +} +#toolbarContainer.dark #debug-bar .toolbar img { + filter: brightness(0) invert(1); +} +#toolbarContainer.dark #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; +} +#toolbarContainer.dark #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; +} +#toolbarContainer.dark #debug-bar .muted { + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar .muted td { + color: #434343; +} +#toolbarContainer.dark #debug-bar .muted:hover td { + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar #toolbar-position, +#toolbarContainer.dark #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#toolbarContainer.dark #debug-bar .ci-label.active { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar .ci-label:hover { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.dark #debug-bar .tab { + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; +} +#toolbarContainer.dark #debug-bar .timeline th, +#toolbarContainer.dark #debug-bar .timeline td { + border-color: #434343; +} +#toolbarContainer.dark #debug-bar .timeline .timer { + background-color: #DD8615; +} #toolbarContainer.dark .debug-view.show-view { - border-color: #DD8615; } - + border-color: #DD8615; +} #toolbarContainer.dark .debug-view-path { background-color: #FDC894; - color: #434343; } - + color: #434343; +} #toolbarContainer.dark td[data-debugbar-route] input[type=text] { background: #000; - color: #fff; } + color: #fff; +} #toolbarContainer.light #debug-icon { background-color: #FFFFFF; box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-icon a:active, - #toolbarContainer.light #debug-icon a:link, - #toolbarContainer.light #debug-icon a:visited { - color: #DD8615; } - + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-icon a:active, +#toolbarContainer.light #debug-icon a:link, +#toolbarContainer.light #debug-icon a:visited { + color: #DD8615; +} #toolbarContainer.light #debug-bar { background-color: #FFFFFF; - color: #434343; } - #toolbarContainer.light #debug-bar h1, - #toolbarContainer.light #debug-bar h2, - #toolbarContainer.light #debug-bar h3, - #toolbarContainer.light #debug-bar p, - #toolbarContainer.light #debug-bar a, - #toolbarContainer.light #debug-bar button, - #toolbarContainer.light #debug-bar table, - #toolbarContainer.light #debug-bar thead, - #toolbarContainer.light #debug-bar tr, - #toolbarContainer.light #debug-bar td, - #toolbarContainer.light #debug-bar button, - #toolbarContainer.light #debug-bar .toolbar { - background-color: transparent; - color: #434343; } - #toolbarContainer.light #debug-bar button { - background-color: #FFFFFF; } - #toolbarContainer.light #debug-bar table strong { - color: #DD8615; } - #toolbarContainer.light #debug-bar table tbody tr:hover { - background-color: #DFDFDF; } - #toolbarContainer.light #debug-bar table tbody tr.current { - background-color: #FDC894; } - #toolbarContainer.light #ci-database table tbody tr.duplicate { - background-color: #DFDFDF;} - #toolbarContainer.light #debug-bar table tbody tr.current:hover td { - background-color: #DD4814; - color: #FFFFFF; } - #toolbarContainer.light #debug-bar .toolbar { - background-color: #FFFFFF; - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-bar .toolbar img { - filter: brightness(0) invert(0.4); } - #toolbarContainer.light #debug-bar.fixed-top .toolbar { - box-shadow: 0 0 4px #DFDFDF; - -moz-box-shadow: 0 0 4px #DFDFDF; - -webkit-box-shadow: 0 0 4px #DFDFDF; } - #toolbarContainer.light #debug-bar.fixed-top .tab { - box-shadow: 0 1px 4px #DFDFDF; - -moz-box-shadow: 0 1px 4px #DFDFDF; - -webkit-box-shadow: 0 1px 4px #DFDFDF; } - #toolbarContainer.light #debug-bar .muted { - color: #797979; } - #toolbarContainer.light #debug-bar .muted td { - color: #797979; } - #toolbarContainer.light #debug-bar .muted:hover td { - color: #434343; } - #toolbarContainer.light #debug-bar #toolbar-position, - #toolbarContainer.light #debug-bar #toolbar-theme { - filter: brightness(0) invert(0.6); } - #toolbarContainer.light #debug-bar .ci-label.active { - background-color: #DFDFDF; } - #toolbarContainer.light #debug-bar .ci-label:hover { - background-color: #DFDFDF; } - #toolbarContainer.light #debug-bar .ci-label .badge { - background-color: #5BC0DE; - color: #FFFFFF; } - #toolbarContainer.light #debug-bar .tab { - background-color: #FFFFFF; - box-shadow: 0 -1px 4px #DFDFDF; - -moz-box-shadow: 0 -1px 4px #DFDFDF; - -webkit-box-shadow: 0 -1px 4px #DFDFDF; } - #toolbarContainer.light #debug-bar .timeline th, - #toolbarContainer.light #debug-bar .timeline td { - border-color: #DFDFDF; } - #toolbarContainer.light #debug-bar .timeline .timer { - background-color: #DD8615; } - + color: #434343; +} +#toolbarContainer.light #debug-bar h1, +#toolbarContainer.light #debug-bar h2, +#toolbarContainer.light #debug-bar h3, +#toolbarContainer.light #debug-bar p, +#toolbarContainer.light #debug-bar a, +#toolbarContainer.light #debug-bar button, +#toolbarContainer.light #debug-bar table, +#toolbarContainer.light #debug-bar thead, +#toolbarContainer.light #debug-bar tr, +#toolbarContainer.light #debug-bar td, +#toolbarContainer.light #debug-bar button, +#toolbarContainer.light #debug-bar .toolbar { + background-color: transparent; + color: #434343; +} +#toolbarContainer.light #debug-bar button { + background-color: #FFFFFF; +} +#toolbarContainer.light #debug-bar table strong { + color: #DD8615; +} +#toolbarContainer.light #debug-bar table tbody tr:hover { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar table tbody tr.current { + background-color: #FDC894; +} +#toolbarContainer.light #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.light #debug-bar .toolbar { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .toolbar img { + filter: brightness(0) invert(0.4); +} +#toolbarContainer.light #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .muted { + color: #434343; +} +#toolbarContainer.light #debug-bar .muted td { + color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .muted:hover td { + color: #434343; +} +#toolbarContainer.light #debug-bar #toolbar-position, +#toolbarContainer.light #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#toolbarContainer.light #debug-bar .ci-label.active { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .ci-label:hover { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.light #debug-bar .tab { + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .timeline th, +#toolbarContainer.light #debug-bar .timeline td { + border-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .timeline .timer { + background-color: #DD8615; +} #toolbarContainer.light .debug-view.show-view { - border-color: #DD8615; } - + border-color: #DD8615; +} #toolbarContainer.light .debug-view-path { background-color: #FDC894; - color: #434343; } + color: #434343; +} .debug-bar-width30 { - width: 30%; } + width: 30%; +} .debug-bar-width10 { - width: 10%; } + width: 10%; +} .debug-bar-width70p { - width: 70px; } + width: 70px; +} -.debug-bar-width140p { - width: 140px; } +.debug-bar-width190p { + width: 190px; +} .debug-bar-width20e { - width: 20em; } + width: 20em; +} .debug-bar-width6r { - width: 6rem; } + width: 6rem; +} .debug-bar-ndisplay { - display: none; } + display: none; +} .debug-bar-alignRight { - text-align: right; } + text-align: right; +} .debug-bar-alignLeft { - text-align: left; } + text-align: left; +} .debug-bar-noverflow { - overflow: hidden; } + overflow: hidden; +} diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js index 3bd05d5e9592..7805a99dda05 100644 --- a/system/Debug/Toolbar/Views/toolbar.js +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -4,684 +4,684 @@ var ciDebugBar = { - toolbarContainer : null, - toolbar : null, - icon : null, - - init : function () { - this.toolbarContainer = document.getElementById('toolbarContainer'); - this.toolbar = document.getElementById('debug-bar'); - this.icon = document.getElementById('debug-icon'); - - ciDebugBar.createListeners(); - ciDebugBar.setToolbarState(); - ciDebugBar.setToolbarPosition(); - ciDebugBar.setToolbarTheme(); - ciDebugBar.toggleViewsHints(); - ciDebugBar.routerLink(); - - document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); - document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); - - // Allows to highlight the row of the current history request - var btn = this.toolbar.querySelector('button[data-time="' + localStorage.getItem('debugbar-time') + '"]'); - ciDebugBar.addClass(btn.parentNode.parentNode, 'current'); - - historyLoad = this.toolbar.getElementsByClassName('ci-history-load'); - - for (var i = 0; i < historyLoad.length; i++) - { - historyLoad[i].addEventListener('click', function () { - loadDoc(this.getAttribute('data-time')); - }, true); - } - - // Display the active Tab on page load - var tab = ciDebugBar.readCookie('debug-bar-tab'); - if (document.getElementById(tab)) - { - var el = document.getElementById(tab); - el.style.display = 'block'; - ciDebugBar.addClass(el, 'active'); - tab = document.querySelector('[data-tab=' + tab + ']'); - if (tab) - { - ciDebugBar.addClass(tab.parentNode, 'active'); - } - } - }, - - createListeners : function () { - var buttons = [].slice.call(this.toolbar.querySelectorAll('.ci-label a')); - - for (var i = 0; i < buttons.length; i++) - { - buttons[i].addEventListener('click', ciDebugBar.showTab, true); - } - - // Hook up generic toggle via data attributes `data-toggle="foo"` - var links = this.toolbar.querySelectorAll('[data-toggle]'); - for (var i = 0; i < links.length; i++) - { - links[i].addEventListener('click', ciDebugBar.toggleRows, true); - } - }, - - showTab: function () { - // Get the target tab, if any - var tab = document.getElementById(this.getAttribute('data-tab')); - - // If the label have not a tab stops here - if (! tab) - { - return; - } - - // Remove debug-bar-tab cookie - ciDebugBar.createCookie('debug-bar-tab', '', -1); - - // Check our current state. - var state = tab.style.display; - - // Hide all tabs - var tabs = document.querySelectorAll('#debug-bar .tab'); - - for (var i = 0; i < tabs.length; i++) - { - tabs[i].style.display = 'none'; - } - - // Mark all labels as inactive - var labels = document.querySelectorAll('#debug-bar .ci-label'); - - for (var i = 0; i < labels.length; i++) - { - ciDebugBar.removeClass(labels[i], 'active'); - } - - // Show/hide the selected tab - if (state != 'block') - { - tab.style.display = 'block'; - ciDebugBar.addClass(this.parentNode, 'active'); - // Create debug-bar-tab cookie to persistent state - ciDebugBar.createCookie('debug-bar-tab', this.getAttribute('data-tab'), 365); - } - }, - - addClass : function (el, className) { - if (el.classList) - { - el.classList.add(className); - } - else - { - el.className += ' ' + className; - } - }, - - removeClass : function (el, className) { - if (el.classList) - { - el.classList.remove(className); - } - else - { - el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); - } - }, - - /** - * Toggle display of another object based on - * the data-toggle value of this object - * - * @param event - */ - toggleRows : function(event) { - if(event.target) - { - let row = event.target.closest('tr'); - let target = document.getElementById(row.getAttribute('data-toggle')); - target.style.display = target.style.display === 'none' ? 'table-row' : 'none'; - } - }, - - /** - * Toggle display of a data table - * - * @param obj - */ - toggleDataTable : function (obj) { - if (typeof obj == 'string') - { - obj = document.getElementById(obj + '_table'); - } - - if (obj) - { - obj.style.display = obj.style.display === 'none' ? 'block' : 'none'; - } - }, - - /** - * Toggle display of timeline child elements - * - * @param obj - */ - toggleChildRows : function (obj) { - if (typeof obj == 'string') - { - par = document.getElementById(obj + '_parent') - obj = document.getElementById(obj + '_children'); - } - - if (par && obj) - { - obj.style.display = obj.style.display === 'none' ? '' : 'none'; - par.classList.toggle('timeline-parent-open'); - } - }, - - - //-------------------------------------------------------------------- - - /** - * Toggle tool bar from full to icon and icon to full - */ - toggleToolbar : function () { - var open = ciDebugBar.toolbar.style.display != 'none'; - - ciDebugBar.icon.style.display = open == true ? 'inline-block' : 'none'; - ciDebugBar.toolbar.style.display = open == false ? 'inline-block' : 'none'; - - // Remember it for other page loads on this site - ciDebugBar.createCookie('debug-bar-state', '', -1); - ciDebugBar.createCookie('debug-bar-state', open == true ? 'minimized' : 'open' , 365); - }, - - /** - * Sets the initial state of the toolbar (open or minimized) when - * the page is first loaded to allow it to remember the state between refreshes. - */ - setToolbarState: function () { - var open = ciDebugBar.readCookie('debug-bar-state'); - - ciDebugBar.icon.style.display = open != 'open' ? 'inline-block' : 'none'; - ciDebugBar.toolbar.style.display = open == 'open' ? 'inline-block' : 'none'; - }, - - toggleViewsHints: function () { - // Avoid toggle hints on history requests that are not the initial - if (localStorage.getItem('debugbar-time') != localStorage.getItem('debugbar-time-new')) - { - var a = document.querySelector('a[data-tab="ci-views"]'); - a.href = '#'; - return; - } - - var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] - var sortedComments = []; - var comments = []; - - var getComments = function () { - var nodes = []; - var result = []; - var xpathResults = document.evaluate( "//comment()[starts-with(., ' DEBUG-VIEW')]", document, null, XPathResult.ANY_TYPE, null); - var nextNode = xpathResults.iterateNext(); - while ( nextNode ) - { - nodes.push( nextNode ); - nextNode = xpathResults.iterateNext(); - } - - // sort comment by opening and closing tags - for (var i = 0; i < nodes.length; ++i) - { - // get file path + name to use as key - var path = nodes[i].nodeValue.substring( 18, nodes[i].nodeValue.length - 1 ); - - if ( nodes[i].nodeValue[12] === 'S' ) // simple check for start comment - { - // create new entry - result[path] = [ nodes[i], null ]; - } - else if (result[path]) - { - // add to existing entry - result[path][1] = nodes[i]; - } - } - - return result; - }; - - // find node that has TargetNode as parentNode - var getParentNode = function ( node, targetNode ) { - if ( node.parentNode === null ) - { - return null; - } - - if ( node.parentNode !== targetNode ) - { - return getParentNode( node.parentNode, targetNode ); - } - - return node; - }; - - // define invalid & outer ( also invalid ) elements - const INVALID_ELEMENTS = [ 'NOSCRIPT', 'SCRIPT', 'STYLE' ]; - const OUTER_ELEMENTS = [ 'HTML', 'BODY', 'HEAD' ]; - - var getValidElementInner = function ( node, reverse ) { - // handle invalid tags - if ( OUTER_ELEMENTS.indexOf( node.nodeName ) !== -1 ) - { - for (var i = 0; i < document.body.children.length; ++i) - { - var index = reverse ? document.body.children.length - ( i + 1 ) : i; - var element = document.body.children[index]; - - // skip invalid tags - if ( INVALID_ELEMENTS.indexOf( element.nodeName ) !== -1 ) - { - continue; - } - - return [ element, reverse ]; - } - - return null; - } - - // get to next valid element - while ( node !== null && INVALID_ELEMENTS.indexOf( node.nodeName ) !== -1 ) - { - node = reverse ? node.previousElementSibling : node.nextElementSibling; - } - - // return non array if we couldnt find something - if ( node === null ) - { - return null; - } - - return [ node, reverse ]; - }; - - // get next valid element ( to be safe to add divs ) - // @return [ element, skip element ] or null if we couldnt find a valid place - var getValidElement = function ( nodeElement ) { - if (nodeElement) - { - if ( nodeElement.nextElementSibling !== null ) - { - return getValidElementInner( nodeElement.nextElementSibling, false ) - || getValidElementInner( nodeElement.previousElementSibling, true ); - } - if ( nodeElement.previousElementSibling !== null ) - { - return getValidElementInner( nodeElement.previousElementSibling, true ); - } - } - - // something went wrong! -> element is not in DOM - return null; - }; - - function showHints() - { - // Had AJAX? Reset view blocks - sortedComments = getComments(); - - for (var key in sortedComments) - { - var startElement = getValidElement( sortedComments[key][0] ); - var endElement = getValidElement( sortedComments[key][1] ); - - // skip if we couldnt get a valid element - if ( startElement === null || endElement === null ) - { - continue; - } - - // find element which has same parent as startelement - var jointParent = getParentNode( endElement[0], startElement[0].parentNode ); - if ( jointParent === null ) - { - // find element which has same parent as endelement - jointParent = getParentNode( startElement[0], endElement[0].parentNode ); - if ( jointParent === null ) - { - // both tries failed - continue; - } - else - { - startElement[0] = jointParent; - } - } - else - { - endElement[0] = jointParent; - } - - var debugDiv = document.createElement( 'div' ); // holder - var debugPath = document.createElement( 'div' ); // path - var childArray = startElement[0].parentNode.childNodes; // target child array - var parent = startElement[0].parentNode; - var start, end; - - // setup container - debugDiv.classList.add( 'debug-view' ); - debugDiv.classList.add( 'show-view' ); - debugPath.classList.add( 'debug-view-path' ); - debugPath.innerText = key; - debugDiv.appendChild( debugPath ); - - // calc distance between them - // start - for (var i = 0; i < childArray.length; ++i) - { - // check for comment ( start & end ) -> if its before valid start element - if ( childArray[i] === sortedComments[key][1] || - childArray[i] === sortedComments[key][0] || - childArray[i] === startElement[0] ) - { - start = i; - if ( childArray[i] === sortedComments[key][0] ) - { - start++; // increase to skip the start comment - } - break; - } - } - // adjust if we want to skip the start element - if ( startElement[1] ) - { - start++; - } - - // end - for (var i = start; i < childArray.length; ++i) - { - if ( childArray[i] === endElement[0] ) - { - end = i; - // dont break to check for end comment after end valid element - } - else if ( childArray[i] === sortedComments[key][1] ) - { - // if we found the end comment, we can break - end = i; - break; - } - } - - // move elements - var number = end - start; - if ( endElement[1] ) - { - number++; - } - for (var i = 0; i < number; ++i) - { - if ( INVALID_ELEMENTS.indexOf( childArray[start] ) !== -1 ) - { - // skip invalid childs that can cause problems if moved - start++; - continue; - } - debugDiv.appendChild( childArray[start] ); - } - - // add container to DOM - nodeList.push( parent.insertBefore( debugDiv, childArray[start] ) ); - } - - ciDebugBar.createCookie('debug-view', 'show', 365); - ciDebugBar.addClass(btn, 'active'); - } - - function hideHints() - { - for (var i = 0; i < nodeList.length; ++i) - { - var index; - - // find index - for (var j = 0; j < nodeList[i].parentNode.childNodes.length; ++j) - { - if ( nodeList[i].parentNode.childNodes[j] === nodeList[i] ) - { - index = j; - break; - } - } - - // move child back - while ( nodeList[i].childNodes.length !== 1 ) - { - nodeList[i].parentNode.insertBefore( nodeList[i].childNodes[1], nodeList[i].parentNode.childNodes[index].nextSibling ); - index++; - } - - nodeList[i].parentNode.removeChild( nodeList[i] ); - } - nodeList.length = 0; - - ciDebugBar.createCookie('debug-view', '', -1); - ciDebugBar.removeClass(btn, 'active'); - } - - var btn = document.querySelector('[data-tab=ci-views]'); - - // If the Views Collector is inactive stops here - if (! btn) - { - return; - } - - btn.parentNode.onclick = function () { - if (ciDebugBar.readCookie('debug-view')) - { - hideHints(); - } - else - { - showHints(); - } - }; - - // Determine Hints state on page load - if (ciDebugBar.readCookie('debug-view')) - { - showHints(); - } - }, - - setToolbarPosition: function () { - var btnPosition = this.toolbar.querySelector('#toolbar-position'); - - if (ciDebugBar.readCookie('debug-bar-position') === 'top') - { - ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); - } - - btnPosition.addEventListener('click', function () { - var position = ciDebugBar.readCookie('debug-bar-position'); - - ciDebugBar.createCookie('debug-bar-position', '', -1); - - if (!position || position === 'bottom') - { - ciDebugBar.createCookie('debug-bar-position', 'top', 365); - ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); - } - else - { - ciDebugBar.createCookie('debug-bar-position', 'bottom', 365); - ciDebugBar.removeClass(ciDebugBar.icon, 'fixed-top'); - ciDebugBar.removeClass(ciDebugBar.toolbar, 'fixed-top'); - } - }, true); - }, - - setToolbarTheme: function () { - var btnTheme = this.toolbar.querySelector('#toolbar-theme'); - var isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; - var isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches; - - // If a cookie is set with a value, we force the color scheme - if (ciDebugBar.readCookie('debug-bar-theme') === 'dark') - { - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); - } - else if (ciDebugBar.readCookie('debug-bar-theme') === 'light') - { - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); - } - - btnTheme.addEventListener('click', function () { - var theme = ciDebugBar.readCookie('debug-bar-theme'); - - if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) - { - // If there is no cookie, and "prefers-color-scheme" is set to "dark" - // It means that the user wants to switch to light mode - ciDebugBar.createCookie('debug-bar-theme', 'light', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); - } - else - { - if (theme === 'dark') - { - ciDebugBar.createCookie('debug-bar-theme', 'light', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); - } - else - { - // In any other cases: if there is no cookie, or the cookie is set to - // "light", or the "prefers-color-scheme" is "light"... - ciDebugBar.createCookie('debug-bar-theme', 'dark', 365); - ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); - ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); - } - } - }, true); - }, - - /** - * Helper to create a cookie. - * - * @param name - * @param value - * @param days - */ - createCookie : function (name,value,days) { - if (days) - { - var date = new Date(); - - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - - var expires = "; expires=" + date.toGMTString(); - } - else - { - var expires = ""; - } - - document.cookie = name + "=" + value + expires + "; path=/; samesite=Lax"; - }, - - readCookie : function (name) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - - for (var i = 0; i < ca.length; i++) - { - var c = ca[i]; - while (c.charAt(0) == ' ') - { - c = c.substring(1,c.length); - } - if (c.indexOf(nameEQ) == 0) - { - return c.substring(nameEQ.length,c.length); - } - } - return null; - }, - - trimSlash: function (text) { - return text.replace(/^\/|\/$/g, ''); - }, - - routerLink: function () { - var row, _location; - var rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"]'); - var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; - - for (var i = 0; i < rowGet.length; i++) - { - row = rowGet[i]; - if (!/\/\(.+?\)/.test(rowGet[i].innerText)) - { - row.style = 'cursor: pointer;'; - row.setAttribute('title', location.origin + '/' + ciDebugBar.trimSlash(row.innerText)); - row.addEventListener('click', function (ev) { - _location = location.origin + '/' + ciDebugBar.trimSlash(ev.target.innerText); - var redirectWindow = window.open(_location, '_blank'); - redirectWindow.location; - }); - } - else - { - row.innerHTML = '
' + row.innerText + '
' - + '' - + row.innerText.replace(patt, '') - + '' - + ''; - } - } - - rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"] form'); - for (var i = 0; i < rowGet.length; i++) - { - row = rowGet[i]; - - row.addEventListener('submit', function (event) { - event.preventDefault() - var inputArray = [], t = 0; - var input = event.target.querySelectorAll('input[type=text]'); - var tpl = event.target.getAttribute('data-debugbar-route-tpl'); - - for (var n = 0; n < input.length; n++) - { - if (input[n].value.length > 0) - { - inputArray.push(input[n].value); - } - } - - if (inputArray.length > 0) - { - _location = location.origin + '/' + tpl.replace(/\?/g, function () { - return inputArray[t++] - }); - - var redirectWindow = window.open(_location, '_blank'); - redirectWindow.location; - } - }) - } - } + toolbarContainer : null, + toolbar : null, + icon : null, + + init : function () { + this.toolbarContainer = document.getElementById('toolbarContainer'); + this.toolbar = document.getElementById('debug-bar'); + this.icon = document.getElementById('debug-icon'); + + ciDebugBar.createListeners(); + ciDebugBar.setToolbarState(); + ciDebugBar.setToolbarPosition(); + ciDebugBar.setToolbarTheme(); + ciDebugBar.toggleViewsHints(); + ciDebugBar.routerLink(); + + document.getElementById('debug-bar-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + document.getElementById('debug-icon-link').addEventListener('click', ciDebugBar.toggleToolbar, true); + + // Allows to highlight the row of the current history request + var btn = this.toolbar.querySelector('button[data-time="' + localStorage.getItem('debugbar-time') + '"]'); + ciDebugBar.addClass(btn.parentNode.parentNode, 'current'); + + historyLoad = this.toolbar.getElementsByClassName('ci-history-load'); + + for (var i = 0; i < historyLoad.length; i++) + { + historyLoad[i].addEventListener('click', function () { + loadDoc(this.getAttribute('data-time')); + }, true); + } + + // Display the active Tab on page load + var tab = ciDebugBar.readCookie('debug-bar-tab'); + if (document.getElementById(tab)) + { + var el = document.getElementById(tab); + el.style.display = 'block'; + ciDebugBar.addClass(el, 'active'); + tab = document.querySelector('[data-tab=' + tab + ']'); + if (tab) + { + ciDebugBar.addClass(tab.parentNode, 'active'); + } + } + }, + + createListeners : function () { + var buttons = [].slice.call(this.toolbar.querySelectorAll('.ci-label a')); + + for (var i = 0; i < buttons.length; i++) + { + buttons[i].addEventListener('click', ciDebugBar.showTab, true); + } + + // Hook up generic toggle via data attributes `data-toggle="foo"` + var links = this.toolbar.querySelectorAll('[data-toggle]'); + for (var i = 0; i < links.length; i++) + { + links[i].addEventListener('click', ciDebugBar.toggleRows, true); + } + }, + + showTab: function () { + // Get the target tab, if any + var tab = document.getElementById(this.getAttribute('data-tab')); + + // If the label have not a tab stops here + if (! tab) + { + return; + } + + // Remove debug-bar-tab cookie + ciDebugBar.createCookie('debug-bar-tab', '', -1); + + // Check our current state. + var state = tab.style.display; + + // Hide all tabs + var tabs = document.querySelectorAll('#debug-bar .tab'); + + for (var i = 0; i < tabs.length; i++) + { + tabs[i].style.display = 'none'; + } + + // Mark all labels as inactive + var labels = document.querySelectorAll('#debug-bar .ci-label'); + + for (var i = 0; i < labels.length; i++) + { + ciDebugBar.removeClass(labels[i], 'active'); + } + + // Show/hide the selected tab + if (state != 'block') + { + tab.style.display = 'block'; + ciDebugBar.addClass(this.parentNode, 'active'); + // Create debug-bar-tab cookie to persistent state + ciDebugBar.createCookie('debug-bar-tab', this.getAttribute('data-tab'), 365); + } + }, + + addClass : function (el, className) { + if (el.classList) + { + el.classList.add(className); + } + else + { + el.className += ' ' + className; + } + }, + + removeClass : function (el, className) { + if (el.classList) + { + el.classList.remove(className); + } + else + { + el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } + }, + + /** + * Toggle display of another object based on + * the data-toggle value of this object + * + * @param event + */ + toggleRows : function(event) { + if(event.target) + { + let row = event.target.closest('tr'); + let target = document.getElementById(row.getAttribute('data-toggle')); + target.style.display = target.style.display === 'none' ? 'table-row' : 'none'; + } + }, + + /** + * Toggle display of a data table + * + * @param obj + */ + toggleDataTable : function (obj) { + if (typeof obj == 'string') + { + obj = document.getElementById(obj + '_table'); + } + + if (obj) + { + obj.style.display = obj.style.display === 'none' ? 'block' : 'none'; + } + }, + + /** + * Toggle display of timeline child elements + * + * @param obj + */ + toggleChildRows : function (obj) { + if (typeof obj == 'string') + { + par = document.getElementById(obj + '_parent') + obj = document.getElementById(obj + '_children'); + } + + if (par && obj) + { + obj.style.display = obj.style.display === 'none' ? '' : 'none'; + par.classList.toggle('timeline-parent-open'); + } + }, + + + //-------------------------------------------------------------------- + + /** + * Toggle tool bar from full to icon and icon to full + */ + toggleToolbar : function () { + var open = ciDebugBar.toolbar.style.display != 'none'; + + ciDebugBar.icon.style.display = open == true ? 'inline-block' : 'none'; + ciDebugBar.toolbar.style.display = open == false ? 'inline-block' : 'none'; + + // Remember it for other page loads on this site + ciDebugBar.createCookie('debug-bar-state', '', -1); + ciDebugBar.createCookie('debug-bar-state', open == true ? 'minimized' : 'open' , 365); + }, + + /** + * Sets the initial state of the toolbar (open or minimized) when + * the page is first loaded to allow it to remember the state between refreshes. + */ + setToolbarState: function () { + var open = ciDebugBar.readCookie('debug-bar-state'); + + ciDebugBar.icon.style.display = open != 'open' ? 'inline-block' : 'none'; + ciDebugBar.toolbar.style.display = open == 'open' ? 'inline-block' : 'none'; + }, + + toggleViewsHints: function () { + // Avoid toggle hints on history requests that are not the initial + if (localStorage.getItem('debugbar-time') != localStorage.getItem('debugbar-time-new')) + { + var a = document.querySelector('a[data-tab="ci-views"]'); + a.href = '#'; + return; + } + + var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] + var sortedComments = []; + var comments = []; + + var getComments = function () { + var nodes = []; + var result = []; + var xpathResults = document.evaluate( "//comment()[starts-with(., ' DEBUG-VIEW')]", document, null, XPathResult.ANY_TYPE, null); + var nextNode = xpathResults.iterateNext(); + while ( nextNode ) + { + nodes.push( nextNode ); + nextNode = xpathResults.iterateNext(); + } + + // sort comment by opening and closing tags + for (var i = 0; i < nodes.length; ++i) + { + // get file path + name to use as key + var path = nodes[i].nodeValue.substring( 18, nodes[i].nodeValue.length - 1 ); + + if ( nodes[i].nodeValue[12] === 'S' ) // simple check for start comment + { + // create new entry + result[path] = [ nodes[i], null ]; + } + else if (result[path]) + { + // add to existing entry + result[path][1] = nodes[i]; + } + } + + return result; + }; + + // find node that has TargetNode as parentNode + var getParentNode = function ( node, targetNode ) { + if ( node.parentNode === null ) + { + return null; + } + + if ( node.parentNode !== targetNode ) + { + return getParentNode( node.parentNode, targetNode ); + } + + return node; + }; + + // define invalid & outer ( also invalid ) elements + const INVALID_ELEMENTS = [ 'NOSCRIPT', 'SCRIPT', 'STYLE' ]; + const OUTER_ELEMENTS = [ 'HTML', 'BODY', 'HEAD' ]; + + var getValidElementInner = function ( node, reverse ) { + // handle invalid tags + if ( OUTER_ELEMENTS.indexOf( node.nodeName ) !== -1 ) + { + for (var i = 0; i < document.body.children.length; ++i) + { + var index = reverse ? document.body.children.length - ( i + 1 ) : i; + var element = document.body.children[index]; + + // skip invalid tags + if ( INVALID_ELEMENTS.indexOf( element.nodeName ) !== -1 ) + { + continue; + } + + return [ element, reverse ]; + } + + return null; + } + + // get to next valid element + while ( node !== null && INVALID_ELEMENTS.indexOf( node.nodeName ) !== -1 ) + { + node = reverse ? node.previousElementSibling : node.nextElementSibling; + } + + // return non array if we couldnt find something + if ( node === null ) + { + return null; + } + + return [ node, reverse ]; + }; + + // get next valid element ( to be safe to add divs ) + // @return [ element, skip element ] or null if we couldnt find a valid place + var getValidElement = function ( nodeElement ) { + if (nodeElement) + { + if ( nodeElement.nextElementSibling !== null ) + { + return getValidElementInner( nodeElement.nextElementSibling, false ) + || getValidElementInner( nodeElement.previousElementSibling, true ); + } + if ( nodeElement.previousElementSibling !== null ) + { + return getValidElementInner( nodeElement.previousElementSibling, true ); + } + } + + // something went wrong! -> element is not in DOM + return null; + }; + + function showHints() + { + // Had AJAX? Reset view blocks + sortedComments = getComments(); + + for (var key in sortedComments) + { + var startElement = getValidElement( sortedComments[key][0] ); + var endElement = getValidElement( sortedComments[key][1] ); + + // skip if we couldnt get a valid element + if ( startElement === null || endElement === null ) + { + continue; + } + + // find element which has same parent as startelement + var jointParent = getParentNode( endElement[0], startElement[0].parentNode ); + if ( jointParent === null ) + { + // find element which has same parent as endelement + jointParent = getParentNode( startElement[0], endElement[0].parentNode ); + if ( jointParent === null ) + { + // both tries failed + continue; + } + else + { + startElement[0] = jointParent; + } + } + else + { + endElement[0] = jointParent; + } + + var debugDiv = document.createElement( 'div' ); // holder + var debugPath = document.createElement( 'div' ); // path + var childArray = startElement[0].parentNode.childNodes; // target child array + var parent = startElement[0].parentNode; + var start, end; + + // setup container + debugDiv.classList.add( 'debug-view' ); + debugDiv.classList.add( 'show-view' ); + debugPath.classList.add( 'debug-view-path' ); + debugPath.innerText = key; + debugDiv.appendChild( debugPath ); + + // calc distance between them + // start + for (var i = 0; i < childArray.length; ++i) + { + // check for comment ( start & end ) -> if its before valid start element + if ( childArray[i] === sortedComments[key][1] || + childArray[i] === sortedComments[key][0] || + childArray[i] === startElement[0] ) + { + start = i; + if ( childArray[i] === sortedComments[key][0] ) + { + start++; // increase to skip the start comment + } + break; + } + } + // adjust if we want to skip the start element + if ( startElement[1] ) + { + start++; + } + + // end + for (var i = start; i < childArray.length; ++i) + { + if ( childArray[i] === endElement[0] ) + { + end = i; + // dont break to check for end comment after end valid element + } + else if ( childArray[i] === sortedComments[key][1] ) + { + // if we found the end comment, we can break + end = i; + break; + } + } + + // move elements + var number = end - start; + if ( endElement[1] ) + { + number++; + } + for (var i = 0; i < number; ++i) + { + if ( INVALID_ELEMENTS.indexOf( childArray[start] ) !== -1 ) + { + // skip invalid childs that can cause problems if moved + start++; + continue; + } + debugDiv.appendChild( childArray[start] ); + } + + // add container to DOM + nodeList.push( parent.insertBefore( debugDiv, childArray[start] ) ); + } + + ciDebugBar.createCookie('debug-view', 'show', 365); + ciDebugBar.addClass(btn, 'active'); + } + + function hideHints() + { + for (var i = 0; i < nodeList.length; ++i) + { + var index; + + // find index + for (var j = 0; j < nodeList[i].parentNode.childNodes.length; ++j) + { + if ( nodeList[i].parentNode.childNodes[j] === nodeList[i] ) + { + index = j; + break; + } + } + + // move child back + while ( nodeList[i].childNodes.length !== 1 ) + { + nodeList[i].parentNode.insertBefore( nodeList[i].childNodes[1], nodeList[i].parentNode.childNodes[index].nextSibling ); + index++; + } + + nodeList[i].parentNode.removeChild( nodeList[i] ); + } + nodeList.length = 0; + + ciDebugBar.createCookie('debug-view', '', -1); + ciDebugBar.removeClass(btn, 'active'); + } + + var btn = document.querySelector('[data-tab=ci-views]'); + + // If the Views Collector is inactive stops here + if (! btn) + { + return; + } + + btn.parentNode.onclick = function () { + if (ciDebugBar.readCookie('debug-view')) + { + hideHints(); + } + else + { + showHints(); + } + }; + + // Determine Hints state on page load + if (ciDebugBar.readCookie('debug-view')) + { + showHints(); + } + }, + + setToolbarPosition: function () { + var btnPosition = this.toolbar.querySelector('#toolbar-position'); + + if (ciDebugBar.readCookie('debug-bar-position') === 'top') + { + ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); + } + + btnPosition.addEventListener('click', function () { + var position = ciDebugBar.readCookie('debug-bar-position'); + + ciDebugBar.createCookie('debug-bar-position', '', -1); + + if (!position || position === 'bottom') + { + ciDebugBar.createCookie('debug-bar-position', 'top', 365); + ciDebugBar.addClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.addClass(ciDebugBar.toolbar, 'fixed-top'); + } + else + { + ciDebugBar.createCookie('debug-bar-position', 'bottom', 365); + ciDebugBar.removeClass(ciDebugBar.icon, 'fixed-top'); + ciDebugBar.removeClass(ciDebugBar.toolbar, 'fixed-top'); + } + }, true); + }, + + setToolbarTheme: function () { + var btnTheme = this.toolbar.querySelector('#toolbar-theme'); + var isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; + var isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches; + + // If a cookie is set with a value, we force the color scheme + if (ciDebugBar.readCookie('debug-bar-theme') === 'dark') + { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); + } + else if (ciDebugBar.readCookie('debug-bar-theme') === 'light') + { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); + } + + btnTheme.addEventListener('click', function () { + var theme = ciDebugBar.readCookie('debug-bar-theme'); + + if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) + { + // If there is no cookie, and "prefers-color-scheme" is set to "dark" + // It means that the user wants to switch to light mode + ciDebugBar.createCookie('debug-bar-theme', 'light', 365); + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); + } + else + { + if (theme === 'dark') + { + ciDebugBar.createCookie('debug-bar-theme', 'light', 365); + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'dark'); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'light'); + } + else + { + // In any other cases: if there is no cookie, or the cookie is set to + // "light", or the "prefers-color-scheme" is "light"... + ciDebugBar.createCookie('debug-bar-theme', 'dark', 365); + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, 'light'); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, 'dark'); + } + } + }, true); + }, + + /** + * Helper to create a cookie. + * + * @param name + * @param value + * @param days + */ + createCookie : function (name,value,days) { + if (days) + { + var date = new Date(); + + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + + var expires = "; expires=" + date.toGMTString(); + } + else + { + var expires = ""; + } + + document.cookie = name + "=" + value + expires + "; path=/; samesite=Lax"; + }, + + readCookie : function (name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + + for (var i = 0; i < ca.length; i++) + { + var c = ca[i]; + while (c.charAt(0) == ' ') + { + c = c.substring(1,c.length); + } + if (c.indexOf(nameEQ) == 0) + { + return c.substring(nameEQ.length,c.length); + } + } + return null; + }, + + trimSlash: function (text) { + return text.replace(/^\/|\/$/g, ''); + }, + + routerLink: function () { + var row, _location; + var rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"]'); + var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; + + for (var i = 0; i < rowGet.length; i++) + { + row = rowGet[i]; + if (!/\/\(.+?\)/.test(rowGet[i].innerText)) + { + row.style = 'cursor: pointer;'; + row.setAttribute('title', location.origin + '/' + ciDebugBar.trimSlash(row.innerText)); + row.addEventListener('click', function (ev) { + _location = location.origin + '/' + ciDebugBar.trimSlash(ev.target.innerText); + var redirectWindow = window.open(_location, '_blank'); + redirectWindow.location; + }); + } + else + { + row.innerHTML = '
' + row.innerText + '
' + + '' + + row.innerText.replace(patt, '') + + '' + + ''; + } + } + + rowGet = this.toolbar.querySelectorAll('td[data-debugbar-route="GET"] form'); + for (var i = 0; i < rowGet.length; i++) + { + row = rowGet[i]; + + row.addEventListener('submit', function (event) { + event.preventDefault() + var inputArray = [], t = 0; + var input = event.target.querySelectorAll('input[type=text]'); + var tpl = event.target.getAttribute('data-debugbar-route-tpl'); + + for (var n = 0; n < input.length; n++) + { + if (input[n].value.length > 0) + { + inputArray.push(input[n].value); + } + } + + if (inputArray.length > 0) + { + _location = location.origin + '/' + tpl.replace(/\?/g, function () { + return inputArray[t++] + }); + + var redirectWindow = window.open(_location, '_blank'); + redirectWindow.location; + } + }) + } + } }; diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php index 65cf2a4c3edf..0e73076f0e02 100644 --- a/system/Debug/Toolbar/Views/toolbar.tpl.php +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -19,250 +19,250 @@ */ ?>
-
- - 🔅 - - - - ms   MB - - +
+ + 🔅 + + + + ms   MB + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - Vars - - + + + + Vars + + -

- - - - - - -

+

+ + + + + + +

- - - - -
+ + + + +
- -
-
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
- + + {datetime}{datetime} {status} {method} {url}
- - - - - - - - - - - - renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?> - -
NAMECOMPONENTDURATION ms
-
+ +
+ + + + + + + + + + + + + renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?> + +
NAMECOMPONENTDURATION ms
+
- - - - -
-

+ + + + +
+

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

-
+ +

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

No data to display.

- - - + +

No data to display.

+ + + - - -

Session User Data

-
+ + +

Session User Data

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

No data to display.

- - -

Session doesn't seem to be active.

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

No data to display.

+ + +

Session doesn't seem to be active.

+ -

Request ( )

+

Request ( )

- - -

$_GET

-
+ + +

$_GET

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

$_POST

-
+ + +

$_POST

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

Headers

-
+ + +

Headers

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

Cookies

-
+ + +

Cookies

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

Response - ( ) -

+

Response + ( ) +

- - -

Headers

-
+ + +

Headers

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

System Configuration

+ +
+

System Configuration

- setData($config)->render('_config.tpl') ?> -
+ setData($config)->render('_config.tpl') ?> +
+.. warning:: If an attacker injects a string like `` + + // Becomes + + + // OR + + Class Reference =============== @@ -282,9 +255,9 @@ The methods provided by the parent class that are available are: :rtype: int Returns the currently status code for this response. If no status code has been set, a BadMethodCallException - will be thrown:: + will be thrown: - echo $response->getStatusCode(); + .. literalinclude:: response/014.php .. php:method:: setStatusCode($code[, $reason='']) @@ -293,23 +266,23 @@ The methods provided by the parent class that are available are: :returns: The current Response instance :rtype: ``CodeIgniter\HTTP\Response`` - Sets the HTTP status code that should be sent with this response:: + Sets the HTTP status code that should be sent with this response: - $response->setStatusCode(404); + .. literalinclude:: response/015.php The reason phrase will be automatically generated based upon the official lists. If you need to set your own - for a custom status code, you can pass the reason phrase as the second parameter:: + for a custom status code, you can pass the reason phrase as the second parameter: - $response->setStatusCode(230, "Tardis initiated"); + .. literalinclude:: response/016.php .. php:method:: getReasonPhrase() :returns: The current reason phrase. :rtype: string - Returns the current status code for this response. If not status has been set, will return an empty string:: + Returns the current status code for this response. If not status has been set, will return an empty string: - echo $response->getReasonPhrase(); + .. literalinclude:: response/017.php .. php:method:: setDate($date) @@ -317,10 +290,9 @@ The methods provided by the parent class that are available are: :returns: The current response instance. :rtype: ``CodeIgniter\HTTP\Response`` - Sets the date used for this response. The ``$date`` argument must be an instance of ``DateTime``:: + Sets the date used for this response. The ``$date`` argument must be an instance of ``DateTime``: - $date = DateTime::createFromFormat('j-M-Y', '15-Feb-2016'); - $response->setDate($date); + .. literalinclude:: response/018.php .. php:method:: setContentType($mime[, $charset='UTF-8']) @@ -329,16 +301,14 @@ The methods provided by the parent class that are available are: :returns: The current response instance. :rtype: ``CodeIgniter\HTTP\Response`` - Sets the content type this response represents:: + Sets the content type this response represents: - $response->setContentType('text/plain'); - $response->setContentType('text/html'); - $response->setContentType('application/json'); + .. literalinclude:: response/019.php By default, the method sets the character set to ``UTF-8``. If you need to change this, you can - pass the character set as the second parameter:: + pass the character set as the second parameter: - $response->setContentType('text/plain', 'x-pig-latin'); + .. literalinclude:: response/020.php .. php:method:: noCache() @@ -346,12 +316,9 @@ The methods provided by the parent class that are available are: :rtype: ``CodeIgniter\HTTP\Response`` Sets the ``Cache-Control`` header to turn off all HTTP caching. This is the default setting - of all response messages:: - - $response->noCache(); + of all response messages: - // Sets the following header: - Cache-Control: no-store, max-age=0, no-cache + .. literalinclude:: response/021.php .. php:method:: setCache($options) @@ -380,10 +347,9 @@ The methods provided by the parent class that are available are: :rtype: ``CodeIgniter\HTTP\Response`` Sets the ``Last-Modified`` header. The ``$date`` object can be either a string or a ``DateTime`` - instance:: + instance: - $response->setLastModified(date('D, d M Y H:i:s')); - $response->setLastModified(DateTime::createFromFormat('u', $time)); + .. literalinclude:: response/022.php .. php:method:: send(): Response @@ -404,7 +370,7 @@ The methods provided by the parent class that are available are: :param string $prefix: Cookie name prefix :param bool $secure: Whether to only transfer the cookie through HTTPS :param bool $httponly: Whether to only make the cookie accessible for HTTP requests (no JavaScript) - :param string $samesite: The value for the SameSite cookie parameter. If set to ``''``, no SameSite attribute will be set on the cookie. If set to `null`, the default value from `config/App.php` will be used + :param string $samesite: The value for the SameSite cookie parameter. If set to ``''``, no SameSite attribute will be set on the cookie. If set to ``null``, the default value from **app/Config/Cookie.php** will be used :rtype: void Sets a cookie containing the values you specify. There are two ways to @@ -414,54 +380,43 @@ The methods provided by the parent class that are available are: **Array Method** Using this method, an associative array is passed as the first - parameter:: - - $cookie = [ - 'name' => 'The Cookie Name', - 'value' => 'The Value', - 'expire' => '86500', - 'domain' => '.some-domain.com', - 'path' => '/', - 'prefix' => 'myprefix_', - 'secure' => true, - 'httponly' => false, - 'samesite' => 'Lax' - ]; - - $response->setCookie($cookie); + parameter: - **Notes** + .. literalinclude:: response/023.php - Only the name and value are required. To delete a cookie set it with the - expiration blank. + Only the ``name`` and ``value`` are required. To delete a cookie set it with the + ``expire`` blank. - The expiration is set in **seconds**, which will be added to the current + The ``expire`` is set in **seconds**, which will be added to the current time. Do not include the time, but rather only the number of seconds - from *now* that you wish the cookie to be valid. If the expiration is + from *now* that you wish the cookie to be valid. If the ``expire`` is set to zero the cookie will only last as long as the browser is open. + .. note:: But if the ``value`` is set to empty string and the ``expire`` is set to ``0``, + the cookie will be deleted. + For site-wide cookies regardless of how your site is requested, add your - URL to the **domain** starting with a period, like this: + URL to the ``domain`` starting with a period, like this: .your-domain.com - The path is usually not needed since the method sets a root path. + The ``path`` is usually not needed since the method sets a root path. - The prefix is only needed if you need to avoid name collisions with + The ``prefix`` is only needed if you need to avoid name collisions with other identically named cookies for your server. - The secure flag is only needed if you want to make it a secure cookie + The ``secure`` flag is only needed if you want to make it a secure cookie by setting it to ``true``. - The SameSite value controls how cookies are shared between domains and sub-domains. - Allowed values are 'None', 'Lax', 'Strict' or a blank string ``''``. + The ``samesite`` value controls how cookies are shared between domains and sub-domains. + Allowed values are ``'None'``, ``'Lax'``, ``'Strict'`` or a blank string ``''``. If set to blank string, default SameSite attribute will be set. **Discrete Parameters** If you prefer, you can set the cookie by passing data using individual - parameters:: + parameters: - $response->setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly, $samesite); + .. literalinclude:: response/024.php .. php:method:: deleteCookie($name = ''[, $domain = ''[, $path = '/'[, $prefix = '']]]) @@ -471,25 +426,23 @@ The methods provided by the parent class that are available are: :param string $prefix: Cookie name prefix :rtype: void - Delete an existing cookie by setting its expiry to ``0``. - - **Notes** + Delete an existing cookie. - Only the name is required. + Only the ``name`` is required. - The prefix is only needed if you need to avoid name collisions with + The ``prefix`` is only needed if you need to avoid name collisions with other identically named cookies for your server. - Provide a prefix if cookies should only be deleted for that subset. - Provide a domain name if cookies should only be deleted for that domain. - Provide a path name if cookies should only be deleted for that path. + Provide a ``prefix`` if cookies should only be deleted for that subset. + Provide a ``domain`` name if cookies should only be deleted for that domain. + Provide a ``path`` name if cookies should only be deleted for that path. If any of the optional parameters are empty, then the same-named cookie will be deleted across all that apply. - Example:: + Example: - $response->deleteCookie($name); + .. literalinclude:: response/025.php .. php:method:: hasCookie($name = ''[, $value = null[, $prefix = '']]) @@ -502,15 +455,15 @@ The methods provided by the parent class that are available are: **Notes** - Only the name is required. If a prefix is specified, it will be prepended to the cookie name. + Only the ``name`` is required. If a ``prefix`` is specified, it will be prepended to the cookie name. - If no value is given, the method just checks for the existence of the named cookie. - If a value is given, then the method checks that the cookie exists, and that it + If no ``value`` is given, the method just checks for the existence of the named cookie. + If a ``value`` is given, then the method checks that the cookie exists, and that it has the prescribed value. - Example:: + Example: - if ($response->hasCookie($name)) ... + .. literalinclude:: response/026.php .. php:method:: getCookie($name = ''[, $prefix = '']) @@ -519,11 +472,11 @@ The methods provided by the parent class that are available are: :rtype: ``Cookie|Cookie[]|null`` Returns the named cookie, if found, or ``null``. - If no name is given, returns the array of ``Cookie`` objects. + If no ``name`` is given, returns the array of ``Cookie`` objects. - Example:: + Example: - $cookie = $response->getCookie($name); + .. literalinclude:: response/027.php .. php:method:: getCookies() diff --git a/user_guide_src/source/outgoing/response/001.php b/user_guide_src/source/outgoing/response/001.php new file mode 100644 index 000000000000..9815f6f1475d --- /dev/null +++ b/user_guide_src/source/outgoing/response/001.php @@ -0,0 +1,3 @@ +response->setStatusCode(404)->setBody($body); diff --git a/user_guide_src/source/outgoing/response/002.php b/user_guide_src/source/outgoing/response/002.php new file mode 100644 index 000000000000..d5ed0fd87d96 --- /dev/null +++ b/user_guide_src/source/outgoing/response/002.php @@ -0,0 +1,3 @@ +response->setStatusCode(404, 'Nope. Not here.'); diff --git a/user_guide_src/source/outgoing/response/003.php b/user_guide_src/source/outgoing/response/003.php new file mode 100644 index 000000000000..e5bff8cabf2f --- /dev/null +++ b/user_guide_src/source/outgoing/response/003.php @@ -0,0 +1,10 @@ + true, + 'id' => 123, +]; + +return $this->response->setJSON($data); +// or +return $this->response->setXML($data); diff --git a/user_guide_src/source/outgoing/response/004.php b/user_guide_src/source/outgoing/response/004.php new file mode 100644 index 000000000000..0497dea06e58 --- /dev/null +++ b/user_guide_src/source/outgoing/response/004.php @@ -0,0 +1,4 @@ +setHeader('Location', 'http://example.com') + ->setHeader('WWW-Authenticate', 'Negotiate'); diff --git a/user_guide_src/source/outgoing/response/005.php b/user_guide_src/source/outgoing/response/005.php new file mode 100644 index 000000000000..39276d3f80f1 --- /dev/null +++ b/user_guide_src/source/outgoing/response/005.php @@ -0,0 +1,4 @@ +setHeader('Cache-Control', 'no-cache') + ->appendHeader('Cache-Control', 'must-revalidate'); diff --git a/user_guide_src/source/outgoing/response/006.php b/user_guide_src/source/outgoing/response/006.php new file mode 100644 index 000000000000..a2fdee1e9ed5 --- /dev/null +++ b/user_guide_src/source/outgoing/response/006.php @@ -0,0 +1,3 @@ +removeHeader('Location'); diff --git a/user_guide_src/source/outgoing/response/007.php b/user_guide_src/source/outgoing/response/007.php new file mode 100644 index 000000000000..9f80218ee669 --- /dev/null +++ b/user_guide_src/source/outgoing/response/007.php @@ -0,0 +1,6 @@ +download($name, $data); diff --git a/user_guide_src/source/outgoing/response/008.php b/user_guide_src/source/outgoing/response/008.php new file mode 100644 index 000000000000..832d75fc0616 --- /dev/null +++ b/user_guide_src/source/outgoing/response/008.php @@ -0,0 +1,4 @@ +download('/path/to/photo.jpg', null); diff --git a/user_guide_src/source/outgoing/response/009.php b/user_guide_src/source/outgoing/response/009.php new file mode 100644 index 000000000000..e75c386647cb --- /dev/null +++ b/user_guide_src/source/outgoing/response/009.php @@ -0,0 +1,3 @@ +download('awkwardEncryptedFileName.fakeExt', null)->setFileName('expenses.csv'); diff --git a/user_guide_src/source/outgoing/response/010.php b/user_guide_src/source/outgoing/response/010.php new file mode 100644 index 000000000000..93acf62e99fa --- /dev/null +++ b/user_guide_src/source/outgoing/response/010.php @@ -0,0 +1,8 @@ + 300, + 's-maxage' => 900, + 'etag' => 'abcde', +]; +$this->response->setCache($options); diff --git a/user_guide_src/source/outgoing/response/011.php b/user_guide_src/source/outgoing/response/011.php new file mode 100644 index 000000000000..4a21b5c29078 --- /dev/null +++ b/user_guide_src/source/outgoing/response/011.php @@ -0,0 +1,12 @@ +CSP->reportOnly(false); + +// specify the origin to use if none provided for a directive +$response->CSP->setDefaultSrc('cdn.example.com'); + +// specify the URL that "report-only" reports get sent to +$response->CSP->setReportURI('http://example.com/csp/reports'); + +// specify that HTTP requests be upgraded to HTTPS +$response->CSP->upgradeInsecureRequests(true); + +// add types or origins to CSP directives +// assuming that the default treatment is to block rather than just report +$response->CSP->addBaseURI('example.com', true); // report only +$response->CSP->addChildSrc('https://youtube.com'); // blocked +$response->CSP->addConnectSrc('https://*.facebook.com', false); // blocked +$response->CSP->addFontSrc('fonts.example.com'); +$response->CSP->addFormAction('self'); +$response->CSP->addFrameAncestor('none', true); // report this one +$response->CSP->addImageSrc('cdn.example.com'); +$response->CSP->addMediaSrc('cdn.example.com'); +$response->CSP->addManifestSrc('cdn.example.com'); +$response->CSP->addObjectSrc('cdn.example.com', false); // reject from here +$response->CSP->addPluginType('application/pdf', false); // reject this media type +$response->CSP->addScriptSrc('scripts.example.com', true); // allow but report requests from here +$response->CSP->addStyleSrc('css.example.com'); +$response->CSP->addSandbox(['allow-forms', 'allow-scripts']); diff --git a/user_guide_src/source/outgoing/response/013.php b/user_guide_src/source/outgoing/response/013.php new file mode 100644 index 000000000000..273d72c29a1d --- /dev/null +++ b/user_guide_src/source/outgoing/response/013.php @@ -0,0 +1,6 @@ +addChildSrc('https://youtube.com'); // allowed +$response->reportOnly(true); +$response->addChildSrc('https://metube.com'); // allowed but reported +$response->addChildSrc('https://ourtube.com', false); // allowed diff --git a/user_guide_src/source/outgoing/response/014.php b/user_guide_src/source/outgoing/response/014.php new file mode 100644 index 000000000000..c05ef6fbe606 --- /dev/null +++ b/user_guide_src/source/outgoing/response/014.php @@ -0,0 +1,3 @@ +getStatusCode(); diff --git a/user_guide_src/source/outgoing/response/015.php b/user_guide_src/source/outgoing/response/015.php new file mode 100644 index 000000000000..3fbc71bc6371 --- /dev/null +++ b/user_guide_src/source/outgoing/response/015.php @@ -0,0 +1,3 @@ +setStatusCode(404); diff --git a/user_guide_src/source/outgoing/response/016.php b/user_guide_src/source/outgoing/response/016.php new file mode 100644 index 000000000000..31a4990eb9ee --- /dev/null +++ b/user_guide_src/source/outgoing/response/016.php @@ -0,0 +1,3 @@ +setStatusCode(230, 'Tardis initiated'); diff --git a/user_guide_src/source/outgoing/response/017.php b/user_guide_src/source/outgoing/response/017.php new file mode 100644 index 000000000000..78ce30be95bb --- /dev/null +++ b/user_guide_src/source/outgoing/response/017.php @@ -0,0 +1,3 @@ +getReasonPhrase(); diff --git a/user_guide_src/source/outgoing/response/018.php b/user_guide_src/source/outgoing/response/018.php new file mode 100644 index 000000000000..606424af8f79 --- /dev/null +++ b/user_guide_src/source/outgoing/response/018.php @@ -0,0 +1,4 @@ +setDate($date); diff --git a/user_guide_src/source/outgoing/response/019.php b/user_guide_src/source/outgoing/response/019.php new file mode 100644 index 000000000000..f74953ae772d --- /dev/null +++ b/user_guide_src/source/outgoing/response/019.php @@ -0,0 +1,5 @@ +setContentType('text/plain'); +$response->setContentType('text/html'); +$response->setContentType('application/json'); diff --git a/user_guide_src/source/outgoing/response/020.php b/user_guide_src/source/outgoing/response/020.php new file mode 100644 index 000000000000..fdbc546b6fad --- /dev/null +++ b/user_guide_src/source/outgoing/response/020.php @@ -0,0 +1,3 @@ +setContentType('text/plain', 'x-pig-latin'); diff --git a/user_guide_src/source/outgoing/response/021.php b/user_guide_src/source/outgoing/response/021.php new file mode 100644 index 000000000000..d5275cfb734e --- /dev/null +++ b/user_guide_src/source/outgoing/response/021.php @@ -0,0 +1,7 @@ +noCache(); +/* + * Sets the following header: + * Cache-Control: no-store, max-age=0, no-cache + */ diff --git a/user_guide_src/source/outgoing/response/022.php b/user_guide_src/source/outgoing/response/022.php new file mode 100644 index 000000000000..5a841f28d2ce --- /dev/null +++ b/user_guide_src/source/outgoing/response/022.php @@ -0,0 +1,4 @@ +setLastModified(date('D, d M Y H:i:s')); +$response->setLastModified(DateTime::createFromFormat('u', $time)); diff --git a/user_guide_src/source/outgoing/response/023.php b/user_guide_src/source/outgoing/response/023.php new file mode 100644 index 000000000000..3d0855739e96 --- /dev/null +++ b/user_guide_src/source/outgoing/response/023.php @@ -0,0 +1,15 @@ + 'The Cookie Name', + 'value' => 'The Value', + 'expire' => '86500', + 'domain' => '.some-domain.com', + 'path' => '/', + 'prefix' => 'myprefix_', + 'secure' => true, + 'httponly' => false, + 'samesite' => 'Lax', +]; + +$response->setCookie($cookie); diff --git a/user_guide_src/source/outgoing/response/024.php b/user_guide_src/source/outgoing/response/024.php new file mode 100644 index 000000000000..3b75c4004362 --- /dev/null +++ b/user_guide_src/source/outgoing/response/024.php @@ -0,0 +1,3 @@ +setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly, $samesite); diff --git a/user_guide_src/source/outgoing/response/025.php b/user_guide_src/source/outgoing/response/025.php new file mode 100644 index 000000000000..a2343928babe --- /dev/null +++ b/user_guide_src/source/outgoing/response/025.php @@ -0,0 +1,3 @@ +deleteCookie($name); diff --git a/user_guide_src/source/outgoing/response/026.php b/user_guide_src/source/outgoing/response/026.php new file mode 100644 index 000000000000..ee92fa38c094 --- /dev/null +++ b/user_guide_src/source/outgoing/response/026.php @@ -0,0 +1,5 @@ +hasCookie($name)) { + // ... +} diff --git a/user_guide_src/source/outgoing/response/027.php b/user_guide_src/source/outgoing/response/027.php new file mode 100644 index 000000000000..1d76679940c8 --- /dev/null +++ b/user_guide_src/source/outgoing/response/027.php @@ -0,0 +1,3 @@ +getCookie($name); diff --git a/user_guide_src/source/outgoing/table.rst b/user_guide_src/source/outgoing/table.rst index 01b7dabe5cef..e40cf5d6ee57 100644 --- a/user_guide_src/source/outgoing/table.rst +++ b/user_guide_src/source/outgoing/table.rst @@ -17,9 +17,9 @@ Initializing the Class ====================== The Table class is not provided as a service, and should be instantiated -"normally", for instance:: +"normally", for instance: - $table = new \CodeIgniter\View\Table(); +.. literalinclude:: table/001.php Examples ======== @@ -29,100 +29,32 @@ multi-dimensional array. Note that the first array index will become the table heading (or you can set your own headings using the ``setHeading()`` method described in the function reference below). -:: - - $table = new \CodeIgniter\View\Table(); - - $data = [ - ['Name', 'Color', 'Size'], - ['Fred', 'Blue', 'Small'], - ['Mary', 'Red', 'Large'], - ['John', 'Green', 'Medium'], - ]; - - echo $table->generate($data); +.. literalinclude:: table/002.php Here is an example of a table created from a database query result. The table class will automatically generate the headings based on the table names (or you can set your own headings using the ``setHeading()`` method described in the class reference below). -:: - - $table = new \CodeIgniter\View\Table(); - - $query = $db->query('SELECT * FROM my_table'); - - echo $table->generate($query); +.. literalinclude:: table/003.php Here is an example showing how you might create a table using discrete -parameters:: +parameters: - $table = new \CodeIgniter\View\Table(); - - $table->setHeading('Name', 'Color', 'Size'); - - $table->addRow('Fred', 'Blue', 'Small'); - $table->addRow('Mary', 'Red', 'Large'); - $table->addRow('John', 'Green', 'Medium'); - - echo $table->generate(); +.. literalinclude:: table/004.php Here is the same example, except instead of individual parameters, -arrays are used:: - - $table = new \CodeIgniter\View\Table(); - - $table->setHeading(array('Name', 'Color', 'Size')); +arrays are used: - $table->addRow(['Fred', 'Blue', 'Small']); - $table->addRow(['Mary', 'Red', 'Large']); - $table->addRow(['John', 'Green', 'Medium']); - - echo $table->generate(); +.. literalinclude:: table/005.php Changing the Look of Your Table =============================== The Table Class permits you to set a table template with which you can -specify the design of your layout. Here is the template prototype:: - - $template = [ - 'table_open' => '', - - 'thead_open' => '', - 'thead_close' => '', - - 'heading_row_start' => '', - 'heading_row_end' => '', - 'heading_cell_start' => '', - - 'tfoot_open' => '', - 'tfoot_close' => '', - - 'footing_row_start' => '', - 'footing_row_end' => '', - 'footing_cell_start' => '', - - 'tbody_open' => '', - 'tbody_close' => '', - - 'row_start' => '', - 'row_end' => '', - 'cell_start' => '', - - 'row_alt_start' => '', - 'row_alt_end' => '', - 'cell_alt_start' => '', - - 'table_close' => '
', - 'heading_cell_end' => '
', - 'footing_cell_end' => '
', - 'cell_end' => '
', - 'cell_alt_end' => '
' - ]; +specify the design of your layout. Here is the template prototype: - $table->setTemplate($template); +.. literalinclude:: table/006.php .. note:: You'll notice there are two sets of "row" blocks in the template. These permit you to create alternating row colors or design @@ -130,23 +62,14 @@ specify the design of your layout. Here is the template prototype:: You are NOT required to submit a complete template. If you only need to change parts of the layout you can simply submit those elements. In this -example, only the table opening tag is being changed:: +example, only the table opening tag is being changed: - $template = [ - 'table_open' => '' - ]; - - $table->setTemplate($template); +.. literalinclude:: table/007.php You can also set defaults for these by passing an array of template settings -to the Table constructor.:: - - $customSettings = [ - 'table_open' => '
' - ]; - - $table = new \CodeIgniter\View\Table($customSettings); +to the Table constructor: +.. literalinclude:: table/008.php *************** Class Reference @@ -157,15 +80,8 @@ Class Reference .. attribute:: $function = null Allows you to specify a native PHP function or a valid function array object to be applied to all cell data. - :: - - $table = new \CodeIgniter\View\Table(); - - $table->setHeading('Name', 'Color', 'Size'); - $table->addRow('Fred', 'Blue', 'Small'); - $table->function = 'htmlspecialchars'; - echo $table->generate(); + .. literalinclude:: table/009.php In the above example, all cell data would be run through PHP's :php:func:`htmlspecialchars()` function, resulting in:: @@ -186,9 +102,8 @@ Class Reference :rtype: Table Permits you to add a caption to the table. - :: - $table->setCaption('Colors'); + .. literalinclude:: table/010.php .. php:method:: setHeading([$args = [] [, ...]]) @@ -196,11 +111,9 @@ Class Reference :returns: Table instance (method chaining) :rtype: Table - Permits you to set the table heading. You can submit an array or discrete params:: + Permits you to set the table heading. You can submit an array or discrete params: - $table->setHeading('Name', 'Color', 'Size'); // or - - $table->setHeading(['Name', 'Color', 'Size']); + .. literalinclude:: table/011.php .. php:method:: setFooting([$args = [] [, ...]]) @@ -208,11 +121,9 @@ Class Reference :returns: Table instance (method chaining) :rtype: Table - Permits you to set the table footing. You can submit an array or discrete params:: - - $table->setFooting('Subtotal', $subtotal, $notes); // or + Permits you to set the table footing. You can submit an array or discrete params: - $table->setFooting(['Subtotal', $subtotal, $notes]); + .. literalinclude:: table/012.php .. php:method:: addRow([$args = [] [, ...]]) @@ -220,20 +131,14 @@ Class Reference :returns: Table instance (method chaining) :rtype: Table - Permits you to add a row to your table. You can submit an array or discrete params:: - - $table->addRow('Blue', 'Red', 'Green'); // or + Permits you to add a row to your table. You can submit an array or discrete params: - $table->addRow(['Blue', 'Red', 'Green']); + .. literalinclude:: table/013.php If you would like to set an individual cell's tag attributes, you can use an associative array for that cell. - The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag:: + The associative key **data** defines the cell's data. Any other key => val pairs are added as key='val' attributes to the tag: - $cell = ['data' => 'Blue', 'class' => 'highlight', 'colspan' => 2]; - $table->addRow($cell, 'Red', 'Green'); - - // generates - // + .. literalinclude:: table/014.php .. php:method:: makeColumns([$array = [] [, $columnLimit = 0]]) @@ -243,27 +148,9 @@ Class Reference :rtype: array This method takes a one-dimensional array as input and creates a multi-dimensional array with a depth equal to the number of columns desired. - This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example:: - - $list = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve']; - - $newList = $table->makeColumns($list, 3); - - $table->generate($newList); - - // Generates a table with this prototype - -
BlueRedGreen
- - - - - - - - -
onetwothree
fourfivesix
seveneightnine
teneleventwelve
+ This allows a single array with many elements to be displayed in a table that has a fixed column count. Consider this example: + .. literalinclude:: table/015.php .. php:method:: setTemplate($template) @@ -272,13 +159,8 @@ Class Reference :rtype: bool Permits you to set your template. You can submit a full or partial template. - :: - - $template = [ - 'table_open' => '' - ]; - $table->setTemplate($template); + .. literalinclude:: table/016.php .. php:method:: setEmpty($value) @@ -287,9 +169,9 @@ Class Reference :rtype: Table Lets you set a default value for use in any table cells that are empty. - You might, for example, set a non-breaking space:: + You might, for example, set a non-breaking space: - $table->setEmpty(" "); + .. literalinclude:: table/017.php .. php:method:: clear() @@ -301,24 +183,6 @@ Class Reference should to call this method after each table has been generated to clear the previous table information. - Example :: - - $table = new \CodeIgniter\View\Table(); - - $table->setCaption('Preferences') - ->setHeading('Name', 'Color', 'Size') - ->addRow('Fred', 'Blue', 'Small') - ->addRow('Mary', 'Red', 'Large') - ->addRow('John', 'Green', 'Medium'); - - echo $table->generate(); - - $table->clear(); - - $table->setCaption('Shipping') - ->setHeading('Name', 'Day', 'Delivery') - ->addRow('Fred', 'Wednesday', 'Express') - ->addRow('Mary', 'Monday', 'Air') - ->addRow('John', 'Saturday', 'Overnight'); + Example - echo $table->generate(); + .. literalinclude:: table/018.php diff --git a/user_guide_src/source/outgoing/table/001.php b/user_guide_src/source/outgoing/table/001.php new file mode 100644 index 000000000000..dbd5ba323335 --- /dev/null +++ b/user_guide_src/source/outgoing/table/001.php @@ -0,0 +1,3 @@ +generate($data); diff --git a/user_guide_src/source/outgoing/table/003.php b/user_guide_src/source/outgoing/table/003.php new file mode 100644 index 000000000000..91218de3f143 --- /dev/null +++ b/user_guide_src/source/outgoing/table/003.php @@ -0,0 +1,7 @@ +query('SELECT * FROM my_table'); + +echo $table->generate($query); diff --git a/user_guide_src/source/outgoing/table/004.php b/user_guide_src/source/outgoing/table/004.php new file mode 100644 index 000000000000..0d0679305409 --- /dev/null +++ b/user_guide_src/source/outgoing/table/004.php @@ -0,0 +1,11 @@ +setHeading('Name', 'Color', 'Size'); + +$table->addRow('Fred', 'Blue', 'Small'); +$table->addRow('Mary', 'Red', 'Large'); +$table->addRow('John', 'Green', 'Medium'); + +echo $table->generate(); diff --git a/user_guide_src/source/outgoing/table/005.php b/user_guide_src/source/outgoing/table/005.php new file mode 100644 index 000000000000..08d6dabb1850 --- /dev/null +++ b/user_guide_src/source/outgoing/table/005.php @@ -0,0 +1,11 @@ +setHeading(['Name', 'Color', 'Size']); + +$table->addRow(['Fred', 'Blue', 'Small']); +$table->addRow(['Mary', 'Red', 'Large']); +$table->addRow(['John', 'Green', 'Medium']); + +echo $table->generate(); diff --git a/user_guide_src/source/outgoing/table/006.php b/user_guide_src/source/outgoing/table/006.php new file mode 100644 index 000000000000..a5955aa5704b --- /dev/null +++ b/user_guide_src/source/outgoing/table/006.php @@ -0,0 +1,38 @@ + '
', + + 'thead_open' => '', + 'thead_close' => '', + + 'heading_row_start' => '', + 'heading_row_end' => '', + 'heading_cell_start' => '', + + 'tfoot_open' => '', + 'tfoot_close' => '', + + 'footing_row_start' => '', + 'footing_row_end' => '', + 'footing_cell_start' => '', + + 'tbody_open' => '', + 'tbody_close' => '', + + 'row_start' => '', + 'row_end' => '', + 'cell_start' => '', + + 'row_alt_start' => '', + 'row_alt_end' => '', + 'cell_alt_start' => '', + + 'table_close' => '
', + 'heading_cell_end' => '
', + 'footing_cell_end' => '
', + 'cell_end' => '
', + 'cell_alt_end' => '
', +]; + +$table->setTemplate($template); diff --git a/user_guide_src/source/outgoing/table/007.php b/user_guide_src/source/outgoing/table/007.php new file mode 100644 index 000000000000..022abfc296ad --- /dev/null +++ b/user_guide_src/source/outgoing/table/007.php @@ -0,0 +1,7 @@ + '', +]; + +$table->setTemplate($template); diff --git a/user_guide_src/source/outgoing/table/008.php b/user_guide_src/source/outgoing/table/008.php new file mode 100644 index 000000000000..99f12f2b2fcd --- /dev/null +++ b/user_guide_src/source/outgoing/table/008.php @@ -0,0 +1,7 @@ + '
', +]; + +$table = new \CodeIgniter\View\Table($customSettings); diff --git a/user_guide_src/source/outgoing/table/009.php b/user_guide_src/source/outgoing/table/009.php new file mode 100644 index 000000000000..c08af5b1fa9a --- /dev/null +++ b/user_guide_src/source/outgoing/table/009.php @@ -0,0 +1,9 @@ +setHeading('Name', 'Color', 'Size'); +$table->addRow('Fred', 'Blue', 'Small'); + +$table->function = 'htmlspecialchars'; +echo $table->generate(); diff --git a/user_guide_src/source/outgoing/table/010.php b/user_guide_src/source/outgoing/table/010.php new file mode 100644 index 000000000000..0c6a74ea5b35 --- /dev/null +++ b/user_guide_src/source/outgoing/table/010.php @@ -0,0 +1,3 @@ +setCaption('Colors'); diff --git a/user_guide_src/source/outgoing/table/011.php b/user_guide_src/source/outgoing/table/011.php new file mode 100644 index 000000000000..a6657c8690e4 --- /dev/null +++ b/user_guide_src/source/outgoing/table/011.php @@ -0,0 +1,5 @@ +setHeading('Name', 'Color', 'Size'); // or + +$table->setHeading(['Name', 'Color', 'Size']); diff --git a/user_guide_src/source/outgoing/table/012.php b/user_guide_src/source/outgoing/table/012.php new file mode 100644 index 000000000000..01a26b237908 --- /dev/null +++ b/user_guide_src/source/outgoing/table/012.php @@ -0,0 +1,5 @@ +setFooting('Subtotal', $subtotal, $notes); // or + +$table->setFooting(['Subtotal', $subtotal, $notes]); diff --git a/user_guide_src/source/outgoing/table/013.php b/user_guide_src/source/outgoing/table/013.php new file mode 100644 index 000000000000..1229bd01d7cd --- /dev/null +++ b/user_guide_src/source/outgoing/table/013.php @@ -0,0 +1,5 @@ +addRow('Blue', 'Red', 'Green'); // or + +$table->addRow(['Blue', 'Red', 'Green']); diff --git a/user_guide_src/source/outgoing/table/014.php b/user_guide_src/source/outgoing/table/014.php new file mode 100644 index 000000000000..d19424fc9fe4 --- /dev/null +++ b/user_guide_src/source/outgoing/table/014.php @@ -0,0 +1,9 @@ + 'Blue', 'class' => 'highlight', 'colspan' => 2]; +$table->addRow($cell, 'Red', 'Green'); + +?> + + + diff --git a/user_guide_src/source/outgoing/table/015.php b/user_guide_src/source/outgoing/table/015.php new file mode 100644 index 000000000000..5bc5a7029ed3 --- /dev/null +++ b/user_guide_src/source/outgoing/table/015.php @@ -0,0 +1,33 @@ +makeColumns($list, 3); + +$table->generate($newList); + +?> + + +
BlueRedGreen
+ + + + + + + + + + + + + + + + + + + + +
onetwothree
fourfivesix
seveneightnine
teneleventwelve
diff --git a/user_guide_src/source/outgoing/table/016.php b/user_guide_src/source/outgoing/table/016.php new file mode 100644 index 000000000000..022abfc296ad --- /dev/null +++ b/user_guide_src/source/outgoing/table/016.php @@ -0,0 +1,7 @@ + '', +]; + +$table->setTemplate($template); diff --git a/user_guide_src/source/outgoing/table/017.php b/user_guide_src/source/outgoing/table/017.php new file mode 100644 index 000000000000..8f7b203a3344 --- /dev/null +++ b/user_guide_src/source/outgoing/table/017.php @@ -0,0 +1,3 @@ +setEmpty(' '); diff --git a/user_guide_src/source/outgoing/table/018.php b/user_guide_src/source/outgoing/table/018.php new file mode 100644 index 000000000000..c67c746bde9b --- /dev/null +++ b/user_guide_src/source/outgoing/table/018.php @@ -0,0 +1,21 @@ +setCaption('Preferences') + ->setHeading('Name', 'Color', 'Size') + ->addRow('Fred', 'Blue', 'Small') + ->addRow('Mary', 'Red', 'Large') + ->addRow('John', 'Green', 'Medium'); + +echo $table->generate(); + +$table->clear(); + +$table->setCaption('Shipping') + ->setHeading('Name', 'Day', 'Delivery') + ->addRow('Fred', 'Wednesday', 'Express') + ->addRow('Mary', 'Monday', 'Air') + ->addRow('John', 'Saturday', 'Overnight'); + +echo $table->generate(); diff --git a/user_guide_src/source/outgoing/view_decorators.rst b/user_guide_src/source/outgoing/view_decorators.rst new file mode 100644 index 000000000000..83f3154f54c5 --- /dev/null +++ b/user_guide_src/source/outgoing/view_decorators.rst @@ -0,0 +1,23 @@ +############### +View Decorators +############### + +View Decorators allow your application to modify the HTML output during the rendering process. This happens just +prior to being cached, and allows you to apply custom functionality to your views. + +******************* +Creating Decorators +******************* + +Creating your own view decorators requires creating a new class that implements ``CodeIgniter\Views\ViewDecoratorInterface``. +This requires a single method that takes the generated HTML string, performs any modifications on it, and returns +the resulting HTML. + +.. literalinclude:: view_decorators/001.php + +Once created, the class must be registered in ``app/Config/View.php``: + +.. literalinclude:: view_decorators/002.php + +Now that it's registered the decorator will be called for every view that is rendered or parsed. +Decorators are called in the order specified in this configuration setting. diff --git a/user_guide_src/source/outgoing/view_decorators/001.php b/user_guide_src/source/outgoing/view_decorators/001.php new file mode 100644 index 000000000000..be05e93b5818 --- /dev/null +++ b/user_guide_src/source/outgoing/view_decorators/001.php @@ -0,0 +1,15 @@ +endSection() ?> endSection() ?> - ****************** Rendering the View ****************** -Rendering the view and it's layout is done exactly as any other view would be displayed within a controller:: +Rendering the view and it's layout is done exactly as any other view would be displayed within a controller: - public function index() - { - echo view('some_view'); - } +.. literalinclude:: view_layouts/001.php It renders the View **app/Views/some_view.php** and if it extends ``default``, the Layout **app/Views/default.php** is also used automatically. diff --git a/user_guide_src/source/outgoing/view_layouts/001.php b/user_guide_src/source/outgoing/view_layouts/001.php new file mode 100644 index 000000000000..f6105365fa47 --- /dev/null +++ b/user_guide_src/source/outgoing/view_layouts/001.php @@ -0,0 +1,11 @@ + 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - ]; - - echo $parser->setData($data) - ->render('blog_template'); +.. literalinclude:: view_parser/003.php View parameters are passed to ``setData()`` as an associative array of data to be replaced in the template. In the above example, the @@ -112,16 +106,11 @@ Several options can be passed to the ``render()`` or ``renderString()`` methods. - ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath; ignored for renderString() - ``saveData`` - true if the view data parameters should be retained for subsequent calls; - default is **false** + default is **true** - ``cascadeData`` - true if pseudo-variable settings should be passed on to nested substitutions; default is **true** -:: - - echo $parser->render('blog_template', [ - 'cache' => HOUR, - 'cache_name' => 'something_unique', - ]); +.. literalinclude:: view_parser/004.php *********************** Substitution Variations @@ -132,14 +121,9 @@ Substitutions are performed in the same sequence that pseudo-variables were adde The **simple substitution** performed by the parser is a one-to-one replacement of pseudo-variables where the corresponding data parameter -has either a scalar or string value, as in this example:: +has either a scalar or string value, as in this example: - $template = '{blog_title}'; - $data = ['blog_title' => 'My ramblings']; - - echo $parser->setData($data)->renderString($template); - - // Result: My ramblings +.. literalinclude:: view_parser/005.php The ``Parser`` takes substitution a lot further with "variable pairs", used for nested substitutions or looping, and with some advanced @@ -184,22 +168,9 @@ the number of rows in the "blog_entries" element of the parameters array. Parsing variable pairs is done using the identical code shown above to parse single variables, except, you will add a multi-dimensional array -corresponding to your variable pair data. Consider this example:: - - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entries' => [ - ['title' => 'Title 1', 'body' => 'Body 1'], - ['title' => 'Title 2', 'body' => 'Body 2'], - ['title' => 'Title 3', 'body' => 'Body 3'], - ['title' => 'Title 4', 'body' => 'Body 4'], - ['title' => 'Title 5', 'body' => 'Body 5'], - ], - ]; +corresponding to your variable pair data. Consider this example: - echo $parser->setData($data) - ->render('blog_template'); +.. literalinclude:: view_parser/006.php The value for the pseudo-variable ``blog_entries`` is a sequential array of associative arrays. The outer level does not have keys associated @@ -207,18 +178,9 @@ with each of the nested "rows". If your "pair" data is coming from a database result, which is already a multi-dimensional array, you can simply use the database ``getResultArray()`` -method:: - - $query = $db->query("SELECT * FROM blog"); - - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entries' => $query->getResultArray(), - ]; +method: - echo $parser->setData($data) - ->render('blog_template'); +.. literalinclude:: view_parser/007.php If the array you are trying to loop over contains objects instead of arrays, the parser will first look for an ``asArray()`` method on the object. If it exists, @@ -234,19 +196,9 @@ Nested Substitutions ==================== A nested substitution happens when the value for a pseudo-variable is -an associative array of values, like a record from a database:: +an associative array of values, like a record from a database: - $data = [ - 'blog_title' => 'My Blog Title', - 'blog_heading' => 'My Blog Heading', - 'blog_entry' => [ - 'title' => 'Title 1', - 'body' => 'Body 1', - ], - ]; - - echo $parser->setData($data) - ->render('blog_template'); +.. literalinclude:: view_parser/008.php The value for the pseudo-variable ``blog_entry`` is an associative array. The key/value pairs defined inside it will be exposed inside @@ -287,37 +239,18 @@ Cascading Data With both a nested and a loop substitution, you have the option of cascading data pairs into the inner substitution. -The following example is not impacted by cascading:: +The following example is not impacted by cascading: - $template = '{name} lives in {location}{city} on {planet}{/location}.'; +.. literalinclude:: view_parser/009.php - $data = [ - 'name' => 'George', - 'location' => ['city' => 'Red City', 'planet' => 'Mars'], - ]; - - echo $parser->setData($data)->renderString($template); - // Result: George lives in Red City on Mars. - -This example gives different results, depending on cascading:: +This example gives different results, depending on cascading: - $template = '{location}{name} lives in {city} on {planet}{/location}.'; - - $data = [ - 'name' => 'George', - 'location' => ['city' => 'Red City', 'planet' => 'Mars'], - ]; - - echo $parser->setData($data)->renderString($template, ['cascadeData'=>false]); - // Result: {name} lives in Red City on Mars. - - echo $parser->setData($data)->renderString($template, ['cascadeData'=>true]); - // Result: George lives in Red City on Mars. +.. literalinclude:: view_parser/010.php Preventing Parsing ================== -You can specify portions of the page to not be parsed with the ``{noparse}{/noparse}`` tag pair. Anything in this +You can specify portions of the page to not be parsed with the ``{noparse}`` ``{/noparse}`` tag pair. Anything in this section will stay exactly as it is, with no variable substitution, looping, etc, happening to the markup between the brackets. :: @@ -336,11 +269,9 @@ blocks must be closed with an ``endif`` tag::

Welcome, Admin!

{endif} -This simple block is converted to the following during parsing:: +This simple block is converted to the following during parsing: - -

Welcome, Admin!

- +.. literalinclude:: view_parser/011.php All variables used within if statements must have been previously set with the same name. Other than that, it is treated exactly like a standard PHP conditional, and all standard PHP rules would apply here. You can use any @@ -359,6 +290,31 @@ of the comparison operators you would normally, like ``==``, ``===``, ``!==``, ` .. warning:: In the background, conditionals are parsed using an ``eval()``, so you must ensure that you take care with the user data that is used within conditionals, or you could open your application up to security risks. +Changing the Conditional Delimiters +----------------------------------- + +If you have JavaScript code like the following in your templates, the Parser raises a syntax error because there are strings that can be interpreted as a conditional:: + + + +In that case, you can change the delimiters for conditionals with the ``setConditionalDelimiters()`` method to avoid misinterpretations: + +.. literalinclude:: view_parser/027.php + +In this case, you will write code in your template:: + + {% if $role=='admin' %} +

Welcome, Admin

+ {% else %} +

Welcome, User

+ {% endif %} + Escaping Data ============= @@ -380,7 +336,7 @@ Filters Any single variable substitution can have one or more filters applied to it to modify the way it is presented. These are not intended to drastically change the output, but provide ways to reuse the same variable data but with different -presentations. The **esc** filter discussed above is one example. Dates are another common use case, where you might +presentations. The ``esc`` filter discussed above is one example. Dates are another common use case, where you might need to format the same data differently in several sections on the same page. Filters are commands that come after the pseudo-variable name, and are separated by the pipe symbol, ``|``:: @@ -470,23 +426,18 @@ Custom Filters You can easily create your own filters by editing **app/Config/View.php** and adding new entries to the ``$filters`` array. Each key is the name of the filter is called by in the view, and its value is any valid PHP -callable:: +callable: - public $filters = [ - 'abs' => '\CodeIgniter\View\Filters::abs', - 'capitalize' => '\CodeIgniter\View\Filters::capitalize', - ]; +.. literalinclude:: view_parser/012.php PHP Native functions as Filters ------------------------------- You can use native php function as filters by editing **app/Config/View.php** and adding new entries to the ``$filters`` array.Each key is the name of the native PHP function is called by in the view, and its value is any valid native PHP -function prefixed with:: +function prefixed with: - public $filters = [ - 'str_repeat' => '\str_repeat', - ]; +.. literalinclude:: view_parser/013.php Parser Plugins ============== @@ -507,7 +458,7 @@ While plugins will often consist of tag pairs, like shown above, they can also b Opening tags can also contain parameters that can customize how the plugin works. The parameters are represented as key/value pairs:: - {+ foo bar=2 baz="x y" } + {+ foo bar=2 baz="x y" +} Parameters can also be single values:: @@ -530,107 +481,54 @@ lang language string Alias for the lang helper function. validation_errors fieldname(optional) Returns either error string for the field {+ validation_errors +} , {+ validation_errors field="email" +} (if specified) or all validation errors. route route name Alias for the route_to helper function. {+ route "login" +} +csp_script_nonce Alias for the csp_script_nonce helper {+ csp_script_nonce +} + function. +csp_style_nonce Alias for the csp_style_nonce helper {+ csp_style_nonce +} + function. ================== ========================= ============================================ ================================================================ Registering a Plugin -------------------- At its simplest, all you need to do to register a new plugin and make it ready for use is to add it to the -**app/Config/View.php**, under the **$plugins** array. The key is the name of the plugin that is -used within the template file. The value is any valid PHP callable, including static class methods, and closures:: - - public $plugins = [ - 'foo' => '\Some\Class::methodName', - 'bar' => function ($str, array $params=[]) { - return $str; - }, - ]; +**app/Config/View.php**, under the ``$plugins`` array. The key is the name of the plugin that is +used within the template file. The value is any valid PHP callable, including static class methods: -Any closures that are being used must be defined in the config file's constructor:: +.. literalinclude:: view_parser/014.php - class View extends \CodeIgniter\Config\View - { - public $plugins = []; +You can also use closures, but these can only be defined in the config file's constructor: - public function __construct() - { - $this->plugins['bar'] = function (array $params=[]) { - return $params[0] ?? ''; - }; - - parent::__construct(); - } - } +.. literalinclude:: view_parser/015.php If the callable is on its own, it is treated as a single tag, not a open/close one. It will be replaced by -the return value from the plugin:: +the return value from the plugin: - public $plugins = [ - 'foo' => '\Some\Class::methodName' - ]; - - // Tag is replaced by the return value of Some\Class::methodName static function. - {+ foo +} +.. literalinclude:: view_parser/016.php If the callable is wrapped in an array, it is treated as an open/close tag pair that can operate on any of -the content between its tags:: - - public $plugins = [ - 'foo' => ['\Some\Class::methodName'] - ]; +the content between its tags: - {+ foo +} inner content {+ /foo +} +.. literalinclude:: view_parser/017.php *********** Usage Notes *********** If you include substitution parameters that are not referenced in your -template, they are ignored:: +template, they are ignored: - $template = 'Hello, {firstname} {lastname}'; - $data = [ - 'title' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe' - ]; - echo $parser->setData($data) - ->renderString($template); - - // Result: Hello, John Doe +.. literalinclude:: view_parser/018.php If you do not include a substitution parameter that is referenced in your -template, the original pseudo-variable is shown in the result:: - - $template = 'Hello, {firstname} {initials} {lastname}'; - $data = [ - 'title' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe', - ]; - echo $parser->setData($data) - ->renderString($template); +template, the original pseudo-variable is shown in the result: - // Result: Hello, John {initials} Doe +.. literalinclude:: view_parser/019.php If you provide a string substitution parameter when an array is expected, i.e., for a variable pair, the substitution is done for the opening variable -pair tag, but the closing variable pair tag is not rendered properly:: - - $template = 'Hello, {firstname} {lastname} ({degrees}{degree} {/degrees})'; - $data = [ - 'degrees' => 'Mr', - 'firstname' => 'John', - 'lastname' => 'Doe', - 'titles' => [ - ['degree' => 'BSc'], - ['degree' => 'PhD'], - ], - ]; - echo $parser->setData($data) - ->renderString($template); +pair tag, but the closing variable pair tag is not rendered properly: - // Result: Hello, John Doe (Mr{degree} {/degrees}) +.. literalinclude:: view_parser/020.php View Fragments ============== @@ -652,8 +550,8 @@ An example with the iteration controlled in the view:: ['title' => 'Second Link', 'link' => '/second'], ] ]; - echo $parser->setData($data) - ->renderString($template); + + return $parser->setData($data)->renderString($template); Result:: @@ -663,25 +561,9 @@ Result:: An example with the iteration controlled in the controller, -using a view fragment:: - - $temp = ''; - $template1 = '
  • {title}
  • '; - $data1 = [ - ['title' => 'First Link', 'link' => '/first'], - ['title' => 'Second Link', 'link' => '/second'], - ]; +using a view fragment: - foreach ($data1 as $menuItem),{ - $temp .= $parser->setData($menuItem)->renderString($template1); - } - - $template2 = '
      {menuitems}
    '; - $data = [ - 'menuitems' => $temp, - ]; - echo $parser->setData($data) - ->renderString($template2); +.. literalinclude:: view_parser/021.php Result:: @@ -696,7 +578,7 @@ Class Reference .. php:class:: CodeIgniter\\View\\Parser - .. php:method:: render($view[, $options[, $saveData = false]]) + .. php:method:: render($view[, $options[, $saveData]]) :param string $view: File name of the view source :param array $options: Array of options, as key/value pairs @@ -704,9 +586,9 @@ Class Reference :returns: The rendered text for the chosen view :rtype: string - Builds the output based upon a file name and any data that has already been set:: + Builds the output based upon a file name and any data that has already been set: - echo $parser->render('myview'); + .. literalinclude:: view_parser/022.php Options supported: @@ -714,13 +596,11 @@ Class Reference - ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath - ``cascadeData`` - true if the data pairs in effect when a nested or loop substitution occurs should be propagated - ``saveData`` - true if the view data parameter should be retained for subsequent calls - - ``leftDelimiter`` - the left delimiter to use in pseudo-variable syntax - - ``rightDelimiter`` - the right delimiter to use in pseudo-variable syntax Any conditional substitutions are performed first, then remaining substitutions are performed for each data pair. - .. php:method:: renderString($template[, $options[, $saveData = false]]) + .. php:method:: renderString($template[, $options[, $saveData]]) :param string $template: View source provided as a string :param array $options: Array of options, as key/value pairs @@ -728,9 +608,9 @@ Class Reference :returns: The rendered text for the chosen view :rtype: string - Builds the output based upon a provided template source and any data that has already been set:: + Builds the output based upon a provided template source and any data that has already been set: - echo $parser->render('myview'); + .. literalinclude:: view_parser/023.php Options supported, and behavior, as above. @@ -741,9 +621,9 @@ Class Reference :returns: The Renderer, for method chaining :rtype: CodeIgniter\\View\\RendererInterface. - Sets several pieces of view data at once:: + Sets several pieces of view data at once: - $renderer->setData(['name' => 'George', 'position' => 'Boss']); + .. literalinclude:: view_parser/024.php Supported escape contexts: html, css, js, url, or attr or raw. If 'raw', no escaping will happen. @@ -756,9 +636,9 @@ Class Reference :returns: The Renderer, for method chaining :rtype: CodeIgniter\\View\\RendererInterface. - Sets a single piece of view data:: + Sets a single piece of view data: - $renderer->setVar('name','Joe','html'); + .. literalinclude:: view_parser/025.php Supported escape contexts: html, css, js, url, attr or raw. If 'raw', no escaping will happen. @@ -770,6 +650,17 @@ Class Reference :returns: The Renderer, for method chaining :rtype: CodeIgniter\\View\\RendererInterface. - Override the substitution field delimiters:: + Override the substitution field delimiters: + + .. literalinclude:: view_parser/026.php + + .. php:method:: setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = '}') + + :param string $leftDelimiter: Left delimiter for conditionals + :param string $rightDelimiter: right delimiter for conditionals + :returns: The Renderer, for method chaining + :rtype: CodeIgniter\\View\\RendererInterface. + + Override the conditional delimiters: - $renderer->setDelimiters('[',']'); + .. literalinclude:: view_parser/027.php diff --git a/user_guide_src/source/outgoing/view_parser/001.php b/user_guide_src/source/outgoing/view_parser/001.php new file mode 100644 index 000000000000..56f7f51e4a07 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/001.php @@ -0,0 +1,3 @@ + 'My Blog Title', + 'blog_heading' => 'My Blog Heading', +]; + +return $parser->setData($data)->render('blog_template'); diff --git a/user_guide_src/source/outgoing/view_parser/004.php b/user_guide_src/source/outgoing/view_parser/004.php new file mode 100644 index 000000000000..643f541a5b3e --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/004.php @@ -0,0 +1,6 @@ +render('blog_template', [ + 'cache' => HOUR, + 'cache_name' => 'something_unique', +]); diff --git a/user_guide_src/source/outgoing/view_parser/005.php b/user_guide_src/source/outgoing/view_parser/005.php new file mode 100644 index 000000000000..e85e49db45a2 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/005.php @@ -0,0 +1,7 @@ +{blog_title}'; +$data = ['blog_title' => 'My ramblings']; + +return $parser->setData($data)->renderString($template); +// Result: My ramblings diff --git a/user_guide_src/source/outgoing/view_parser/006.php b/user_guide_src/source/outgoing/view_parser/006.php new file mode 100644 index 000000000000..40d379aec2ea --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/006.php @@ -0,0 +1,15 @@ + 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entries' => [ + ['title' => 'Title 1', 'body' => 'Body 1'], + ['title' => 'Title 2', 'body' => 'Body 2'], + ['title' => 'Title 3', 'body' => 'Body 3'], + ['title' => 'Title 4', 'body' => 'Body 4'], + ['title' => 'Title 5', 'body' => 'Body 5'], + ], +]; + +return $parser->setData($data)->render('blog_template'); diff --git a/user_guide_src/source/outgoing/view_parser/007.php b/user_guide_src/source/outgoing/view_parser/007.php new file mode 100644 index 000000000000..5128ed3cbe4a --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/007.php @@ -0,0 +1,11 @@ +query('SELECT * FROM blog'); + +$data = [ + 'blog_title' => 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entries' => $query->getResultArray(), +]; + +return $parser->setData($data)->render('blog_template'); diff --git a/user_guide_src/source/outgoing/view_parser/008.php b/user_guide_src/source/outgoing/view_parser/008.php new file mode 100644 index 000000000000..8cac9bbbaa9d --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/008.php @@ -0,0 +1,12 @@ + 'My Blog Title', + 'blog_heading' => 'My Blog Heading', + 'blog_entry' => [ + 'title' => 'Title 1', + 'body' => 'Body 1', + ], +]; + +return $parser->setData($data)->render('blog_template'); diff --git a/user_guide_src/source/outgoing/view_parser/009.php b/user_guide_src/source/outgoing/view_parser/009.php new file mode 100644 index 000000000000..6f4b96e82274 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/009.php @@ -0,0 +1,11 @@ + 'George', + 'location' => ['city' => 'Red City', 'planet' => 'Mars'], +]; + +return $parser->setData($data)->renderString($template); +// Result: George lives in Red City on Mars. diff --git a/user_guide_src/source/outgoing/view_parser/010.php b/user_guide_src/source/outgoing/view_parser/010.php new file mode 100644 index 000000000000..4eb11fd6d908 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/010.php @@ -0,0 +1,16 @@ + 'George', + 'location' => ['city' => 'Red City', 'planet' => 'Mars'], +]; + +return $parser->setData($data)->renderString($template, ['cascadeData' => false]); +// Result: {name} lives in Red City on Mars. + +// or + +return $parser->setData($data)->renderString($template, ['cascadeData' => true]); +// Result: George lives in Red City on Mars. diff --git a/user_guide_src/source/outgoing/view_parser/011.php b/user_guide_src/source/outgoing/view_parser/011.php new file mode 100644 index 000000000000..88e2060b1ed5 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/011.php @@ -0,0 +1,3 @@ + +

    Welcome, Admin!

    + diff --git a/user_guide_src/source/outgoing/view_parser/012.php b/user_guide_src/source/outgoing/view_parser/012.php new file mode 100644 index 000000000000..000642837149 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/012.php @@ -0,0 +1,15 @@ + '\CodeIgniter\View\Filters::abs', + 'capitalize' => '\CodeIgniter\View\Filters::capitalize', + ]; + + // ... +} diff --git a/user_guide_src/source/outgoing/view_parser/013.php b/user_guide_src/source/outgoing/view_parser/013.php new file mode 100644 index 000000000000..e5546bdda272 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/013.php @@ -0,0 +1,14 @@ + '\str_repeat', + ]; + + // ... +} diff --git a/user_guide_src/source/outgoing/view_parser/014.php b/user_guide_src/source/outgoing/view_parser/014.php new file mode 100644 index 000000000000..5153c4ee6757 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/014.php @@ -0,0 +1,14 @@ + '\Some\Class::methodName', + ]; + + // ... +} diff --git a/user_guide_src/source/outgoing/view_parser/015.php b/user_guide_src/source/outgoing/view_parser/015.php new file mode 100644 index 000000000000..c0aa68b3381f --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/015.php @@ -0,0 +1,19 @@ +plugins['bar'] = static fn (array $params = []) => $params[0] ?? ''; + + parent::__construct(); + } + + // ... +} diff --git a/user_guide_src/source/outgoing/view_parser/016.php b/user_guide_src/source/outgoing/view_parser/016.php new file mode 100644 index 000000000000..5120c1eebf27 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/016.php @@ -0,0 +1,19 @@ + '\Some\Class::methodName', + ]; + + // ... +} + +/* + * Tag is replaced by the return value of Some\Class::methodName() static function. + * {+ foo +} + */ diff --git a/user_guide_src/source/outgoing/view_parser/017.php b/user_guide_src/source/outgoing/view_parser/017.php new file mode 100644 index 000000000000..d9427bde4bf4 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/017.php @@ -0,0 +1,16 @@ + ['\Some\Class::methodName'], + ]; + + // ... +} + +// {+ foo +} inner content {+ /foo +} diff --git a/user_guide_src/source/outgoing/view_parser/018.php b/user_guide_src/source/outgoing/view_parser/018.php new file mode 100644 index 000000000000..5a88dd651c25 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/018.php @@ -0,0 +1,11 @@ + 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe', +]; + +return $parser->setData($data)->renderString($template); +// Result: Hello, John Doe diff --git a/user_guide_src/source/outgoing/view_parser/019.php b/user_guide_src/source/outgoing/view_parser/019.php new file mode 100644 index 000000000000..5f465be2296b --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/019.php @@ -0,0 +1,11 @@ + 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe', +]; + +return $parser->setData($data)->renderString($template); +// Result: Hello, John {initials} Doe diff --git a/user_guide_src/source/outgoing/view_parser/020.php b/user_guide_src/source/outgoing/view_parser/020.php new file mode 100644 index 000000000000..fd072cbc0e5c --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/020.php @@ -0,0 +1,15 @@ + 'Mr', + 'firstname' => 'John', + 'lastname' => 'Doe', + 'titles' => [ + ['degree' => 'BSc'], + ['degree' => 'PhD'], + ], +]; + +return $parser->setData($data)->renderString($template); +// Result: Hello, John Doe (Mr{degree} {/degrees}) diff --git a/user_guide_src/source/outgoing/view_parser/021.php b/user_guide_src/source/outgoing/view_parser/021.php new file mode 100644 index 000000000000..60fbb625da79 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/021.php @@ -0,0 +1,19 @@ +{title}'; +$data1 = [ + ['title' => 'First Link', 'link' => '/first'], + ['title' => 'Second Link', 'link' => '/second'], +]; + +foreach ($data1 as $menuItem) { + $temp .= $parser->setData($menuItem)->renderString($template1); +} + +$template2 = '
      {menuitems}
    '; +$data = [ + 'menuitems' => $temp, +]; + +return $parser->setData($data)->renderString($template2); diff --git a/user_guide_src/source/outgoing/view_parser/022.php b/user_guide_src/source/outgoing/view_parser/022.php new file mode 100644 index 000000000000..1bdb256d92f1 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/022.php @@ -0,0 +1,3 @@ +render('myview'); diff --git a/user_guide_src/source/outgoing/view_parser/023.php b/user_guide_src/source/outgoing/view_parser/023.php new file mode 100644 index 000000000000..1bdb256d92f1 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/023.php @@ -0,0 +1,3 @@ +render('myview'); diff --git a/user_guide_src/source/outgoing/view_parser/024.php b/user_guide_src/source/outgoing/view_parser/024.php new file mode 100644 index 000000000000..17a0a477e756 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/024.php @@ -0,0 +1,3 @@ +setData(['name' => 'George', 'position' => 'Boss']); diff --git a/user_guide_src/source/outgoing/view_parser/025.php b/user_guide_src/source/outgoing/view_parser/025.php new file mode 100644 index 000000000000..fe67fe17aeb4 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/025.php @@ -0,0 +1,3 @@ +setVar('name', 'Joe', 'html'); diff --git a/user_guide_src/source/outgoing/view_parser/026.php b/user_guide_src/source/outgoing/view_parser/026.php new file mode 100644 index 000000000000..a7efd14837f2 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/026.php @@ -0,0 +1,3 @@ +setDelimiters('[', ']'); diff --git a/user_guide_src/source/outgoing/view_parser/027.php b/user_guide_src/source/outgoing/view_parser/027.php new file mode 100644 index 000000000000..84730679ced5 --- /dev/null +++ b/user_guide_src/source/outgoing/view_parser/027.php @@ -0,0 +1,3 @@ +setConditionalDelimiters('{%', '%}'); diff --git a/user_guide_src/source/outgoing/view_renderer.rst b/user_guide_src/source/outgoing/view_renderer.rst index 62fc30f5920e..feda1affda3d 100644 --- a/user_guide_src/source/outgoing/view_renderer.rst +++ b/user_guide_src/source/outgoing/view_renderer.rst @@ -12,14 +12,14 @@ Using the View Renderer The ``view()`` function is a convenience function that grabs an instance of the ``renderer`` service, sets the data, and renders the view. While this is often exactly what you want, you may find times where you want to work with it more directly. -In that case you can access the View service directly:: +In that case you can access the View service directly: - $view = \Config\Services::renderer(); +.. literalinclude:: view_renderer/001.php Alternately, if you are not using the ``View`` class as your default renderer, you -can instantiate it directly:: +can instantiate it directly: - $view = new \CodeIgniter\View\View(); +.. literalinclude:: view_renderer/002.php .. important:: You should create services only within controllers. If you need access to the View class from a library, you should set that as a dependency @@ -48,12 +48,10 @@ to you to process the array appropriately in your PHP code. Method Chaining =============== -The `setVar()` and `setData()` methods are chainable, allowing you to combine a -number of different calls together in a chain:: +The ``setVar()`` and ``setData()`` methods are chainable, allowing you to combine a +number of different calls together in a chain: - $view->setVar('one', $one) - ->setVar('two', $two) - ->render('myView'); +.. literalinclude:: view_renderer/003.php Escaping Data ============= @@ -62,9 +60,9 @@ When you pass data to the ``setVar()`` and ``setData()`` functions you have the against cross-site scripting attacks. As the last parameter in either method, you can pass the desired context to escape the data for. See below for context descriptions. -If you don't want the data to be escaped, you can pass `null` or `raw` as the final parameter to each function:: +If you don't want the data to be escaped, you can pass ``null`` or ``'raw'`` as the final parameter to each function: - $view->setVar('one', $one, 'raw'); +.. literalinclude:: view_renderer/004.php If you choose not to escape data, or you are passing in an object instance, you can manually escape the data within the view with the ``esc()`` function. The first parameter is the string to escape. The second parameter is the @@ -78,7 +76,7 @@ Escaping Contexts By default, the ``esc()`` and, in turn, the ``setVar()`` and ``setData()`` functions assume that the data you want to escape is intended to be used within standard HTML. However, if the data is intended for use in Javascript, CSS, or in an href attribute, you would need different escaping rules to be effective. You can pass in the name of the -context as the second parameter. Valid contexts are 'html', 'js', 'css', 'url', and 'attr':: +context as the second parameter. Valid contexts are ``'html'``, ``'js'``, ``'css'``, ``'url'``, and ``'attr'``:: Some Link @@ -98,8 +96,7 @@ View Renderer Options Several options can be passed to the ``render()`` or ``renderString()`` methods: - ``cache`` - the time in seconds, to save a view's results; ignored for renderString() -- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath; - ignored for renderString() +- ``cache_name`` - the ID used to save/retrieve a cached view result; defaults to the viewpath; ignored for ``renderString()`` - ``saveData`` - true if the view data parameters should be retained for subsequent calls .. note:: ``saveData`` as defined by the interface must be a boolean, but implementing @@ -110,7 +107,7 @@ Class Reference .. php:class:: CodeIgniter\\View\\View - .. php:method:: render($view[, $options[, $saveData=false]]) + .. php:method:: render($view[, $options[, $saveData = false]]) :noindex: :param string $view: File name of the view source @@ -119,11 +116,11 @@ Class Reference :returns: The rendered text for the chosen view :rtype: string - Builds the output based upon a file name and any data that has already been set:: + Builds the output based upon a file name and any data that has already been set: - echo $view->render('myview'); + .. literalinclude:: view_renderer/005.php - .. php:method:: renderString($view[, $options[, $saveData=false]]) + .. php:method:: renderString($view[, $options[, $saveData = false]]) :noindex: :param string $view: Contents of the view to render, for instance content retrieved from a database @@ -132,16 +129,16 @@ Class Reference :returns: The rendered text for the chosen view :rtype: string - Builds the output based upon a view fragment and any data that has already been set:: + Builds the output based upon a view fragment and any data that has already been set: - echo $view->renderString('
    My Sharona
    '); + .. literalinclude:: view_renderer/006.php - This could be used for displaying content that might have been stored in a database, + .. warning:: This could be used for displaying content that might have been stored in a database, but you need to be aware that this is a potential security vulnerability, and that you **must** validate any such data, and probably escape it appropriately! - .. php:method:: setData([$data[, $context=null]]) + .. php:method:: setData([$data[, $context = null]]) :noindex: :param array $data: Array of view data strings, as key/value pairs @@ -149,17 +146,17 @@ Class Reference :returns: The Renderer, for method chaining :rtype: CodeIgniter\\View\\RendererInterface. - Sets several pieces of view data at once:: + Sets several pieces of view data at once: - $view->setData(['name'=>'George', 'position'=>'Boss']); + .. literalinclude:: view_renderer/007.php - Supported escape contexts: html, css, js, url, or attr or raw. - If 'raw', no escaping will happen. + Supported escape contexts: ``html``, ``css``, ``js``, ``url``, or ``attr`` or ``raw``. + If ``'raw'``, no escaping will happen. Each call adds to the array of data that the object is accumulating, until the view is rendered. - .. php:method:: setVar($name[, $value=null[, $context=null]]) + .. php:method:: setVar($name[, $value = null[, $context = null]]) :noindex: :param string $name: Name of the view data variable @@ -168,12 +165,12 @@ Class Reference :returns: The Renderer, for method chaining :rtype: CodeIgniter\\View\\RendererInterface. - Sets a single piece of view data:: + Sets a single piece of view data: - $view->setVar('name','Joe','html'); + .. literalinclude:: view_renderer/008.php - Supported escape contexts: html, css, js, url, attr or raw. - If 'raw', no escaping will happen. + Supported escape contexts: ``html``, ``css``, ``js``, ``url``, ``attr`` or ``raw``. + If ``'raw'``, no escaping will happen. If you use the a view data variable that you have previously used for this object, the new value will replace the existing one. diff --git a/user_guide_src/source/outgoing/view_renderer/001.php b/user_guide_src/source/outgoing/view_renderer/001.php new file mode 100644 index 000000000000..2772e22cad0d --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/001.php @@ -0,0 +1,3 @@ +setVar('one', $one) + ->setVar('two', $two) + ->render('myView'); diff --git a/user_guide_src/source/outgoing/view_renderer/004.php b/user_guide_src/source/outgoing/view_renderer/004.php new file mode 100644 index 000000000000..b690f3478c82 --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/004.php @@ -0,0 +1,3 @@ +setVar('one', $one, 'raw'); diff --git a/user_guide_src/source/outgoing/view_renderer/005.php b/user_guide_src/source/outgoing/view_renderer/005.php new file mode 100644 index 000000000000..9bb35502f352 --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/005.php @@ -0,0 +1,3 @@ +render('myview'); diff --git a/user_guide_src/source/outgoing/view_renderer/006.php b/user_guide_src/source/outgoing/view_renderer/006.php new file mode 100644 index 000000000000..8f3c21abd99d --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/006.php @@ -0,0 +1,3 @@ +renderString('
    My Sharona
    '); diff --git a/user_guide_src/source/outgoing/view_renderer/007.php b/user_guide_src/source/outgoing/view_renderer/007.php new file mode 100644 index 000000000000..99080056b1bc --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/007.php @@ -0,0 +1,3 @@ +setData(['name' => 'George', 'position' => 'Boss']); diff --git a/user_guide_src/source/outgoing/view_renderer/008.php b/user_guide_src/source/outgoing/view_renderer/008.php new file mode 100644 index 000000000000..f0b868b54bca --- /dev/null +++ b/user_guide_src/source/outgoing/view_renderer/008.php @@ -0,0 +1,3 @@ +setVar('name', 'Joe', 'html'); diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index 96e350216ce7..3eb80427fc2a 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -14,7 +14,7 @@ Views are never called directly, they must be loaded by a controller. Remember t the Controller acts as the traffic cop, so it is responsible for fetching a particular view. If you have not read the :doc:`Controllers ` page, you should do so before continuing. -Using the example controller you created in the controller page, let’s add a view to it. +Using the example controller you created in the controller page, let's add a view to it. Creating a View =============== @@ -35,60 +35,30 @@ Then save the file in your **app/Views** directory. Displaying a View ================= -To load and display a particular view file you will use the following function:: +To load and display a particular view file you will use the following function: - echo view('name'); +.. literalinclude:: views/001.php Where *name* is the name of your view file. .. important:: If the file extension is omitted, then the views are expected to end with the .php extension. -Now, open the controller file you made earlier called ``Blog.php``, and replace the echo statement with the view function:: +Now, open the controller file you made earlier called ``Blog.php``, and replace the echo statement with the view function: - 'Your title', - ]; +content view, and a footer view. That might look something like this: - echo view('header'); - echo view('menu'); - echo view('content', $data); - echo view('footer'); - } - } +.. literalinclude:: views/003.php In the example above, we are using "dynamically added data", which you will see below. @@ -96,9 +66,9 @@ Storing Views within Sub-directories ==================================== Your view files can also be stored within sub-directories if you prefer that type of organization. -When doing so you will need to include the directory name loading the view. Example:: +When doing so you will need to include the directory name loading the view. Example: - echo view('directory_name/file_name'); +.. literalinclude:: views/004.php Namespaced Views ================ @@ -109,9 +79,9 @@ to package your views together in a module-like fashion for easy re-use or distr If you have ``example/blog`` directory that has a PSR-4 mapping set up in the :doc:`Autoloader ` living under the namespace ``Example\Blog``, you could retrieve view files as if they were namespaced also. Following this -example, you could load the **blog_view.php** file from **example/blog/Views** by prepending the namespace to the view name:: +example, you could load the **blog_view.php** file from **example/blog/Views** by prepending the namespace to the view name: - echo view('Example\Blog\Views\blog_view'); +.. literalinclude:: views/005.php .. _caching-views: @@ -119,47 +89,26 @@ Caching Views ============= You can cache a view with the ``view`` command by passing a ``cache`` option with the number of seconds to cache -the view for, in the third parameter:: +the view for, in the third parameter: - // Cache the view for 60 seconds - echo view('file_name', $data, ['cache' => 60]); +.. literalinclude:: views/006.php By default, the view will be cached using the same name as the view file itself. You can customize this by passing -along ``cache_name`` and the cache ID you wish to use:: +along ``cache_name`` and the cache ID you wish to use: - // Cache the view for 60 seconds - echo view('file_name', $data, ['cache' => 60, 'cache_name' => 'my_cached_view']); +.. literalinclude:: views/007.php Adding Dynamic Data to the View =============================== -Data is passed from the controller to the view by way of an array in the second parameter of the view function. -Here's an example:: +Data is passed from the controller to the view by way of an array in the second parameter of the ``view()`` function. +Here's an example: - $data = [ - 'title' => 'My title', - 'heading' => 'My Heading', - 'message' => 'My Message', - ]; +.. literalinclude:: views/008.php - echo view('blog_view', $data); +Let's try it with your controller file. Open it and add this code: -Let's try it with your controller file. Open it and add this code:: - - 'My title', - 'heading' => 'My Heading', - 'message' => 'My Message', - ]; +But this might not keep any data from "bleeding" into +other views, potentially causing issues. If you would prefer to clean the data after one call, you can pass the ``saveData`` option +into the ``$option`` array in the third parameter. - echo view('blog_view', $data, ['saveData' => true]); +.. literalinclude:: views/010.php -Additionally, if you would like the default functionality of the view function to be that it does save the data -between calls, you can set ``$saveData`` to **true** in **app/Config/Views.php**. +Additionally, if you would like the default functionality of the ``view()`` function to be that it does clear the data +between calls, you can set ``$saveData`` to **false** in **app/Config/Views.php**. Creating Loops ============== @@ -198,44 +145,10 @@ The data array you pass to your view files is not limited to simple variables. Y arrays, which can be looped to generate multiple rows. For example, if you pull data from your database it will typically be in the form of a multi-dimensional array. -Here’s a simple example. Add this to your controller:: +Here's a simple example. Add this to your controller: - ['Clean House', 'Call Mom', 'Run Errands'], - 'title' => 'My Real Title', - 'heading' => 'My Real Heading', - ]; - - echo view('blog_view', $data); - } - } - -Now open your view file and create a loop:: - - - - <?= esc($title) ?> - - -

    - -

    My Todo List

    - -
      - - -
    • - - -
    - - - +.. literalinclude:: views/012.php diff --git a/user_guide_src/source/outgoing/views/001.php b/user_guide_src/source/outgoing/views/001.php new file mode 100644 index 000000000000..c1b1ff23a6dd --- /dev/null +++ b/user_guide_src/source/outgoing/views/001.php @@ -0,0 +1,3 @@ + 'Your title', + ]; + + return view('header') + . view('menu') + . view('content', $data) + . view('footer'); + } +} diff --git a/user_guide_src/source/outgoing/views/004.php b/user_guide_src/source/outgoing/views/004.php new file mode 100644 index 000000000000..7c665c263391 --- /dev/null +++ b/user_guide_src/source/outgoing/views/004.php @@ -0,0 +1,3 @@ + 60]); diff --git a/user_guide_src/source/outgoing/views/007.php b/user_guide_src/source/outgoing/views/007.php new file mode 100644 index 000000000000..1b020abbc5a5 --- /dev/null +++ b/user_guide_src/source/outgoing/views/007.php @@ -0,0 +1,4 @@ + 60, 'cache_name' => 'my_cached_view']); diff --git a/user_guide_src/source/outgoing/views/008.php b/user_guide_src/source/outgoing/views/008.php new file mode 100644 index 000000000000..8f3218c7029b --- /dev/null +++ b/user_guide_src/source/outgoing/views/008.php @@ -0,0 +1,9 @@ + 'My title', + 'heading' => 'My Heading', + 'message' => 'My Message', +]; + +return view('blog_view', $data); diff --git a/user_guide_src/source/outgoing/views/009.php b/user_guide_src/source/outgoing/views/009.php new file mode 100644 index 000000000000..008d1cf85d26 --- /dev/null +++ b/user_guide_src/source/outgoing/views/009.php @@ -0,0 +1,16 @@ + 'My title', + 'heading' => 'My Heading', + 'message' => 'My Message', +]; + +return view('blog_view', $data, ['saveData' => false]); diff --git a/user_guide_src/source/outgoing/views/011.php b/user_guide_src/source/outgoing/views/011.php new file mode 100644 index 000000000000..bb1a5fc21725 --- /dev/null +++ b/user_guide_src/source/outgoing/views/011.php @@ -0,0 +1,19 @@ + ['Clean House', 'Call Mom', 'Run Errands'], + 'title' => 'My Real Title', + 'heading' => 'My Real Heading', + ]; + + return view('blog_view', $data); + } +} diff --git a/user_guide_src/source/outgoing/views/012.php b/user_guide_src/source/outgoing/views/012.php new file mode 100644 index 000000000000..5a3781fe6720 --- /dev/null +++ b/user_guide_src/source/outgoing/views/012.php @@ -0,0 +1,19 @@ + + + <?= esc($title) ?> + + +

    + +

    My Todo List

    + +
      + + +
    • + + +
    + + + diff --git a/user_guide_src/source/testing/benchmark.rst b/user_guide_src/source/testing/benchmark.rst index 780dcfe8ac08..1e7a5c9607b7 100644 --- a/user_guide_src/source/testing/benchmark.rst +++ b/user_guide_src/source/testing/benchmark.rst @@ -23,48 +23,34 @@ it simple to measure the performance of different aspects of your application. A the ``start()`` and ``stop()`` methods. The ``start()`` methods takes a single parameter: the name of this timer. You can use any string as the name -of the timer. It is only used for you to reference later to know which measurement is which:: +of the timer. It is only used for you to reference later to know which measurement is which: - $benchmark = \Config\Services::timer(); - $benchmark->start('render view'); +.. literalinclude:: benchmark/001.php -The ``stop()`` method takes the name of the timer that you want to stop as the only parameter, also:: +The ``stop()`` method takes the name of the timer that you want to stop as the only parameter, also: - $benchmark->stop('render view'); +.. literalinclude:: benchmark/002.php The name is not case-sensitive, but otherwise must match the name you gave it when you started the timer. Alternatively, you can use the :doc:`global function ` ``timer()`` to start -and stop timers:: +and stop timers: - // Start the timer - timer('render view'); - // Stop a running timer, - // if one of this name has been started - timer('render view'); +.. literalinclude:: benchmark/003.php Viewing Your Benchmark Points ============================= When your application runs, all of the timers that you have set are collected by the Timer class. It does not automatically display them, though. You can retrieve all of your timers by calling the ``getTimers()`` method. -This returns an array of benchmark information, including start, end, and duration:: +This returns an array of benchmark information, including start, end, and duration: - $timers = $benchmark->getTimers(); - - // Timers = - [ - 'render view' => [ - 'start' => 1234567890, - 'end' => 1345678920, - 'duration' => 15.4315, // number of seconds - ] - ] +.. literalinclude:: benchmark/004.php You can change the precision of the calculated duration by passing in the number of decimal places you want to be shown as -the only parameter. The default value is 4 numbers behind the decimal point:: +the only parameter. The default value is 4 numbers behind the decimal point: - $timers = $benchmark->getTimers(6); +.. literalinclude:: benchmark/005.php The timers are automatically displayed in the :doc:`Debub Toolbar `. @@ -73,10 +59,9 @@ Displaying Execution Time While the ``getTimers()`` method will give you the raw data for all of the timers in your project, you can retrieve the duration of a single timer, in seconds, with the `getElapsedTime()` method. The first parameter is the name of -the timer to display. The second is the number of decimal places to display. This defaults to 4:: +the timer to display. The second is the number of decimal places to display. This defaults to 4: - echo timer()->getElapsedTime('render view'); - // Displays: 0.0234 +.. literalinclude:: benchmark/006.php ================== Using the Iterator @@ -92,32 +77,20 @@ Creating Tasks To Run Tasks are defined within Closures. Any output the task creates will be discarded automatically. They are added to the Iterator class through the `add()` method. The first parameter is a name you want to refer to -this test by. The second parameter is the Closure, itself:: - - $iterator = new \CodeIgniter\Benchmark\Iterator(); - - // Add a new task - $iterator->add('single_concat', function () { - $str = 'Some basic'.'little'.'string concatenation test.'; - }); +this test by. The second parameter is the Closure, itself: - // Add another task - $iterator->add('double', function ($a = 'little') { - $str = "Some basic {$little} string test."; - }); +.. literalinclude:: benchmark/007.php Running the Tasks ================= Once you've added the tasks to run, you can use the ``run()`` method to loop over the tasks many times. By default, it will run each task 1000 times. This is probably sufficient for most simple tests. If you need -to run the tests more times than that, you can pass the number as the first parameter:: +to run the tests more times than that, you can pass the number as the first parameter: - // Run the tests 3000 times. - $iterator->run(3000); +.. literalinclude:: benchmark/008.php Once it has run, it will return an HTML table with the results of the test. If you don't want the results -displayed, you can pass in `false` as the second parameter:: +displayed, you can pass in `false` as the second parameter: - // Don't display the results. - $iterator->run(1000, false); +.. literalinclude:: benchmark/009.php diff --git a/user_guide_src/source/testing/benchmark/001.php b/user_guide_src/source/testing/benchmark/001.php new file mode 100644 index 000000000000..ce3bd09ac87f --- /dev/null +++ b/user_guide_src/source/testing/benchmark/001.php @@ -0,0 +1,4 @@ +start('render view'); diff --git a/user_guide_src/source/testing/benchmark/002.php b/user_guide_src/source/testing/benchmark/002.php new file mode 100644 index 000000000000..003d1cc18cf7 --- /dev/null +++ b/user_guide_src/source/testing/benchmark/002.php @@ -0,0 +1,3 @@ +stop('render view'); diff --git a/user_guide_src/source/testing/benchmark/003.php b/user_guide_src/source/testing/benchmark/003.php new file mode 100644 index 000000000000..861dbd8232a5 --- /dev/null +++ b/user_guide_src/source/testing/benchmark/003.php @@ -0,0 +1,7 @@ +getTimers(); +/* + * Produces: + * [ + * 'render view' => [ + * 'start' => 1234567890, + * 'end' => 1345678920, + * 'duration' => 15.4315, // number of seconds + * ] + * ] + */ diff --git a/user_guide_src/source/testing/benchmark/005.php b/user_guide_src/source/testing/benchmark/005.php new file mode 100644 index 000000000000..309c02f92a9b --- /dev/null +++ b/user_guide_src/source/testing/benchmark/005.php @@ -0,0 +1,3 @@ +getTimers(6); diff --git a/user_guide_src/source/testing/benchmark/006.php b/user_guide_src/source/testing/benchmark/006.php new file mode 100644 index 000000000000..a5f93a20fbf9 --- /dev/null +++ b/user_guide_src/source/testing/benchmark/006.php @@ -0,0 +1,4 @@ +getElapsedTime('render view'); +// Displays: 0.0234 diff --git a/user_guide_src/source/testing/benchmark/007.php b/user_guide_src/source/testing/benchmark/007.php new file mode 100644 index 000000000000..13a95ec66e29 --- /dev/null +++ b/user_guide_src/source/testing/benchmark/007.php @@ -0,0 +1,13 @@ +add('single_concat', static function () { + $str = 'Some basic' . 'little' . 'string concatenation test.'; +}); + +// Add another task +$iterator->add('double', static function ($a = 'little') { + $str = "Some basic {$little} string test."; +}); diff --git a/user_guide_src/source/testing/benchmark/008.php b/user_guide_src/source/testing/benchmark/008.php new file mode 100644 index 000000000000..da0f6a83b5fa --- /dev/null +++ b/user_guide_src/source/testing/benchmark/008.php @@ -0,0 +1,4 @@ +run(3000); diff --git a/user_guide_src/source/testing/benchmark/009.php b/user_guide_src/source/testing/benchmark/009.php new file mode 100644 index 000000000000..1f30692559b2 --- /dev/null +++ b/user_guide_src/source/testing/benchmark/009.php @@ -0,0 +1,4 @@ +run(1000, false); diff --git a/user_guide_src/source/testing/controllers.rst b/user_guide_src/source/testing/controllers.rst index d5dd2d379b8b..113c5ef2b66e 100644 --- a/user_guide_src/source/testing/controllers.rst +++ b/user_guide_src/source/testing/controllers.rst @@ -17,146 +17,97 @@ case you need it. The Helper Trait ================ -To enable Controller Testing you need to use the ``ControllerTestTrait`` trait within your tests:: +To enable Controller Testing you need to use the ``ControllerTestTrait`` trait within your tests: - withURI('http://example.com/categories') - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); - - $this->assertTrue($result->isOK()); - } - } +.. literalinclude:: controllers/002.php Helper Methods ============== -**controller($class)** +controller($class) +------------------ Specifies the class name of the controller to test. The first parameter must be a fully qualified class name -(i.e., include the namespace):: +(i.e., include the namespace): - $this->controller(\App\Controllers\ForumController::class); +.. literalinclude:: controllers/003.php -**execute(string $method, ...$params)** +execute(string $method, ...$params) +----------------------------------- -Executes the specified method within the controller. The first parameter is the name of the method to run:: +Executes the specified method within the controller. The first parameter is the name of the method to run: - $results = $this->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/004.php By specifying the second and subsequent parameters, you can pass them to the controller method. This returns a new helper class that provides a number of routines for checking the response itself. See below for details. -**withConfig($config)** - -Allows you to pass in a modified version of **Config\App.php** to test with different settings:: +withConfig($config) +------------------- - $config = new Config\App(); - $config->appTimezone = 'America/Chicago'; +Allows you to pass in a modified version of **Config\App.php** to test with different settings: - $results = $this->withConfig($config) - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/005.php If you do not provide one, the application's App config file will be used. -**withRequest($request)** +withRequest($request) +--------------------- -Allows you to provide an **IncomingRequest** instance tailored to your testing needs:: +Allows you to provide an **IncomingRequest** instance tailored to your testing needs: - $request = new \CodeIgniter\HTTP\IncomingRequest(new \Config\App(), new URI('http://example.com')); - $request->setLocale($locale); - - $results = $this->withRequest($request) - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/006.php If you do not provide one, a new IncomingRequest instance with the default application values will be passed into your controller. -**withResponse($response)** - -Allows you to provide a **Response** instance:: +withResponse($response) +----------------------- - $response = new \CodeIgniter\HTTP\Response(new \Config\App()); +Allows you to provide a **Response** instance: - $results = $this->withResponse($response) - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/007.php If you do not provide one, a new Response instance with the default application values will be passed into your controller. -**withLogger($logger)** - -Allows you to provide a **Logger** instance:: +withLogger($logger) +------------------- - $logger = new \CodeIgniter\Log\Handlers\FileHandler(); +Allows you to provide a **Logger** instance: - $results = $this->withResponse($response) - ->withLogger($logger) - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/008.php If you do not provide one, a new Logger instance with the default configuration values will be passed into your controller. -**withURI(string $uri)** +withURI(string $uri) +-------------------- Allows you to provide a new URI that simulates the URL the client was visiting when this controller was run. This is helpful if you need to check URI segments within your controller. The only parameter is a string -representing a valid URI:: +representing a valid URI: - $results = $this->withURI('http://example.com/forums/categories') - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/009.php It is a good practice to always provide the URI during testing to avoid surprises. -**withBody($body)** +withBody($body) +--------------- Allows you to provide a custom body for the request. This can be helpful when testing API controllers where -you need to set a JSON value as the body. The only parameter is a string that represents the body of the request:: - - $body = json_encode(['foo' => 'bar']); +you need to set a JSON value as the body. The only parameter is a string that represents the body of the request: - $results = $this->withBody($body) - ->controller(\App\Controllers\ForumController::class) - ->execute('showCategories'); +.. literalinclude:: controllers/010.php Checking the Response ===================== @@ -174,19 +125,9 @@ The Helper Trait ---------------- Just like with the Controller Tester you need to include the ``FilterTestTrait`` in your test -cases to enable these features:: - - filtersConfig->globals['before'] = ['admin-only-filter']; - - $this->assertHasFilters('unfiltered/route', 'before'); - } - ... +.. literalinclude:: controllers/012.php Checking Routes --------------- @@ -234,9 +165,9 @@ a large performance advantage over Controller and HTTP Testing. :returns: Aliases for each filter that would have run :rtype: string[] - Usage example:: + Usage example: - $result = $this->getFiltersForRoute('/', 'after'); // ['toolbar'] + .. literalinclude:: controllers/013.php Calling Filter Methods ---------------------- @@ -252,15 +183,9 @@ method using these properties to test your Filter code safely and check the resu :returns: A callable method to run the simulated Filter event :rtype: Closure - Usage example:: - - protected function testUnauthorizedAccessRedirects() - { - $caller = $this->getFilterCaller('permission', 'before'); - $result = $caller('MayEditWidgets'); + Usage example: - $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result); - } + .. literalinclude:: controllers/014.php Notice how the ``Closure`` can take input parameters which are passed to your filter method. @@ -270,22 +195,30 @@ Assertions In addition to the helper methods above ``FilterTestTrait`` also comes with some assertions to streamline your test methods. -The **assertFilter()** method checks that the given route at position uses the filter (by its alias):: +assertFilter() +^^^^^^^^^^^^^^ + +The ``assertFilter()`` method checks that the given route at position uses the filter (by its alias): + +.. literalinclude:: controllers/015.php + +assertNotFilter() +^^^^^^^^^^^^^^^^^ + +The ``assertNotFilter()`` method checks that the given route at position does not use the filter (by its alias): - // Make sure users are logged in before checking their account - $this->assertFilter('users/account', 'before', 'login'); +.. literalinclude:: controllers/016.php -The **assertNotFilter()** method checks that the given route at position does not use the filter (by its alias):: +assertHasFilters() +^^^^^^^^^^^^^^^^^^ - // Make sure API calls do not try to use the Debug Toolbar - $this->assertNotFilter('api/v1/widgets', 'after', 'toolbar'); +The ``assertHasFilters()`` method checks that the given route at position has at least one filter set: -The **assertHasFilters()** method checks that the given route at position has at least one filter set:: +.. literalinclude:: controllers/017.php - // Make sure that filters are enabled - $this->assertHasFilters('filtered/route', 'after'); +assertNotHasFilters() +^^^^^^^^^^^^^^^^^^^^^ -The **assertNotHasFilters()** method checks that the given route at position has no filters set:: +The ``assertNotHasFilters()`` method checks that the given route at position has no filters set: - // Make sure no filters run for our static pages - $this->assertNotHasFilters('about/contact', 'before'); +.. literalinclude:: controllers/018.php diff --git a/user_guide_src/source/testing/controllers/001.php b/user_guide_src/source/testing/controllers/001.php new file mode 100644 index 000000000000..9162fa0aeabd --- /dev/null +++ b/user_guide_src/source/testing/controllers/001.php @@ -0,0 +1,13 @@ +withURI('http://example.com/categories') + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); + + $this->assertTrue($result->isOK()); + } +} diff --git a/user_guide_src/source/testing/controllers/003.php b/user_guide_src/source/testing/controllers/003.php new file mode 100644 index 000000000000..51e810538e87 --- /dev/null +++ b/user_guide_src/source/testing/controllers/003.php @@ -0,0 +1,3 @@ +controller(\App\Controllers\ForumController::class); diff --git a/user_guide_src/source/testing/controllers/004.php b/user_guide_src/source/testing/controllers/004.php new file mode 100644 index 000000000000..17b5f42590ce --- /dev/null +++ b/user_guide_src/source/testing/controllers/004.php @@ -0,0 +1,4 @@ +controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/005.php b/user_guide_src/source/testing/controllers/005.php new file mode 100644 index 000000000000..7d2864f5fcbf --- /dev/null +++ b/user_guide_src/source/testing/controllers/005.php @@ -0,0 +1,8 @@ +appTimezone = 'America/Chicago'; + +$results = $this->withConfig($config) + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/006.php b/user_guide_src/source/testing/controllers/006.php new file mode 100644 index 000000000000..0fc7670487de --- /dev/null +++ b/user_guide_src/source/testing/controllers/006.php @@ -0,0 +1,8 @@ +setLocale($locale); + +$results = $this->withRequest($request) + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/007.php b/user_guide_src/source/testing/controllers/007.php new file mode 100644 index 000000000000..8f7d55448035 --- /dev/null +++ b/user_guide_src/source/testing/controllers/007.php @@ -0,0 +1,7 @@ +withResponse($response) + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/008.php b/user_guide_src/source/testing/controllers/008.php new file mode 100644 index 000000000000..641a7f92f68b --- /dev/null +++ b/user_guide_src/source/testing/controllers/008.php @@ -0,0 +1,8 @@ +withResponse($response) + ->withLogger($logger) + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/009.php b/user_guide_src/source/testing/controllers/009.php new file mode 100644 index 000000000000..711d947b4267 --- /dev/null +++ b/user_guide_src/source/testing/controllers/009.php @@ -0,0 +1,5 @@ +withURI('http://example.com/forums/categories') + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/010.php b/user_guide_src/source/testing/controllers/010.php new file mode 100644 index 000000000000..e257dcbc4928 --- /dev/null +++ b/user_guide_src/source/testing/controllers/010.php @@ -0,0 +1,7 @@ + 'bar']); + +$results = $this->withBody($body) + ->controller(\App\Controllers\ForumController::class) + ->execute('showCategories'); diff --git a/user_guide_src/source/testing/controllers/011.php b/user_guide_src/source/testing/controllers/011.php new file mode 100644 index 000000000000..f2ab558b8da2 --- /dev/null +++ b/user_guide_src/source/testing/controllers/011.php @@ -0,0 +1,11 @@ +filtersConfig->globals['before'] = ['admin-only-filter']; + + $this->assertHasFilters('unfiltered/route', 'before'); + } + + // ... +} diff --git a/user_guide_src/source/testing/controllers/013.php b/user_guide_src/source/testing/controllers/013.php new file mode 100644 index 000000000000..e205f0f4ea54 --- /dev/null +++ b/user_guide_src/source/testing/controllers/013.php @@ -0,0 +1,3 @@ +getFiltersForRoute('/', 'after'); // ['toolbar'] diff --git a/user_guide_src/source/testing/controllers/014.php b/user_guide_src/source/testing/controllers/014.php new file mode 100644 index 000000000000..feb1d4448540 --- /dev/null +++ b/user_guide_src/source/testing/controllers/014.php @@ -0,0 +1,19 @@ +getFilterCaller('permission', 'before'); + $result = $caller('MayEditWidgets'); + + $this->assertInstanceOf('CodeIgniter\HTTP\RedirectResponse', $result); + } +} diff --git a/user_guide_src/source/testing/controllers/015.php b/user_guide_src/source/testing/controllers/015.php new file mode 100644 index 000000000000..866d2e80ce43 --- /dev/null +++ b/user_guide_src/source/testing/controllers/015.php @@ -0,0 +1,4 @@ +assertFilter('users/account', 'before', 'login'); diff --git a/user_guide_src/source/testing/controllers/016.php b/user_guide_src/source/testing/controllers/016.php new file mode 100644 index 000000000000..cdbf08ccc36b --- /dev/null +++ b/user_guide_src/source/testing/controllers/016.php @@ -0,0 +1,4 @@ +assertNotFilter('api/v1/widgets', 'after', 'toolbar'); diff --git a/user_guide_src/source/testing/controllers/017.php b/user_guide_src/source/testing/controllers/017.php new file mode 100644 index 000000000000..15db694e5419 --- /dev/null +++ b/user_guide_src/source/testing/controllers/017.php @@ -0,0 +1,4 @@ +assertHasFilters('filtered/route', 'after'); diff --git a/user_guide_src/source/testing/controllers/018.php b/user_guide_src/source/testing/controllers/018.php new file mode 100644 index 000000000000..33606c0e122e --- /dev/null +++ b/user_guide_src/source/testing/controllers/018.php @@ -0,0 +1,4 @@ +assertNotHasFilters('about/contact', 'before'); diff --git a/user_guide_src/source/testing/database.rst b/user_guide_src/source/testing/database.rst index 86b91537917e..382b4ee4aa56 100644 --- a/user_guide_src/source/testing/database.rst +++ b/user_guide_src/source/testing/database.rst @@ -1,63 +1,27 @@ -===================== +##################### Testing Your Database -===================== +##################### .. contents:: :local: :depth: 2 The Test Class -============== +************** In order to take advantage of the built-in database tools that CodeIgniter provides for testing, your -tests must extend ``CIUnitTestCase`` and use the ``DatabaseTestTrait``:: +tests must extend ``CIUnitTestCase`` and use the ``DatabaseTestTrait``: - 'joe@example.com', - 'active' => 1, - ]; - $this->dontSeeInDatabase('users', $criteria); - -**seeInDatabase($table, $criteria)** +Inserts a new row into the database. This row is removed after the current test runs. ``$data`` is an associative +array with the data to insert into the table. -Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES exist in the database. -:: +.. literalinclude:: database/007.php - $criteria = [ - 'email' => 'joe@example.com', - 'active' => 1, - ]; - $this->seeInDatabase('users', $criteria); +Getting Data from Database +========================== -**grabFromDatabase($table, $column, $criteria)** +grabFromDatabase($table, $column, $criteria) +-------------------------------------------- Returns the value of ``$column`` from the specified table where the row matches ``$criteria``. If more than one -row is found, it will only test against the first one. -:: +row is found, it will only return the first one. - $username = $this->grabFromDatabase('users', 'username', ['email' => 'joe@example.com']); +Assertions +========== -**hasInDatabase($table, $data)** +dontSeeInDatabase($table, $criteria) +------------------------------------ -Inserts a new row into the database. This row is removed after the current test runs. ``$data`` is an associative -array with the data to insert into the table. -:: +Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES NOT exist in the database. + +.. literalinclude:: database/004.php + +seeInDatabase($table, $criteria) +-------------------------------- + +Asserts that a row with criteria matching the key/value pairs in ``$criteria`` DOES exist in the database. - $data = [ - 'email' => 'joe@example.com', - 'name' => 'Joe Cool', - ]; - $this->hasInDatabase('users', $data); +.. literalinclude:: database/005.php -**seeNumRecords($expected, $table, $criteria)** +seeNumRecords($expected, $table, $criteria) +------------------------------------------- Asserts that a number of matching rows are found in the database that match ``$criteria``. -:: - $criteria = [ - 'active' => 1, - ]; - $this->seeNumRecords(2, 'users', $criteria); +.. literalinclude:: database/008.php diff --git a/user_guide_src/source/testing/database/001.php b/user_guide_src/source/testing/database/001.php new file mode 100644 index 000000000000..809cd59d8c0f --- /dev/null +++ b/user_guide_src/source/testing/database/001.php @@ -0,0 +1,13 @@ + 'joe@example.com', + 'active' => 1, +]; +$this->dontSeeInDatabase('users', $criteria); diff --git a/user_guide_src/source/testing/database/005.php b/user_guide_src/source/testing/database/005.php new file mode 100644 index 000000000000..960ba5e5b91e --- /dev/null +++ b/user_guide_src/source/testing/database/005.php @@ -0,0 +1,7 @@ + 'joe@example.com', + 'active' => 1, +]; +$this->seeInDatabase('users', $criteria); diff --git a/user_guide_src/source/testing/database/006.php b/user_guide_src/source/testing/database/006.php new file mode 100644 index 000000000000..951736b95611 --- /dev/null +++ b/user_guide_src/source/testing/database/006.php @@ -0,0 +1,3 @@ +grabFromDatabase('users', 'username', ['email' => 'joe@example.com']); diff --git a/user_guide_src/source/testing/database/007.php b/user_guide_src/source/testing/database/007.php new file mode 100644 index 000000000000..46ebb8fbe695 --- /dev/null +++ b/user_guide_src/source/testing/database/007.php @@ -0,0 +1,7 @@ + 'joe@example.com', + 'name' => 'Joe Cool', +]; +$this->hasInDatabase('users', $data); diff --git a/user_guide_src/source/testing/database/008.php b/user_guide_src/source/testing/database/008.php new file mode 100644 index 000000000000..9a60e154d331 --- /dev/null +++ b/user_guide_src/source/testing/database/008.php @@ -0,0 +1,6 @@ + 1, +]; +$this->seeNumRecords(2, 'users', $criteria); diff --git a/user_guide_src/source/testing/debugging.rst b/user_guide_src/source/testing/debugging.rst index 22c77141e116..b8da022d018f 100644 --- a/user_guide_src/source/testing/debugging.rst +++ b/user_guide_src/source/testing/debugging.rst @@ -26,22 +26,25 @@ This is defined in the boot files (e.g. **app/Config/Boot/development.php**). Using Kint ========== -**d()** +d() +--- The ``d()`` method dumps all of the data it knows about the contents passed as the only parameter to the screen, and -allows the script to continue executing:: +allows the script to continue executing: - d($_SERVER); +.. literalinclude:: debugging/001.php -**dd()** +dd() +---- -This method is identical to ``d()``, except that it also ``dies()`` and no further code is executed this request. +This method is identical to ``d()``, except that it also ``die()`` and no further code is executed this request. -**trace()** +trace() +------- -This provides a backtrace to the current execution point, with Kint's own unique spin:: +This provides a backtrace to the current execution point, with Kint's own unique spin: - trace(); +.. literalinclude:: debugging/002.php For more information, see `Kint's page `_. @@ -74,18 +77,9 @@ Choosing What to Show CodeIgniter ships with several Collectors that, as the name implies, collect data to display on the toolbar. You can easily make your own to customize the toolbar. To determine which collectors are shown, again head over to -the **app/Config/Toolbar.php** configuration file:: - - public $collectors = [ - \CodeIgniter\Debug\Toolbar\Collectors\Timers::class, - \CodeIgniter\Debug\Toolbar\Collectors\Database::class, - \CodeIgniter\Debug\Toolbar\Collectors\Logs::class, - \CodeIgniter\Debug\Toolbar\Collectors\Views::class, - \CodeIgniter\Debug\Toolbar\Collectors\Cache::class, - \CodeIgniter\Debug\Toolbar\Collectors\Files::class, - \CodeIgniter\Debug\Toolbar\Collectors\Routes::class, - \CodeIgniter\Debug\Toolbar\Collectors\Events::class, - ]; +the **app/Config/Toolbar.php** configuration file: + +.. literalinclude:: debugging/003.php Comment out any collectors that you do not want to show. Add custom Collectors here by providing the fully-qualified class name. The exact collectors that appear here will affect which tabs are shown, as well as what information is @@ -119,24 +113,8 @@ Creating custom collectors is a straightforward task. You create a new class, fu can locate it, that extends ``CodeIgniter\Debug\Toolbar\Collectors\BaseCollector``. This provides a number of methods that you can override, and has four required class properties that you must correctly set depending on how you want the Collector to work -:: - - '', // Name displayed on the left of the timeline - 'component' => '', // Name of the Component listed in the middle of timeline - 'start' => 0.00, // start time, like microtime(true) - 'duration' => 0.00, // duration, like mircrotime(true) - microtime(true) - ]; +.. literalinclude:: debugging/005.php Providing Vars -------------- @@ -196,15 +169,6 @@ To add data to the Vars tab you must: 2. Implement ``getVarData()`` method. The ``getVarData()`` method should return an array containing arrays of key/value pairs to display. The name of the -outer array's key is the name of the section on the Vars tab:: - - $data = [ - 'section 1' => [ - 'foo' => 'bar', - 'bar' => 'baz', - ], - 'section 2' => [ - 'foo' => 'bar', - 'bar' => 'baz', - ], - ]; +outer array's key is the name of the section on the Vars tab: + +.. literalinclude:: debugging/006.php diff --git a/user_guide_src/source/testing/debugging/001.php b/user_guide_src/source/testing/debugging/001.php new file mode 100644 index 000000000000..f54b4701f98e --- /dev/null +++ b/user_guide_src/source/testing/debugging/001.php @@ -0,0 +1,3 @@ + '', // Name displayed on the left of the timeline + 'component' => '', // Name of the Component listed in the middle of timeline + 'start' => 0.00, // start time, like microtime(true) + 'duration' => 0.00, // duration, like mircrotime(true) - microtime(true) +]; diff --git a/user_guide_src/source/testing/debugging/006.php b/user_guide_src/source/testing/debugging/006.php new file mode 100644 index 000000000000..3ad25be67894 --- /dev/null +++ b/user_guide_src/source/testing/debugging/006.php @@ -0,0 +1,12 @@ + [ + 'foo' => 'bar', + 'bar' => 'baz', + ], + 'section 2' => [ + 'foo' => 'bar', + 'bar' => 'baz', + ], +]; diff --git a/user_guide_src/source/testing/fabricator.rst b/user_guide_src/source/testing/fabricator.rst index 933db982b2ee..826e435d0821 100644 --- a/user_guide_src/source/testing/fabricator.rst +++ b/user_guide_src/source/testing/fabricator.rst @@ -3,7 +3,7 @@ Generating Test Data #################### Often you will need sample data for your application to run its tests. The ``Fabricator`` class -uses fzaninotto's `Faker `_ to turn models into generators +uses fzaninotto's `Faker `_ to turn models into generators of random data. Use fabricators in your seeds or test cases to stage fake data for your unit tests. .. contents:: @@ -14,27 +14,22 @@ Supported Models ================ ``Fabricator`` supports any model that extends the framework's core model, ``CodeIgniter\Model``. -You may use your own custom models by ensuring they implement ``CodeIgniter\Test\Interfaces\FabricatorModel``:: +You may use your own custom models by ensuring they implement ``CodeIgniter\Test\Interfaces\FabricatorModel``: - class MyModel implements CodeIgniter\Test\Interfaces\FabricatorModel +.. literalinclude:: fabricator/001.php .. note:: In addition to methods, the interface outlines some necessary properties for the target model. Please see the interface code for details. Loading Fabricators =================== -At its most basic a fabricator takes the model to act on:: +At its most basic a fabricator takes the model to act on: - use App\Models\UserModel; - use CodeIgniter\Test\Fabricator; +.. literalinclude:: fabricator/002.php - $fabricator = new Fabricator(UserModel::class); +The parameter can be a string specifying the name of the model, or an instance of the model itself: -The parameter can be a string specifying the name of the model, or an instance of the model itself:: - - $model = new UserModel($testDbConnection); - - $fabricator = new Fabricator($model); +.. literalinclude:: fabricator/003.php Defining Formatters =================== @@ -43,58 +38,36 @@ Faker generates data by requesting it from a formatter. With no formatters defin attempt to guess at the most appropriate fit based on the field name and properties of the model it represents, falling back on ``$fabricator->defaultFormatter``. This may be fine if your field names correspond with common formatters, or if you don't care much about the content of the fields, but most -of the time you will want to specify the formatters to use as the second parameter to the constructor:: - - $formatters = [ - 'first' => 'firstName', - 'email' => 'email', - 'phone' => 'phoneNumber', - 'avatar' => 'imageUrl', - ]; +of the time you will want to specify the formatters to use as the second parameter to the constructor: - $fabricator = new Fabricator(UserModel::class, $formatters); +.. literalinclude:: fabricator/004.php You can also change the formatters after a fabricator is initialized by using the ``setFormatters()`` method. -**Advanced Formatting** +Advanced Formatting +------------------- Sometimes the default return of a formatter is not enough. Faker providers allow parameters to most formatters to further limit the scope of random data. A fabricator will check its representative model for the ``fake()`` -method where you can define exactly what the faked data should look like:: - - class UserModel - { - public function fake(Generator &$faker) - { - return [ - 'first' => $faker->firstName, - 'email' => $faker->email, - 'phone' => $faker->phoneNumber, - 'avatar' => Faker\Provider\Image::imageUrl(800, 400), - 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, - ]; - } +method where you can define exactly what the faked data should look like: + +.. literalinclude:: fabricator/005.php Notice in this example how the first three values are equivalent to the formatters from before. However for ``avatar`` we have requested an image size other than the default and ``login`` uses a conditional based on app configuration, neither of which are possible using the ``$formatters`` parameter. You may want to keep your test data separate from your production models, so it is a good practice to define -a child class in your test support folder:: - - namespace Tests\Support\Models; +a child class in your test support folder: - class UserFabricator extends \App\Models\UserModel - { - public function fake(&$faker) - { +.. literalinclude:: fabricator/006.php Localization ============ Faker supports a lot of different locales. Check their documentation to determine which providers -support your locale. Specify a locale in the third parameter while initiating a fabricator:: +support your locale. Specify a locale in the third parameter while initiating a fabricator: - $fabricator = new Fabricator(UserModel::class, null, 'fr_FR'); +.. literalinclude:: fabricator/007.php If no locale is specified it will use the one defined in **app/Config/App.php** as ``defaultLocale``. You can check the locale of an existing fabricator using its ``getLocale()`` method. @@ -102,114 +75,61 @@ You can check the locale of an existing fabricator using its ``getLocale()`` met Faking the Data =============== -Once you have a properly-initialized fabricator it is easy to generate test data with the ``make()`` command:: +Once you have a properly-initialized fabricator it is easy to generate test data with the ``make()`` command: - $fabricator = new Fabricator(UserFabricator::class); - $testUser = $fabricator->make(); - print_r($testUser); +.. literalinclude:: fabricator/008.php -You might get back something like this:: +You might get back something like this: - array( - 'first' => "Maynard", - 'email' => "king.alford@example.org", - 'phone' => "201-886-0269 x3767", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - ) +.. literalinclude:: fabricator/009.php -You can also get a lot of them back by supplying a count:: +You can also get a lot of them back by supplying a count: - $users = $fabricator->make(10); +.. literalinclude:: fabricator/010.php The return type of ``make()`` mimics what is defined in the representative model, but you can -force a type using the methods directly:: +force a type using the methods directly: - $userArray = $fabricator->makeArray(); - $userObject = $fabricator->makeObject(); - $userEntity = $fabricator->makeObject('App\Entities\User'); +.. literalinclude:: fabricator/011.php The return from ``make()`` is ready to be used in tests or inserted into the database. Alternatively ``Fabricator`` includes the ``create()`` command to insert it for you, and return the result. Due to model callbacks, database formatting, and special keys like primary and timestamps the return -from ``create()`` can differ from ``make()``. You might get back something like this:: +from ``create()`` can differ from ``make()``. You might get back something like this: - array( - 'id' => 1, - 'first' => "Rachel", - 'email' => "bradley72@gmail.com", - 'phone' => "741-241-2356", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - 'created_at' => "2020-05-08 14:52:10", - 'updated_at' => "2020-05-08 14:52:10", - ) +.. literalinclude:: fabricator/012.php -Similar to ``make()`` you can supply a count to insert and return an array of objects:: +Similar to ``make()`` you can supply a count to insert and return an array of objects: - $users = $fabricator->create(100); +.. literalinclude:: fabricator/013.php Finally, there may be times you want to test with the full database object but you are not actually using a database. ``create()`` takes a second parameter to allowing mocking the object, returning -the object with extra database fields above without actually touching the database:: - - $user = $fabricator(null, true); +the object with extra database fields above without actually touching the database: - $this->assertIsNumeric($user->id); - $this->dontSeeInDatabase('user', ['id' => $user->id]); +.. literalinclude:: fabricator/014.php Specifying Test Data ==================== Generated data is great, but sometimes you may want to supply a specific field for a test without compromising your formatters configuration. Rather then creating a new fabricator for each variant -you can use ``setOverrides()`` to specify the value for any fields:: +you can use ``setOverrides()`` to specify the value for any fields: - $fabricator->setOverrides(['first' => 'Bobby']); - $bobbyUser = $fabricator->make(); +.. literalinclude:: fabricator/015.php -Now any data generated with ``make()`` or ``create()`` will always use "Bobby" for the ``first`` field:: +Now any data generated with ``make()`` or ``create()`` will always use "Bobby" for the ``first`` field: - array( - 'first' => "Bobby", - 'email' => "latta.kindel@company.org", - 'phone' => "251-806-2169", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - ) - - array( - 'first' => "Bobby", - 'email' => "melissa.strike@fabricon.us", - 'phone' => "525-214-2656 x23546", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - ) +.. literalinclude:: fabricator/016.php ``setOverrides()`` can take a second parameter to indicate whether this should be a persistent -override or only for a single action:: - - $fabricator->setOverrides(['first' => 'Bobby'], $persist = false); - $bobbyUser = $fabricator->make(); - $bobbyUser = $fabricator->make(); - -Notice after the first return the fabricator stops using the overrides:: - - array( - 'first' => "Bobby", - 'email' => "belingadon142@example.org", - 'phone' => "741-857-1933 x1351", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - ) - - array( - 'first' => "Hans", - 'email' => "hoppifur@metraxalon.com", - 'phone' => "487-235-7006", - 'avatar' => "http://lorempixel.com/800/400/", - 'login' => null, - ) +override or only for a single action: + +.. literalinclude:: fabricator/017.php + +Notice after the first return the fabricator stops using the overrides: + +.. literalinclude:: fabricator/018.php If no second parameter is supplied then passed values will persist by default. @@ -217,16 +137,13 @@ Test Helper =========== Often all you will need is a one-and-done fake object for testing. The Test Helper provides -the ``fake($model, $overrides, $persist = true)`` function to do just this:: +the ``fake($model, $overrides, $persist = true)`` function to do just this: - helper('test'); - $user = fake('App\Models\UserModel', ['name' => 'Gerry']); +.. literalinclude:: fabricator/019.php -This is equivalent to:: +This is equivalent to: - $fabricator = new Fabricator('App\Models\UserModel'); - $fabricator->setOverrides(['name' => 'Gerry']); - $user = $fabricator->create(); +.. literalinclude:: fabricator/020.php If you just need a fake object without saving it to the database you can pass false into the persist parameter. @@ -240,46 +157,43 @@ example: Your project has users and groups. In your test case you want to create various scenarios with groups of different sizes, so you use ``Fabricator`` to create a bunch of groups. Now you want to create fake users but don't want to assign them to a non-existant group ID. -Your model's fake method could look like this:: - - class UserModel - { - protected $table = 'users'; +Your model's fake method could look like this: - public function fake(Generator &$faker) - { - return [ - 'first' => $faker->firstName, - 'email' => $faker->email, - 'group_id' => rand(1, Fabricator::getCount('groups')), - ]; - } +.. literalinclude:: fabricator/021.php Now creating a new user will ensure it is a part of a valid group: ``$user = fake(UserModel::class);`` +Methods +------- + ``Fabricator`` handles the counts internally but you can also access these static methods to assist with using them: -**getCount(string $table): int** +getCount(string $table): int +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the current value for a specific table (default: 0). -**setCount(string $table, int $count): int** +setCount(string $table, int $count): int +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Set the value for a specific table manually, for example if you create some test items without using a fabricator that you still wanted factored into the final counts. -**upCount(string $table): int** +upCount(string $table): int +^^^^^^^^^^^^^^^^^^^^^^^^^^^ Increment the value for a specific table by one and return the new value. (This is what is used internally with ``Fabricator::create()``). -**downCount(string $table): int** +downCount(string $table): int +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Decrement the value for a specific table by one and return the new value, for example if you deleted a fake item but wanted to track the change. -**resetCounts()** +resetCounts() +^^^^^^^^^^^^^ Resets all counts. Good idea to call this between test cases (though using ``CIUnitTestCase::$refresh = true`` does it automatically). diff --git a/user_guide_src/source/testing/fabricator/001.php b/user_guide_src/source/testing/fabricator/001.php new file mode 100644 index 000000000000..8fbf136032ca --- /dev/null +++ b/user_guide_src/source/testing/fabricator/001.php @@ -0,0 +1,10 @@ + 'firstName', + 'email' => 'email', + 'phone' => 'phoneNumber', + 'avatar' => 'imageUrl', +]; + +$fabricator = new Fabricator(UserModel::class, $formatters); diff --git a/user_guide_src/source/testing/fabricator/005.php b/user_guide_src/source/testing/fabricator/005.php new file mode 100644 index 000000000000..c61fa527125e --- /dev/null +++ b/user_guide_src/source/testing/fabricator/005.php @@ -0,0 +1,32 @@ + $faker->firstName, + 'email' => $faker->email, + 'phone' => $faker->phoneNumber, + 'avatar' => Faker\Provider\Image::imageUrl(800, 400), + 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, + ]; + + /* + * Or you can return a return type object. + + return new User([ + 'first' => $faker->firstName, + 'email' => $faker->email, + 'phone' => $faker->phoneNumber, + 'avatar' => Faker\Provider\Image::imageUrl(800, 400), + 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, + ]); + + */ + } +} diff --git a/user_guide_src/source/testing/fabricator/006.php b/user_guide_src/source/testing/fabricator/006.php new file mode 100644 index 000000000000..56f34781622c --- /dev/null +++ b/user_guide_src/source/testing/fabricator/006.php @@ -0,0 +1,13 @@ +make(); +print_r($testUser); diff --git a/user_guide_src/source/testing/fabricator/009.php b/user_guide_src/source/testing/fabricator/009.php new file mode 100644 index 000000000000..a82d6063213e --- /dev/null +++ b/user_guide_src/source/testing/fabricator/009.php @@ -0,0 +1,9 @@ + 'Maynard', + 'email' => 'king.alford@example.org', + 'phone' => '201-886-0269 x3767', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, +]; diff --git a/user_guide_src/source/testing/fabricator/010.php b/user_guide_src/source/testing/fabricator/010.php new file mode 100644 index 000000000000..54b795ee2f72 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/010.php @@ -0,0 +1,3 @@ +make(10); diff --git a/user_guide_src/source/testing/fabricator/011.php b/user_guide_src/source/testing/fabricator/011.php new file mode 100644 index 000000000000..9b9f62fd84ee --- /dev/null +++ b/user_guide_src/source/testing/fabricator/011.php @@ -0,0 +1,5 @@ +makeArray(); +$userObject = $fabricator->makeObject(); +$userEntity = $fabricator->makeObject('App\Entities\User'); diff --git a/user_guide_src/source/testing/fabricator/012.php b/user_guide_src/source/testing/fabricator/012.php new file mode 100644 index 000000000000..98e83b610318 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/012.php @@ -0,0 +1,12 @@ + 1, + 'first' => 'Rachel', + 'email' => 'bradley72@gmail.com', + 'phone' => '741-241-2356', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, + 'created_at' => '2020-05-08 14:52:10', + 'updated_at' => '2020-05-08 14:52:10', +]; diff --git a/user_guide_src/source/testing/fabricator/013.php b/user_guide_src/source/testing/fabricator/013.php new file mode 100644 index 000000000000..6e89dd774eb9 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/013.php @@ -0,0 +1,3 @@ +create(100); diff --git a/user_guide_src/source/testing/fabricator/014.php b/user_guide_src/source/testing/fabricator/014.php new file mode 100644 index 000000000000..60f0161b795f --- /dev/null +++ b/user_guide_src/source/testing/fabricator/014.php @@ -0,0 +1,6 @@ +assertIsNumeric($user->id); +$this->dontSeeInDatabase('user', ['id' => $user->id]); diff --git a/user_guide_src/source/testing/fabricator/015.php b/user_guide_src/source/testing/fabricator/015.php new file mode 100644 index 000000000000..277c32fa370b --- /dev/null +++ b/user_guide_src/source/testing/fabricator/015.php @@ -0,0 +1,4 @@ +setOverrides(['first' => 'Bobby']); +$bobbyUser = $fabricator->make(); diff --git a/user_guide_src/source/testing/fabricator/016.php b/user_guide_src/source/testing/fabricator/016.php new file mode 100644 index 000000000000..8b3f9513280d --- /dev/null +++ b/user_guide_src/source/testing/fabricator/016.php @@ -0,0 +1,17 @@ + 'Bobby', + 'email' => 'latta.kindel@company.org', + 'phone' => '251-806-2169', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, +]; + +[ + 'first' => 'Bobby', + 'email' => 'melissa.strike@fabricon.us', + 'phone' => '525-214-2656 x23546', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, +]; diff --git a/user_guide_src/source/testing/fabricator/017.php b/user_guide_src/source/testing/fabricator/017.php new file mode 100644 index 000000000000..ee79ea6f8d03 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/017.php @@ -0,0 +1,5 @@ +setOverrides(['first' => 'Bobby'], $persist = false); +$bobbyUser = $fabricator->make(); +$bobbyUser = $fabricator->make(); diff --git a/user_guide_src/source/testing/fabricator/018.php b/user_guide_src/source/testing/fabricator/018.php new file mode 100644 index 000000000000..9eb1dbf0334d --- /dev/null +++ b/user_guide_src/source/testing/fabricator/018.php @@ -0,0 +1,17 @@ + 'Bobby', + 'email' => 'belingadon142@example.org', + 'phone' => '741-857-1933 x1351', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, +]; + +[ + 'first' => 'Hans', + 'email' => 'hoppifur@metraxalon.com', + 'phone' => '487-235-7006', + 'avatar' => 'http://lorempixel.com/800/400/', + 'login' => null, +]; diff --git a/user_guide_src/source/testing/fabricator/019.php b/user_guide_src/source/testing/fabricator/019.php new file mode 100644 index 000000000000..4c1de5f23dd2 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/019.php @@ -0,0 +1,4 @@ + 'Gerry']); diff --git a/user_guide_src/source/testing/fabricator/020.php b/user_guide_src/source/testing/fabricator/020.php new file mode 100644 index 000000000000..385646b2ea2c --- /dev/null +++ b/user_guide_src/source/testing/fabricator/020.php @@ -0,0 +1,5 @@ +setOverrides(['name' => 'Gerry']); +$user = $fabricator->create(); diff --git a/user_guide_src/source/testing/fabricator/021.php b/user_guide_src/source/testing/fabricator/021.php new file mode 100644 index 000000000000..236dd553d492 --- /dev/null +++ b/user_guide_src/source/testing/fabricator/021.php @@ -0,0 +1,17 @@ + $faker->firstName, + 'email' => $faker->email, + 'group_id' => mt_rand(1, Fabricator::getCount('groups')), + ]; + } +} diff --git a/user_guide_src/source/testing/feature.rst b/user_guide_src/source/testing/feature.rst index 2a9eb96a348a..df40937ed14b 100644 --- a/user_guide_src/source/testing/feature.rst +++ b/user_guide_src/source/testing/feature.rst @@ -18,33 +18,8 @@ Feature testing requires that all of your test classes use the ``CodeIgniter\Tes and ``CodeIgniter\Test\FeatureTestTrait`` traits. Since these testing tools rely on proper database staging you must always ensure that ``parent::setUp()`` and ``parent::tearDown()`` are called if you implement your own methods. -:: - myClassMethod(); - } - - protected function tearDown(): void - { - parent::tearDown(); - - $this->anotherClassMethod(); - } - } +.. literalinclude:: feature/001.php Requesting A Page ================= @@ -54,25 +29,12 @@ to do this, you use the ``call()`` method. The first parameter is the HTTP metho The second parameter is the path on your site to test. The third parameter accepts an array that is used to populate the superglobal variables for the HTTP verb you are using. So, a method of **GET** would have the **$_GET** variable populated, while a **post** request would have the **$_POST** array populated. -:: - - // Get a simple page - $result = $this->call('get', '/'); - // Submit a form - $result = $this->call('post', 'contact'), [ - 'name' => 'Fred Flintstone', - 'email' => 'flintyfred@example.com' - ]); +.. literalinclude:: feature/002.php -Shorthand methods for each of the HTTP verbs exist to ease typing and make things clearer:: +Shorthand methods for each of the HTTP verbs exist to ease typing and make things clearer: - $this->get($path, $params); - $this->post($path, $params); - $this->put($path, $params); - $this->patch($path, $params); - $this->delete($path, $params); - $this->options($path, $params); +.. literalinclude:: feature/003.php .. note:: The ``$params`` array does not make sense for every HTTP verb, but is included for consistency. @@ -80,57 +42,37 @@ Setting Different Routes ------------------------ You can use a custom collection of routes by passing an array of "routes" into the ``withRoutes()`` method. This will -override any existing routes in the system:: +override any existing routes in the system: - $routes = [ - ['get', 'users', 'UserController::list'], - ]; - - $result = $this->withRoutes($routes)->get('users'); +.. literalinclude:: feature/004.php Each of the "routes" is a 3 element array containing the HTTP verb (or "add" for all), the URI to match, and the routing destination. - Setting Session Values ---------------------- You can set custom session values to use during a single test with the ``withSession()`` method. This takes an array of key/value pairs that should exist within the ``$_SESSION`` variable when this request is made, or ``null`` to indicate that the current values of ``$_SESSION`` should be used. This is handy for testing authentication and more. -:: - - $values = [ - 'logged_in' => 123, - ]; - - $result = $this->withSession($values)->get('admin'); - // Or... - - $_SESSION['logged_in'] = 123; - - $result = $this->withSession()->get('admin'); +.. literalinclude:: feature/005.php Setting Headers --------------- You can set header values with the ``withHeaders()`` method. This takes an array of key/value pairs that would be -passed as a header into the call.:: +passed as a header into the call: - $headers = [ - 'CONTENT_TYPE' => 'application/json', - ]; - - $result = $this->withHeaders($headers)->post('users'); +.. literalinclude:: feature/006.php Bypassing Events ---------------- Events are handy to use in your application, but can be problematic during testing. Especially events that are used -to send out emails. You can tell the system to skip any event handling with the ``skipEvents()`` method:: +to send out emails. You can tell the system to skip any event handling with the ``skipEvents()`` method: - $result = $this->skipEvents()->post('users', $userInfo); +.. literalinclude:: feature/007.php Formatting The Request ----------------------- @@ -139,13 +81,8 @@ You can set the format of your request's body using the ``withBodyFormat()`` met `json` or `xml`. This will take the parameters passed into ``call()``, ``post()``, ``get()``... and assign them to the body of the request in the given format. This will also set the `Content-Type` header for your request accordingly. This is useful when testing JSON or XML API's so that you can set the request in the form that the controller will expect. -:: - - // If your feature test contains this: - $result = $this->withBodyFormat('json')->post('users', $userInfo); - // Your controller can then get the parameters passed in with: - $userInfo = $this->request->getJson(); +.. literalinclude:: feature/008.php Setting the Body ---------------- diff --git a/user_guide_src/source/testing/feature/001.php b/user_guide_src/source/testing/feature/001.php new file mode 100644 index 000000000000..0ab640465052 --- /dev/null +++ b/user_guide_src/source/testing/feature/001.php @@ -0,0 +1,26 @@ +myClassMethod(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->anotherClassMethod(); + } +} diff --git a/user_guide_src/source/testing/feature/002.php b/user_guide_src/source/testing/feature/002.php new file mode 100644 index 000000000000..8af5fda7b7c6 --- /dev/null +++ b/user_guide_src/source/testing/feature/002.php @@ -0,0 +1,10 @@ +call('get', '/'); + +// Submit a form +$result = $this->call('post', 'contact', [ + 'name' => 'Fred Flintstone', + 'email' => 'flintyfred@example.com', +]); diff --git a/user_guide_src/source/testing/feature/003.php b/user_guide_src/source/testing/feature/003.php new file mode 100644 index 000000000000..1905a2dcf110 --- /dev/null +++ b/user_guide_src/source/testing/feature/003.php @@ -0,0 +1,8 @@ +get($path, $params); +$this->post($path, $params); +$this->put($path, $params); +$this->patch($path, $params); +$this->delete($path, $params); +$this->options($path, $params); diff --git a/user_guide_src/source/testing/feature/004.php b/user_guide_src/source/testing/feature/004.php new file mode 100644 index 000000000000..4f98ced2f8d6 --- /dev/null +++ b/user_guide_src/source/testing/feature/004.php @@ -0,0 +1,7 @@ +withRoutes($routes)->get('users'); diff --git a/user_guide_src/source/testing/feature/005.php b/user_guide_src/source/testing/feature/005.php new file mode 100644 index 000000000000..448e68800a4e --- /dev/null +++ b/user_guide_src/source/testing/feature/005.php @@ -0,0 +1,13 @@ + 123, +]; + +$result = $this->withSession($values)->get('admin'); + +// Or... + +$_SESSION['logged_in'] = 123; + +$result = $this->withSession()->get('admin'); diff --git a/user_guide_src/source/testing/feature/006.php b/user_guide_src/source/testing/feature/006.php new file mode 100644 index 000000000000..e3cf72b69b47 --- /dev/null +++ b/user_guide_src/source/testing/feature/006.php @@ -0,0 +1,7 @@ + 'application/json', +]; + +$result = $this->withHeaders($headers)->post('users'); diff --git a/user_guide_src/source/testing/feature/007.php b/user_guide_src/source/testing/feature/007.php new file mode 100644 index 000000000000..f44369c79865 --- /dev/null +++ b/user_guide_src/source/testing/feature/007.php @@ -0,0 +1,3 @@ +skipEvents()->post('users', $userInfo); diff --git a/user_guide_src/source/testing/feature/008.php b/user_guide_src/source/testing/feature/008.php new file mode 100644 index 000000000000..f5118ea42091 --- /dev/null +++ b/user_guide_src/source/testing/feature/008.php @@ -0,0 +1,7 @@ +withBodyFormat('json')->post('users', $userInfo); + +// Your controller can then get the parameters passed in with: +$userInfo = $this->request->getJson(); diff --git a/user_guide_src/source/testing/mocking.rst b/user_guide_src/source/testing/mocking.rst index 758f6c2f3a50..967cfba7f634 100644 --- a/user_guide_src/source/testing/mocking.rst +++ b/user_guide_src/source/testing/mocking.rst @@ -15,9 +15,8 @@ Cache ===== You can mock the cache with the ``mock()`` method, using the ``CacheFactory`` as its only parameter. -:: - $mock = mock(CodeIgniter\Cache\CacheFactory::class); +.. literalinclude:: mocking/001.php While this returns an instance of ``CodeIgniter\Test\Mock\MockCache`` that you can use directly, it also inserts the mock into the Service class, so any calls within your code to ``service('cache')`` or ``Config\Services::cache()`` will @@ -31,23 +30,12 @@ Additional Methods You can instruct the mocked cache handler to never do any caching with the ``bypass()`` method. This will emulate using the dummy handler and ensures that your test does not rely on cached data for your tests. -:: - $mock = mock(CodeIgniter\Cache\CacheFactory::class); - // Never cache any items during this test. - $mock->bypass(); +.. literalinclude:: mocking/002.php Available Assertions -------------------- The following new assertions are available on the mocked class for using during testing: -:: - $mock = mock(CodeIgniter\Cache\CacheFactory::class); - - // Assert that a cached item named $key exists - $mock->assertHas($key); - // Assert that a cached item named $key exists with a value of $value - $mock->assertHasValue($key, $value); - // Assert that a cached item named $key does NOT exist - $mock->assertMissing($key); +.. literalinclude:: mocking/003.php diff --git a/user_guide_src/source/testing/mocking/001.php b/user_guide_src/source/testing/mocking/001.php new file mode 100644 index 000000000000..69e69239049d --- /dev/null +++ b/user_guide_src/source/testing/mocking/001.php @@ -0,0 +1,3 @@ +bypass(); diff --git a/user_guide_src/source/testing/mocking/003.php b/user_guide_src/source/testing/mocking/003.php new file mode 100644 index 000000000000..4a3babddaf54 --- /dev/null +++ b/user_guide_src/source/testing/mocking/003.php @@ -0,0 +1,10 @@ +assertHas($key); +// Assert that a cached item named $key exists with a value of $value +$mock->assertHasValue($key, $value); +// Assert that a cached item named $key does NOT exist +$mock->assertMissing($key); diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 4b4cdce7beed..77dbf9f3da80 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -3,12 +3,12 @@ Testing ####### CodeIgniter has been built to make testing both the framework and your application as simple as possible. -Support for ``PHPUnit`` is built in, and the framework provides a number of convenient +Support for `PHPUnit `__ is built in, and the framework provides a number of convenient helper methods to make testing every aspect of your application as painless as possible. .. contents:: :local: - :depth: 2 + :depth: 3 ************* System Set Up @@ -35,15 +35,18 @@ application and system directories) type the following from the command line:: This will install the correct version for your current PHP version. Once that is done, you can run all of the tests for this project by typing:: - > ./vendor/bin/phpunit + > vendor/bin/phpunit + +If you are using Windows, use the following command:: + + > vendor\bin\phpunit Phar ---- -The other option is to download the .phar file from the `PHPUnit `__ site. +The other option is to download the .phar file from the `PHPUnit `__ site. This is a standalone file that should be placed within your project root. - ************************ Testing Your Application ************************ @@ -64,38 +67,13 @@ The Test Class In order to take advantage of the additional tools provided, your tests must extend ``CIUnitTestCase``. All tests are expected to be located in the **tests/app** directory by default. -To test a new library, **Foo**, you would create a new file at **tests/app/Libraries/FooTest.php**:: - - model->purgeDeleted() - } +.. literalinclude:: overview/005.php Traits ------ @@ -157,152 +118,98 @@ A common way to enhance your tests is by using traits to consolidate staging acr test cases. ``CIUnitTestCase`` will detect any class traits and look for staging methods to run named for the trait itself. For example, if you needed to add authentication to some of your test cases you could create an authentication trait with a set up method to fake a -logged in user:: - - trait AuthTrait - { - protected setUpAuthTrait() - { - $user = $this->createFakeUser(); - $this->logInUser($user); - } - ... - - class AuthenticationFeatureTest - { - use AuthTrait; - ... +logged in user: +.. literalinclude:: overview/006.php Additional Assertions --------------------- ``CIUnitTestCase`` provides additional unit testing assertions that you might find useful. -**assertLogged($level, $expectedMessage)** - -Ensure that something you expected to be logged actually was:: - - $config = new LoggerConfig(); - $logger = new Logger($config); - - ... do something that you expect a log entry from - $logger->log('error', "That's no moon"); +assertLogged($level, $expectedMessage) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - $this->assertLogged('error', "That's no moon"); +Ensure that something you expected to be logged actually was: -**assertEventTriggered($eventName)** +.. literalinclude:: overview/007.php -Ensure that an event you expected to be triggered actually was:: +assertEventTriggered($eventName) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Events::on('foo', function ($arg) use(&$result) { - $result = $arg; - }); +Ensure that an event you expected to be triggered actually was: - Events::trigger('foo', 'bar'); +.. literalinclude:: overview/008.php - $this->assertEventTriggered('foo'); +assertHeaderEmitted($header, $ignoreCase = false) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -**assertHeaderEmitted($header, $ignoreCase = false)** +Ensure that a header or cookie was actually emitted: -Ensure that a header or cookie was actually emitted:: +.. literalinclude:: overview/009.php - $response->setCookie('foo', 'bar'); +.. note:: the test case with this should be `run as a separate process + in PHPunit `_. - ob_start(); - $this->response->send(); - $output = ob_get_clean(); // in case you want to check the actual body +assertHeaderNotEmitted($header, $ignoreCase = false) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - $this->assertHeaderEmitted("Set-Cookie: foo=bar"); +Ensure that a header or cookie was not emitted: -Note: the test case with this should be `run as a separate process -in PHPunit `_. +.. literalinclude:: overview/010.php -**assertHeaderNotEmitted($header, $ignoreCase = false)** +.. note:: the test case with this should be `run as a separate process + in PHPunit `_. -Ensure that a header or cookie was not emitted:: - - $response->setCookie('foo', 'bar'); - - ob_start(); - $this->response->send(); - $output = ob_get_clean(); // in case you want to check the actual body - - $this->assertHeaderNotEmitted("Set-Cookie: banana"); - -Note: the test case with this should be `run as a separate process -in PHPunit `_. - -**assertCloseEnough($expected, $actual, $message = '', $tolerance = 1)** +assertCloseEnough($expected, $actual, $message = '', $tolerance = 1) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For extended execution time testing, tests that the absolute difference -between expected and actual time is within the prescribed tolerance.:: +between expected and actual time is within the prescribed tolerance: - $timer = new Timer(); - $timer->start('longjohn', strtotime('-11 minutes')); - $this->assertCloseEnough(11 * 60, $timer->getElapsedTime('longjohn')); +.. literalinclude:: overview/011.php The above test will allow the actual time to be either 660 or 661 seconds. -**assertCloseEnoughString($expected, $actual, $message = '', $tolerance = 1)** +assertCloseEnoughString($expected, $actual, $message = '', $tolerance = 1) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For extended execution time testing, tests that the absolute difference -between expected and actual time, formatted as strings, is within the prescribed tolerance.:: +between expected and actual time, formatted as strings, is within the prescribed tolerance: - $timer = new Timer(); - $timer->start('longjohn', strtotime('-11 minutes')); - $this->assertCloseEnoughString(11 * 60, $timer->getElapsedTime('longjohn')); +.. literalinclude:: overview/012.php The above test will allow the actual time to be either 660 or 661 seconds. - Accessing Protected/Private Properties -------------------------------------- When testing, you can use the following setter and getter methods to access protected and private methods and properties in the classes that you are testing. -**getPrivateMethodInvoker($instance, $method)** +getPrivateMethodInvoker($instance, $method) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Enables you to call private methods from outside the class. This returns a function that can be called. The first parameter is an instance of the class to test. The second parameter is the name of the method you want to call. -:: - - // Create an instance of the class to test - $obj = new Foo(); - - // Get the invoker for the 'privateMethod' method. - $method = $this->getPrivateMethodInvoker($obj, 'privateMethod'); - - // Test the results - $this->assertEquals('bar', $method('param1', 'param2')); +.. literalinclude:: overview/013.php -**getPrivateProperty($instance, $property)** +getPrivateProperty($instance, $property) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Retrieves the value of a private/protected class property from an instance of a class. The first parameter is an instance of the class to test. The second parameter is the name of the property. -:: +.. literalinclude:: overview/014.php - // Create an instance of the class to test - $obj = new Foo(); - - // Test the value - $this->assertEquals('bar', $this->getPrivateProperty($obj, 'baz')); - -**setPrivateProperty($instance, $property, $value)** +setPrivateProperty($instance, $property, $value) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Set a protected value within a class instance. The first parameter is an instance of the class to test. The second -parameter is the name of the property to set the value of. The third parameter is the value to set it to:: - - // Create an instance of the class to test - $obj = new Foo(); - - // Set the value - $this->setPrivateProperty($obj, 'baz', 'oops!'); +parameter is the name of the property to set the value of. The third parameter is the value to set it to: - // Do normal testing... +.. literalinclude:: overview/015.php Mocking Services ================ @@ -312,30 +219,26 @@ your tests to only the code in question, while simulating various responses from true when testing controllers and other integration testing. The **Services** class provides the following methods to simplify this. -**injectMock()** +Services::injectMock() +---------------------- This method allows you to define the exact instance that will be returned by the Services class. You can use this to set properties of a service so that it behaves in a certain way, or replace a service with a mocked class. -:: - public function testSomething() - { - $curlrequest = $this->getMockBuilder('CodeIgniter\HTTP\CURLRequest') - ->setMethods(['request']) - ->getMock(); - Services::injectMock('curlrequest', $curlrequest); - - // Do normal testing here.... - } +.. literalinclude:: overview/016.php The first parameter is the service that you are replacing. The name must match the function name in the Services class exactly. The second parameter is the instance to replace it with. -**reset()** +Services::reset() +----------------- Removes all mocked classes from the Services class, bringing it back to its original state. -**resetSingle(string $name)** +You can also use the ``$this->resetServices()`` method that ``CIUnitTestCase`` provides. + +Services::resetSingle(string $name) +----------------------------------- Removes any mock and shared instances for a single service, by its name. @@ -345,17 +248,11 @@ Mocking Factory Instances ========================= Similar to Services, you may find yourself needing to supply a pre-configured class instance -during testing that will be used with ``Factories``. Use the same ``injectMock()`` and ``reset()`` +during testing that will be used with ``Factories``. Use the same ``Factories::injectMock()`` and ``Factories::reset()`` static methods like **Services**, but they take an additional preceding parameter for the -component name:: +component name: - protected function setUp() - { - parent::setUp(); - - $model = new MockUserModel(); - Factories::injectMock('models', 'App\Models\UserModel', $model); - } +.. literalinclude:: overview/017.php .. note:: All component Factories are reset by default between each test. Modify your test case's ``$setUpMethods`` if you need instances to persist. @@ -367,22 +264,6 @@ Stream Filters You may need to test things that are difficult to test. Sometimes, capturing a stream, like PHP's own STDOUT, or STDERR, might be helpful. The ``CITestStreamFilter`` helps you capture the output from the stream of your choice. -An example demonstrating this inside one of your test cases:: - - public function setUp() - { - CITestStreamFilter::$buffer = ''; - $this->stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); - } - - public function tearDown() - { - stream_filter_remove($this->stream_filter); - } - - public function testSomeOutput() - { - CLI::write('first.'); - $expected = "first.\n"; - $this->assertSame($expected, CITestStreamFilter::$buffer); - } +An example demonstrating this inside one of your test cases: + +.. literalinclude:: overview/018.php diff --git a/user_guide_src/source/testing/overview/001.php b/user_guide_src/source/testing/overview/001.php new file mode 100644 index 000000000000..6dac95d2b671 --- /dev/null +++ b/user_guide_src/source/testing/overview/001.php @@ -0,0 +1,13 @@ +model->purgeDeleted(); + } +} diff --git a/user_guide_src/source/testing/overview/006.php b/user_guide_src/source/testing/overview/006.php new file mode 100644 index 000000000000..6193eb88bf50 --- /dev/null +++ b/user_guide_src/source/testing/overview/006.php @@ -0,0 +1,21 @@ +createFakeUser(); + $this->logInUser($user); + } + + // ... +} + +use CodeIgniter\Test\CIUnitTestCase; + +final class AuthenticationFeatureTest extends CIUnitTestCase +{ + use AuthTrait; + + // ... +} diff --git a/user_guide_src/source/testing/overview/007.php b/user_guide_src/source/testing/overview/007.php new file mode 100644 index 000000000000..55077ed36818 --- /dev/null +++ b/user_guide_src/source/testing/overview/007.php @@ -0,0 +1,9 @@ +log('error', "That's no moon"); + +$this->assertLogged('error', "That's no moon"); diff --git a/user_guide_src/source/testing/overview/008.php b/user_guide_src/source/testing/overview/008.php new file mode 100644 index 000000000000..5d69c01c16d5 --- /dev/null +++ b/user_guide_src/source/testing/overview/008.php @@ -0,0 +1,9 @@ +assertEventTriggered('foo'); diff --git a/user_guide_src/source/testing/overview/009.php b/user_guide_src/source/testing/overview/009.php new file mode 100644 index 000000000000..506d75a8d433 --- /dev/null +++ b/user_guide_src/source/testing/overview/009.php @@ -0,0 +1,9 @@ +setCookie('foo', 'bar'); + +ob_start(); +$this->response->send(); +$output = ob_get_clean(); // in case you want to check the actual body + +$this->assertHeaderEmitted('Set-Cookie: foo=bar'); diff --git a/user_guide_src/source/testing/overview/010.php b/user_guide_src/source/testing/overview/010.php new file mode 100644 index 000000000000..10e9d4a0d31b --- /dev/null +++ b/user_guide_src/source/testing/overview/010.php @@ -0,0 +1,9 @@ +setCookie('foo', 'bar'); + +ob_start(); +$this->response->send(); +$output = ob_get_clean(); // in case you want to check the actual body + +$this->assertHeaderNotEmitted('Set-Cookie: banana'); diff --git a/user_guide_src/source/testing/overview/011.php b/user_guide_src/source/testing/overview/011.php new file mode 100644 index 000000000000..c9b85aef7f87 --- /dev/null +++ b/user_guide_src/source/testing/overview/011.php @@ -0,0 +1,5 @@ +start('longjohn', strtotime('-11 minutes')); +$this->assertCloseEnough(11 * 60, $timer->getElapsedTime('longjohn')); diff --git a/user_guide_src/source/testing/overview/012.php b/user_guide_src/source/testing/overview/012.php new file mode 100644 index 000000000000..1c03733868e4 --- /dev/null +++ b/user_guide_src/source/testing/overview/012.php @@ -0,0 +1,5 @@ +start('longjohn', strtotime('-11 minutes')); +$this->assertCloseEnoughString(11 * 60, $timer->getElapsedTime('longjohn')); diff --git a/user_guide_src/source/testing/overview/013.php b/user_guide_src/source/testing/overview/013.php new file mode 100644 index 000000000000..f228dcf28963 --- /dev/null +++ b/user_guide_src/source/testing/overview/013.php @@ -0,0 +1,10 @@ +getPrivateMethodInvoker($obj, 'privateMethod'); + +// Test the results +$this->assertEquals('bar', $method('param1', 'param2')); diff --git a/user_guide_src/source/testing/overview/014.php b/user_guide_src/source/testing/overview/014.php new file mode 100644 index 000000000000..1befb710253e --- /dev/null +++ b/user_guide_src/source/testing/overview/014.php @@ -0,0 +1,7 @@ +assertEquals('bar', $this->getPrivateProperty($obj, 'baz')); diff --git a/user_guide_src/source/testing/overview/015.php b/user_guide_src/source/testing/overview/015.php new file mode 100644 index 000000000000..c2e815953dd9 --- /dev/null +++ b/user_guide_src/source/testing/overview/015.php @@ -0,0 +1,9 @@ +setPrivateProperty($obj, 'baz', 'oops!'); + +// Do normal testing... diff --git a/user_guide_src/source/testing/overview/016.php b/user_guide_src/source/testing/overview/016.php new file mode 100644 index 000000000000..d3a769d8379c --- /dev/null +++ b/user_guide_src/source/testing/overview/016.php @@ -0,0 +1,17 @@ +getMockBuilder('CodeIgniter\HTTP\CURLRequest') + ->setMethods(['request']) + ->getMock(); + Services::injectMock('curlrequest', $curlrequest); + + // Do normal testing here.... + } +} diff --git a/user_guide_src/source/testing/overview/017.php b/user_guide_src/source/testing/overview/017.php new file mode 100644 index 000000000000..c31ac96eb459 --- /dev/null +++ b/user_guide_src/source/testing/overview/017.php @@ -0,0 +1,15 @@ +stream_filter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + } + + protected function tearDown(): void + { + stream_filter_remove($this->stream_filter); + } + + public function testSomeOutput(): void + { + CLI::write('first.'); + + $expected = "first.\n"; + $this->assertSame($expected, CITestStreamFilter::$buffer); + } +} diff --git a/user_guide_src/source/testing/response.rst b/user_guide_src/source/testing/response.rst index 6c22e7e9cbdb..c28fd46bbb81 100644 --- a/user_guide_src/source/testing/response.rst +++ b/user_guide_src/source/testing/response.rst @@ -5,331 +5,285 @@ Testing Responses The ``TestResponse`` class provides a number of helpful functions for parsing and testing responses from your test cases. Usually a ``TestResponse`` will be provided for you as a result of your :doc:`Controller Tests ` or :doc:`HTTP Feature Tests `, but you can always -create your own directly using any ``ResponseInterface``:: +create your own directly using any ``ResponseInterface``: - $result = new \CodeIgniter\Test\TestResponse($response); - $result->assertOK(); +.. literalinclude:: response/001.php .. contents:: :local: :depth: 2 Testing the Response -==================== +******************** Whether you have received a ``TestResponse`` as a result of your tests or created one yourself, there are a number of new assertions that you can use in your tests. Accessing Request/Response --------------------------- +========================== -**request()** +request() +--------- -You can access directly the Request object, if it was set during testing:: +You can access directly the Request object, if it was set during testing: - $request = $results->request(); +.. literalinclude:: response/002.php -**response()** +response() +---------- -This allows you direct access to the response object:: +This allows you direct access to the response object: - $response = $results->response(); +.. literalinclude:: response/003.php Checking Response Status ------------------------- +======================== -**isOK()** +isOK() +------ Returns a boolean true/false based on whether the response is perceived to be "ok". This is primarily determined by a response status code in the 200 or 300's. An empty body is not considered valid, unless in redirects. -:: - if ($result->isOK()) { - ... - } +.. literalinclude:: response/004.php -**assertOK()** +assertOK() +---------- -This assertion simply uses the **isOK()** method to test a response. **assertNotOK** is the inverse of this assertion. -:: +This assertion simply uses the ``isOK()`` method to test a response. ``assertNotOK()`` is the inverse of this assertion. - $result->assertOK(); +.. literalinclude:: response/005.php -**isRedirect()** +isRedirect() +------------ Returns a boolean true/false based on whether the response is a redirected response. -:: - if ($result->isRedirect()) { - ... - } +.. literalinclude:: response/006.php -**assertRedirect()** +assertRedirect() +---------------- -Asserts that the Response is an instance of RedirectResponse. **assertNotRedirect** is the inverse of this assertion. -:: +Asserts that the Response is an instance of RedirectResponse. ``assertNotRedirect()`` is the inverse of this assertion. - $result->assertRedirect(); +.. literalinclude:: response/007.php -**assertRedirectTo()** +assertRedirectTo() +------------------ Asserts that the Response is an instance of RedirectResponse and the destination matches the uri given. -:: - $result->assertRedirectTo('foo/bar'); +.. literalinclude:: response/008.php -**getRedirectUrl()** +getRedirectUrl() +---------------- Returns the URL set for a RedirectResponse, or null for failure. -:: - $url = $result->getRedirectUrl(); - $this->assertEquals(site_url('foo/bar'), $url); +.. literalinclude:: response/009.php -**assertStatus(int $code)** +assertStatus(int $code) +----------------------- Asserts that the HTTP status code returned matches $code. -:: - - $result->assertStatus(403); +.. literalinclude:: response/010.php Session Assertions ------------------- +================== -**assertSessionHas(string $key, $value = null)** +assertSessionHas(string $key, $value = null) +-------------------------------------------- Asserts that a value exists in the resulting session. If $value is passed, will also assert that the variable's value matches what was specified. -:: - $result->assertSessionHas('logged_in', 123); +.. literalinclude:: response/011.php -**assertSessionMissing(string $key)** +assertSessionMissing(string $key) +--------------------------------- Asserts that the resulting session does not include the specified $key. -:: - - $result->assertSessionMissin('logged_in'); +.. literalinclude:: response/012.php Header Assertions ------------------ +================= -**assertHeader(string $key, $value = null)** +assertHeader(string $key, $value = null) +---------------------------------------- -Asserts that a header named **$key** exists in the response. If **$value** is not empty, will also assert that +Asserts that a header named ``$key`` exists in the response. If ``$value`` is not empty, will also assert that the values match. -:: - - $result->assertHeader('Content-Type', 'text/html'); -**assertHeaderMissing(string $key)** +.. literalinclude:: response/013.php -Asserts that a header name **$key** does not exist in the response. -:: +assertHeaderMissing(string $key) +-------------------------------- - $result->assertHeader('Accepts'); +Asserts that a header name ``$key`` does not exist in the response. +.. literalinclude:: response/014.php Cookie Assertions ------------------ +================= -**assertCookie(string $key, $value = null, string $prefix = '')** +assertCookie(string $key, $value = null, string $prefix = '') +------------------------------------------------------------- -Asserts that a cookie named **$key** exists in the response. If **$value** is not empty, will also assert that +Asserts that a cookie named ``$key`` exists in the response. If ``$value`` is not empty, will also assert that the values match. You can set the cookie prefix, if needed, by passing it in as the third parameter. -:: - $result->assertCookie('foo', 'bar'); +.. literalinclude:: response/015.php -**assertCookieMissing(string $key)** +assertCookieMissing(string $key) +-------------------------------- -Asserts that a cookie named **$key** does not exist in the response. -:: +Asserts that a cookie named ``$key`` does not exist in the response. - $result->assertCookieMissing('ci_session'); +.. literalinclude:: response/016.php -**assertCookieExpired(string $key, string $prefix = '')** +assertCookieExpired(string $key, string $prefix = '') +----------------------------------------------------- -Asserts that a cookie named **$key** exists, but has expired. You can set the cookie prefix, if needed, by passing it +Asserts that a cookie named ``$key`` exists, but has expired. You can set the cookie prefix, if needed, by passing it in as the second parameter. -:: - $result->assertCookieExpired('foo'); +.. literalinclude:: response/017.php DOM Helpers ------------ +=========== The response you get back contains a number of helper methods to inspect the HTML output within the response. These are useful for using within assertions in your tests. -The **see()** method checks the text on the page to see if it exists either by itself, or more specifically within -a tag, as specified by type, class, or id:: +see() +----- + +The ``see()`` method checks the text on the page to see if it exists either by itself, or more specifically within +a tag, as specified by type, class, or id: + +.. literalinclude:: response/018.php + +The ``dontSee()`` method is the exact opposite: - // Check that "Hello World" is on the page - $results->see('Hello World'); - // Check that "Hello World" is within an h1 tag - $results->see('Hello World', 'h1'); - // Check that "Hello World" is within an element with the "notice" class - $results->see('Hello World', '.notice'); - // Check that "Hello World" is within an element with id of "title" - $results->see('Hellow World', '#title'); +.. literalinclude:: response/019.php -The **dontSee()** method is the exact opposite:: +seeElement() +------------ - // Checks that "Hello World" does NOT exist on the page - $results->dontSee('Hello World'); - // Checks that "Hellow World" does NOT exist within any h1 tag - $results->dontSee('Hello World', 'h1'); +The ``seeElement()`` and ``dontSeeElement()`` are very similar to the previous methods, but do not look at the +values of the elements. Instead, they simply check that the elements exist on the page: -The **seeElement()** and **dontSeeElement()** are very similar to the previous methods, but do not look at the -values of the elements. Instead, they simply check that the elements exist on the page:: +.. literalinclude:: response/020.php - // Check that an element with class 'notice' exists - $results->seeElement('.notice'); - // Check that an element with id 'title' exists - $results->seeElement('#title') - // Verify that an element with id 'title' does NOT exist - $results->dontSeeElement('#title'); +seeLink() +--------- -You can use **seeLink()** to ensure that a link appears on the page with the specified text:: +You can use ``seeLink()`` to ensure that a link appears on the page with the specified text: - // Check that a link exists with 'Upgrade Account' as the text:: - $results->seeLink('Upgrade Account'); - // Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell' - $results->seeLink('Upgrade Account', '.upsell'); +.. literalinclude:: response/021.php -The **seeInField()** method checks for any input tags exist with the name and value:: +seeInField() +------------ - // Check that an input exists named 'user' with the value 'John Snow' - $results->seeInField('user', 'John Snow'); - // Check a multi-dimensional input - $results->seeInField('user[name]', 'John Snow'); +The ``seeInField()`` method checks for any input tags exist with the name and value: -Finally, you can check if a checkbox exists and is checked with the **seeCheckboxIsChecked()** method:: +.. literalinclude:: response/022.php - // Check if checkbox is checked with class of 'foo' - $results->seeCheckboxIsChecked('.foo'); - // Check if checkbox with id of 'bar' is checked - $results->seeCheckboxIsChecked('#bar'); +seeCheckboxIsChecked() +---------------------- + +Finally, you can check if a checkbox exists and is checked with the ``seeCheckboxIsChecked()`` method: + +.. literalinclude:: response/023.php DOM Assertions --------------- +============== You can perform tests to see if specific elements/text/etc exist with the body of the response with the following assertions. -**assertSee(string $search = null, string $element = null)** +assertSee(string $search = null, string $element = null) +-------------------------------------------------------- Asserts that text/HTML is on the page, either by itself or - more specifically - within -a tag, as specified by type, class, or id:: - - // Check that "Hello World" is on the page - $result->assertSee('Hello World'); - // Check that "Hello World" is within an h1 tag - $result->assertSee('Hello World', 'h1'); - // Check that "Hello World" is within an element with the "notice" class - $result->assertSee('Hello World', '.notice'); - // Check that "Hello World" is within an element with id of "title" - $result->assertSee('Hellow World', '#title'); +a tag, as specified by type, class, or id: +.. literalinclude:: response/024.php -**assertDontSee(string $search = null, string $element = null)** +assertDontSee(string $search = null, string $element = null) +------------------------------------------------------------ -Asserts the exact opposite of the **assertSee()** method:: +Asserts the exact opposite of the ``assertSee()`` method: - // Checks that "Hello World" does NOT exist on the page - $results->dontSee('Hello World'); - // Checks that "Hello World" does NOT exist within any h1 tag - $results->dontSee('Hello World', 'h1'); +.. literalinclude:: response/025.php -**assertSeeElement(string $search)** +assertSeeElement(string $search) +-------------------------------- -Similar to **assertSee()**, however this only checks for an existing element. It does not check for specific text:: +Similar to ``assertSee()``, however this only checks for an existing element. It does not check for specific text: - // Check that an element with class 'notice' exists - $results->seeElement('.notice'); - // Check that an element with id 'title' exists - $results->seeElement('#title') +.. literalinclude:: response/026.php -**assertDontSeeElement(string $search)** +assertDontSeeElement(string $search) +------------------------------------ -Similar to **assertSee()**, however this only checks for an existing element that is missing. It does not check for -specific text:: +Similar to ``assertSee()``, however this only checks for an existing element that is missing. It does not check for +specific text: - // Verify that an element with id 'title' does NOT exist - $results->dontSeeElement('#title'); +.. literalinclude:: response/027.php -**assertSeeLink(string $text, string $details=null)** +assertSeeLink(string $text, string $details = null) +--------------------------------------------------- -Asserts that an anchor tag is found with matching **$text** as the body of the tag:: +Asserts that an anchor tag is found with matching ``$text`` as the body of the tag: - // Check that a link exists with 'Upgrade Account' as the text:: - $results->seeLink('Upgrade Account'); - // Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell' - $results->seeLink('Upgrade Account', '.upsell'); +.. literalinclude:: response/028.php -**assertSeeInField(string $field, string $value=null)** +assertSeeInField(string $field, string $value = null) +----------------------------------------------------- -Asserts that an input tag exists with the name and value:: - - // Check that an input exists named 'user' with the value 'John Snow' - $results->assertSeeInField('user', 'John Snow'); - // Check a multi-dimensional input - $results->assertSeeInField('user[name]', 'John Snow'); +Asserts that an input tag exists with the name and value: +.. literalinclude:: response/029.php Working With JSON ------------------ +================= Responses will frequently contain JSON responses, especially when working with API methods. The following methods can help to test the responses. -**getJSON()** - -This method will return the body of the response as a JSON string:: +getJSON() +--------- - // Response body is this: - ['foo' => 'bar'] +This method will return the body of the response as a JSON string: - $json = $result->getJSON(); +.. literalinclude:: response/030.php - // $json is this: - { - "foo": "bar" - } +You can use this method to determine if ``$response`` actually holds JSON content: -You can use this method to determine if ``$response`` actually holds JSON content:: - - // Verify the response is JSON - $this->assertTrue($result->getJSON() !== false) +.. literalinclude:: response/031.php .. note:: Be aware that the JSON string will be pretty-printed in the result. -**assertJSONFragment(array $fragment)** +assertJSONFragment(array $fragment) +----------------------------------- Asserts that $fragment is found within the JSON response. It does not need to match the entire JSON value. -:: - - // Response body is this: - [ - 'config' => ['key-a', 'key-b'], - ] - - // Is true - $result->assertJSONFragment(['config' => ['key-a']]); +.. literalinclude:: response/032.php -**assertJSONExact($test)** - -Similar to **assertJSONFragment()**, but checks the entire JSON response to ensure exact matches. +assertJSONExact($test) +---------------------- +Similar to ``assertJSONFragment()``, but checks the entire JSON response to ensure exact matches. Working With XML ----------------- +================ -**getXML()** +getXML() +-------- If your application returns XML, you can retrieve it through this method. diff --git a/user_guide_src/source/testing/response/001.php b/user_guide_src/source/testing/response/001.php new file mode 100644 index 000000000000..0e37cfec37ba --- /dev/null +++ b/user_guide_src/source/testing/response/001.php @@ -0,0 +1,4 @@ +assertOK(); diff --git a/user_guide_src/source/testing/response/002.php b/user_guide_src/source/testing/response/002.php new file mode 100644 index 000000000000..a835659904db --- /dev/null +++ b/user_guide_src/source/testing/response/002.php @@ -0,0 +1,3 @@ +request(); diff --git a/user_guide_src/source/testing/response/003.php b/user_guide_src/source/testing/response/003.php new file mode 100644 index 000000000000..c0cb73899f30 --- /dev/null +++ b/user_guide_src/source/testing/response/003.php @@ -0,0 +1,3 @@ +response(); diff --git a/user_guide_src/source/testing/response/004.php b/user_guide_src/source/testing/response/004.php new file mode 100644 index 000000000000..d3ccbf4f4d12 --- /dev/null +++ b/user_guide_src/source/testing/response/004.php @@ -0,0 +1,5 @@ +isOK()) { + // ... +} diff --git a/user_guide_src/source/testing/response/005.php b/user_guide_src/source/testing/response/005.php new file mode 100644 index 000000000000..9c01964bc050 --- /dev/null +++ b/user_guide_src/source/testing/response/005.php @@ -0,0 +1,3 @@ +assertOK(); diff --git a/user_guide_src/source/testing/response/006.php b/user_guide_src/source/testing/response/006.php new file mode 100644 index 000000000000..dcca55a4938b --- /dev/null +++ b/user_guide_src/source/testing/response/006.php @@ -0,0 +1,5 @@ +isRedirect()) { + // ... +} diff --git a/user_guide_src/source/testing/response/007.php b/user_guide_src/source/testing/response/007.php new file mode 100644 index 000000000000..38b43a99b200 --- /dev/null +++ b/user_guide_src/source/testing/response/007.php @@ -0,0 +1,3 @@ +assertRedirect(); diff --git a/user_guide_src/source/testing/response/008.php b/user_guide_src/source/testing/response/008.php new file mode 100644 index 000000000000..62b8da3db6af --- /dev/null +++ b/user_guide_src/source/testing/response/008.php @@ -0,0 +1,3 @@ +assertRedirectTo('foo/bar'); diff --git a/user_guide_src/source/testing/response/009.php b/user_guide_src/source/testing/response/009.php new file mode 100644 index 000000000000..ba58bc23bb9e --- /dev/null +++ b/user_guide_src/source/testing/response/009.php @@ -0,0 +1,4 @@ +getRedirectUrl(); +$this->assertEquals(site_url('foo/bar'), $url); diff --git a/user_guide_src/source/testing/response/010.php b/user_guide_src/source/testing/response/010.php new file mode 100644 index 000000000000..70faf34078e1 --- /dev/null +++ b/user_guide_src/source/testing/response/010.php @@ -0,0 +1,3 @@ +assertStatus(403); diff --git a/user_guide_src/source/testing/response/011.php b/user_guide_src/source/testing/response/011.php new file mode 100644 index 000000000000..e9fccd725002 --- /dev/null +++ b/user_guide_src/source/testing/response/011.php @@ -0,0 +1,3 @@ +assertSessionHas('logged_in', 123); diff --git a/user_guide_src/source/testing/response/012.php b/user_guide_src/source/testing/response/012.php new file mode 100644 index 000000000000..671330ac32a4 --- /dev/null +++ b/user_guide_src/source/testing/response/012.php @@ -0,0 +1,3 @@ +assertSessionMissin('logged_in'); diff --git a/user_guide_src/source/testing/response/013.php b/user_guide_src/source/testing/response/013.php new file mode 100644 index 000000000000..761b39943842 --- /dev/null +++ b/user_guide_src/source/testing/response/013.php @@ -0,0 +1,3 @@ +assertHeader('Content-Type', 'text/html'); diff --git a/user_guide_src/source/testing/response/014.php b/user_guide_src/source/testing/response/014.php new file mode 100644 index 000000000000..109ce46d1097 --- /dev/null +++ b/user_guide_src/source/testing/response/014.php @@ -0,0 +1,3 @@ +assertHeader('Accepts'); diff --git a/user_guide_src/source/testing/response/015.php b/user_guide_src/source/testing/response/015.php new file mode 100644 index 000000000000..ded077e66a9b --- /dev/null +++ b/user_guide_src/source/testing/response/015.php @@ -0,0 +1,3 @@ +assertCookie('foo', 'bar'); diff --git a/user_guide_src/source/testing/response/016.php b/user_guide_src/source/testing/response/016.php new file mode 100644 index 000000000000..3aa73f14899a --- /dev/null +++ b/user_guide_src/source/testing/response/016.php @@ -0,0 +1,3 @@ +assertCookieMissing('ci_session'); diff --git a/user_guide_src/source/testing/response/017.php b/user_guide_src/source/testing/response/017.php new file mode 100644 index 000000000000..5ce2b3e2ab9f --- /dev/null +++ b/user_guide_src/source/testing/response/017.php @@ -0,0 +1,3 @@ +assertCookieExpired('foo'); diff --git a/user_guide_src/source/testing/response/018.php b/user_guide_src/source/testing/response/018.php new file mode 100644 index 000000000000..e6860f412e08 --- /dev/null +++ b/user_guide_src/source/testing/response/018.php @@ -0,0 +1,10 @@ +see('Hello World'); +// Check that "Hello World" is within an h1 tag +$results->see('Hello World', 'h1'); +// Check that "Hello World" is within an element with the "notice" class +$results->see('Hello World', '.notice'); +// Check that "Hello World" is within an element with id of "title" +$results->see('Hellow World', '#title'); diff --git a/user_guide_src/source/testing/response/019.php b/user_guide_src/source/testing/response/019.php new file mode 100644 index 000000000000..f96493b52e91 --- /dev/null +++ b/user_guide_src/source/testing/response/019.php @@ -0,0 +1,6 @@ +dontSee('Hello World'); +// Checks that "Hellow World" does NOT exist within any h1 tag +$results->dontSee('Hello World', 'h1'); diff --git a/user_guide_src/source/testing/response/020.php b/user_guide_src/source/testing/response/020.php new file mode 100644 index 000000000000..8b716717b696 --- /dev/null +++ b/user_guide_src/source/testing/response/020.php @@ -0,0 +1,8 @@ +seeElement('.notice'); +// Check that an element with id 'title' exists +$results->seeElement('#title'); +// Verify that an element with id 'title' does NOT exist +$results->dontSeeElement('#title'); diff --git a/user_guide_src/source/testing/response/021.php b/user_guide_src/source/testing/response/021.php new file mode 100644 index 000000000000..bc74e3ad9124 --- /dev/null +++ b/user_guide_src/source/testing/response/021.php @@ -0,0 +1,6 @@ +seeLink('Upgrade Account'); +// Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell' +$results->seeLink('Upgrade Account', '.upsell'); diff --git a/user_guide_src/source/testing/response/022.php b/user_guide_src/source/testing/response/022.php new file mode 100644 index 000000000000..9bb549c9db5f --- /dev/null +++ b/user_guide_src/source/testing/response/022.php @@ -0,0 +1,6 @@ +seeInField('user', 'John Snow'); +// Check a multi-dimensional input +$results->seeInField('user[name]', 'John Snow'); diff --git a/user_guide_src/source/testing/response/023.php b/user_guide_src/source/testing/response/023.php new file mode 100644 index 000000000000..3378144f6eb7 --- /dev/null +++ b/user_guide_src/source/testing/response/023.php @@ -0,0 +1,6 @@ +seeCheckboxIsChecked('.foo'); +// Check if checkbox with id of 'bar' is checked +$results->seeCheckboxIsChecked('#bar'); diff --git a/user_guide_src/source/testing/response/024.php b/user_guide_src/source/testing/response/024.php new file mode 100644 index 000000000000..021910b4e3f7 --- /dev/null +++ b/user_guide_src/source/testing/response/024.php @@ -0,0 +1,10 @@ +assertSee('Hello World'); +// Check that "Hello World" is within an h1 tag +$result->assertSee('Hello World', 'h1'); +// Check that "Hello World" is within an element with the "notice" class +$result->assertSee('Hello World', '.notice'); +// Check that "Hello World" is within an element with id of "title" +$result->assertSee('Hellow World', '#title'); diff --git a/user_guide_src/source/testing/response/025.php b/user_guide_src/source/testing/response/025.php new file mode 100644 index 000000000000..b255f4155ac5 --- /dev/null +++ b/user_guide_src/source/testing/response/025.php @@ -0,0 +1,6 @@ +dontSee('Hello World'); +// Checks that "Hello World" does NOT exist within any h1 tag +$results->dontSee('Hello World', 'h1'); diff --git a/user_guide_src/source/testing/response/026.php b/user_guide_src/source/testing/response/026.php new file mode 100644 index 000000000000..4afd20161b23 --- /dev/null +++ b/user_guide_src/source/testing/response/026.php @@ -0,0 +1,6 @@ +seeElement('.notice'); +// Check that an element with id 'title' exists +$results->seeElement('#title'); diff --git a/user_guide_src/source/testing/response/027.php b/user_guide_src/source/testing/response/027.php new file mode 100644 index 000000000000..9780489133ee --- /dev/null +++ b/user_guide_src/source/testing/response/027.php @@ -0,0 +1,4 @@ +dontSeeElement('#title'); diff --git a/user_guide_src/source/testing/response/028.php b/user_guide_src/source/testing/response/028.php new file mode 100644 index 000000000000..bc74e3ad9124 --- /dev/null +++ b/user_guide_src/source/testing/response/028.php @@ -0,0 +1,6 @@ +seeLink('Upgrade Account'); +// Check that a link exists with 'Upgrade Account' as the text, AND a class of 'upsell' +$results->seeLink('Upgrade Account', '.upsell'); diff --git a/user_guide_src/source/testing/response/029.php b/user_guide_src/source/testing/response/029.php new file mode 100644 index 000000000000..1ae7910e9776 --- /dev/null +++ b/user_guide_src/source/testing/response/029.php @@ -0,0 +1,6 @@ +assertSeeInField('user', 'John Snow'); +// Check a multi-dimensional input +$results->assertSeeInField('user[name]', 'John Snow'); diff --git a/user_guide_src/source/testing/response/030.php b/user_guide_src/source/testing/response/030.php new file mode 100644 index 000000000000..47e480c6081f --- /dev/null +++ b/user_guide_src/source/testing/response/030.php @@ -0,0 +1,14 @@ + 'bar'] + */ + +$json = $result->getJSON(); +/* + * $json is this: + * { + * "foo": "bar" + * } +`*/ diff --git a/user_guide_src/source/testing/response/031.php b/user_guide_src/source/testing/response/031.php new file mode 100644 index 000000000000..6e746800e034 --- /dev/null +++ b/user_guide_src/source/testing/response/031.php @@ -0,0 +1,4 @@ +assertTrue($result->getJSON() !== false); diff --git a/user_guide_src/source/testing/response/032.php b/user_guide_src/source/testing/response/032.php new file mode 100644 index 000000000000..14f13f1942f5 --- /dev/null +++ b/user_guide_src/source/testing/response/032.php @@ -0,0 +1,11 @@ + ['key-a', 'key-b'], + * ] + */ + +// Is true +$result->assertJSONFragment(['config' => ['key-a']]); diff --git a/user_guide_src/source/tutorial/conclusion.rst b/user_guide_src/source/tutorial/conclusion.rst index afb38465904b..94b8ba5e3f2d 100644 --- a/user_guide_src/source/tutorial/conclusion.rst +++ b/user_guide_src/source/tutorial/conclusion.rst @@ -10,8 +10,9 @@ design patterns, which you can expand upon. Now that you've completed this tutorial, we recommend you check out the rest of the documentation. CodeIgniter is often praised because of its comprehensive documentation. Use this to your advantage and read the -"Overview" and "General Topics" sections thoroughly. You should read -the class and helper references when needed. +:doc:`Overview ` and :doc:`/general/index` +sections thoroughly. You should read +the :doc:`Library ` and :doc:`/helpers/index` references when needed. Every intermediate PHP programmer should be able to get the hang of CodeIgniter within a few days. @@ -19,4 +20,5 @@ CodeIgniter within a few days. If you still have questions about the framework or your own CodeIgniter code, you can: -- Check out our `forums `_ +- Check out our `Forum `_ +- Check out our `Slack `_ diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/tutorial/create_news_items.rst index 604c2373314e..99d28abf3cdb 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -1,4 +1,4 @@ -Create news items +Create News Items ################# You now know how you can read data from a database using CodeIgniter, but @@ -7,29 +7,29 @@ you'll expand your news controller and model created earlier to include this functionality. Enable CSRF Filter ------------------- +****************** Before creating a form, let's enable the CSRF protection. -Open the **app/Config/Filters.php** file and update the ``$methods`` property like the following:: +Open the **app/Config/Filters.php** file and update the ``$methods`` property like the following: - public $methods = [ - 'post' => ['csrf'], - ]; +.. literalinclude:: create_news_items/001.php It configures the CSRF filter to be enabled for all **POST** requests. You can read more about the CSRF protection in :doc:`Security ` library. -Create a form -------------- +.. Warning:: In general, if you use ``$methods`` filters, you should :ref:`disable auto-routing ` + because auto-routing permits any HTTP method to access a controller. + Accessing the controller with a method you don't expect could bypass the filter. + +Create a Form +************* To input data into the database, you need to create a form where you can input the information to be stored. This means you'll be needing a form with two fields, one for the title and one for the text. You'll derive the slug from our title in the model. Create a new view at -**app/Views/news/create.php**. - -:: +**app/Views/news/create.php**::

    @@ -50,7 +50,7 @@ the slug from our title in the model. Create a new view at There are probably only three things here that look unfamiliar. -The ``getFlashdata('error') ?>`` function is used to report +The ``session()->getFlashdata('error')`` function is used to report errors related to CSRF protection. The ``service('validation')->listErrors()`` function is used to report @@ -60,32 +60,10 @@ The ``csrf_field()`` function creates a hidden input with a CSRF token that help Go back to your ``News`` controller. You're going to do two things here, check whether the form was submitted and whether the submitted data -passed the validation rules. You'll use the :doc:`form -validation <../libraries/validation>` library to do this. - -:: - - public function create() - { - $model = model(NewsModel::class); - - if ($this->request->getMethod() === 'post' && $this->validate([ - 'title' => 'required|min_length[3]|max_length[255]', - 'body' => 'required', - ])) { - $model->save([ - 'title' => $this->request->getPost('title'), - 'slug' => url_title($this->request->getPost('title'), '-', true), - 'body' => $this->request->getPost('body'), - ]); - - echo view('news/success'); - } else { - echo view('templates/header', ['title' => 'Create a news item']); - echo view('news/create'); - echo view('templates/footer'); - } - } +passed the validation rules. +You'll use the :ref:`validation method in Controller ` to do this. + +.. literalinclude:: create_news_items/002.php The code above adds a lot of functionality. First we load the NewsModel. After that, we check if we deal with the **POST** request and then @@ -109,14 +87,12 @@ slug, perfect for creating URIs. After this, a view is loaded to display a success message. Create a view at **app/Views/news/success.php** and write a success message. -This could be as simple as: - -:: +This could be as simple as:: News item created successfully. Model Updating -------------------------------------------------------- +************** The only thing that remains is ensuring that your model is set up to allow data to be saved properly. The ``save()`` method that was @@ -130,20 +106,7 @@ not actually save any data because it doesn't know what fields are safe to be updated. Edit the **NewsModel** to provide it a list of updatable fields in the ``$allowedFields`` property. -:: - - `. -:: - - $routes->match(['get', 'post'], 'news/create', 'News::create'); - $routes->get('news/(:segment)', 'News::view/$1'); - $routes->get('news', 'News::index'); - $routes->get('(:any)', 'Pages::view/$1'); +.. literalinclude:: create_news_items/004.php Now point your browser to your local development environment where you installed CodeIgniter and add ``/news/create`` to the URL. @@ -182,7 +140,7 @@ Add some news and check out the different pages you made. :width: 45% Congratulations -------------------------------------------------------- +*************** You just completed your first CodeIgniter4 application! diff --git a/user_guide_src/source/tutorial/create_news_items/001.php b/user_guide_src/source/tutorial/create_news_items/001.php new file mode 100644 index 000000000000..4e6615b6bbda --- /dev/null +++ b/user_guide_src/source/tutorial/create_news_items/001.php @@ -0,0 +1,14 @@ + ['csrf'], + ]; + + // ... +} diff --git a/user_guide_src/source/tutorial/create_news_items/002.php b/user_guide_src/source/tutorial/create_news_items/002.php new file mode 100644 index 000000000000..66ff5e26776b --- /dev/null +++ b/user_guide_src/source/tutorial/create_news_items/002.php @@ -0,0 +1,28 @@ +request->getMethod() === 'post' && $this->validate([ + 'title' => 'required|min_length[3]|max_length[255]', + 'body' => 'required', + ])) { + $model->save([ + 'title' => $this->request->getPost('title'), + 'slug' => url_title($this->request->getPost('title'), '-', true), + 'body' => $this->request->getPost('body'), + ]); + + return view('news/success'); + } + + return view('templates/header', ['title' => 'Create a news item']) + . view('news/create') + . view('templates/footer'); + } +} diff --git a/user_guide_src/source/tutorial/create_news_items/003.php b/user_guide_src/source/tutorial/create_news_items/003.php new file mode 100644 index 000000000000..83e03f18eb2b --- /dev/null +++ b/user_guide_src/source/tutorial/create_news_items/003.php @@ -0,0 +1,12 @@ +match(['get', 'post'], 'news/create', 'News::create'); +$routes->get('news/(:segment)', 'News::view/$1'); +$routes->get('news', 'News::index'); +$routes->get('pages', 'Pages::index'); +$routes->get('(:any)', 'Pages::view/$1'); + +// ... diff --git a/user_guide_src/source/tutorial/index.rst b/user_guide_src/source/tutorial/index.rst index 798341a6ce96..e940ec849057 100644 --- a/user_guide_src/source/tutorial/index.rst +++ b/user_guide_src/source/tutorial/index.rst @@ -22,7 +22,7 @@ This tutorial will primarily focus on: - Model-View-Controller basics - Routing basics - Form validation -- Performing basic database queries using CodeIgniter's "Query Builder" +- Performing basic database queries using CodeIgniter's Model The entire tutorial is split up over several pages, each explaining a small part of the functionality of the CodeIgniter framework. You'll go @@ -55,9 +55,7 @@ Getting Up and Running You can download a release manually from the site, but for this tutorial we will use the recommended way and install the AppStarter package through Composer. -From your command line type the following: - -:: +From your command line type the following:: > composer create-project codeigniter4/appstarter ci-news @@ -84,14 +82,11 @@ command line from the root of your project:: > php spark serve - The Welcome Page **************** Now point your browser to the correct URL you will be greeted by a welcome screen. -Try it now by heading to the following URL: - -:: +Try it now by heading to the following URL:: http://localhost:8080 @@ -127,6 +122,5 @@ There are a couple of things to note here: Everything else should be clear when you see it. - Now that we know how to get started and how to debug a little, let's get started building this small news application. diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index 396c74255962..aca19a25cad3 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -1,13 +1,13 @@ -News section -############################################################################### +News Section +############ In the last section, we went over some basic concepts of the framework by writing a class that references static pages. We cleaned up the URI by adding custom routing rules. Now it's time to introduce dynamic content and start using a database. -Create a database to work with -------------------------------------------------------- +Create a Database to Work with +****************************** The CodeIgniter installation assumes that you have set up an appropriate database, as outlined in the :doc:`requirements `. @@ -18,13 +18,7 @@ commands (mysql, MySQL Workbench, or phpMyAdmin). You need to create a database that can be used for this tutorial, and then configure CodeIgniter to use it. -Using your database client, connect to your database and run the SQL command below (MySQL). -Also, add some seed records. For now, we'll just show you the SQL statements needed -to create the table, but you should be aware that this can be done programmatically -once you are more familiar with CodeIgniter; you can read about :doc:`Migrations <../dbmgmt/migration>` -and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later. - -:: +Using your database client, connect to your database and run the SQL command below (MySQL):: CREATE TABLE news ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, @@ -35,27 +29,28 @@ and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later. KEY slug (slug) ); +Also, add some seed records. For now, we'll just show you the SQL statements needed +to create the table, but you should be aware that this can be done programmatically +once you are more familiar with CodeIgniter; you can read about :doc:`Migrations <../dbmgmt/migration>` +and :doc:`Seeds <../dbmgmt/seeds>` to create more useful database setups later. + A note of interest: a "slug", in the context of web publishing, is a user- and SEO-friendly short text used in a URL to identify and describe a resource. -The seed records might be something like: - -:: +The seed records might be something like:: INSERT INTO news VALUES (1,'Elvis sighted','elvis-sighted','Elvis was sighted at the Podunk internet cafe. It looked like he was writing a CodeIgniter app.'), (2,'Say it isn\'t so!','say-it-isnt-so','Scientists conclude that some programmers have a sense of humor.'), (3,'Caffeination, Yes!','caffeination-yes','World\'s largest coffee shop open onsite nested coffee shop for staff only.'); -Connect to your database -------------------------------------------------------- +Connect to Your Database +************************ The local configuration file, ``.env``, that you created when you installed CodeIgniter, should have the database property settings uncommented and set appropriately for the database you want to use. Make sure you've configured -your database properly as described :doc:`here <../database/configuration>`. - -:: +your database properly as described :doc:`here <../database/configuration>`:: database.default.hostname = localhost database.default.database = ci4tutorial @@ -63,8 +58,8 @@ your database properly as described :doc:`here <../database/configuration>`. database.default.password = root database.default.DBDriver = MySQLi -Setting up your model -------------------------------------------------------- +Setting up Your Model +********************* Instead of writing database operations right in the controller, queries should be placed in a model, so they can easily be reused later. Models @@ -75,18 +70,7 @@ You can read more about it :doc:`here `. Open up the **app/Models/** directory and create a new file called **NewsModel.php** and add the following code. -:: - - ` — is used. This makes it +abstraction layer that is included with CodeIgniter - +:doc:`Query Builder <../database/query_builder>` - is used in the ``CodeIgnite\Model``. This makes it possible to write your 'queries' once and make them work on :doc:`all supported database systems <../intro/requirements>`. The Model class also allows you to easily work with the Query Builder and provides some additional tools to make working with data simpler. Add the following code to your model. -:: - - public function getNews($slug = false) - { - if ($slug === false) { - return $this->findAll(); - } - - return $this->where(['slug' => $slug])->first(); - } +.. literalinclude:: news_section/002.php With this code, you can perform two different queries. You can get all news records, or get a news item by its slug. You might have @@ -126,8 +101,8 @@ that use the Query Builder to run their commands on the current table, and returning an array of results in the format of your choice. In this example, ``findAll()`` returns an array of array. -Display the news -------------------------------------------------------- +Display the News +**************** Now that the queries are written, the model should be tied to the views that are going to display the news items to the user. This could be done @@ -135,30 +110,7 @@ in our ``Pages`` controller created earlier, but for the sake of clarity, a new ``News`` controller is defined. Create the new controller at **app/Controllers/News.php**. -:: - - getNews(); - } - - public function view($slug = null) - { - $model = model(NewsModel::class); - - $data['news'] = $model->getNews($slug); - } - } +.. literalinclude:: news_section/003.php Looking at the code, you may see some similarity with the files we created earlier. First, it extends a core CodeIgniter class, ``Controller``, @@ -179,21 +131,9 @@ news item to be returned. Now the data is retrieved by the controller through our model, but nothing is displayed yet. The next thing to do is, passing this data to -the views. Modify the ``index()`` method to look like this:: - - public function index() - { - $model = model(NewsModel::class); +the views. Modify the ``index()`` method to look like this: - $data = [ - 'news' => $model->getNews(), - 'title' => 'News archive', - ]; - - echo view('templates/header', $data); - echo view('news/overview', $data); - echo view('templates/footer', $data); - } +.. literalinclude:: news_section/004.php The code above gets all news records from the model and assigns it to a variable. The value for the title is also assigned to the ``$data['title']`` @@ -201,31 +141,7 @@ element and all data is passed to the views. You now need to create a view to render the news items. Create **app/Views/news/overview.php** and add the next piece of code. -:: - -

    - - - - - -

    - -
    - -
    -

    View article

    - - - - - -

    No News

    - -

    Unable to find any news for you.

    - - - +.. literalinclude:: news_section/005.php .. note:: We are again using using ``esc()`` to help prevent XSS attacks. But this time we also passed "url" as a second parameter. That's because @@ -243,50 +159,25 @@ a way that it can easily be used for this functionality. You only need to add some code to the controller and create a new view. Go back to the ``News`` controller and update the ``view()`` method with the following: -:: - - public function view($slug = null) - { - $model = model(NewsModel::class); - - $data['news'] = $model->getNews($slug); - - if (empty($data['news'])) { - throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the news item: ' . $slug); - } - - $data['title'] = $data['news']['title']; - - echo view('templates/header', $data); - echo view('news/view', $data); - echo view('templates/footer', $data); - } +.. literalinclude:: news_section/006.php Instead of calling the ``getNews()`` method without a parameter, the ``$slug`` variable is passed, so it will return the specific news item. The only thing left to do is create the corresponding view at **app/Views/news/view.php**. Put the following code in this file. -:: - -

    -

    +.. literalinclude:: news_section/007.php Routing -------------------------------------------------------- +******* -Because of the wildcard routing rule created earlier, you need an extra -route to view the controller that you just made. Modify your routing file +Modify your routing file (**app/Config/Routes.php**) so it looks as follows. This makes sure the requests reach the ``News`` controller instead of going directly to the ``Pages`` controller. The first line routes URI's with a slug to the ``view()`` method in the ``News`` controller. -:: - - $routes->get('news/(:segment)', 'News::view/$1'); - $routes->get('news', 'News::index'); - $routes->get('(:any)', 'Pages::view/$1'); +.. literalinclude:: news_section/008.php Point your browser to your "news" page, i.e., ``localhost:8080/news``, you should see a list of the news items, each of which has a link diff --git a/user_guide_src/source/tutorial/news_section/001.php b/user_guide_src/source/tutorial/news_section/001.php new file mode 100644 index 000000000000..b565d4a82f25 --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/001.php @@ -0,0 +1,10 @@ +findAll(); + } + + return $this->where(['slug' => $slug])->first(); + } +} diff --git a/user_guide_src/source/tutorial/news_section/003.php b/user_guide_src/source/tutorial/news_section/003.php new file mode 100644 index 000000000000..13c3b7263db6 --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/003.php @@ -0,0 +1,22 @@ +getNews(); + } + + public function view($slug = null) + { + $model = model(NewsModel::class); + + $data['news'] = $model->getNews($slug); + } +} diff --git a/user_guide_src/source/tutorial/news_section/004.php b/user_guide_src/source/tutorial/news_section/004.php new file mode 100644 index 000000000000..9e823b6f7589 --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/004.php @@ -0,0 +1,22 @@ + $model->getNews(), + 'title' => 'News archive', + ]; + + return view('templates/header', $data) + . view('news/overview') + . view('templates/footer'); + } +} diff --git a/user_guide_src/source/tutorial/news_section/005.php b/user_guide_src/source/tutorial/news_section/005.php new file mode 100644 index 000000000000..39db0a4319f7 --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/005.php @@ -0,0 +1,22 @@ +

    + + + + + +

    + +
    + +
    +

    View article

    + + + + + +

    No News

    + +

    Unable to find any news for you.

    + + diff --git a/user_guide_src/source/tutorial/news_section/006.php b/user_guide_src/source/tutorial/news_section/006.php new file mode 100644 index 000000000000..94548ac9307f --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/006.php @@ -0,0 +1,25 @@ +getNews($slug); + + if (empty($data['news'])) { + throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find the news item: ' . $slug); + } + + $data['title'] = $data['news']['title']; + + return view('templates/header', $data) + . view('news/view') + . view('templates/footer'); + } +} diff --git a/user_guide_src/source/tutorial/news_section/007.php b/user_guide_src/source/tutorial/news_section/007.php new file mode 100644 index 000000000000..9975baad0cbb --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/007.php @@ -0,0 +1,2 @@ +

    +

    diff --git a/user_guide_src/source/tutorial/news_section/008.php b/user_guide_src/source/tutorial/news_section/008.php new file mode 100644 index 000000000000..4ba41638ef7f --- /dev/null +++ b/user_guide_src/source/tutorial/news_section/008.php @@ -0,0 +1,10 @@ +get('news/(:segment)', 'News::view/$1'); +$routes->get('news', 'News::index'); +$routes->get('pages', 'Pages::index'); +$routes->get('(:any)', 'Pages::view/$1'); + +// ... diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index ecf6e87e03c5..6fde0a671da7 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -1,5 +1,5 @@ -Static pages -############################################################################### +Static Pages +############ .. note:: This tutorial assumes you've downloaded CodeIgniter and :doc:`installed the framework <../installation/index>` in your @@ -9,49 +9,13 @@ The first thing you're going to do is set up a **controller** to handle static pages. A controller is simply a class that helps delegate work. It is the glue of your web application. -For example, when a call is made to: - -:: - - http://example.com/news/latest/10 - -We might imagine that there is a controller named "news". The method -being called on news would be "latest". The news method's job could be to -grab 10 news items, and render them on the page. Very often in MVC, -you'll see URL patterns that match: - -:: - - http://example.com/[controller-class]/[controller-method]/[arguments] - -As URL schemes become more complex, this may change. But for now, this -is all we will need to know. - -Let's make our first controller -------------------------------------------------------- +Let's Make our First Controller +******************************* Create a file at **app/Controllers/Pages.php** with the following code. -:: - - @@ -95,9 +57,7 @@ The header contains the basic HTML code that you'll want to display before loading the main view, together with a heading. It will also output the ``$title`` variable, which we'll define later in the controller. Now, create a footer at **app/Views/templates/footer.php** that -includes the following code: - -:: +includes the following code:: © 2021 @@ -107,8 +67,8 @@ includes the following code: function. It's a global function provided by CodeIgniter to help prevent XSS attacks. You can read more about it :doc:`here `. -Adding logic to the controller -------------------------------------------------------- +Adding Logic to the Controller +****************************** Earlier you set up a controller with a ``view()`` method. The method accepts one parameter, which is the name of the page to be loaded. The @@ -116,28 +76,14 @@ static page bodies will be located in the **app/Views/pages/** directory. In that directory, create two files named **home.php** and **about.php**. -Within those files, type some text − anything you'd like − and save them. +Within those files, type some text - anything you'd like - and save them. If you like to be particularly un-original, try "Hello World!". In order to load those pages, you'll have to check whether the requested page actually exists. This will be the body of the ``view()`` method in the ``Pages`` controller created above: -:: - - public function view($page = 'home') - { - if (! is_file(APPPATH . 'Views/pages/' . $page . '.php')) { - // Whoops, we don't have a page for that! - throw new \CodeIgniter\Exceptions\PageNotFoundException($page); - } - - $data['title'] = ucfirst($page); // Capitalize the first letter - - echo view('templates/header', $data); - echo view('pages/' . $page, $data); - echo view('templates/footer', $data); - } +.. literalinclude:: static_pages/002.php Now, when the requested page does exist, it is loaded, including the header and footer, and displayed to the user. If the requested page doesn't exist, a "404 @@ -166,64 +112,11 @@ view. throw errors on case-sensitive platforms. You can read more about it :doc:`here `. -Running the App -------------------------------------------------------- - -Ready to test? You cannot run the app using PHP's built-in server, -since it will not properly process the ``.htaccess`` rules that are provided in -``public``, and which eliminate the need to specify "index.php/" -as part of a URL. CodeIgniter has its own command that you can use though. - -From the command line, at the root of your project: - -:: - - > php spark serve - -will start a web server, accessible on port 8080. If you set the location field -in your browser to ``localhost:8080``, you should see the CodeIgniter welcome page. - -You can now try several URLs in the browser location field, to see what the ``Pages`` -controller you made above produces... - -.. table:: - :widths: 20 80 - - +---------------------------------+-----------------------------------------------------------------+ - | URL | Will show | - +=================================+=================================================================+ - | localhost:8080/pages | the results from the `index` method inside our `Pages` | - | | controller, which is to display the CodeIgniter "welcome" page, | - | | because "index" is the default controller method | - +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/pages/index | the CodeIgniter "welcome" page, because we explicitly asked for | - | | the "index" method | - +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/pages/view | the "home" page that you made above, because it is the default | - | | "page" parameter to the ``view()`` method. | - +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/pages/view/home | show the "home" page that you made above, because we explicitly | - | | asked for it | - +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/pages/view/about | the "about" page that you made above, because we explicitly | - | | asked for it | - +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/pages/view/shop | a "404 - File Not Found" error page, because there is no | - | | `app/Views/pages/shop.php` | - +---------------------------------+-----------------------------------------------------------------+ - - Routing -------------------------------------------------------- - -The controller is now functioning! - -Using custom routing rules, you have the power to map any URI to any -controller and method, and break free from the normal convention: +******* -:: - - http://example.com/[controller-class]/[controller-method]/[arguments] +We have made the controller. The next thing is to set routing rules. +Routing associates a URI with a controller's method. Let's do that. Open the routing file located at **app/Config/Routes.php** and look for the "Route Definitions" @@ -231,18 +124,15 @@ section of the configuration file. The only uncommented line there to start with should be: -:: - - $routes->get('/', 'Home::index'); +.. literalinclude:: static_pages/003.php This directive says that any incoming request without any content specified should be handled by the ``index()`` method inside the ``Home`` controller. -Add the following line, **after** the route directive for '/'. - -:: +Add the following lines, **after** the route directive for '/'. - $routes->get('(:any)', 'Pages::view/$1'); +.. literalinclude:: static_pages/004.php + :lines: 2- CodeIgniter reads its routing rules from top to bottom and routes the request to the first matching rule. Each rule is a regular expression @@ -254,18 +144,57 @@ arguments. More information about routing can be found in the URI Routing :doc:`documentation `. -Here, the second rule in the ``$routes`` object matches **any** request -using the wildcard string ``(:any)``. and passes the parameter to the +Here, the second rule in the ``$routes`` object matches GET request +to the URI path ``/pages`` maps the ``index()`` method of the ``Pages`` class. + +The third rule in the ``$routes`` object matches GET request to **any** URI path +using the wildcard string ``(:any)``, and passes the parameter to the ``view()`` method of the ``Pages`` class. +Running the App +*************** + +Ready to test? You cannot run the app using PHP's built-in server, +since it will not properly process the ``.htaccess`` rules that are provided in +``public``, and which eliminate the need to specify "index.php/" +as part of a URL. CodeIgniter has its own command that you can use though. + +From the command line, at the root of your project:: + + > php spark serve + +will start a web server, accessible on port 8080. If you set the location field +in your browser to ``localhost:8080``, you should see the CodeIgniter welcome page. + Now visit ``localhost:8080/home``. Did it get routed correctly to the ``view()`` -method in the pages controller? Awesome! +method in the ``Pages`` controller? Awesome! You should see something like the following: .. image:: ../images/tutorial1.png :align: center -.. note:: When manually specifying routes, it is recommended to disable - auto-routing by setting ``$routes->setAutoRoute(false);`` in the **Routes.php** file. - This ensures that only routes you define can be accessed. +You can now try several URLs in the browser location field, to see what the ``Pages`` +controller you made above produces... + +.. table:: + :widths: 20 80 + + +---------------------------------+-----------------------------------------------------------------+ + | URL | Will show | + +=================================+=================================================================+ + | localhost:8080/pages | the results from the ``index()`` method inside our ``Pages`` | + | | controller, which is to display the CodeIgniter "welcome" page. | + +---------------------------------+-----------------------------------------------------------------+ + | localhost:8080/pages/view | the "home" page that you made above, because it is the default | + | | "page" parameter to the ``view()`` method. | + +---------------------------------+-----------------------------------------------------------------+ + | localhost:8080/pages/view/home | show the "home" page that you made above, because we explicitly | + | | asked for it. | + +---------------------------------+-----------------------------------------------------------------+ + | localhost:8080/pages/view/about | the "about" page that you made above, because we explicitly | + | | asked for it. | + +---------------------------------+-----------------------------------------------------------------+ + | localhost:8080/pages/view/shop | a "404 - File Not Found" error page, because there is no | + | | **app/Views/pages/shop.php**. | + +---------------------------------+-----------------------------------------------------------------+ diff --git a/user_guide_src/source/tutorial/static_pages/001.php b/user_guide_src/source/tutorial/static_pages/001.php new file mode 100644 index 000000000000..0cedb8e44906 --- /dev/null +++ b/user_guide_src/source/tutorial/static_pages/001.php @@ -0,0 +1,16 @@ +get('/', 'Home::index'); + +// ... diff --git a/user_guide_src/source/tutorial/static_pages/004.php b/user_guide_src/source/tutorial/static_pages/004.php new file mode 100644 index 000000000000..a48a8caf18cb --- /dev/null +++ b/user_guide_src/source/tutorial/static_pages/004.php @@ -0,0 +1,4 @@ +get('pages', 'Pages::index'); +$routes->get('(:any)', 'Pages::view/$1'); diff --git a/utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php b/utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php index b083b5daff87..d1b660b037aa 100644 --- a/utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php +++ b/utils/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php @@ -60,9 +60,7 @@ public function refactor(Node $node): ?Node return null; } - $inStmts = (bool) $this->betterNodeFinder->findFirst((array) $tryCatch->stmts, static function (Node $n) use ($node): bool { - return $n === $node; - }); + $inStmts = (bool) $this->betterNodeFinder->findFirst((array) $tryCatch->stmts, static fn (Node $n): bool => $n === $node); // not in stmts, means it in catch or finally if (! $inStmts) { diff --git a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php index 690a63c6febe..530fe18b029e 100644 --- a/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php +++ b/utils/Rector/UnderscoreToCamelCaseVariableNameRector.php @@ -34,10 +34,7 @@ final class UnderscoreToCamelCaseVariableNameRector extends AbstractRector */ private const PARAM_NAME_REGEX = '#(?@param\s.*\s+\$)(?%s)#ms'; - /** - * @var ReservedKeywordAnalyzer - */ - private $reservedKeywordAnalyzer; + private ReservedKeywordAnalyzer $reservedKeywordAnalyzer; public function __construct( ReservedKeywordAnalyzer $reservedKeywordAnalyzer