diff --git a/.github/scripts/deploy-appstarter b/.github/scripts/deploy-appstarter index 870f24760d6d..29dfe66db64c 100644 --- a/.github/scripts/deploy-appstarter +++ b/.github/scripts/deploy-appstarter @@ -20,7 +20,7 @@ git checkout master rm -rf * # Copy common files -releasable='app public writable env LICENSE spark' +releasable='app public writable env LICENSE spark preload.php' for fff in $releasable; do cp -Rf ${SOURCE}/$fff ./ diff --git a/.github/scripts/deploy-framework b/.github/scripts/deploy-framework index 9396ed88aa04..cc9d89e7acc7 100644 --- a/.github/scripts/deploy-framework +++ b/.github/scripts/deploy-framework @@ -20,7 +20,7 @@ git checkout master rm -rf * # Copy common files -releasable='app public writable env LICENSE spark system' +releasable='app public writable env LICENSE spark system preload.php' for fff in $releasable; do cp -Rf ${SOURCE}/$fff ./ diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml new file mode 100644 index 000000000000..984b657a623e --- /dev/null +++ b/.github/workflows/reusable-coveralls.yml @@ -0,0 +1,65 @@ +name: Reusable Coveralls Upload + +on: + workflow_call: + inputs: + php-version: + description: The PHP version the workflow should run + type: string + required: true + +jobs: + coveralls: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + coverage: xdebug + + - name: Download coverage files + uses: actions/download-artifact@v3 + with: + path: build/cov + + - name: Display structure of downloaded files + run: ls -R + working-directory: build/cov + + - name: Get composer cache directory + run: | + echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_FILES_DIR }} + key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} + restore-keys: | + ${{ github.job }}-php-${{ inputs.php-version }}- + ${{ github.job }}- + + - name: Cache PHPUnit's static analysis cache + uses: actions/cache@v3 + with: + path: build/.phpunit.cache/code-coverage + key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} + restore-keys: | + phpunit-code-coverage- + + - name: Install dependencies + run: composer update --ansi + + - name: Merge coverage files + run: vendor/bin/phpcov merge --clover build/logs/clover.xml build/cov + + - name: Upload coverage to Coveralls + run: vendor/bin/php-coveralls --verbose --exclude-no-stmt --ansi + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml new file mode 100644 index 000000000000..2f43602668c1 --- /dev/null +++ b/.github/workflows/reusable-phpunit-test.yml @@ -0,0 +1,214 @@ +name: Reusable PHPUnit Test + +on: + workflow_call: + inputs: + job-name: + description: Name of the job to appear in GitHub UI + type: string + required: true + php-version: + description: The PHP version the workflow should run + type: string + required: true + job-id: + description: Job ID to be used as part of cache key and artifact name + type: string + required: false + db-platform: + description: The database platform to be tested + type: string + required: false + mysql-version: + description: Version of the mysql Docker image + type: string + required: false + group-name: + description: The @group to test + type: string + required: false + enable-artifact-upload: + description: Whether artifact uploading of coverage results should be enabled + type: boolean + required: false + enable-coverage: + description: Whether coverage should be enabled + type: boolean + required: false + enable-profiling: + description: Whether slow tests should be profiled + type: boolean + required: false + extra-extensions: + description: Additional PHP extensions that are needed to be enabled + type: string + required: false + extra-composer-options: + description: Additional Composer options that should be appended to the `composer update` call + type: string + required: false + extra-phpunit-options: + description: Additional PHPUnit options that should be appended to the `vendor/bin/phpunit` call + type: string + required: false + +env: + NLS_LANG: 'AMERICAN_AMERICA.UTF8' + NLS_DATE_FORMAT: 'YYYY-MM-DD HH24:MI:SS' + NLS_TIMESTAMP_FORMAT: 'YYYY-MM-DD HH24:MI:SS' + NLS_TIMESTAMP_TZ_FORMAT: 'YYYY-MM-DD HH24:MI:SS' + +jobs: + tests: + name: ${{ inputs.job-name }} + runs-on: ubuntu-22.04 + + # Service containers cannot be extracted to caller workflows yet + services: + mysql: + image: mysql:${{ inputs.mysql-version || '8.0' }} + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test + ports: + - 5432:5432 + options: >- + --health-cmd=pg_isready + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + mssql: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + SA_PASSWORD: 1Secure*Password1 + ACCEPT_EULA: Y + MSSQL_PID: Developer + ports: + - 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: gvenzl/oracle-xe:21 + env: + ORACLE_RANDOM_PASSWORD: true + APP_USER: ORACLE + APP_USER_PASSWORD: ORACLE + ports: + - 1521:1521 + options: >- + --health-cmd healthcheck.sh + --health-interval 20s + --health-timeout 10s + --health-retries 10 + + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + memcached: + image: memcached:1.6-alpine + ports: + - 11211:11211 + + steps: + - name: Create database for MSSQL Server + if: ${{ inputs.db-platform == 'SQLSRV' }} + run: sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q "CREATE DATABASE test" + + - name: Install latest ImageMagick + if: ${{ contains(inputs.extra-extensions, 'imagick') }} + run: | + sudo apt-get update + sudo apt-get install --reinstall libgs9-common fonts-noto-mono libgs9:amd64 libijs-0.35:amd64 fonts-urw-base35 ghostscript poppler-data libjbig2dec0:amd64 gsfonts libopenjp2-7:amd64 fonts-droid-fallback fonts-dejavu-core + sudo apt-get install -y imagemagick + sudo apt-get install --fix-broken + + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + extensions: gd, ${{ inputs.extra-extensions }} + coverage: ${{ env.COVERAGE_DRIVER }} + env: + COVERAGE_DRIVER: ${{ inputs.enable-coverage && 'xdebug' || 'none' }} + + - name: Setup global environment variables + run: | + echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}" >> $GITHUB_ENV + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_FILES_DIR }} + key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}-${{ hashFiles('**/composer.*') }} + restore-keys: | + ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}- + ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}- + ${{ inputs.job-id || github.job }}- + + - name: Cache PHPUnit's static analysis cache + if: ${{ inputs.enable-artifact-upload }} + uses: actions/cache@v3 + with: + path: build/.phpunit.cache/code-coverage + key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} + restore-keys: | + phpunit-code-coverage- + + - name: Install dependencies + run: | + composer config --global github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + composer update --ansi ${{ inputs.extra-composer-options }} + + - name: Compute additional PHPUnit options + run: | + echo "EXTRA_PHPUNIT_OPTIONS=${{ format('{0} {1} {2}', env.GROUP_OPTION, env.COVERAGE_OPTION, inputs.extra-phpunit-options) }}" >> $GITHUB_ENV + env: + COVERAGE_OPTION: ${{ inputs.enable-coverage && format('--coverage-php build/cov/coverage-{0}.cov', env.ARTIFACT_NAME) || '--no-coverage' }} + GROUP_OPTION: ${{ inputs.group-name && format('--group {0}', inputs.group-name) || '' }} + + - name: Run tests + run: script -e -c "vendor/bin/phpunit --color=always ${{ env.EXTRA_PHPUNIT_OPTIONS }}" + env: + DB: ${{ inputs.db-platform }} + TACHYCARDIA_MONITOR_GA: ${{ inputs.enable-profiling && 'enabled' || '' }} + TERM: xterm-256color + + - name: Upload coverage results as artifact + if: ${{ inputs.enable-artifact-upload }} + uses: actions/upload-artifact@v3 + with: + name: ${{ env.ARTIFACT_NAME }} + path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml new file mode 100644 index 000000000000..c044f2178ea3 --- /dev/null +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -0,0 +1,124 @@ +# Reusable workflow for PHPUnit testing +# without Docker services and databases +name: Reusable Serviceless PHPUnit Test + +on: + workflow_call: + inputs: + job-name: + description: Name of the job to appear in GitHub UI + type: string + required: true + php-version: + description: The PHP version the workflow should run + type: string + required: true + job-id: + description: Job ID to be used as part of cache key and artifact name + type: string + required: false + group-name: + description: The @group to test + type: string + required: false + enable-artifact-upload: + description: Whether artifact uploading of coverage results should be enabled + type: boolean + required: false + enable-coverage: + description: Whether coverage should be enabled + type: boolean + required: false + enable-profiling: + description: Whether slow tests should be profiled + type: boolean + required: false + extra-extensions: + description: Additional PHP extensions that are needed to be enabled + type: string + required: false + extra-composer-options: + description: Additional Composer options that should be appended to the `composer update` call + type: string + required: false + extra-phpunit-options: + description: Additional PHPUnit options that should be appended to the `vendor/bin/phpunit` call + type: string + required: false + +jobs: + tests: + name: ${{ inputs.job-name }} + runs-on: ubuntu-22.04 + + steps: + - name: Install latest ImageMagick + if: ${{ contains(inputs.extra-extensions, 'imagick') }} + run: | + sudo apt-get update + sudo apt-get install --reinstall libgs9-common fonts-noto-mono libgs9:amd64 libijs-0.35:amd64 fonts-urw-base35 ghostscript poppler-data libjbig2dec0:amd64 gsfonts libopenjp2-7:amd64 fonts-droid-fallback fonts-dejavu-core + sudo apt-get install -y imagemagick + sudo apt-get install --fix-broken + + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + tools: composer + extensions: gd, ${{ inputs.extra-extensions }} + coverage: ${{ env.COVERAGE_DRIVER }} + env: + COVERAGE_DRIVER: ${{ inputs.enable-coverage && 'xdebug' || 'none' }} + + - name: Setup global environment variables + run: | + echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_ENV + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_FILES_DIR }} + key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} + restore-keys: | + ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}- + ${{ inputs.job-id || github.job }}- + + - name: Cache PHPUnit's static analysis cache + if: ${{ inputs.enable-artifact-upload }} + uses: actions/cache@v3 + with: + path: build/.phpunit.cache/code-coverage + key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} + restore-keys: | + phpunit-code-coverage- + + - name: Install dependencies + run: | + composer config --global github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} + composer update --ansi ${{ inputs.extra-composer-options }} + + - name: Compute additional PHPUnit options + run: | + echo "EXTRA_PHPUNIT_OPTIONS=${{ format('{0} {1} {2}', env.GROUP_OPTION, env.COVERAGE_OPTION, inputs.extra-phpunit-options) }}" >> $GITHUB_ENV + env: + COVERAGE_OPTION: ${{ inputs.enable-coverage && format('--coverage-php build/cov/coverage-{0}.cov', env.ARTIFACT_NAME) || '--no-coverage' }} + GROUP_OPTION: ${{ inputs.group-name && format('--group {0}', inputs.group-name) || '' }} + + - name: Run tests + run: script -e -c "vendor/bin/phpunit --color=always ${{ env.EXTRA_PHPUNIT_OPTIONS }}" + env: + TACHYCARDIA_MONITOR_GA: ${{ inputs.enable-profiling && 'enabled' || '' }} + TERM: xterm-256color + + - name: Upload coverage results as artifact + if: ${{ inputs.enable-artifact-upload }} + uses: actions/upload-artifact@v3 + with: + name: ${{ env.ARTIFACT_NAME }} + path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index 3d86065a407d..b78180249e73 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -20,33 +20,9 @@ concurrency: jobs: auto-review-tests: - name: Automatic Code Review - runs-on: ubuntu-22.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 - run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_FILES_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 + uses: ./.github/workflows/reusable-serviceless-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo + with: + job-name: Automatic Code Review [PHP 8.1] + php-version: '8.1' + job-id: auto-review-tests + group-name: AutoReview diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index c892ef320d63..796d78bdba9a 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -46,6 +46,7 @@ jobs: with: php-version: '8.1' extensions: intl + coverage: none - name: Use latest Composer run: composer self-update diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index bd8ed58fd579..7c6182cac13c 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -31,205 +31,153 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true -env: - COVERAGE_PHP_VERSION: '8.1' - NLS_LANG: 'AMERICAN_AMERICA.UTF8' - NLS_DATE_FORMAT: 'YYYY-MM-DD HH24:MI:SS' - NLS_TIMESTAMP_FORMAT: 'YYYY-MM-DD HH24:MI:SS' - NLS_TIMESTAMP_TZ_FORMAT: 'YYYY-MM-DD HH24:MI:SS' - jobs: - tests: - name: PHP ${{ matrix.php-versions }} - ${{ matrix.db-platforms }} + # Any environment variables set in an env context defined at the workflow level + # in the caller workflow are not propagated to the called workflow. + coverage-php-version: + name: Setup PHP Version for Code Coverage runs-on: ubuntu-22.04 - if: "!contains(github.event.head_commit.message, '[ci skip]')" + outputs: + version: ${{ steps.coverage-php-version.outputs.version }} + steps: + - id: coverage-php-version + run: | + echo "version=8.1" >> $GITHUB_OUTPUT + + sanity-tests: + needs: coverage-php-version + + strategy: + matrix: + php-version: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + include: + - php-version: '8.2' + composer-option: '--ignore-platform-req=php' + + uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo + with: + job-name: Sanity Tests + php-version: ${{ matrix.php-version }} + job-id: sanity-tests + group-name: Others + enable-artifact-upload: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-coverage: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-profiling: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + extra-extensions: imagick, redis, memcached + extra-composer-options: ${{ matrix.composer-option }} + + database-live-tests: + needs: + - coverage-php-version + - sanity-tests strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0', '8.1', '8.2'] - db-platforms: ['MySQLi', 'Postgre', 'SQLite3', 'SQLSRV', 'OCI8'] - mysql-versions: ['5.7'] + php-version: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + db-platform: + - MySQLi + - OCI8 + - Postgre + - SQLSRV + - SQLite3 + mysql-version: + - '5.7' include: - - php-versions: '7.4' - db-platforms: MySQLi - mysql-versions: '8.0' - - php-versions: '8.2' + - php-version: '7.4' + db-platform: MySQLi + mysql-version: '8.0' + - php-version: '8.2' composer-option: '--ignore-platform-req=php' - services: - mysql: - image: mysql:${{ matrix.mysql-versions }} - env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: test - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - postgres: - image: postgres - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: test - ports: - - 5432:5432 - options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 - - mssql: - image: mcr.microsoft.com/mssql/server:2022-latest - env: - SA_PASSWORD: 1Secure*Password1 - ACCEPT_EULA: Y - MSSQL_PID: Developer - ports: - - 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: gvenzl/oracle-xe:21 - env: - ORACLE_RANDOM_PASSWORD: true - APP_USER: ORACLE - APP_USER_PASSWORD: ORACLE - ports: - - 1521:1521 - options: >- - --health-cmd healthcheck.sh - --health-interval 20s - --health-timeout 10s - --health-retries 10 - - redis: - image: redis - ports: - - 6379:6379 - options: --health-cmd "redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - memcached: - image: memcached:1.6-alpine - ports: - - 11211:11211 + uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo + with: + job-name: Database Live Tests + php-version: ${{ matrix.php-version }} + job-id: database-live-tests + db-platform: ${{ matrix.db-platform }} + mysql-version: ${{ matrix.mysql-version }} + group-name: DatabaseLive + enable-artifact-upload: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-coverage: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-profiling: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + extra-extensions: mysqli, oci8, pgsql, sqlsrv-5.10.1, sqlite3 + extra-composer-options: ${{ matrix.composer-option }} + + separate-process-tests: + needs: + - coverage-php-version + - sanity-tests - steps: - - name: Create database for MSSQL Server - if: matrix.db-platforms == 'SQLSRV' - run: sqlcmd -S 127.0.0.1 -U sa -P 1Secure*Password1 -Q "CREATE DATABASE test" - - - name: Checkout - 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, oci8, pgsql - coverage: ${{ env.COVERAGE_DRIVER }} - env: - update: true - COVERAGE_DRIVER: ${{ matrix.php-versions == env.COVERAGE_PHP_VERSION && 'xdebug' || 'none'}} - - - name: Install latest ImageMagick - run: | - sudo apt-get update - sudo apt-get install --reinstall libgs9-common fonts-noto-mono libgs9:amd64 libijs-0.35:amd64 fonts-urw-base35 ghostscript poppler-data libjbig2dec0:amd64 gsfonts libopenjp2-7:amd64 fonts-droid-fallback fonts-dejavu-core - sudo apt-get install -y imagemagick - sudo apt-get install --fix-broken - - - name: Get composer cache directory - run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: | - composer update --ansi --no-interaction ${{ matrix.composer-option }} - composer remove --ansi --dev --unused ${{ matrix.composer-option }} -W -- rector/rector phpstan/phpstan friendsofphp/php-cs-fixer nexusphp/cs-config codeigniter/coding-standard - - - name: Profile slow tests in PHP ${{ env.COVERAGE_PHP_VERSION }} - if: matrix.php-versions == env.COVERAGE_PHP_VERSION - run: echo "TACHYCARDIA_MONITOR_GA=enabled" >> $GITHUB_ENV - - - name: Compute coverage option - uses: actions/github-script@v6 - id: phpunit-coverage-option - with: - script: | - const { COVERAGE_NAME } = process.env - - return "${{ matrix.php-versions }}" == "${{ env.COVERAGE_PHP_VERSION }}" ? `--coverage-php build/cov/coverage-${COVERAGE_NAME}.cov` : "--no-coverage" - result-encoding: string - env: - COVERAGE_NAME: php-v${{ env.COVERAGE_PHP_VERSION }}-${{ matrix.db-platforms }} - - - name: Test with PHPUnit - 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 - - - name: Upload coverage file - if: matrix.php-versions == env.COVERAGE_PHP_VERSION - uses: actions/upload-artifact@v3 - with: - name: ${{ env.COVERAGE_NAME }} - path: build/cov/coverage-${{ env.COVERAGE_NAME }}.cov - if-no-files-found: error - retention-days: 1 - env: - COVERAGE_NAME: php-v${{ env.COVERAGE_PHP_VERSION }}-${{ matrix.db-platforms }} + strategy: + matrix: + php-version: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + include: + - php-version: '8.2' + composer-option: '--ignore-platform-req=php' + + uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo + with: + job-name: Separate Process Tests + php-version: ${{ matrix.php-version }} + job-id: separate-process-tests + group-name: SeparateProcess + enable-artifact-upload: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-coverage: true # needs xdebug for assertHeaderEmitted() tests + enable-profiling: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + extra-extensions: mysqli, oci8, pgsql, sqlsrv-5.10.1, sqlite3 + extra-composer-options: ${{ matrix.composer-option }} + + cache-live-tests: + needs: + - coverage-php-version + - sanity-tests + + strategy: + matrix: + php-version: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + include: + - php-version: '8.2' + composer-option: '--ignore-platform-req=php' + + uses: ./.github/workflows/reusable-phpunit-test.yml # @TODO Extract to codeigniter4/.github repo + with: + job-name: Cache Live Tests + php-version: ${{ matrix.php-version }} + job-id: cache-live-tests + group-name: CacheLive + enable-artifact-upload: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-coverage: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + enable-profiling: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }} + extra-extensions: redis, memcached + extra-composer-options: ${{ matrix.composer-option }} coveralls: + name: Upload coverage results to Coveralls if: github.repository_owner == 'codeigniter4' - needs: tests - runs-on: ubuntu-22.04 - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ env.COVERAGE_PHP_VERSION }} - tools: composer - coverage: xdebug - env: - update: true - - - name: Download coverage files - uses: actions/download-artifact@v3 - with: - path: build/cov - - - name: Display structure of downloaded files - run: ls -R - working-directory: build/cov - - - name: Get composer cache directory - run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer update --ansi --no-interaction - - - name: Merge coverage files - run: vendor/bin/phpcov merge --clover build/logs/clover.xml build/cov - - - name: Upload coverage to Coveralls - run: vendor/bin/php-coveralls --verbose --exclude-no-stmt --ansi - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + needs: + - coverage-php-version + - sanity-tests + - cache-live-tests + - database-live-tests + - separate-process-tests + + uses: ./.github/workflows/reusable-coveralls.yml # @TODO Extract to codeigniter4/.github repo + with: + php-version: ${{ needs.coverage-php-version.outputs.version }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 89605c9f92d2..6a857588f64f 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -38,11 +38,7 @@ __DIR__ . '/spark', ]); -$overrides = [ - 'no_useless_concat_operator' => [ - 'juggle_simple_strings' => true, - ], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', diff --git a/.php-cs-fixer.no-header.php b/.php-cs-fixer.no-header.php index 1425b637706c..cd10717aa5a2 100644 --- a/.php-cs-fixer.no-header.php +++ b/.php-cs-fixer.no-header.php @@ -29,11 +29,7 @@ __DIR__ . '/admin/starter/builds', ]); -$overrides = [ - 'no_useless_concat_operator' => [ - 'juggle_simple_strings' => true, - ], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.no-header.cache', diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php index 7bb88ccc210a..1fa41bc4fb42 100644 --- a/.php-cs-fixer.user-guide.php +++ b/.php-cs-fixer.user-guide.php @@ -33,9 +33,6 @@ 'php_unit_internal_class' => false, 'no_unused_imports' => false, 'class_attributes_separation' => false, - 'no_useless_concat_operator' => [ - 'juggle_simple_strings' => true, - ], ]; $options = [ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3364aa3d2bc..18f45d61580c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## [v4.2.11](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.10) (2022-12-21) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.2.10...v4.2.11) + +### Fixed Bugs +* fix: Request::getIPAddress() by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6820 +* fix: Model cannot insert when $useAutoIncrement is false by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6827 +* fix: View Parser regexp does not support UTF-8 by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6835 +* Handle key generation when key is not present in .env by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6839 +* Fix: Controller Test withBody() by @MGatner in https://github.com/codeigniter4/CodeIgniter4/pull/6843 +* fix: body assigned via options array in CURLRequest class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/6854 +* Fix CreateDatabase leaving altered database config in connection by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6856 +* fix: cast to string all values except arrays in Header class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/6862 +* add missing @method Query grouping in Model by @paul45 in https://github.com/codeigniter4/CodeIgniter4/pull/6874 +* fix: `composer update` might cause error "Failed to open directory" by @LeMyst in https://github.com/codeigniter4/CodeIgniter4/pull/6833 +* fix: required PHP extentions by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6897 +* fix: Use Services for the FeatureTestTrait request. by @lonnieezell in https://github.com/codeigniter4/CodeIgniter4/pull/6966 +* fix: FileLocator::locateFile() bug with a similar namespace name by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/6964 +* fix: socket connection in RedisHandler class by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/6972 +* fix: `spark namespaces` cannot show a namespace with mutilple paths by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6977 +* fix: Undefined constant "CodeIgniter\Debug\VENDORPATH" by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6985 +* fix: large HTTP input crashes framework by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6984 +* fix: empty paths for `rewrite.php` by @datamweb in https://github.com/codeigniter4/CodeIgniter4/pull/6991 +* fix: `PHPStan` $cols not defined in `CLI` by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/6994 +* Fix MigrationRunnerTest for Windows by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6855 +* fix: turn off `Xdebug` note when running phpstan by @ddevsr in https://github.com/codeigniter4/CodeIgniter4/pull/6851 +* Fix ShowTableInfoTest to pass on Windows by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6853 +* Fix MigrateStatusTest for Windows by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6866 +* Fix ShowTableInfoTest when migration records are numerous by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6868 +* Fix CreateDatabaseTest to not leave database by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6867 +* Fix coverage merge warning by @paulbalandan in https://github.com/codeigniter4/CodeIgniter4/pull/6885 +* fix: replace tabs to spaces by @zl59503020 in https://github.com/codeigniter4/CodeIgniter4/pull/6898 +* fix: slack links by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6907 +* Fix typo in database/queries.rst by @philFernandez in https://github.com/codeigniter4/CodeIgniter4/pull/6920 +* Fix testInsertWithSetAndEscape to make not time dependent by @sclubricants in https://github.com/codeigniter4/CodeIgniter4/pull/6974 +* fix: remove unnecessary global variables in rewrite.php by @kenjis in https://github.com/codeigniter4/CodeIgniter4/pull/6973 + ## [v4.2.10](https://github.com/codeigniter4/CodeIgniter4/tree/v4.2.10) (2022-11-05) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.2.9...v4.2.10) diff --git a/README.md b/README.md index d8525f299037..174f19692fbd 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ ## What is CodeIgniter? CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. -More information can be found at the [official site](http://codeigniter.com). +More information can be found at the [official site](https://codeigniter.com). This repository holds the source code for CodeIgniter 4 only. Version 4 is a complete rewrite to bring the quality and the code into a more modern version, while still keeping as many of the things intact that has made people love the framework over the years. -More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. ### Documentation @@ -69,12 +69,12 @@ to optional packages, with their own repository. ## Contributing -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. +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. +CodeIgniter has had thousands on contributions from people since its creation. This project would not be what it is without them. @@ -86,16 +86,14 @@ Made with [contrib.rocks](https://contrib.rocks). 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 - [mbstring](http://php.net/manual/en/mbstring.installation.php) Additionally, make sure that the following extensions are enabled in your PHP: - json (enabled by default - don't turn it off) -- xml (enabled by default - don't turn it off) -- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library ## Running CodeIgniter Tests diff --git a/admin/RELEASE.md b/admin/RELEASE.md index d37868bf1d5b..e5ab1b4d70ba 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -2,7 +2,7 @@ > Documentation guide based on the releases of `4.0.5` and `4.1.0` on January 31, 2021. > -> Updated for `4.2.3` on August 6, 2022. +> Updated for `4.2.10` on November 5, 2022. > > -MGatner @@ -17,7 +17,7 @@ When generating the changelog each Pull Request to be included must have one of PRs with breaking changes must have the following additional label: - **breaking change** ... PRs that may break existing functionalities -To auto-generate, start drafting a new Release and use the "Auto-generate release notes" button. +To auto-generate, start drafting a new Release and use the "Generate release notes" button. Copy the resulting content into **CHANGELOG.md** and adjust the format to match the existing content. ## Preparation @@ -43,6 +43,7 @@ git clone git@github.com:codeigniter4/userguide.git * 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** * Create **user_guide_src/source/installation/upgrade_{ver}.rst**, fill in the "All Changes" section, and add it to **upgrading.rst** + * git diff --name-status master -- . ':!system' * 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" @@ -53,12 +54,16 @@ git clone git@github.com:codeigniter4/userguide.git * Description: blank * Merge the PR then create a new Release: * Version: "v4.x.x" + * Target: master * Title: "CodeIgniter 4.x.x" * Description: ``` CodeIgniter 4.x.x release. See the changelog: https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md + +## New Contributors +* ``` * Watch for the "Deploy Distributable Repos" action to make sure **framework**, **appstarter**, and **userguide** get updated * Run the following commands to install and test `appstarter` and verify the new version: @@ -70,8 +75,15 @@ composer test && composer info codeigniter4/framework * Verify that the user guide actions succeeded: * "Deploy Distributable Repos", framework repo * "Deploy Production", UG repo - * "pages-build-deployment", both repos + * "pages-build-deployment", UG repo * Fast-forward `develop` branch to catch the merge commit from `master` +```console +git fetch origin +git checkout develop +git merge origin/develop +git merge origin/master +git push origin HEAD +``` * Update the next minor upgrade branch `4.x`: ```console git fetch origin diff --git a/admin/framework/README.md b/admin/framework/README.md index ebc758ed530e..870e5f96adff 100644 --- a/admin/framework/README.md +++ b/admin/framework/README.md @@ -3,18 +3,17 @@ ## What is CodeIgniter? CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. -More information can be found at the [official site](http://codeigniter.com). +More information can be found at the [official site](https://codeigniter.com). -This repository holds the distributable version of the framework, -including the user guide. It has been built from the +This repository holds the distributable version of the framework. +It has been built from the [development repository](https://github.com/codeigniter4/CodeIgniter4). -More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. -The user guide corresponding to this version of the framework can be found +The user guide corresponding to the latest version of the framework can be found [here](https://codeigniter4.github.io/userguide/). - ## Important Change with index.php `index.php` is no longer in the root of the project! It has been moved inside the *public* folder, @@ -46,11 +45,10 @@ Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/ 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 +- [mbstring](http://php.net/manual/en/mbstring.installation.php) Additionally, make sure that the following extensions are enabled in your PHP: - json (enabled by default - don't turn it off) -- [mbstring](http://php.net/manual/en/mbstring.installation.php) -- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) -- xml (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [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 fe66bb2934e4..9fa939445e53 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -6,7 +6,6 @@ "license": "MIT", "require": { "php": "^7.4 || ^8.0", - "ext-curl": "*", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -17,13 +16,14 @@ "require-dev": { "codeigniter/coding-standard": "^1.5", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "~3.13.0", + "friendsofphp/php-cs-fixer": "3.13.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0" }, "suggest": { + "ext-curl": "If you use CURLRequest class", "ext-imagick": "If you use Image class ImageMagickHandler", "ext-gd": "If you use Image class GDHandler", "ext-exif": "If you run Image class tests", @@ -57,7 +57,7 @@ "test": "phpunit" }, "support": { - "forum": "http://forum.codeigniter.com/", + "forum": "https://forum.codeigniter.com/", "source": "https://github.com/codeigniter4/CodeIgniter4", "slack": "https://codeigniterchat.slack.com" } diff --git a/admin/next-changelog-minor.rst b/admin/next-changelog-minor.rst new file mode 100644 index 000000000000..310dffdeafa2 --- /dev/null +++ b/admin/next-changelog-minor.rst @@ -0,0 +1,74 @@ +Version {version} +################# + +Release Date: Unreleased + +**{version} release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +Highlights +********** + +- TBD + +BREAKING +******** + +Behavior Changes +================ + +Interface Changes +================= + +Method Signature Changes +======================== + +Enhancements +************ + +Commands +======== + +Testing +======= + +Database +======== + +Query Builder +------------- + +Forge +----- + +Others +------ + +Model +===== + +Libraries +========= + +Helpers and Functions +===================== + +Others +====== + +Message Changes +*************** + +Changes +******* + +Deprecations +************ + +Bugs Fixed +********** + +See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/admin/next-changelog-patch.rst b/admin/next-changelog-patch.rst new file mode 100644 index 000000000000..2761f6b90434 --- /dev/null +++ b/admin/next-changelog-patch.rst @@ -0,0 +1,30 @@ +Version {version} +################# + +Release Date: Unreleased + +**{version} release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +BREAKING +******** + +Enhancements +************ + +Message Changes +*************** + +Changes +******* + +Deprecations +************ + +Bugs Fixed +********** + +See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/admin/next.rst b/admin/next.rst deleted file mode 100644 index 94e4dcad7f4c..000000000000 --- a/admin/next.rst +++ /dev/null @@ -1,14 +0,0 @@ -Version |version| -==================================================== - -Release Date: Not released - -**Next alpha release of CodeIgniter4** - - -The list of changed files follows, with PR numbers shown. - - -PRs merged: ------------ - diff --git a/admin/pre-commit b/admin/pre-commit index 1fa6b15e5984..ef13c4c7a43a 100644 --- a/admin/pre-commit +++ b/admin/pre-commit @@ -25,9 +25,9 @@ if [ "$FILES" != "" ]; then # Run on whole codebase if [ -d /proc/cygdrive ]; then - ./vendor/bin/phpstan analyse + XDEBUG_MODE=off ./vendor/bin/phpstan analyse else - php ./vendor/bin/phpstan analyse + XDEBUG_MODE=off php ./vendor/bin/phpstan analyse fi if [ $? != 0 ]; then diff --git a/admin/starter/README.md b/admin/starter/README.md index 2e17d9c98888..461e949f2f26 100644 --- a/admin/starter/README.md +++ b/admin/starter/README.md @@ -3,15 +3,15 @@ ## What is CodeIgniter? CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. -More information can be found at the [official site](http://codeigniter.com). +More information can be found at the [official site](https://codeigniter.com). This repository holds a composer-installable app starter. It has been built from the [development repository](https://github.com/codeigniter4/CodeIgniter4). -More information about the plans for version 4 can be found in [the announcement](http://forum.codeigniter.com/thread-62615.html) on the forums. +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. -The user guide corresponding to this version of the framework can be found +The user guide corresponding to the latest version of the framework can be found [here](https://codeigniter4.github.io/userguide/). ## Installation & updates @@ -53,11 +53,10 @@ Problems with it can be raised on our forum, or as issues in the main repository 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 +- [mbstring](http://php.net/manual/en/mbstring.installation.php) Additionally, make sure that the following extensions are enabled in your PHP: - json (enabled by default - don't turn it off) -- [mbstring](http://php.net/manual/en/mbstring.installation.php) -- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) -- xml (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/admin/starter/composer.json b/admin/starter/composer.json index 026329ca9a24..e52218478b04 100644 --- a/admin/starter/composer.json +++ b/admin/starter/composer.json @@ -30,7 +30,7 @@ "test": "phpunit" }, "support": { - "forum": "http://forum.codeigniter.com/", + "forum": "https://forum.codeigniter.com/", "source": "https://github.com/codeigniter4/CodeIgniter4", "slack": "https://codeigniterchat.slack.com" } diff --git a/admin/starter/tests/database/ExampleDatabaseTest.php b/admin/starter/tests/database/ExampleDatabaseTest.php index f9edc4d235d8..400fd2413846 100644 --- a/admin/starter/tests/database/ExampleDatabaseTest.php +++ b/admin/starter/tests/database/ExampleDatabaseTest.php @@ -31,6 +31,7 @@ public function testSoftDeleteLeavesRow() $this->setPrivateProperty($model, 'useSoftDeletes', true); $this->setPrivateProperty($model, 'tempUseSoftDeletes', true); + /** @var stdClass $object */ $object = $model->first(); $model->delete($object->id); diff --git a/admin/userguide/composer.json b/admin/userguide/composer.json index 61428fa50ff1..007664b9b2ec 100644 --- a/admin/userguide/composer.json +++ b/admin/userguide/composer.json @@ -9,7 +9,7 @@ "codeigniter4/framework": "^4" }, "support": { - "forum": "http://forum.codeigniter.com/", + "forum": "https://forum.codeigniter.com/", "source": "https://github.com/codeigniter4/CodeIgniter4", "slack": "https://codeigniterchat.slack.com" } diff --git a/app/Config/App.php b/app/Config/App.php index 79e5741b4c9c..c6eec07a6e3a 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -332,18 +332,21 @@ class App extends BaseConfig * * If your server is behind a reverse proxy, you must whitelist the proxy * IP addresses from which CodeIgniter should trust headers such as - * HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify + * X-Forwarded-For or Client-IP in order to properly identify * the visitor's IP address. * - * You can use both an array or a comma-separated list of proxy addresses, - * as well as specifying whole subnets. Here are a few examples: + * You need to set a proxy IP address or IP address with subnets and + * the HTTP header for the client IP address. * - * Comma-separated: '10.0.1.200,192.168.5.0/24' - * Array: ['10.0.1.200', '192.168.5.0/24'] + * Here are some examples: + * [ + * '10.0.1.200' => 'X-Forwarded-For', + * '192.168.5.0/24' => 'X-Real-IP', + * ] * - * @var string|string[] + * @var array */ - public $proxyIPs = ''; + public $proxyIPs = []; /** * -------------------------------------------------------------------------- diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index ee27e3b2ce18..61721a0f713e 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -34,11 +34,12 @@ class Autoload extends AutoloadConfig *``` * $psr4 = [ * 'CodeIgniter' => SYSTEMPATH, - * 'App' => APPPATH + * 'App' => APPPATH * ]; *``` * - * @var array + * @var array|string> + * @phpstan-var array> */ public $psr4 = [ APP_NAMESPACE => APPPATH, // For custom app namespace @@ -76,8 +77,8 @@ class Autoload extends AutoloadConfig * * Prototype: * ``` - * $files = [ - * '/path/to/my/file.php', + * $files = [ + * '/path/to/my/file.php', * ]; * ``` * diff --git a/app/Config/Logger.php b/app/Config/Logger.php index 743815032280..1dfeb339a890 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -141,14 +141,14 @@ class Logger extends BaseConfig * Uncomment this block to use it. */ // 'CodeIgniter\Log\Handlers\ErrorlogHandler' => [ - // /* The log levels this handler can handle. */ - // 'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'], + // /* The log levels this handler can handle. */ + // 'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'], // - // /* - // * The message type where the error should go. Can be 0 or 4, or use the - // * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4) - // */ - // 'messageType' => 0, + // /* + // * The message type where the error should go. Can be 0 or 4, or use the + // * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4) + // */ + // 'messageType' => 0, // ], ]; } diff --git a/app/Config/Toolbar.php b/app/Config/Toolbar.php index 7183e1336a28..2860f74d3d9c 100644 --- a/app/Config/Toolbar.php +++ b/app/Config/Toolbar.php @@ -49,7 +49,7 @@ class Toolbar extends BaseConfig * Collect Var Data * -------------------------------------------------------------------------- * - * If set to false var data from the views will not be colleted. Usefull to + * If set to false var data from the views will not be colleted. Useful to * avoid high memory usage when there are lots of data passed to the view. * * @var bool diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php index c5325b57a813..57489116f490 100644 --- a/app/Views/welcome_message.php +++ b/app/Views/welcome_message.php @@ -270,7 +270,7 @@

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 CodeIgniter's forum, or chat on Slack !

diff --git a/composer.json b/composer.json index dccc0379924f..3fb81f249591 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,6 @@ "license": "MIT", "require": { "php": "^7.4 || ^8.0", - "ext-curl": "*", "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", @@ -17,7 +16,7 @@ "require-dev": { "codeigniter/coding-standard": "^1.5", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "~3.13.0", + "friendsofphp/php-cs-fixer": "3.13.0", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.6", "nexusphp/tachycardia": "^1.0", @@ -26,9 +25,10 @@ "phpunit/phpcov": "^8.2", "phpunit/phpunit": "^9.1", "predis/predis": "^1.1 || ^2.0", - "rector/rector": "0.14.6" + "rector/rector": "0.15.1" }, "suggest": { + "ext-curl": "If you use CURLRequest class", "ext-imagick": "If you use Image class ImageMagickHandler", "ext-gd": "If you use Image class GDHandler", "ext-exif": "If you run Image class tests", @@ -68,7 +68,6 @@ "autoload-dev": { "psr-4": { "CodeIgniter\\": "tests/system/", - "CodeIgniter\\AutoReview\\": "tests/AutoReview/", "Utils\\": "utils/" } }, @@ -78,7 +77,7 @@ "bash -c \"if [ -f admin/setup.sh ]; then bash admin/setup.sh; fi\"" ], "analyze": [ - "phpstan analyze", + "bash -c \"XDEBUG_MODE=off phpstan analyse\"", "rector process --dry-run" ], "sa": "@analyze", @@ -102,7 +101,7 @@ "cs-fix": "Fix the coding style" }, "support": { - "forum": "http://forum.codeigniter.com/", + "forum": "https://forum.codeigniter.com/", "source": "https://github.com/codeigniter4/CodeIgniter4", "slack": "https://codeigniterchat.slack.com" } diff --git a/contributing/bug_report.md b/contributing/bug_report.md index 43767cdd1766..648e8da56dc0 100644 --- a/contributing/bug_report.md +++ b/contributing/bug_report.md @@ -20,7 +20,7 @@ Please note that GitHub is not for general support questions! If you are having trouble using a feature, you can: - Start a new thread on our [Forums](http://forum.codeigniter.com/) -- Ask your questions on [Slack](https://codeigniterchat.slack.com/) +- Ask your questions on [Slack](https://join.slack.com/t/codeigniterchat/shared_invite/zt-rl30zw00-obL1Hr1q1ATvkzVkFp8S0Q) If you are not sure whether you are using something correctly or if you have found a bug, again - please ask on the forums first. diff --git a/contributing/signing.md b/contributing/signing.md index d312d3fc217f..e925327e9b71 100644 --- a/contributing/signing.md +++ b/contributing/signing.md @@ -33,13 +33,13 @@ To verify your commits, you will need to setup a GPG key, and attach it to your GitHub account. See the [git tools](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) page -for directions on doing this. The complete story is part of [GitHub help](https://help.github.com/categories/gpg/). +for directions on doing this. The complete story is part of [GitHub help](https://support.github.com/?q=GPG). The basic steps are - [generate your GPG key](https://help.github.com/articles/generating-a-new-gpg-key/), and copy the ASCII representation of it. -- [Add your GPG key to your GitHub account](https://help.github.com/articles/adding-a-new-gpg-key-to-your-github-account/). +- [Add your GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account). - [Tell Git](https://help.github.com/articles/telling-git-about-your-gpg-key/) about your GPG key. - [Set default signing](https://help.github.com/articles/signing-commits-using-gpg/) diff --git a/deptrac.yaml b/deptrac.yaml index 8d43af6117ae..bf39d4f54734 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -155,16 +155,24 @@ parameters: API: - Format - HTTP + Cache: + - I18n Controller: - HTTP - Validation + Cookie: + - I18n Database: - Entity - Events + - I18n Email: + - I18n - Events Entity: - I18n + Files: + - I18n Filters: - HTTP Honeypot: @@ -173,10 +181,12 @@ parameters: HTTP: - Cookie - Files + - I18n - Security - URI Images: - Files + - I18n Model: - Database - I18n @@ -195,14 +205,17 @@ parameters: - HTTP Security: - Cookie + - I18n - Session - HTTP Session: - Cookie - HTTP - Database + - I18n Throttle: - Cache + - I18n Validation: - HTTP View: diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index 7eb120306dd2..5a0f0459e66a 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -10,21 +10,6 @@ parameters: count: 1 path: system/Autoloader/Autoloader.php - - - message: "#^Property Config\\\\Autoload\\:\\:\\$classmap \\(array\\\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Autoloader/Autoloader.php - - - - message: "#^Property Config\\\\Autoload\\:\\:\\$files \\(array\\\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Autoloader/Autoloader.php - - - - message: "#^Property Config\\\\Autoload\\:\\:\\$psr4 \\(array\\\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: system/Autoloader/Autoloader.php - - message: "#^Property Config\\\\Cache\\:\\:\\$backupHandler \\(string\\) in isset\\(\\) is not nullable\\.$#" count: 1 @@ -41,7 +26,7 @@ parameters: path: system/Cache/CacheFactory.php - - message: "#^Comparison operation \"\\>\" between int\\<1, max\\> and \\(array\\|float\\|int\\) results in an error\\.$#" + message: "#^Comparison operation \"\\>\" between int and \\(array\\|float\\|int\\) results in an error\\.$#" count: 1 path: system/Cache/Handlers/FileHandler.php @@ -515,11 +500,6 @@ parameters: count: 1 path: system/HTTP/RedirectResponse.php - - - message: "#^Property CodeIgniter\\\\HTTP\\\\Request\\:\\:\\$proxyIPs \\(array\\|string\\) on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: system/HTTP/Request.php - - message: "#^Property CodeIgniter\\\\HTTP\\\\Request\\:\\:\\$uri \\(CodeIgniter\\\\HTTP\\\\URI\\) in empty\\(\\) is not falsy\\.$#" count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 741cd106e080..251a90b946eb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,27 +7,30 @@ beStrictAboutTodoAnnotatedTests="true" cacheResultFile="build/.phpunit.cache/test-results" colors="true" + columns="max" failOnRisky="true" failOnWarning="true" verbose="true"> - + - ./system + system - ./system/Commands/Generators/Views - ./system/Debug/Toolbar/Views - ./system/Pager/Views - ./system/ThirdParty - ./system/Validation/Views - ./system/bootstrap.php - ./system/ComposerScripts.php - ./system/Config/Routes.php - ./system/Test/bootstrap.php - ./system/Test/ControllerTester.php - ./system/Test/FeatureTestCase.php + system/Commands/Generators/Views + system/Debug/Toolbar/Views + system/Pager/Views + system/ThirdParty + system/Validation/Views + system/bootstrap.php + system/ComposerScripts.php + system/Config/Routes.php + system/Test/bootstrap.php + system/Test/ControllerTester.php + system/Test/FeatureTestCase.php @@ -36,15 +39,8 @@ - - ./tests/AutoReview - - ./tests/system - ./tests/system/Database - - - ./tests/system/Database + tests/system diff --git a/admin/starter/preload.php b/preload.php similarity index 96% rename from admin/starter/preload.php rename to preload.php index ae935b0b9de2..63c781c220c5 100644 --- a/admin/starter/preload.php +++ b/preload.php @@ -16,7 +16,8 @@ * See https://www.php.net/manual/en/opcache.preloading.php * * How to Use: - * 1. Set preload::$paths. + * 0. Copy this file to your project root folder. + * 1. Set the $paths property of the preload class below. * 2. Set opcache.preload in php.ini. * php.ini: * opcache.preload=/path/to/preload.php diff --git a/rector.php b/rector.php index 901917424250..bb4da9d59033 100644 --- a/rector.php +++ b/rector.php @@ -60,7 +60,7 @@ PHPUnitSetList::REMOVE_MOCKS, ]); - $rectorConfig->disableParallel(); + $rectorConfig->parallel(240, 8, 1); // paths to refactor; solid alternative to CLI arguments $rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/system', __DIR__ . '/tests', __DIR__ . '/utils']); @@ -97,15 +97,6 @@ __DIR__ . '/system/Session/Handlers', ], - 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', - ], - // sometime too detail CountOnNullRector::class, diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 93dd976eaefb..107bc5d56803 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -66,7 +66,7 @@ trait ResponseTrait /** * How to format the response data. * Either 'json' or 'xml'. If blank will be - * determine through content negotiation. + * determined through content negotiation. * * @var string */ diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index f87e4485c6a7..e901ab1dd7c1 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -88,19 +88,19 @@ public function initialize(Autoload $config, Modules $modules) // 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)) { + if ($config->psr4 === [] && $config->classmap === []) { throw new InvalidArgumentException('Config array must contain either the \'psr4\' key or the \'classmap\' key.'); } - if (isset($config->psr4)) { + if ($config->psr4 !== []) { $this->addNamespace($config->psr4); } - if (isset($config->classmap)) { + if ($config->classmap !== []) { $this->classmap = $config->classmap; } - if (isset($config->files)) { + if ($config->files !== []) { $this->files = $config->files; } @@ -148,7 +148,8 @@ public function register() /** * Registers namespaces with the autoloader. * - * @param array|string $namespace + * @param array|string>|string $namespace + * @phpstan-param array|string>|string $namespace * * @return $this */ diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 14d7982c49b4..f58d5c791d97 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -71,13 +71,13 @@ public function locateFile(string $file, ?string $folder = null, string $ext = ' $namespaces = $this->autoloader->getNamespace(); foreach (array_keys($namespaces) as $namespace) { - if (substr($file, 0, strlen($namespace)) === $namespace) { + if (substr($file, 0, strlen($namespace) + 1) === $namespace . '\\') { + $fileWithoutNamespace = substr($file, strlen($namespace)); + // There may be sub-namespaces of the same vendor, // so overwrite them with namespaces found later. - $paths = $namespaces[$namespace]; - - $fileWithoutNamespace = substr($file, strlen($namespace)); - $filename = ltrim(str_replace('\\', '/', $fileWithoutNamespace), '/'); + $paths = $namespaces[$namespace]; + $filename = ltrim(str_replace('\\', '/', $fileWithoutNamespace), '/'); } } diff --git a/system/BaseModel.php b/system/BaseModel.php index 888d89854f32..a41be9816cac 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -16,6 +16,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Database\Query; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\I18n\Time; use CodeIgniter\Pager\Pager; @@ -311,8 +312,8 @@ protected function initialize() } /** - * Fetches the row of database - * This methods works only with dbCalls + * Fetches the row of database. + * This method works only with dbCalls. * * @param bool $singleton Single or multiple results * @param array|int|string|null $id One primary key or an array of primary keys @@ -322,8 +323,8 @@ protected function initialize() abstract protected function doFind(bool $singleton, $id = null); /** - * Fetches the column of database - * This methods works only with dbCalls + * Fetches the column of database. + * This method works only with dbCalls. * * @param string $columnName Column Name * @@ -335,7 +336,7 @@ abstract protected function doFindColumn(string $columnName); /** * Fetches all results, while optionally limiting them. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @param int $limit Limit * @param int $offset Offset @@ -346,15 +347,15 @@ abstract protected function doFindAll(int $limit = 0, int $offset = 0); /** * Returns the first row of the result set. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @return array|object|null */ abstract protected function doFirst(); /** - * Inserts data into the current database - * This method works only with dbCalls + * Inserts data into the current database. + * This method works only with dbCalls. * * @param array $data Data * @@ -364,7 +365,7 @@ abstract protected function doInsert(array $data); /** * Compiles batch insert and runs the queries, validating each row prior. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @param array|null $set An associative array of insert values * @param bool|null $escape Whether to escape values @@ -377,7 +378,7 @@ abstract protected function doInsertBatch(?array $set = null, ?bool $escape = nu /** * Updates a single record in the database. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @param array|int|string|null $id ID * @param array|null $data Data @@ -385,23 +386,23 @@ abstract protected function doInsertBatch(?array $set = null, ?bool $escape = nu abstract protected function doUpdate($id = null, $data = null): bool; /** - * Compiles an update and runs the query - * This methods works only with dbCalls + * Compiles an update and runs the query. + * This method works only with dbCalls. * * @param array|null $set An associative array of update values * @param string|null $index The where key * @param int $batchSize The size of the batch to run * @param bool $returnSQL True means SQL is returned, false will execute the query * - * @return mixed Number of rows affected or FALSE on failure + * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode * * @throws DatabaseException */ abstract protected function doUpdateBatch(?array $set = null, ?string $index = null, int $batchSize = 100, bool $returnSQL = false); /** - * Deletes a single record from the database where $id matches - * This methods works only with dbCalls + * Deletes a single record from the database where $id matches. + * This method works only with dbCalls. * * @param array|int|string|null $id The rows primary key(s) * @param bool $purge Allows overriding the soft deletes setting. @@ -413,9 +414,9 @@ abstract protected function doUpdateBatch(?array $set = null, ?string $index = n abstract protected function doDelete($id = null, bool $purge = false); /** - * Permanently deletes all rows that have been marked as deleted - * through soft deletes (deleted = 1) - * This methods works only with dbCalls + * Permanently deletes all rows that have been marked as deleted. + * through soft deletes (deleted = 1). + * This method works only with dbCalls. * * @return bool|string Returns a string if in test mode. */ @@ -424,31 +425,31 @@ abstract protected function doPurgeDeleted(); /** * Works with the find* methods to return only the rows that * have been deleted. - * This methods works only with dbCalls + * This method works only with dbCalls. */ abstract protected function doOnlyDeleted(); /** - * Compiles a replace and runs the query - * This methods works only with dbCalls + * Compiles a replace and runs the query. + * This method works only with dbCalls. * * @param array|null $data Data * @param bool $returnSQL Set to true to return Query String * - * @return mixed + * @return BaseResult|false|Query|string */ abstract protected function doReplace(?array $data = null, bool $returnSQL = false); /** * Grabs the last error(s) that occurred from the Database connection. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @return array|null */ abstract protected function doErrors(); /** - * Returns the id value for the data array or object + * Returns the id value for the data array or object. * * @param array|object $data Data * @@ -459,8 +460,8 @@ abstract protected function doErrors(); abstract protected function idValue($data); /** - * Public getter to return the id value using the idValue() method - * For example with SQL this will return $data->$this->primaryKey + * Public getter to return the id value using the idValue() method. + * For example with SQL this will return $data->$this->primaryKey. * * @param array|object $data * @@ -475,18 +476,18 @@ public function getIdValue($data) /** * Override countAllResults to account for soft deleted accounts. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @param bool $reset Reset * @param bool $test Test * - * @return mixed + * @return int|string */ abstract public function countAllResults(bool $reset = true, bool $test = false); /** * Loops over records in batches, allowing you to operate on them. - * This methods works only with dbCalls + * This method works only with dbCalls. * * @param int $size Size * @param Closure $userFunc Callback Function @@ -496,7 +497,7 @@ abstract public function countAllResults(bool $reset = true, bool $test = false) abstract public function chunk(int $size, Closure $userFunc); /** - * Fetches the row of database + * Fetches the row of database. * * @param array|int|string|null $id One primary key or an array of primary keys * @@ -538,7 +539,7 @@ public function find($id = null) } /** - * Fetches the column of database + * Fetches the column of database. * * @param string $columnName Column Name * @@ -667,8 +668,8 @@ public function save($data): bool } /** - * This method is called on save to determine if entry have to be updated - * If this method return false insert operation will be executed + * This method is called on save to determine if entry have to be updated. + * If this method returns false insert operation will be executed * * @param array|object $data Data */ @@ -719,7 +720,7 @@ public function insert($data = null, bool $returnID = true) // Restore $cleanValidationRules $this->cleanValidationRules = $cleanValidationRules; - // Must be called first so we don't + // Must be called first, so we don't // strip out created_at values. $data = $this->doProtectFields($data); @@ -896,14 +897,14 @@ public function update($id = null, $data = null): bool } /** - * Compiles an update and runs the query + * Compiles an update and runs the query. * * @param array|null $set An associative array of update values * @param string|null $index The where key * @param int $batchSize The size of the batch to run * @param bool $returnSQL True means SQL is returned, false will execute the query * - * @return mixed Number of rows affected or FALSE on failure + * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode * * @throws DatabaseException * @throws ReflectionException @@ -953,7 +954,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc } /** - * Deletes a single record from the database where $id matches + * Deletes a single record from the database where $id matches. * * @param array|int|string|null $id The rows primary key(s) * @param bool $purge Allows overriding the soft deletes setting. @@ -995,9 +996,9 @@ public function delete($id = null, bool $purge = false) /** * Permanently deletes all rows that have been marked as deleted - * through soft deletes (deleted = 1) + * through soft deletes (deleted = 1). * - * @return mixed + * @return bool|string Returns a string if in test mode. */ public function purgeDeleted() { @@ -1038,12 +1039,12 @@ public function onlyDeleted() } /** - * Compiles a replace and runs the query + * Compiles a replace and runs the query. * * @param array|null $data Data * @param bool $returnSQL Set to true to return Query String * - * @return mixed + * @return BaseResult|false|Query|string */ public function replace(?array $data = null, bool $returnSQL = false) { @@ -1063,6 +1064,7 @@ public function replace(?array $data = null, bool $returnSQL = false) * Grabs the last error(s) that occurred. If data was validated, * it will first check for errors there, otherwise will try to * grab the last error from the Database connection. + * * The return array should be in the following format: * ['source' => 'message'] * @@ -1170,17 +1172,17 @@ protected function doProtectFields(array $data): array } /** - * Sets the date or current date if null value is passed + * Sets the date or current date if null value is passed. * * @param int|null $userData An optional PHP timestamp to be converted. * - * @return mixed + * @return int|string * * @throws ModelException */ protected function setDate(?int $userData = null) { - $currentDate = $userData ?? time(); + $currentDate = $userData ?? Time::now()->getTimestamp(); return $this->intToDate($currentDate); } @@ -1220,7 +1222,7 @@ protected function intToDate(int $value) } /** - * Converts Time value to string using $this->dateFormat + * Converts Time value to string using $this->dateFormat. * * The available time formats are: * - 'int' - Stores the date as an integer timestamp @@ -1397,7 +1399,7 @@ public function getValidationRules(array $options = []): array } /** - * Returns the model's define validation messages so they + * Returns the model's validation messages, so they * can be used elsewhere, if needed. */ public function getValidationMessages(): array @@ -1454,14 +1456,14 @@ public function allowCallbacks(bool $val = true) * * Each $eventData array MUST have a 'data' key with the relevant * data for callback methods (like an array of key/value pairs to insert - * or update, an array of results, etc) + * or update, an array of results, etc.) * * If callbacks are not allowed then returns $eventData immediately. * * @param string $event Event * @param array $eventData Event Data * - * @return mixed + * @return array * * @throws DataException */ @@ -1513,14 +1515,14 @@ public function asObject(string $class = 'object') } /** - * Takes a class and 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 suitable for use in creates and updates. * This method uses objectToRawArray() internally and does conversion * to string on all Time instances * * @param object|string $data Data * @param bool $onlyChanged Only Changed Property - * @param bool $recursive If true, inner entities will be casted as array as well + * @param bool $recursive If true, inner entities will be cast as array as well * * @return array Array * @@ -1579,7 +1581,7 @@ protected function objectToRawArray($data, bool $onlyChanged = true, bool $recur } /** - * Transform data to array + * Transform data to array. * * @param array|object|null $data Data * @param string $type Type of data (insert|update) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index f20a08a103ff..ff1d32995a0b 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -965,6 +965,7 @@ public static function table(array $tbody, array $thead = []) } $table = ''; + $cols = ''; // Joins columns and append the well formatted rows to the table for ($row = 0; $row < $totalRows; $row++) { @@ -982,7 +983,7 @@ public static function table(array $tbody, array $thead = []) $table .= '| ' . implode(' | ', $tableRows[$row]) . ' |' . PHP_EOL; // Set the thead and table borders-bottom - if (isset($cols) && (($row === 0 && ! empty($thead)) || ($row + 1 === $totalRows))) { + if (($row === 0 && ! empty($thead)) || ($row + 1 === $totalRows)) { $table .= $cols . PHP_EOL; } } diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index babb7c037be1..98a4ccbf78c6 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Cache\Exceptions\CacheException; +use CodeIgniter\I18n\Time; use Config\Cache; use Throwable; @@ -91,7 +92,7 @@ public function save(string $key, $value, int $ttl = 60) $key = static::validateKey($key, $this->prefix); $contents = [ - 'time' => time(), + 'time' => Time::now()->getTimestamp(), 'ttl' => $ttl, 'data' => $value, ]; @@ -241,7 +242,7 @@ protected function getItem(string $filename) return false; } - if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl']) { + if ($data['ttl'] > 0 && Time::now()->getTimestamp() > $data['time'] + $data['ttl']) { // If the file is still there then try to remove it if (is_file($this->path . $filename)) { @unlink($this->path . $filename); diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index 6b6f60e0a6b7..f7d5ec8bcd1a 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Exceptions\CriticalError; +use CodeIgniter\I18n\Time; use Config\Cache; use Exception; use Memcache; @@ -155,7 +156,7 @@ public function save(string $key, $value, int $ttl = 60) if (! $this->config['raw']) { $value = [ $value, - time(), + Time::now()->getTimestamp(), $ttl, ]; } diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index 87ab4e333dca..cf44f634e86b 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Exceptions\CriticalError; +use CodeIgniter\I18n\Time; use Config\Cache; use Exception; use Predis\Client; @@ -128,7 +129,7 @@ public function save(string $key, $value, int $ttl = 60) } if ($ttl) { - $this->redis->expireat($key, time() + $ttl); + $this->redis->expireat($key, Time::now()->getTimestamp() + $ttl); } return true; @@ -204,11 +205,11 @@ public function getMetaData(string $key) $data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value'])); if (isset($data['__ci_value']) && $data['__ci_value'] !== false) { - $time = time(); + $time = Time::now()->getTimestamp(); $ttl = $this->redis->ttl($key); return [ - 'expire' => $ttl > 0 ? time() + $ttl : null, + 'expire' => $ttl > 0 ? $time + $ttl : null, 'mtime' => $time, 'data' => $data['__ci_value'], ]; diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index b84c0115b1f8..8b3af5856319 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Exceptions\CriticalError; +use CodeIgniter\I18n\Time; use Config\Cache; use Redis; use RedisException; @@ -154,7 +155,7 @@ public function save(string $key, $value, int $ttl = 60) } if ($ttl) { - $this->redis->expireAt($key, time() + $ttl); + $this->redis->expireAt($key, Time::now()->getTimestamp() + $ttl); } return true; @@ -236,11 +237,11 @@ public function getMetaData(string $key) $value = $this->get($key); if ($value !== null) { - $time = time(); + $time = Time::now()->getTimestamp(); $ttl = $this->redis->ttl($key); return [ - 'expire' => $ttl > 0 ? time() + $ttl : null, + 'expire' => $ttl > 0 ? $time + $ttl : null, 'mtime' => $time, 'data' => $value, ]; diff --git a/system/Cache/Handlers/WincacheHandler.php b/system/Cache/Handlers/WincacheHandler.php index b13ee30c3ff6..e422dc1fad00 100644 --- a/system/Cache/Handlers/WincacheHandler.php +++ b/system/Cache/Handlers/WincacheHandler.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\I18n\Time; use Config\Cache; use Exception; @@ -124,7 +125,7 @@ public function getMetaData(string $key) $hitcount = $stored['ucache_entries'][1]['hitcount']; return [ - 'expire' => $ttl > 0 ? time() + $ttl : null, + 'expire' => $ttl > 0 ? Time::now()->getTimestamp() + $ttl : null, 'hitcount' => $hitcount, 'age' => $age, 'ttl' => $ttl, diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 4d738aed462f..e7076c4ba167 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -47,7 +47,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.2.10'; + public const CI_VERSION = '4.2.11'; /** * App startup time. @@ -205,11 +205,9 @@ public function initialize() protected function resolvePlatformExtensions() { $requiredExtensions = [ - 'curl', 'intl', 'json', 'mbstring', - 'xml', ]; $missingExtensions = []; diff --git a/system/Commands/Database/CreateDatabase.php b/system/Commands/Database/CreateDatabase.php index 17e1e51bb68b..a3922bd66dee 100644 --- a/system/Commands/Database/CreateDatabase.php +++ b/system/Commands/Database/CreateDatabase.php @@ -148,8 +148,8 @@ public function run(array $params) } catch (Throwable $e) { $this->showError($e); } finally { - // Reset the altered config no matter what happens. Factories::reset('config'); + Database::connect(null, false); } } } diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index b9d1794ff1fb..5837bfb02636 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -67,6 +67,7 @@ class GenerateKey extends BaseCommand public function run(array $params) { $prefix = $params['prefix'] ?? CLI::getOption('prefix'); + if (in_array($prefix, [null, true], true)) { $prefix = 'hex2bin'; } elseif (! in_array($prefix, ['hex2bin', 'base64'], true)) { @@ -74,6 +75,7 @@ public function run(array $params) } $length = $params['length'] ?? CLI::getOption('length'); + if (in_array($length, [null, true], true)) { $length = 32; } @@ -127,9 +129,7 @@ protected function setNewEncryptionKey(string $key, array $params): bool if ($currentKey !== '' && ! $this->confirmOverwrite($params)) { // Not yet testable since it requires keyboard input - // @codeCoverageIgnoreStart - return false; - // @codeCoverageIgnoreEnd + return false; // @codeCoverageIgnore } return $this->writeNewEncryptionKeyToFile($currentKey, $key); @@ -163,13 +163,24 @@ protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): copy($baseEnv, $envFile); } - $ret = file_put_contents($envFile, preg_replace( - $this->keyPattern($oldKey), - "\nencryption.key = {$newKey}", - file_get_contents($envFile) - )); + $oldFileContents = (string) file_get_contents($envFile); + $replacementKey = "\nencryption.key = {$newKey}"; + + if (strpos($oldFileContents, 'encryption.key') === false) { + return file_put_contents($envFile, $replacementKey, FILE_APPEND) !== false; + } + + $newFileContents = preg_replace($this->keyPattern($oldKey), $replacementKey, $oldFileContents); + + if ($newFileContents === $oldFileContents) { + $newFileContents = preg_replace( + '/^[#\s]*encryption.key[=\s]*(?:hex2bin\:[a-f0-9]{64}|base64\:(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?)$/m', + $replacementKey, + $oldFileContents + ); + } - return $ret !== false; + return file_put_contents($envFile, $newFileContents) !== false; } /** diff --git a/system/Commands/Server/rewrite.php b/system/Commands/Server/rewrite.php index 500699331e67..45a10936b1e0 100644 --- a/system/Commands/Server/rewrite.php +++ b/system/Commands/Server/rewrite.php @@ -24,16 +24,15 @@ return; } -$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)); +$uri = urldecode( + parse_url('https://codeigniter.com' . $_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '' +); // All request handle by index.php file. $_SERVER['SCRIPT_NAME'] = '/index.php'; -// Front Controller path - expected to be in the default folder -$fcpath = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; - // Full path -$path = $fcpath . ltrim($uri, '/'); +$path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . ltrim($uri, '/'); // If $path is an existing file or folder within the public folder // then let the request handle it like normal. @@ -41,7 +40,9 @@ return false; } +unset($uri, $path); + // Otherwise, we'll load the index file and let // the framework handle the request from here. -require_once $fcpath . 'index.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php'; // @codeCoverageIgnoreEnd diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 989f02036e86..41c3710c3000 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -74,14 +74,16 @@ public function run(array $params) $tbody = []; - foreach ($config->psr4 as $ns => $path) { - $path = realpath($path) ?: $path; + foreach ($config->psr4 as $ns => $paths) { + foreach ((array) $paths as $path) { + $path = realpath($path) ?: $path; - $tbody[] = [ - $ns, - realpath($path) ?: $path, - is_dir($path) ? 'Yes' : 'MISSING', - ]; + $tbody[] = [ + $ns, + $path, + is_dir($path) ? 'Yes' : 'MISSING', + ]; + } } $thead = [ diff --git a/system/Common.php b/system/Common.php index a4c9d38b5d55..a38ee975ee93 100644 --- a/system/Common.php +++ b/system/Common.php @@ -63,6 +63,7 @@ function app_timezone(): string * $foo = cache('bar'); * * @return CacheInterface|mixed + * @phpstan-return ($key is null ? CacheInterface : mixed) */ function cache(?string $key = null) { @@ -196,7 +197,7 @@ function command(string $command) /** * More simple way of getting config instances from Factories * - * @return mixed + * @return object|null */ function config(string $name, bool $getShared = true) { @@ -376,7 +377,7 @@ function dd(...$vars) * * @param string|null $default * - * @return mixed + * @return bool|string|null */ function env(string $key, $default = null) { @@ -952,7 +953,7 @@ function session(?string $val = null) * * @param mixed ...$params * - * @return mixed + * @return object */ function service(string $name, ...$params) { @@ -966,7 +967,7 @@ function service(string $name, ...$params) * * @param mixed ...$params * - * @return mixed + * @return object|null */ function single_service(string $name, ...$params) { @@ -1076,7 +1077,7 @@ function stringify_attributes($attributes, bool $js = false): string * If no parameter is passed, it will return the timer instance, * otherwise will start or stop the timer intelligently. * - * @return mixed|Timer + * @return Timer */ function timer(?string $name = null) { diff --git a/system/ComposerScripts.php b/system/ComposerScripts.php index 62e5d828e1ce..e4fd208815e6 100644 --- a/system/ComposerScripts.php +++ b/system/ComposerScripts.php @@ -87,7 +87,9 @@ public static function postUpdate() private static function recursiveDelete(string $directory): void { if (! is_dir($directory)) { - echo sprintf('Cannot recursively delete "%s" as it does not exist.', $directory); + echo sprintf('Cannot recursively delete "%s" as it does not exist.', $directory) . PHP_EOL; + + return; } /** @var SplFileInfo $file */ @@ -126,7 +128,11 @@ private static function recursiveMirror(string $originDir, string $targetDir): v exit(1); } - @mkdir($targetDir, 0755, true); + if (! @mkdir($targetDir, 0755, true)) { + echo sprintf('Cannot create the target directory: "%s"', $targetDir) . PHP_EOL; + + exit(1); + } $dirLen = strlen($originDir); diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 79cad2ab8d25..da08dd6f6640 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -45,7 +45,8 @@ class AutoloadConfig * but this should be done prior to creating any namespaced classes, * else you will need to modify all of those classes for this to work. * - * @var array + * @var array|string> + * @phpstan-var array> */ public $psr4 = []; @@ -72,6 +73,7 @@ class AutoloadConfig * or for loading functions. * * @var array + * @phpstan-var list */ public $files = []; diff --git a/system/Config/Factories.php b/system/Config/Factories.php index c6bb25de0794..7abd3f5c4449 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -23,7 +23,7 @@ * large performance boost and helps keep code clean of lengthy * instantiation checks. * - * @method static BaseConfig config(...$arguments) + * @method static BaseConfig|null config(...$arguments) */ class Factories { diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index b0b49a0a0b7b..9362994329fd 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -13,6 +13,7 @@ use ArrayAccess; use CodeIgniter\Cookie\Exceptions\CookieException; +use CodeIgniter\I18n\Time; use Config\Cookie as CookieConfig; use DateTimeInterface; use InvalidArgumentException; @@ -206,7 +207,7 @@ final public function __construct(string $name, string $value = '', array $optio // If both `Expires` and `Max-Age` are set, `Max-Age` has precedence. if (isset($options['max-age']) && is_numeric($options['max-age'])) { - $options['expires'] = time() + (int) $options['max-age']; + $options['expires'] = Time::now()->getTimestamp() + (int) $options['max-age']; unset($options['max-age']); } @@ -314,7 +315,7 @@ public function getExpiresString(): string */ public function isExpired(): bool { - return $this->expires === 0 || $this->expires < time(); + return $this->expires === 0 || $this->expires < Time::now()->getTimestamp(); } /** @@ -322,7 +323,7 @@ public function isExpired(): bool */ public function getMaxAge(): int { - $maxAge = $this->expires - time(); + $maxAge = $this->expires - Time::now()->getTimestamp(); return $maxAge >= 0 ? $maxAge : 0; } @@ -466,7 +467,7 @@ public function withNeverExpiring() { $cookie = clone $this; - $cookie->expires = time() + 5 * YEAR; + $cookie->expires = Time::now()->getTimestamp() + 5 * YEAR; return $cookie; } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 3433de491fa6..4941ba78a739 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2111,7 +2111,7 @@ protected function _update(string $table, array $values): string /** * This method is used by both update() and getCompiledUpdate() to * validate that data is actually being set and that a table has been - * chosen to be update. + * chosen to be updated. * * @throws DatabaseException */ diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 1f5009167f2c..c0f4835f1bf7 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1334,20 +1334,21 @@ protected function getDriverFunctionPrefix(): string /** * Returns an array of table names * - * @return array|bool + * @return array|false * * @throws DatabaseException */ public function listTables(bool $constrainByPrefix = false) { - // Is there a cached result? if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) { - return $constrainByPrefix ? - preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names']) + return $constrainByPrefix + ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names']) : $this->dataCache['table_names']; } - if (false === ($sql = $this->_listTables($constrainByPrefix))) { + $sql = $this->_listTables($constrainByPrefix); + + if ($sql === false) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } @@ -1360,24 +1361,9 @@ public function listTables(bool $constrainByPrefix = false) $query = $this->query($sql); foreach ($query->getResultArray() as $row) { - // Do we know from which column to get the table name? - if (! isset($key)) { - if (isset($row['table_name'])) { - $key = 'table_name'; - } elseif (isset($row['TABLE_NAME'])) { - $key = 'TABLE_NAME'; - } else { - /* We have no other choice but to just get the first element's key. - * Due to array_shift() accepting its argument by reference, if - * E_STRICT is on, this would trigger a warning. So we'll have to - * assign it first. - */ - $key = array_keys($row); - $key = array_shift($key); - } - } + $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)]; - $this->dataCache['table_names'][] = $row[$key]; + $this->dataCache['table_names'][] = $table; } return $this->dataCache['table_names']; diff --git a/system/Database/Database.php b/system/Database/Database.php index 9fdf54c7db45..93d966fd3f65 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -35,7 +35,7 @@ class Database * Parses the connection binds and returns an instance of the driver * ready to go. * - * @return mixed + * @return BaseConnection * * @throws InvalidArgumentException */ diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index ac2dc38e6bd6..6eb92125237f 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -14,6 +14,7 @@ use CodeIgniter\CLI\CLI; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\I18n\Time; use Config\Database; use Config\Migrations as MigrationsConfig; use Config\Services; @@ -596,7 +597,7 @@ protected function addHistory($migration, int $batch) 'class' => $migration->class, 'group' => $this->group, 'namespace' => $migration->namespace, - 'time' => time(), + 'time' => Time::now()->getTimestamp(), 'batch' => $batch, ]); diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index f550c71c785a..c4e1593a30ae 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -174,6 +174,7 @@ private function isFakerDeprecationError(int $severity, string $message, ?string { if ( $severity === E_DEPRECATED + && defined('VENDORPATH') && strpos($file, VENDORPATH . 'fakerphp/faker/') !== false && $message === 'Use of "static" in callables is deprecated' ) { diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 9ca51d1c9b08..9b1779818f5b 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -34,8 +34,8 @@ class Timer * Multiple calls can be made to this method so that several * execution points can be measured. * - * @param string $name The name of this timer. - * @param float $time Allows user to provide time. + * @param string $name The name of this timer. + * @param float|null $time Allows user to provide time. * * @return Timer */ diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 5e70dbc437ff..b5bdcb9c38aa 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -22,6 +22,7 @@ use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\I18n\Time; use Config\Services; use Config\Toolbar as ToolbarConfig; use Kint\Kint; @@ -380,7 +381,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r helper('filesystem'); // Updated to microtime() so we can get history - $time = sprintf('%.6f', microtime(true)); + $time = sprintf('%.6f', Time::now()->format('U.u')); if (! is_dir(WRITEPATH . 'debugbar')) { mkdir(WRITEPATH . 'debugbar', 0777); diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 6a845ae51bf3..2991840c8de1 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; use CodeIgniter\Database\Query; +use CodeIgniter\I18n\Time; /** * Collector for the Database tab of the Debug Toolbar. @@ -184,7 +185,7 @@ public function display(): array 'sql' => $query['query']->debugToolbarDisplay(), 'trace' => $query['trace'], 'trace-file' => $firstNonSystemLine, - 'qid' => md5($query['query'] . microtime()), + 'qid' => md5($query['query'] . Time::now()->format('0.u00 U')), ]; }, static::$queries); diff --git a/system/Email/Email.php b/system/Email/Email.php index 3ef459a8d08f..384d22fcc63b 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Email; use CodeIgniter\Events\Events; +use CodeIgniter\I18n\Time; use Config\Mimes; use ErrorException; @@ -391,7 +392,7 @@ class Email protected static $func_overload; /** - * @param array|null $config + * @param array|\Config\Email|null $config */ public function __construct($config = null) { @@ -404,7 +405,7 @@ public function __construct($config = null) /** * Initialize preferences * - * @param array|\Config\Email $config + * @param array|\Config\Email|null $config * * @return Email */ @@ -2038,8 +2039,8 @@ protected function sendData($data) // See https://bugs.php.net/bug.php?id=39598 and http://php.net/manual/en/function.fwrite.php#96951 if ($result === 0) { if ($timestamp === 0) { - $timestamp = time(); - } elseif ($timestamp < (time() - $this->SMTPTimeout)) { + $timestamp = Time::now()->getTimestamp(); + } elseif ($timestamp < (Time::now()->getTimestamp() - $this->SMTPTimeout)) { $result = false; break; diff --git a/system/Files/File.php b/system/Files/File.php index 4d4067d4caa5..67e098da1a93 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -13,6 +13,7 @@ use CodeIgniter\Files\Exceptions\FileException; use CodeIgniter\Files\Exceptions\FileNotFoundException; +use CodeIgniter\I18n\Time; use Config\Mimes; use ReturnTypeWillChange; use SplFileInfo; @@ -126,7 +127,7 @@ public function getRandomName(): string $extension = $this->getExtension(); $extension = empty($extension) ? '' : '.' . $extension; - return time() . '_' . bin2hex(random_bytes(10)) . $extension; + return Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . $extension; } /** diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 1e8a1aedbaad..9a15325a530c 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -293,6 +293,11 @@ protected function parseOptions(array $options) unset($options['delay']); } + if (array_key_exists('body', $options)) { + $this->setBody($options['body']); + unset($options['body']); + } + foreach ($options as $key => $value) { $this->config[$key] = $value; } diff --git a/system/HTTP/Header.php b/system/HTTP/Header.php index 9cab888d4473..e9bebe960097 100644 --- a/system/HTTP/Header.php +++ b/system/HTTP/Header.php @@ -28,15 +28,23 @@ class Header /** * The value of the header. May have more than one * value. If so, will be an array of strings. + * E.g., + * [ + * 'foo', + * [ + * 'bar' => 'fizz', + * ], + * 'baz' => 'buzz', + * ] * - * @var array|string + * @var array|string>|string */ protected $value; /** * Header constructor. name is mandatory, if a value is provided, it will be set. * - * @param array|string|null $value + * @param array|string>|string|null $value */ public function __construct(string $name, $value = null) { @@ -56,7 +64,7 @@ public function getName(): string * Gets the raw value of the header. This may return either a string * of an array, depending on whether the header has multiple values or not. * - * @return array|string + * @return array|string>|string */ public function getValue() { @@ -78,13 +86,13 @@ public function setName(string $name) /** * Sets the value of the header, overwriting any previous value(s). * - * @param array|string|null $value + * @param array|string>|string|null $value * * @return $this */ public function setValue($value = null) { - $this->value = $value ?? ''; + $this->value = is_array($value) ? $value : (string) $value; return $this; } @@ -93,7 +101,7 @@ public function setValue($value = null) * Appends a value to the list of values for this header. If the * header is a single value string, it will be converted to an array. * - * @param array|string|null $value + * @param array|string|null $value * * @return $this */ @@ -108,7 +116,7 @@ public function appendValue($value = null) } if (! in_array($value, $this->value, true)) { - $this->value[] = $value; + $this->value[] = is_array($value) ? $value : (string) $value; } return $this; @@ -118,7 +126,7 @@ public function appendValue($value = null) * Prepends a value to the list of values for this header. If the * header is a single value string, it will be converted to an array. * - * @param array|string|null $value + * @param array|string|null $value * * @return $this */ diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 61492adc4275..a21f23e76a9c 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -18,6 +18,7 @@ use Config\Services; use InvalidArgumentException; use Locale; +use stdClass; /** * Class IncomingRequest @@ -153,8 +154,16 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U throw new InvalidArgumentException('You must supply the parameters: uri, userAgent.'); } - // Get our body from php://input - if ($body === 'php://input') { + $this->populateHeaders(); + + if ( + $body === 'php://input' + // php://input is not available with enctype="multipart/form-data". + // See https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input + && strpos($this->getHeaderLine('Content-Type'), 'multipart/form-data') === false + && (int) $this->getHeaderLine('Content-Length') <= $this->getPostMaxSize() + ) { + // Get our body from php://input $body = file_get_contents('php://input'); } @@ -166,11 +175,34 @@ public function __construct($config, ?URI $uri = null, $body = 'php://input', ?U parent::__construct($config); - $this->populateHeaders(); $this->detectURI($config->uriProtocol, $config->baseURL); $this->detectLocale($config); } + private function getPostMaxSize(): int + { + $postMaxSize = ini_get('post_max_size'); + + switch (strtoupper(substr($postMaxSize, -1))) { + case 'G': + $postMaxSize = (int) str_replace('G', '', $postMaxSize) * 1024 ** 3; + break; + + case 'M': + $postMaxSize = (int) str_replace('M', '', $postMaxSize) * 1024 ** 2; + break; + + case 'K': + $postMaxSize = (int) str_replace('K', '', $postMaxSize) * 1024; + break; + + default: + $postMaxSize = (int) $postMaxSize; + } + + return $postMaxSize; + } + /** * Handles setting up the locale, perhaps auto-detecting through * content negotiation. @@ -478,7 +510,7 @@ public function getDefaultLocale(): string * @param int|null $filter Filter constant * @param mixed $flags * - * @return mixed + * @return array|bool|float|int|stdClass|string|null */ public function getVar($index = null, $filter = null, $flags = null) { @@ -516,7 +548,7 @@ public function getVar($index = null, $filter = null, $flags = null) * * @see http://php.net/manual/en/function.json-decode.php * - * @return mixed + * @return array|bool|float|int|stdClass|null */ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) { @@ -531,7 +563,7 @@ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) * @param int|null $filter Filter Constant * @param array|int|null $flags Option * - * @return mixed + * @return array|bool|float|int|stdClass|string|null */ public function getJsonVar(string $index, bool $assoc = false, ?int $filter = null, $flags = null) { @@ -565,7 +597,7 @@ public function getJsonVar(string $index, bool $assoc = false, ?int $filter = nu * A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes * the String into an array. * - * @return mixed + * @return array */ public function getRawInput() { diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 6294295db4df..2b582c41867a 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -23,7 +23,7 @@ class Request extends Message implements MessageInterface, RequestInterface /** * Proxy IPs * - * @var array|string + * @var array * * @deprecated Check the App config directly */ @@ -98,7 +98,7 @@ public function getMethod(bool $upper = false): string /** * Sets the request method. Used when spoofing the request. * - * @return Request + * @return $this * * @deprecated Use withMethod() instead for immutability * diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index c551dee9e09d..a9d55224c557 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Validation\FormatRules; /** @@ -43,7 +44,9 @@ trait RequestTrait /** * Gets the user's IP address. * - * @return string IP address + * @return string IP address if it can be detected, or empty string. + * If the IP address is not a valid IP address, + * then will return '0.0.0.0'. */ public function getIPAddress(): string { @@ -59,88 +62,86 @@ public function getIPAddress(): string /** * @deprecated $this->proxyIPs property will be removed in the future */ + // @phpstan-ignore-next-line $proxyIPs = $this->proxyIPs ?? config('App')->proxyIPs; - if (! empty($proxyIPs) && ! is_array($proxyIPs)) { - $proxyIPs = explode(',', str_replace(' ', '', $proxyIPs)); + if (! empty($proxyIPs)) { + // @phpstan-ignore-next-line + if (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs))) { + throw new ConfigException( + 'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.' + ); + } } $this->ipAddress = $this->getServer('REMOTE_ADDR'); if ($proxyIPs) { - foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP'] as $header) { - if (($spoof = $this->getServer($header)) !== null) { - // Some proxies typically list the whole chain of IP - // addresses through which the client has reached us. - // e.g. client_ip, proxy_ip1, proxy_ip2, etc. - sscanf($spoof, '%[^,]', $spoof); - - if (! $ipValidator($spoof)) { - $spoof = null; - } else { - break; - } - } - } - - if ($spoof) { - foreach ($proxyIPs as $proxyIP) { - // Check if we have an IP address or a subnet - if (strpos($proxyIP, '/') === false) { - // An IP address (and not a subnet) is specified. - // We can compare right away. - if ($proxyIP === $this->ipAddress) { + // @TODO Extract all this IP address logic to another class. + foreach ($proxyIPs as $proxyIP => $header) { + // Check if we have an IP address or a subnet + if (strpos($proxyIP, '/') === false) { + // An IP address (and not a subnet) is specified. + // We can compare right away. + if ($proxyIP === $this->ipAddress) { + $spoof = $this->getClientIP($header); + + if ($spoof !== null) { $this->ipAddress = $spoof; break; } - - continue; } - // We have a subnet ... now the heavy lifting begins - if (! isset($separator)) { - $separator = $ipValidator($this->ipAddress, 'ipv6') ? ':' : '.'; - } + continue; + } - // If the proxy entry doesn't match the IP protocol - skip it - if (strpos($proxyIP, $separator) === false) { - continue; - } + // We have a subnet ... now the heavy lifting begins + if (! isset($separator)) { + $separator = $ipValidator($this->ipAddress, 'ipv6') ? ':' : '.'; + } - // Convert the REMOTE_ADDR IP address to binary, if needed - if (! isset($ip, $sprintf)) { - if ($separator === ':') { - // Make sure we're have the "full" IPv6 format - $ip = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($this->ipAddress, ':')), $this->ipAddress)); + // If the proxy entry doesn't match the IP protocol - skip it + if (strpos($proxyIP, $separator) === false) { + continue; + } - for ($j = 0; $j < 8; $j++) { - $ip[$j] = intval($ip[$j], 16); - } + // Convert the REMOTE_ADDR IP address to binary, if needed + if (! isset($ip, $sprintf)) { + if ($separator === ':') { + // Make sure we're having the "full" IPv6 format + $ip = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($this->ipAddress, ':')), $this->ipAddress)); - $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b'; - } else { - $ip = explode('.', $this->ipAddress); - $sprintf = '%08b%08b%08b%08b'; + for ($j = 0; $j < 8; $j++) { + $ip[$j] = intval($ip[$j], 16); } - $ip = vsprintf($sprintf, $ip); + $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b'; + } else { + $ip = explode('.', $this->ipAddress); + $sprintf = '%08b%08b%08b%08b'; } - // Split the netmask length off the network address - sscanf($proxyIP, '%[^/]/%d', $netaddr, $masklen); + $ip = vsprintf($sprintf, $ip); + } - // Again, an IPv6 address is most likely in a compressed form - if ($separator === ':') { - $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr)); + // Split the netmask length off the network address + sscanf($proxyIP, '%[^/]/%d', $netaddr, $masklen); - for ($i = 0; $i < 8; $i++) { - $netaddr[$i] = intval($netaddr[$i], 16); - } - } else { - $netaddr = explode('.', $netaddr); + // Again, an IPv6 address is most likely in a compressed form + if ($separator === ':') { + $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr)); + + for ($i = 0; $i < 8; $i++) { + $netaddr[$i] = intval($netaddr[$i], 16); } + } else { + $netaddr = explode('.', $netaddr); + } - // Convert to binary and finally compare - if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) { + // Convert to binary and finally compare + if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0) { + $spoof = $this->getClientIP($header); + + if ($spoof !== null) { $this->ipAddress = $spoof; break; } @@ -155,6 +156,34 @@ public function getIPAddress(): string return empty($this->ipAddress) ? '' : $this->ipAddress; } + /** + * Gets the client IP address from the HTTP header. + */ + private function getClientIP(string $header): ?string + { + $ipValidator = [ + new FormatRules(), + 'valid_ip', + ]; + $spoof = null; + $headerObj = $this->header($header); + + if ($headerObj !== null) { + $spoof = $headerObj->getValue(); + + // Some proxies typically list the whole chain of IP + // addresses through which the client has reached us. + // e.g. client_ip, proxy_ip1, proxy_ip2, etc. + sscanf($spoof, '%[^,]', $spoof); + + if (! $ipValidator($spoof)) { + $spoof = null; + } + } + + return $spoof; + } + /** * Fetch an item from the $_SERVER array. * diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index bd69a41376f8..7aa9ab9e4eba 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -15,6 +15,7 @@ use CodeIgniter\Cookie\CookieStore; use CodeIgniter\Cookie\Exceptions\CookieException; use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\I18n\Time; use CodeIgniter\Pager\PagerInterface; use CodeIgniter\Security\Exceptions\SecurityException; use Config\Cookie as CookieConfig; @@ -464,7 +465,7 @@ public function sendHeaders() // Per spec, MUST be sent with each request, if possible. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html if (! isset($this->headers['Date']) && PHP_SAPI !== 'cli-server') { - $this->setDate(DateTime::createFromFormat('U', (string) time())); + $this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp())); } // HTTP Status @@ -587,7 +588,7 @@ public function setCookie( } if (is_numeric($expire)) { - $expire = $expire > 0 ? time() + $expire : 0; + $expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0; } $cookie = new Cookie($name, $value, [ diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 7634b415ec7e..6e5674bdffc3 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -138,11 +138,11 @@ class URI /** * Builds a representation of the string from the component parts. * - * @param string $scheme - * @param string $authority - * @param string $path - * @param string $query - * @param string $fragment + * @param string|null $scheme URI scheme. E.g., http, ftp + * @param string $authority + * @param string $path + * @param string $query + * @param string $fragment */ public static function createURIString(?string $scheme = null, ?string $authority = null, ?string $path = null, ?string $query = null, ?string $fragment = null): string { diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php index cbb618462f60..677e55e64ab2 100644 --- a/system/Helpers/date_helper.php +++ b/system/Helpers/date_helper.php @@ -11,11 +11,13 @@ // CodeIgniter Date Helpers +use CodeIgniter\I18n\Time; + if (! function_exists('now')) { /** * Get "now" time * - * Returns time() based on the timezone parameter or on the + * Returns Time::now()->getTimestamp() based on the timezone parameter or on the * app_timezone() setting * * @param string $timezone @@ -27,7 +29,7 @@ function now(?string $timezone = null): int $timezone = empty($timezone) ? app_timezone() : $timezone; if ($timezone === 'local' || $timezone === date_default_timezone_get()) { - return time(); + return Time::now()->getTimestamp(); } $datetime = new DateTime('now', new DateTimeZone($timezone)); diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index f0dcc9d786c8..566ab3050478 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -75,6 +75,7 @@ function _get_uri(string $relativePath = '', ?App $config = null): URI * Returns a site URL as defined by the App config. * * @param array|string $relativePath URI string or array of URI segments + * @param string|null $scheme URI scheme. E.g., http, ftp * @param App|null $config Alternate configuration to use */ function site_url($relativePath = '', ?string $scheme = null, ?App $config = null): string @@ -96,6 +97,7 @@ function site_url($relativePath = '', ?string $scheme = null, ?App $config = nul * Base URLs are trimmed site URLs without the index page. * * @param array|string $relativePath URI string or array of URI segments + * @param string|null $scheme URI scheme. E.g., http, ftp */ function base_url($relativePath = '', ?string $scheme = null): string { diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index bd7f5b7e7afa..b96fdc06d20d 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Images\Handlers; +use CodeIgniter\I18n\Time; use CodeIgniter\Images\Exceptions\ImageException; use Config\Images; use Exception; @@ -276,7 +277,7 @@ protected function getResourcePath() return $this->resource; } - $this->resource = WRITEPATH . 'cache/' . time() . '_' . bin2hex(random_bytes(10)) . '.png'; + $this->resource = WRITEPATH . 'cache/' . Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . '.png'; $name = basename($this->resource); $path = pathinfo($this->resource, PATHINFO_DIRNAME); diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 96674c52e6c1..4dd13cb0e8ee 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -349,11 +349,9 @@ protected function interpolate($message, array $context = []) if (strpos($message, 'env:') !== false) { preg_match('/env:[^}]+/', $message, $matches); - if ($matches) { - foreach ($matches as $str) { - $key = str_replace('env:', '', $str); - $replace["{{$str}}"] = $_ENV[$key] ?? 'n/a'; - } + foreach ($matches as $str) { + $key = str_replace('env:', '', $str); + $replace["{{$str}}"] = $_ENV[$key] ?? 'n/a'; } } diff --git a/system/Model.php b/system/Model.php index ae540bb50213..55b3b881de37 100644 --- a/system/Model.php +++ b/system/Model.php @@ -41,21 +41,31 @@ * @property BaseConnection $db * * @method $this groupBy($by, ?bool $escape = null) + * @method $this groupEnd() + * @method $this groupStart() + * @method $this havingGroupEnd() + * @method $this havingGroupStart() * @method $this havingIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this havingNotIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this join(string $table, string $cond, string $type = '', ?bool $escape = null) * @method $this like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this limit(?int $value = null, ?int $offset = 0) + * @method $this notGroupStart() + * @method $this notHavingGroupStart() * @method $this notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this offset(int $offset) * @method $this orderBy(string $orderBy, string $direction = '', ?bool $escape = null) + * @method $this orGroupStart() * @method $this orHaving($key, $value = null, ?bool $escape = null) + * @method $this orHavingGroupStart() * @method $this orHavingIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) + * @method $this orNotGroupStart() + * @method $this orNotHavingGroupStart() * @method $this orNotHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this orNotLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false) * @method $this orWhere($key, $value = null, ?bool $escape = null) @@ -118,6 +128,13 @@ class Model extends BaseModel */ protected $escape = []; + /** + * Primary Key value when inserting and useAutoIncrement is false. + * + * @var int|string|null + */ + private $tempPrimaryKeyValue; + /** * Builder method names that should not be used in the Model. * @@ -263,7 +280,14 @@ protected function doInsert(array $data) $escape = $this->escape; $this->escape = []; - // Require non empty primaryKey when + // If $useAutoIncrement is false, add the primary key data. + if ($this->useAutoIncrement === false && $this->tempPrimaryKeyValue !== null) { + $data[$this->primaryKey] = $this->tempPrimaryKeyValue; + + $this->tempPrimaryKeyValue = null; + } + + // Require non-empty primaryKey when // not using auto-increment feature if (! $this->useAutoIncrement && empty($data[$this->primaryKey])) { throw DataException::forEmptyPrimaryKey('insert'); @@ -301,7 +325,7 @@ protected function doInsertBatch(?array $set = null, ?bool $escape = null, int $ { if (is_array($set)) { foreach ($set as $row) { - // Require non empty primaryKey when + // Require non-empty primaryKey when // not using auto-increment feature if (! $this->useAutoIncrement && empty($row[$this->primaryKey])) { throw DataException::forEmptyPrimaryKey('insertBatch'); @@ -347,7 +371,7 @@ protected function doUpdate($id = null, $data = null): bool * @param int $batchSize The size of the batch to run * @param bool $returnSQL True means SQL is returned, false will execute the query * - * @return mixed Number of rows affected or FALSE on failure + * @return false|int|string[] Number of rows affected or FALSE on failure, SQL array when testMode * * @throws DatabaseException */ @@ -432,7 +456,7 @@ protected function doOnlyDeleted() * @param array|null $data Data * @param bool $returnSQL Set to true to return Query String * - * @return mixed + * @return BaseResult|false|Query|string */ protected function doReplace(?array $data = null, bool $returnSQL = false) { @@ -533,7 +557,7 @@ public function chunk(int $size, Closure $userFunc) /** * Override countAllResults to account for soft deleted accounts. * - * @return mixed + * @return int|string */ public function countAllResults(bool $reset = true, bool $test = false) { @@ -599,9 +623,9 @@ public function builder(?string $table = null) * data here. This allows it to be used with any of the other * builder methods and still get validated data, like replace. * - * @param mixed $key Field name, or an array of field/value pairs - * @param mixed $value Field value, if $key is a single field - * @param bool|null $escape Whether to escape values + * @param array|object|string $key Field name, or an array of field/value pairs + * @param bool|float|int|object|string|null $value Field value, if $key is a single field + * @param bool|null $escape Whether to escape values * * @return $this */ @@ -661,6 +685,10 @@ public function insert($data = null, bool $returnID = true) } } + if ($this->useAutoIncrement === false && isset($data[$this->primaryKey])) { + $this->tempPrimaryKeyValue = $data[$this->primaryKey]; + } + $this->escape = $this->tempData['escape'] ?? []; $this->tempData = []; @@ -710,8 +738,13 @@ protected function objectToRawArray($data, bool $onlyChanged = true, bool $recur // Always grab the primary key otherwise updates will fail. if ( - method_exists($data, 'toRawArray') && (! empty($properties) && ! empty($this->primaryKey) && ! in_array($this->primaryKey, $properties, true) - && ! empty($data->{$this->primaryKey})) + method_exists($data, 'toRawArray') + && ( + ! empty($properties) + && ! empty($this->primaryKey) + && ! in_array($this->primaryKey, $properties, true) + && ! empty($data->{$this->primaryKey}) + ) ) { $properties[$this->primaryKey] = $data->{$this->primaryKey}; } diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index 42bf93831e85..fcac85c6384f 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -78,7 +78,7 @@ public function edit($id = null) /** * Add or update a model resource, from "posted" properties * - * @param string|null|int$id + * @param int|string|null $id * * @return Response|string|void */ @@ -102,6 +102,8 @@ public function delete($id = null) /** * Set/change the expected response representation for returned objects * + * @param string $format json/xml + * * @return void */ public function setFormat(string $format = 'json') diff --git a/system/Security/Security.php b/system/Security/Security.php index a0bf5056b949..bb86471afee1 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -16,6 +16,7 @@ use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; +use CodeIgniter\I18n\Time; use CodeIgniter\Security\Exceptions\SecurityException; use CodeIgniter\Session\Session; use Config\App; @@ -567,7 +568,7 @@ private function saveHashInCookie(): void $this->rawCookieName, $this->hash, [ - 'expires' => $this->expires === 0 ? 0 : time() + $this->expires, + 'expires' => $this->expires === 0 ? 0 : Time::now()->getTimestamp() + $this->expires, ] ); diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index 8cbf43ca280a..10a16a8a1f26 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -60,12 +60,18 @@ class DatabaseHandler extends BaseHandler */ protected $rowExists = false; + /** + * ID prefix for multiple session cookies + */ + protected string $idPrefix; + /** * @throws SessionException */ public function __construct(AppConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); + $this->table = $config->sessionSavePath; if (empty($this->table)) { @@ -77,6 +83,9 @@ public function __construct(AppConfig $config, string $ipAddress) $this->db = Database::connect($this->DBGroup); $this->platform = $this->db->getPlatform(); + + // Add sessionCookieName for multiple session cookies. + $this->idPrefix = $config->sessionCookieName . ':'; } /** @@ -115,7 +124,7 @@ public function read($id) $this->sessionID = $id; } - $builder = $this->db->table($this->table)->where('id', $id); + $builder = $this->db->table($this->table)->where('id', $this->idPrefix . $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); @@ -182,7 +191,7 @@ public function write($id, $data): bool if ($this->rowExists === false) { $insertData = [ - 'id' => $id, + 'id' => $this->idPrefix . $id, 'ip_address' => $this->ipAddress, 'data' => $this->prepareData($data), ]; @@ -197,7 +206,7 @@ public function write($id, $data): bool return true; } - $builder = $this->db->table($this->table)->where('id', $id); + $builder = $this->db->table($this->table)->where('id', $this->idPrefix . $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); @@ -242,7 +251,7 @@ public function close(): bool public function destroy($id): bool { if ($this->lock) { - $builder = $this->db->table($this->table)->where('id', $id); + $builder = $this->db->table($this->table)->where('id', $this->idPrefix . $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); @@ -276,7 +285,11 @@ public function gc($max_lifetime) $separator = ' '; $interval = implode($separator, ['', "{$max_lifetime} second", '']); - return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? 1 : $this->fail(); + return $this->db->table($this->table)->where( + 'timestamp <', + "now() - INTERVAL {$interval}", + false + )->delete() ? 1 : $this->fail(); } /** diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index 9a6d2de7eb34..c05cb84227b2 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Session\Handlers; +use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; use ReturnTypeWillChange; @@ -276,7 +277,7 @@ public function gc($max_lifetime) return false; } - $ts = time() - $max_lifetime; + $ts = Time::now()->getTimestamp() - $max_lifetime; $pattern = $this->matchIP === true ? '[0-9a-f]{32}' : ''; diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index cc6365264e79..2879ae7eee8e 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Session\Handlers; +use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; use Memcached; @@ -60,6 +61,9 @@ public function __construct(AppConfig $config, string $ipAddress) throw SessionException::forEmptySavepath(); } + // Add sessionCookieName for multiple session cookies. + $this->keyPrefix .= $config->sessionCookieName . ':'; + if ($this->matchIP === true) { $this->keyPrefix .= $this->ipAddress . ':'; } @@ -88,7 +92,14 @@ public function open($path, $name): bool $serverList[] = $server['host'] . ':' . $server['port']; } - if (! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, PREG_SET_ORDER)) { + if ( + ! preg_match_all( + '#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', + $this->savePath, + $matches, + PREG_SET_ORDER + ) + ) { $this->memcached = null; $this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath); @@ -98,13 +109,17 @@ public function open($path, $name): bool foreach ($matches as $match) { // If Memcached already has this server (or if the port is invalid), skip it if (in_array($match[1] . ':' . $match[2], $serverList, true)) { - $this->logger->debug('Session: Memcached server pool already has ' . $match[1] . ':' . $match[2]); + $this->logger->debug( + 'Session: Memcached server pool already has ' . $match[1] . ':' . $match[2] + ); continue; } if (! $this->memcached->addServer($match[1], (int) $match[2], $match[3] ?? 0)) { - $this->logger->error('Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.'); + $this->logger->error( + 'Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.' + ); } else { $serverList[] = $match[1] . ':' . $match[2]; } @@ -167,7 +182,7 @@ public function write($id, $data): bool } if (isset($this->lockKey)) { - $this->memcached->replace($this->lockKey, time(), 300); + $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300); if ($this->fingerprint !== ($fingerprint = md5($data))) { if ($this->memcached->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) { @@ -245,7 +260,7 @@ public function gc($max_lifetime) protected function lockSession(string $sessionID): bool { if (isset($this->lockKey)) { - return $this->memcached->replace($this->lockKey, time(), 300); + return $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300); } $lockKey = $this->keyPrefix . $sessionID . ':lock'; @@ -258,8 +273,10 @@ protected function lockSession(string $sessionID): bool continue; } - if (! $this->memcached->set($lockKey, time(), 300)) { - $this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID); + if (! $this->memcached->set($lockKey, Time::now()->getTimestamp(), 300)) { + $this->logger->error( + 'Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID + ); return false; } @@ -269,7 +286,9 @@ protected function lockSession(string $sessionID): bool } while (++$attempt < 30); if ($attempt === 30) { - $this->logger->error('Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.'); + $this->logger->error( + 'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.' + ); return false; } @@ -289,7 +308,9 @@ protected function releaseLock(): bool ! $this->memcached->delete($this->lockKey) && $this->memcached->getResultCode() !== Memcached::RES_NOTFOUND ) { - $this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey); + $this->logger->error( + 'Session: Error while trying to free lock for ' . $this->lockKey + ); return false; } diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 1e996b5f30b9..ba7c1359fbc0 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Session\Handlers; +use CodeIgniter\I18n\Time; use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; use Redis; @@ -70,6 +71,9 @@ public function __construct(AppConfig $config, string $ipAddress) $this->setSavePath(); + // Add sessionCookieName for multiple session cookies. + $this->keyPrefix .= $config->sessionCookieName . ':'; + if ($this->matchIP === true) { $this->keyPrefix .= $this->ipAddress . ':'; } @@ -118,7 +122,7 @@ public function open($path, $name): bool $redis = new Redis(); - if (! $redis->connect($this->savePath['host'], $this->savePath['port'], $this->savePath['timeout'])) { + if (! $redis->connect($this->savePath['host'], ($this->savePath['host'][0] === '/' ? 0 : $this->savePath['port']), $this->savePath['timeout'])) { $this->logger->error('Session: Unable to connect to Redis with the configured settings.'); } elseif (isset($this->savePath['password']) && ! $redis->auth($this->savePath['password'])) { $this->logger->error('Session: Unable to authenticate to Redis instance.'); @@ -293,7 +297,7 @@ protected function lockSession(string $sessionID): bool continue; } - if (! $this->redis->setex($lockKey, 300, (string) time())) { + if (! $this->redis->setex($lockKey, 300, (string) Time::now()->getTimestamp())) { $this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID); return false; diff --git a/system/Session/Session.php b/system/Session/Session.php index fa44b8267e7b..d45a7b75fc74 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -13,6 +13,7 @@ use CodeIgniter\Cookie\Cookie; use CodeIgniter\HTTP\Response; +use CodeIgniter\I18n\Time; use Config\App; use Config\Cookie as CookieConfig; use Config\Services; @@ -182,7 +183,7 @@ public function __construct(SessionHandlerInterface $driver, App $config) $cookie = config('Cookie'); $this->cookie = (new Cookie($this->sessionCookieName, '', [ - 'expires' => $this->sessionExpiration === 0 ? 0 : time() + $this->sessionExpiration, + 'expires' => $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration, 'path' => $cookie->path ?? $config->cookiePath, 'domain' => $cookie->domain ?? $config->cookieDomain, 'secure' => $cookie->secure ?? $config->cookieSecure, @@ -238,8 +239,8 @@ public function start() && ($regenerateTime = $this->sessionTimeToUpdate) > 0 ) { if (! isset($_SESSION['__ci_last_regenerate'])) { - $_SESSION['__ci_last_regenerate'] = time(); - } elseif ($_SESSION['__ci_last_regenerate'] < (time() - $regenerateTime)) { + $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp(); + } elseif ($_SESSION['__ci_last_regenerate'] < (Time::now()->getTimestamp() - $regenerateTime)) { $this->regenerate((bool) $this->sessionRegenerateDestroy); } } @@ -379,7 +380,7 @@ protected function initVars() return; } - $currentTime = time(); + $currentTime = Time::now()->getTimestamp(); foreach ($_SESSION['__ci_vars'] as $key => &$value) { if ($value === 'new') { @@ -403,7 +404,7 @@ protected function initVars() */ public function regenerate(bool $destroy = false) { - $_SESSION['__ci_last_regenerate'] = time(); + $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp(); session_regenerate_id($destroy); $this->removeOldSessionCookie(); @@ -804,7 +805,7 @@ public function removeTempdata(string $key) */ public function markAsTempdata($key, int $ttl = 300): bool { - $ttl += time(); + $ttl += Time::now()->getTimestamp(); if (is_array($key)) { $temp = []; @@ -815,9 +816,9 @@ public function markAsTempdata($key, int $ttl = 300): bool $k = $v; $v = $ttl; } elseif (is_string($v)) { - $v = time() + $ttl; + $v = Time::now()->getTimestamp() + $ttl; } else { - $v += time(); + $v += Time::now()->getTimestamp(); } if (! array_key_exists($k, $_SESSION)) { @@ -919,7 +920,7 @@ protected function startSession() */ protected function setCookie() { - $expiration = $this->sessionExpiration === 0 ? 0 : time() + $this->sessionExpiration; + $expiration = $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration; $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration); /** @var Response $response */ diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 6a784eb8c7e9..da53c7eb6c3d 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -108,7 +108,7 @@ protected function setUpControllerTestTrait(): void $tempUri = Services::uri(); Services::injectMock('uri', $this->uri); - $this->withRequest(Services::request($this->appConfig, false)->setBody($this->body)); + $this->withRequest(Services::request($this->appConfig, false)); // Restore the URI service Services::injectMock('uri', $tempUri); @@ -156,6 +156,7 @@ public function execute(string $method, ...$params) } $response = null; + $this->request->setBody($this->body); try { ob_start(); diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index d540351e82e0..df1906b6416c 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Test; use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\I18n\Time; use CodeIgniter\Model; use Faker\Factory; use Faker\Generator; @@ -503,7 +504,7 @@ protected function createMock(?int $count = null) break; default: - $datetime = time(); + $datetime = Time::now()->getTimestamp(); } // Determine which fields we will need diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index 17b9008861dc..28597b21a4d3 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -15,7 +15,6 @@ use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\URI; -use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\RouteCollection; use Config\App; @@ -292,7 +291,7 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR { $path = URI::removeDotSegments($path); $config = config(App::class); - $request = new IncomingRequest($config, new URI(), null, new UserAgent()); + $request = Services::request($config, true); // $path may have a query in it $parts = explode('?', $path); diff --git a/system/Test/Mock/MockAppConfig.php b/system/Test/Mock/MockAppConfig.php index 15aff95809f0..36e25be39d2f 100644 --- a/system/Test/Mock/MockAppConfig.php +++ b/system/Test/Mock/MockAppConfig.php @@ -23,7 +23,7 @@ class MockAppConfig extends App public $cookieSecure = false; public $cookieHTTPOnly = false; public $cookieSameSite = 'Lax'; - public $proxyIPs = ''; + public $proxyIPs = []; public $CSRFTokenName = 'csrf_test_name'; public $CSRFHeaderName = 'X-CSRF-TOKEN'; public $CSRFCookieName = 'csrf_cookie_name'; diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index 6eb0dd70a8b8..510bda906f3e 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -23,7 +23,7 @@ class MockCLIConfig extends App public $cookieSecure = false; public $cookieHTTPOnly = false; public $cookieSameSite = 'Lax'; - public $proxyIPs = ''; + public $proxyIPs = []; public $CSRFTokenName = 'csrf_test_name'; public $CSRFCookieName = 'csrf_cookie_name'; public $CSRFExpire = 7200; diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index 69afc0e47039..f0414bb8bf02 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -14,6 +14,7 @@ use Closure; use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Cache\Handlers\BaseHandler; +use CodeIgniter\I18n\Time; use PHPUnit\Framework\Assert; class MockCache extends BaseHandler implements CacheInterface @@ -100,7 +101,7 @@ public function save(string $key, $value, int $ttl = 60, bool $raw = false) $key = static::validateKey($key, $this->prefix); $this->cache[$key] = $value; - $this->expirations[$key] = $ttl > 0 ? time() + $ttl : null; + $this->expirations[$key] = $ttl > 0 ? Time::now()->getTimestamp() + $ttl : null; return true; } @@ -221,7 +222,7 @@ public function getMetaData(string $key) } // Count expired items as a miss - if (is_int($this->expirations[$key]) && $this->expirations[$key] > time()) { + if (is_int($this->expirations[$key]) && $this->expirations[$key] > Time::now()->getTimestamp()) { return null; } diff --git a/system/Test/Mock/MockSession.php b/system/Test/Mock/MockSession.php index f686f17eab39..f5290f26525d 100644 --- a/system/Test/Mock/MockSession.php +++ b/system/Test/Mock/MockSession.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Test\Mock; use CodeIgniter\Cookie\Cookie; +use CodeIgniter\I18n\Time; use CodeIgniter\Session\Session; /** @@ -56,7 +57,7 @@ protected function startSession() */ protected function setCookie() { - $expiration = $this->sessionExpiration === 0 ? 0 : time() + $this->sessionExpiration; + $expiration = $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration; $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration); $this->cookies[] = $this->cookie; @@ -65,6 +66,6 @@ protected function setCookie() public function regenerate(bool $destroy = false) { $this->didRegenerate = true; - $_SESSION['__ci_last_regenerate'] = time(); + $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp(); } } diff --git a/system/Test/TestResponse.php b/system/Test/TestResponse.php index d84d01b681ee..700b27ef4f54 100644 --- a/system/Test/TestResponse.php +++ b/system/Test/TestResponse.php @@ -14,6 +14,7 @@ use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\I18n\Time; use Config\Services; use Exception; use PHPUnit\Framework\Constraint\IsEqual; @@ -339,7 +340,7 @@ public function assertCookieMissing(string $key) public function assertCookieExpired(string $key, string $prefix = '') { $this->assertTrue($this->response->hasCookie($key, null, $prefix)); - $this->assertGreaterThan(time(), $this->response->getCookie($key, $prefix)->getExpiresTimestamp()); + $this->assertGreaterThan(Time::now()->getTimestamp(), $this->response->getCookie($key, $prefix)->getExpiresTimestamp()); } // -------------------------------------------------------------------- diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php index 89c32981a841..bdca42ed8e9f 100644 --- a/system/Throttle/Throttler.php +++ b/system/Throttle/Throttler.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Throttle; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\I18n\Time; /** * Class Throttler @@ -176,6 +177,6 @@ public function setTestTime(int $time) */ public function time(): int { - return $this->testTime ?? time(); + return $this->testTime ?? Time::now()->getTimestamp(); } } diff --git a/system/View/Parser.php b/system/View/Parser.php index ea8c41fd1cfa..8d0331a9e1cf 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -257,7 +257,7 @@ protected function parse(string $template, array $data = [], ?array $options = n */ protected function parseSingle(string $key, string $val): array { - $pattern = '#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#ms'; + $pattern = '#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#ums'; return [$pattern => $val]; } @@ -277,7 +277,7 @@ protected function parsePair(string $variable, array $data, string $template): a // have something to loop over. preg_match_all( '#' . $this->leftDelimiter . '\s*' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '(.+?)' . - $this->leftDelimiter . '\s*/' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '#s', + $this->leftDelimiter . '\s*/' . preg_quote($variable, '#') . '\s*' . $this->rightDelimiter . '#us', $template, $matches, PREG_SET_ORDER @@ -329,7 +329,7 @@ protected function parsePair(string $variable, array $data, string $template): a $val = 'Resource'; } - $temp['#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#s'] = $val; + $temp['#' . $this->leftDelimiter . '!?\s*' . preg_quote($key, '#') . '(?(?=\s*\|\s*)(\s*\|*\s*([|\w<>=\(\),:.\-\s\+\\\\/]+)*\s*))(\s*)!?' . $this->rightDelimiter . '#us'] = $val; } // Now replace our placeholders with the new content. @@ -342,7 +342,7 @@ protected function parsePair(string $variable, array $data, string $template): a $escapedMatch = preg_quote($match[0], '#'); - $replace['#' . $escapedMatch . '#s'] = $str; + $replace['#' . $escapedMatch . '#us'] = $str; } return $replace; @@ -355,7 +355,7 @@ protected function parsePair(string $variable, array $data, string $template): a */ protected function parseComments(string $template): string { - return preg_replace('/\{#.*?#\}/s', '', $template); + return preg_replace('/\{#.*?#\}/us', '', $template); } /** @@ -364,7 +364,7 @@ protected function parseComments(string $template): string */ protected function extractNoparse(string $template): string { - $pattern = '/\{\s*noparse\s*\}(.*?)\{\s*\/noparse\s*\}/ms'; + $pattern = '/\{\s*noparse\s*\}(.*?)\{\s*\/noparse\s*\}/ums'; /* * $matches[][0] is the raw match @@ -413,7 +413,7 @@ protected function parseConditionals(string $template): string . $leftDelimiter . '\s*(if|elseif)\s*((?:\()?(.*?)(?:\))?)\s*' . $rightDelimiter - . '/ms'; + . '/ums'; /* * For each match: @@ -433,12 +433,12 @@ protected function parseConditionals(string $template): string } $template = preg_replace( - '/' . $leftDelimiter . '\s*else\s*' . $rightDelimiter . '/ms', + '/' . $leftDelimiter . '\s*else\s*' . $rightDelimiter . '/ums', '', $template ); $template = preg_replace( - '/' . $leftDelimiter . '\s*endif\s*' . $rightDelimiter . '/ms', + '/' . $leftDelimiter . '\s*endif\s*' . $rightDelimiter . '/ums', '', $template ); @@ -559,7 +559,7 @@ public function shouldAddEscaping(string $key) $escape = false; } // If no `esc` filter is found, then we'll need to add one. - elseif (! preg_match('/\s+esc/', $key)) { + elseif (! preg_match('/\s+esc/u', $key)) { $escape = 'html'; } @@ -575,7 +575,7 @@ protected function applyFilters(string $replace, array $filters): string // Determine the requested filters foreach ($filters as $filter) { // Grab any parameter we might need to send - preg_match('/\([\w<>=\/\\\,:.\-\s\+]+\)/', $filter, $param); + preg_match('/\([\w<>=\/\\\,:.\-\s\+]+\)/u', $filter, $param); // Remove the () and spaces to we have just the parameter left $param = ! empty($param) ? trim($param[0], '() ') : null; @@ -623,8 +623,8 @@ protected function parsePlugins(string $template) // See https://regex101.com/r/BCBBKB/1 $pattern = $isPair - ? '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}(.+?)\{\+\s*/' . $plugin . '\s*\+\}#ims' - : '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}#ims'; + ? '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}(.+?)\{\+\s*/' . $plugin . '\s*\+\}#uims' + : '#\{\+\s*' . $plugin . '([\w=\-_:\+\s\(\)/"@.]*)?\s*\+\}#uims'; /** * Match tag pairs @@ -641,7 +641,7 @@ protected function parsePlugins(string $template) foreach ($matches as $match) { $params = []; - preg_match_all('/([\w-]+=\"[^"]+\")|([\w-]+=[^\"\s=]+)|(\"[^"]+\")|(\S+)/', trim($match[1]), $matchesParams); + preg_match_all('/([\w-]+=\"[^"]+\")|([\w-]+=[^\"\s=]+)|(\"[^"]+\")|(\S+)/u', trim($match[1]), $matchesParams); foreach ($matchesParams[0] as $item) { $keyVal = explode('=', $item); diff --git a/tests/README.md b/tests/README.md index 6ea177ddb588..52ddc1e6711f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -19,17 +19,18 @@ writing we are running version 9.x. Support for this has been built into the via [Composer](https://getcomposer.org/) if you don't already have it installed globally. ```console -> composer install +composer install ``` If running under macOS or Linux, you can create a symbolic link to make running tests a touch nicer. ```console -> ln -s ./vendor/bin/phpunit ./phpunit +ln -s ./vendor/bin/phpunit ./phpunit ``` -You also need to install [XDebug](https://xdebug.org/docs/install) in order -for code coverage to be calculated successfully. After installing `XDebug`, you must add `xdebug.mode=coverage` in the **php.ini** file to enable code coverage. +You also need to install [Xdebug](https://xdebug.org/docs/install) in order +for code coverage to be calculated successfully. After installing `Xdebug`, you must +add `xdebug.mode=coverage` in the **php.ini** file to enable code coverage. ## Setting Up @@ -56,41 +57,55 @@ More details on a test database setup are in the [Testing Your Database](https://codeigniter4.github.io/CodeIgniter4/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** - -call it **phpunit.xml** - and comment out the `` named `Database`. This will make -the tests run quite a bit faster. +exclude `@DatabaseLive` group. This will make the tests run quite a bit faster. + +## Groups + +Each test class that we are running should belong to at least one +[@group](https://phpunit.readthedocs.io/en/9.5/annotations.html#group) that is written at class-level +doc block. + +The available groups to use are: + +| Group | Purpose | +| --------------- | --------------------------------------------------------------------- | +| AutoReview | Used for tests that perform automatic code reviews | +| CacheLive | Used for cache tests that need external services (redis, memcached) | +| DatabaseLive | Used for database tests that need to run on actual database drivers | +| SeparateProcess | Used for tests that need to run on separate PHP processes | +| Others | Used as a "catch-all" group for tests not falling in the above groups | ## Running the tests The entire test suite can be run by simply typing one command-line command from the main directory. ```console -> ./phpunit +./phpunit ``` If you are using Windows, use the following command. ```console -> vendor\bin\phpunit +vendor\bin\phpunit ``` You can limit tests to those within a single test directory by specifying the directory name after phpunit. All core tests are stored under **tests/system**. ```console -> ./phpunit tests/system/HTTP/ +./phpunit tests/system/HTTP/ ``` Individual tests can be run by including the relative path to the test file. ```console -> ./phpunit tests/system/HTTP/RequestTest.php +./phpunit tests/system/HTTP/RequestTest.php ``` You can run the tests without running the live database and the live cache tests. ```console -> ./phpunit --exclude-group DatabaseLive,CacheLive +./phpunit --exclude-group DatabaseLive,CacheLive ``` ## Generating Code Coverage @@ -99,7 +114,7 @@ To generate coverage information, including HTML reports you can view in your br you can use the following command: ```console -> ./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m +./phpunit --colors --coverage-text=tests/coverage.txt --coverage-html=tests/coverage/ -d memory_limit=1024m ``` This runs all of the tests again collecting information about how many lines, diff --git a/tests/_support/Models/WithoutAutoIncrementModel.php b/tests/_support/Models/WithoutAutoIncrementModel.php index dcbb8c4aba3c..de62bb3061e7 100644 --- a/tests/_support/Models/WithoutAutoIncrementModel.php +++ b/tests/_support/Models/WithoutAutoIncrementModel.php @@ -18,7 +18,6 @@ class WithoutAutoIncrementModel extends Model protected $table = 'without_auto_increment'; protected $primaryKey = 'key'; protected $allowedFields = [ - 'key', 'value', ]; protected $useAutoIncrement = false; diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index befb0184d781..0c060691b0b3 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -29,8 +29,16 @@ */ final class ResponseTraitTest extends CIUnitTestCase { + /** + * @var MockIncomingRequest|null + */ private $request; + + /** + * @var MockResponse|null + */ private $response; + private ?FormatterInterface $formatter = null; protected function setUp(): void diff --git a/tests/AutoReview/ComposerJsonTest.php b/tests/system/AutoReview/ComposerJsonTest.php similarity index 96% rename from tests/AutoReview/ComposerJsonTest.php rename to tests/system/AutoReview/ComposerJsonTest.php index 39763b079eca..80ebcaeeb172 100644 --- a/tests/AutoReview/ComposerJsonTest.php +++ b/tests/system/AutoReview/ComposerJsonTest.php @@ -21,7 +21,7 @@ * * @coversNothing * - * @group auto-review + * @group AutoReview */ final class ComposerJsonTest extends TestCase { @@ -32,8 +32,8 @@ protected function setUp(): void { parent::setUp(); - $this->devComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/composer.json'); - $this->frameworkComposer = $this->getComposerJson(dirname(__DIR__, 2) . '/admin/framework/composer.json'); + $this->devComposer = $this->getComposerJson(dirname(__DIR__, 3) . '/composer.json'); + $this->frameworkComposer = $this->getComposerJson(dirname(__DIR__, 3) . '/admin/framework/composer.json'); } public function testFrameworkRequireIsTheSameWithDevRequire(): void diff --git a/tests/system/AutoReview/FrameworkCodeTest.php b/tests/system/AutoReview/FrameworkCodeTest.php new file mode 100644 index 000000000000..f72e7ae9987f --- /dev/null +++ b/tests/system/AutoReview/FrameworkCodeTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\AutoReview; + +use FilesystemIterator; +use PHPUnit\Framework\TestCase; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use ReflectionClass; +use SplFileInfo; + +/** + * @internal + * + * @group AutoReview + */ +final class FrameworkCodeTest extends TestCase +{ + /** + * Cache of discovered test class names. + */ + private static array $testClasses = []; + + private static array $recognizedGroupAnnotations = [ + 'AutoReview', + 'CacheLive', + 'DatabaseLive', + 'Others', + 'SeparateProcess', + ]; + + /** + * @dataProvider provideTestClassCases + * + * @phpstan-param class-string $class + */ + public function testEachTestClassHasCorrectGroupAnnotation(string $class): void + { + $reflection = new ReflectionClass($class); + + if ($reflection->isAbstract()) { + $this->addToAssertionCount(1); + + return; + } + + $docComment = (string) $reflection->getDocComment(); + $this->assertNotEmpty($docComment, sprintf('[%s] Test class is missing a class-level PHPDoc.', $class)); + + preg_match_all('/@group (\S+)/', $docComment, $matches); + array_shift($matches); + $this->assertNotEmpty($matches[0], sprintf('[%s] Test class is missing a @group annotation.', $class)); + + $unrecognizedGroups = array_diff($matches[0], self::$recognizedGroupAnnotations); + $this->assertEmpty($unrecognizedGroups, sprintf( + "[%s] Unexpected @group annotation%s:\n%s\nExpected annotations to be in \"%s\".", + $class, + count($unrecognizedGroups) > 1 ? 's' : '', + implode("\n", array_map( + static fn (string $group): string => sprintf(' * @group %s', $group), + $unrecognizedGroups + )), + implode(', ', self::$recognizedGroupAnnotations) + )); + } + + public function provideTestClassCases(): iterable + { + foreach ($this->getTestClasses() as $class) { + yield $class => [$class]; + } + } + + private function getTestClasses(): array + { + if (self::$testClasses !== []) { + return self::$testClasses; + } + + helper('filesystem'); + + $directory = set_realpath(dirname(__DIR__), true); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $directory, + FilesystemIterator::SKIP_DOTS + ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + $testClasses = array_map( + static function (SplFileInfo $file) use ($directory): string { + $relativePath = substr_replace( + $file->getPathname(), + '', + 0, + strlen($directory) + ); + $relativePath = substr_replace( + $relativePath, + '', + strlen($relativePath) - strlen(DIRECTORY_SEPARATOR . $file->getBasename()) + ); + + return sprintf( + 'CodeIgniter\\%s%s%s', + strtr($relativePath, DIRECTORY_SEPARATOR, '\\'), + $relativePath === '' ? '' : '\\', + $file->getBasename('.' . $file->getExtension()) + ); + }, + array_filter( + iterator_to_array($iterator, false), + static fn (SplFileInfo $file): bool => $file->isFile() + && strpos($file->getPathname(), DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR) === false + && strpos($file->getPathname(), DIRECTORY_SEPARATOR . 'Views' . DIRECTORY_SEPARATOR) === false + ) + ); + + $testClasses = array_filter( + $testClasses, + static fn (string $class) => is_subclass_of($class, TestCase::class) + ); + + sort($testClasses); + + self::$testClasses = $testClasses; + + return $testClasses; + } +} diff --git a/tests/system/Autoloader/FileLocatorTest.php b/tests/system/Autoloader/FileLocatorTest.php index da2ec74eaed0..cf966cb0f5da 100644 --- a/tests/system/Autoloader/FileLocatorTest.php +++ b/tests/system/Autoloader/FileLocatorTest.php @@ -44,6 +44,8 @@ protected function setUp(): void 'CodeIgniter\\Devkit' => [ TESTPATH . '_support/', ], + 'Acme\SampleProject' => TESTPATH . '_support', + 'Acme\Sample' => TESTPATH . '_support/does/not/exists', ]); $this->locator = new FileLocator($autoloader); @@ -151,6 +153,15 @@ public function testLocateFileNotFoundWithBadNamespace() $this->assertFalse($this->locator->locateFile($file, 'Views')); } + public function testLocateFileWithProperNamespace() + { + $file = 'Acme\SampleProject\View\Views\simple'; + + $expected = ROOTPATH . 'tests/_support/View/Views/simple.php'; + + $this->assertSame($expected, $this->locator->locateFile($file, 'Views')); + } + public function testSearchSimple() { $expected = APPPATH . 'Config/App.php'; diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 3da2cc1ec1b9..fde654988252 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -23,6 +23,9 @@ */ final class CLITest extends CIUnitTestCase { + /** + * @var false|resource + */ private $stream_filter; protected function setUp(): void diff --git a/tests/system/CLI/CommandRunnerTest.php b/tests/system/CLI/CommandRunnerTest.php index b8fe7b19f896..d518b0aaa5df 100644 --- a/tests/system/CLI/CommandRunnerTest.php +++ b/tests/system/CLI/CommandRunnerTest.php @@ -24,7 +24,7 @@ final class CommandRunnerTest extends CIUnitTestCase { /** - * @var resource + * @var false|resource */ private $streamFilter; diff --git a/tests/system/CLI/ConsoleTest.php b/tests/system/CLI/ConsoleTest.php index 44610177189a..32fa3c1046ac 100644 --- a/tests/system/CLI/ConsoleTest.php +++ b/tests/system/CLI/ConsoleTest.php @@ -27,6 +27,10 @@ final class ConsoleTest extends CIUnitTestCase { private DotEnv $env; + + /** + * @var false|resource + */ private $stream_filter; protected function setUp(): void diff --git a/tests/system/Cache/Handlers/AbstractHandlerTest.php b/tests/system/Cache/Handlers/AbstractHandlerTest.php index 7ff02a729b49..75e45218e215 100644 --- a/tests/system/Cache/Handlers/AbstractHandlerTest.php +++ b/tests/system/Cache/Handlers/AbstractHandlerTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\I18n\Time; use CodeIgniter\Test\CIUnitTestCase; /** @@ -31,7 +32,7 @@ public function testGetMetaDataMiss() public function testGetMetaData() { - $time = time(); + $time = Time::now()->getTimestamp(); $this->handler->save(self::$key1, 'value'); $actual = $this->handler->getMetaData(self::$key1); diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index abba309ff516..dde4ca86e13d 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -13,6 +13,7 @@ use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\CLI\CLI; +use CodeIgniter\I18n\Time; use Config\Cache; /** @@ -156,7 +157,7 @@ public function testSavePermanent() $metaData = $this->handler->getMetaData(self::$key1); $this->assertNull($metaData['expire']); - $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp()); $this->assertSame('value', $metaData['data']); $this->assertTrue($this->handler->delete(self::$key1)); diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index f6e2e45309c9..24465af9e7de 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\CLI\CLI; +use CodeIgniter\I18n\Time; use Config\Cache; use Exception; @@ -105,7 +106,7 @@ public function testSavePermanent() $metaData = $this->handler->getMetaData(self::$key1); $this->assertNull($metaData['expire']); - $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp()); $this->assertSame('value', $metaData['data']); $this->assertTrue($this->handler->delete(self::$key1)); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 60ed0a0e0c07..17a43e36263f 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\CLI\CLI; +use CodeIgniter\I18n\Time; use Config\Cache; /** @@ -108,7 +109,7 @@ public function testSavePermanent() $metaData = $this->handler->getMetaData(self::$key1); $this->assertNull($metaData['expire']); - $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp()); $this->assertSame('value', $metaData['data']); $this->assertTrue($this->handler->delete(self::$key1)); diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 2c79be076eca..a31561336873 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\CLI\CLI; +use CodeIgniter\I18n\Time; use Config\Cache; /** @@ -112,7 +113,7 @@ public function testSavePermanent() $metaData = $this->handler->getMetaData(self::$key1); $this->assertNull($metaData['expire']); - $this->assertLessThanOrEqual(1, $metaData['mtime'] - time()); + $this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp()); $this->assertSame('value', $metaData['data']); $this->assertTrue($this->handler->delete(self::$key1)); diff --git a/tests/system/Commands/BaseCommandTest.php b/tests/system/Commands/BaseCommandTest.php index a399f84f00d4..e1b857e40060 100644 --- a/tests/system/Commands/BaseCommandTest.php +++ b/tests/system/Commands/BaseCommandTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Commands; +use CodeIgniter\Log\Logger; use CodeIgniter\Test\CIUnitTestCase; use Config\Services; use Tests\Support\Commands\AppInfo; @@ -22,7 +23,7 @@ */ final class BaseCommandTest extends CIUnitTestCase { - private $logger; + private Logger $logger; protected function setUp(): void { diff --git a/tests/system/Commands/ClearCacheTest.php b/tests/system/Commands/ClearCacheTest.php index 49c844a42292..6fbf7e3f0054 100644 --- a/tests/system/Commands/ClearCacheTest.php +++ b/tests/system/Commands/ClearCacheTest.php @@ -23,6 +23,9 @@ */ final class ClearCacheTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/ClearDebugbarTest.php b/tests/system/Commands/ClearDebugbarTest.php index ab068a41a530..ea3afd921e7d 100644 --- a/tests/system/Commands/ClearDebugbarTest.php +++ b/tests/system/Commands/ClearDebugbarTest.php @@ -21,8 +21,12 @@ */ final class ClearDebugbarTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; - private $time; + + private int $time; protected function setUp(): void { diff --git a/tests/system/Commands/ClearLogsTest.php b/tests/system/Commands/ClearLogsTest.php index e8ca7d1a7443..b5a51ebd0997 100644 --- a/tests/system/Commands/ClearLogsTest.php +++ b/tests/system/Commands/ClearLogsTest.php @@ -21,8 +21,12 @@ */ final class ClearLogsTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; - private $date; + + private string $date; protected function setUp(): void { diff --git a/tests/system/Commands/CommandGeneratorTest.php b/tests/system/Commands/CommandGeneratorTest.php index f674b1fcf6b3..1fed972dbf71 100644 --- a/tests/system/Commands/CommandGeneratorTest.php +++ b/tests/system/Commands/CommandGeneratorTest.php @@ -21,6 +21,9 @@ */ final class CommandGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/CommandTest.php b/tests/system/Commands/CommandTest.php index 3e4b64a50d3a..39c1bda995c7 100644 --- a/tests/system/Commands/CommandTest.php +++ b/tests/system/Commands/CommandTest.php @@ -11,6 +11,8 @@ namespace CodeIgniter\Commands; +use CodeIgniter\CLI\Commands; +use CodeIgniter\Log\Logger; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Filters\CITestStreamFilter; use Config\Services; @@ -23,9 +25,13 @@ */ final class CommandTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; - private $logger; - private $commands; + + private Logger $logger; + private Commands $commands; protected function setUp(): void { diff --git a/tests/system/Commands/ConfigGeneratorTest.php b/tests/system/Commands/ConfigGeneratorTest.php index 9c043013fe4d..ab3e09d5c35e 100644 --- a/tests/system/Commands/ConfigGeneratorTest.php +++ b/tests/system/Commands/ConfigGeneratorTest.php @@ -21,6 +21,9 @@ */ final class ConfigGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/ConfigurableSortImportsTest.php b/tests/system/Commands/ConfigurableSortImportsTest.php index 1888b8fb06f1..d80bd74f1ab9 100644 --- a/tests/system/Commands/ConfigurableSortImportsTest.php +++ b/tests/system/Commands/ConfigurableSortImportsTest.php @@ -21,6 +21,9 @@ */ final class ConfigurableSortImportsTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/ControllerGeneratorTest.php b/tests/system/Commands/ControllerGeneratorTest.php index 24d72c5f5bf1..ac391b751c50 100644 --- a/tests/system/Commands/ControllerGeneratorTest.php +++ b/tests/system/Commands/ControllerGeneratorTest.php @@ -21,6 +21,9 @@ */ final class ControllerGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php index 4a55df843777..7fcc3c673304 100644 --- a/tests/system/Commands/CreateDatabaseTest.php +++ b/tests/system/Commands/CreateDatabaseTest.php @@ -26,19 +26,42 @@ */ final class CreateDatabaseTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; + private BaseConnection $connection; protected function setUp(): void { + parent::setUp(); + CITestStreamFilter::$buffer = ''; $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); $this->connection = Database::connect(); - parent::setUp(); + $this->dropDatabase(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + stream_filter_remove($this->streamFilter); + + $this->dropDatabase(); + } + protected function getBuffer() + { + return CITestStreamFilter::$buffer; + } + + private function dropDatabase(): void + { if ($this->connection instanceof SQLite3Connection) { $file = WRITEPATH . 'foobar.db'; if (is_file($file)) { @@ -53,18 +76,6 @@ protected function setUp(): void } } - protected function tearDown(): void - { - stream_filter_remove($this->streamFilter); - - parent::tearDown(); - } - - protected function getBuffer() - { - return CITestStreamFilter::$buffer; - } - public function testCreateDatabase() { if ($this->connection instanceof OCI8Connection) { diff --git a/tests/system/Commands/Database/MigrateStatusTest.php b/tests/system/Commands/Database/MigrateStatusTest.php index 20e7485ca17f..041cfbe57b4b 100644 --- a/tests/system/Commands/Database/MigrateStatusTest.php +++ b/tests/system/Commands/Database/MigrateStatusTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Commands\Database; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Filters\CITestStreamFilter; @@ -21,7 +22,11 @@ */ final class MigrateStatusTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; + private string $migrationFileFrom = SUPPORTPATH . 'MigrationTestMigrations/Database/Migrations/2018-01-24-102301_Some_migration.php'; private string $migrationFileTo = APPPATH . 'Database/Migrations/2018-01-24-102301_Some_migration.php'; @@ -47,6 +52,9 @@ protected function setUp(): void ); file_put_contents($this->migrationFileTo, $contents); + putenv('NO_COLOR=1'); + CLI::init(); + CITestStreamFilter::$buffer = ''; $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); @@ -64,6 +72,9 @@ protected function tearDown(): void @unlink($this->migrationFileTo); } + putenv('NO_COLOR'); + CLI::init(); + stream_filter_remove($this->streamFilter); } @@ -74,7 +85,7 @@ public function testMigrateAllWithWithTwoNamespaces(): void command('migrate:status'); - $result = str_replace(["\033[0;33m", "\033[0m"], '', CITestStreamFilter::$buffer); + $result = str_replace(PHP_EOL, "\n", CITestStreamFilter::$buffer); $result = preg_replace('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', 'YYYY-MM-DD HH:MM:SS', $result); $expected = <<<'EOL' +---------------+-------------------+--------------------+-------+---------------------+-------+ @@ -97,7 +108,7 @@ public function testMigrateWithWithTwoNamespaces(): void command('migrate:status'); - $result = str_replace(["\033[0;33m", "\033[0m"], '', CITestStreamFilter::$buffer); + $result = str_replace(PHP_EOL, "\n", CITestStreamFilter::$buffer); $result = preg_replace('/\d{4}-\d\d-\d\d \d\d:\d\d:\d\d/', 'YYYY-MM-DD HH:MM:SS', $result); $expected = <<<'EOL' +---------------+-------------------+--------------------+-------+---------------------+-------+ diff --git a/tests/system/Commands/Database/ShowTableInfoTest.php b/tests/system/Commands/Database/ShowTableInfoTest.php index 1cde7280c164..b871862b072d 100644 --- a/tests/system/Commands/Database/ShowTableInfoTest.php +++ b/tests/system/Commands/Database/ShowTableInfoTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Commands\Database; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\Filters\CITestStreamFilter; @@ -26,13 +27,20 @@ final class ShowTableInfoTest extends CIUnitTestCase { use DatabaseTestTrait; + /** + * @var false|resource + */ private $streamFilter; + protected $migrateOnce = true; protected function setUp(): void { parent::setUp(); + putenv('NO_COLOR=1'); + CLI::init(); + CITestStreamFilter::$buffer = ''; $this->streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); @@ -43,40 +51,35 @@ protected function tearDown(): void { parent::tearDown(); + putenv('NO_COLOR'); + CLI::init(); + stream_filter_remove($this->streamFilter); } - private function getResultWithoutControlCode(): string + private function getNormalizedResult(): string { - return str_replace( - ["\033[0;30m", "\033[0;33m", "\033[43m", "\033[0m"], - '', - CITestStreamFilter::$buffer - ); + return str_replace(PHP_EOL, "\n", CITestStreamFilter::$buffer); } public function testDbTable(): void { command('db:table db_migrations'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'Data of Table "db_migrations":'; $this->assertStringContainsString($expected, $result); - $expected = <<<'EOL' - +----+----------------+--------------------+-------+---------------+------------+-------+ - | id | version | class | group | namespace | time | batch | - +----+----------------+--------------------+-------+---------------+------------+-------+ - EOL; - $this->assertStringContainsString($expected, $result); + $expectedPattern = '/\| id[[:blank:]]+\| version[[:blank:]]+\| class[[:blank:]]+\| group[[:blank:]]+\| namespace[[:blank:]]+\| time[[:blank:]]+\| batch \|/'; + $this->assertMatchesRegularExpression($expectedPattern, $result); } public function testDbTableShow(): void { command('db:table --show'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'The following is a list of the names of all database tables:'; $this->assertStringContainsString($expected, $result); @@ -93,7 +96,7 @@ public function testDbTableMetadata(): void { command('db:table db_migrations --metadata'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'List of Metadata Information in Table "db_migrations":'; $this->assertStringContainsString($expected, $result); @@ -112,7 +115,7 @@ public function testDbTableDesc(): void command('db:table db_user --desc'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'Data of Table "db_user":'; $this->assertStringContainsString($expected, $result); @@ -134,7 +137,7 @@ public function testDbTableLimitFieldValueLength(): void { command('db:table db_user --limit-field-value 5'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'Data of Table "db_user":'; $this->assertStringContainsString($expected, $result); @@ -156,7 +159,7 @@ public function testDbTableLimitRows(): void { command('db:table db_user --limit-rows 2'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'Data of Table "db_user":'; $this->assertStringContainsString($expected, $result); @@ -176,7 +179,7 @@ public function testDbTableAllOptions(): void { command('db:table db_user --limit-rows 2 --limit-field-value 5 --desc'); - $result = $this->getResultWithoutControlCode(); + $result = $this->getNormalizedResult(); $expected = 'Data of Table "db_user":'; $this->assertStringContainsString($expected, $result); diff --git a/tests/system/Commands/DatabaseCommandsTest.php b/tests/system/Commands/DatabaseCommandsTest.php index 3938ea012aa7..db301911e5a8 100644 --- a/tests/system/Commands/DatabaseCommandsTest.php +++ b/tests/system/Commands/DatabaseCommandsTest.php @@ -21,6 +21,9 @@ */ final class DatabaseCommandsTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/EntityGeneratorTest.php b/tests/system/Commands/EntityGeneratorTest.php index bd4c60e49969..2e8a38b0f52b 100644 --- a/tests/system/Commands/EntityGeneratorTest.php +++ b/tests/system/Commands/EntityGeneratorTest.php @@ -21,6 +21,9 @@ */ final class EntityGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/EnvironmentCommandTest.php b/tests/system/Commands/EnvironmentCommandTest.php index 0cff8ec1e8ab..5ba8837271c8 100644 --- a/tests/system/Commands/EnvironmentCommandTest.php +++ b/tests/system/Commands/EnvironmentCommandTest.php @@ -21,7 +21,11 @@ */ final class EnvironmentCommandTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; + private string $envPath = ROOTPATH . '.env'; private string $backupEnvPath = ROOTPATH . '.env.backup'; diff --git a/tests/system/Commands/FilterGeneratorTest.php b/tests/system/Commands/FilterGeneratorTest.php index 388e44ab8aff..d899432d7241 100644 --- a/tests/system/Commands/FilterGeneratorTest.php +++ b/tests/system/Commands/FilterGeneratorTest.php @@ -21,6 +21,9 @@ */ final class FilterGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/GenerateKeyTest.php b/tests/system/Commands/GenerateKeyTest.php index fbe95abc5564..b822354bc8e0 100644 --- a/tests/system/Commands/GenerateKeyTest.php +++ b/tests/system/Commands/GenerateKeyTest.php @@ -21,7 +21,11 @@ */ final class GenerateKeyTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; + private string $envPath; private string $backupEnvPath; @@ -74,13 +78,13 @@ protected function resetEnvironment() public function testGenerateKeyShowsEncodedKey() { - command('key:generate -show'); + command('key:generate --show'); $this->assertStringContainsString('hex2bin:', $this->getBuffer()); - command('key:generate -prefix base64 -show'); + command('key:generate --prefix base64 --show'); $this->assertStringContainsString('base64:', $this->getBuffer()); - command('key:generate -prefix hex2bin -show'); + command('key:generate --prefix hex2bin --show'); $this->assertStringContainsString('hex2bin:', $this->getBuffer()); } @@ -95,12 +99,12 @@ public function testGenerateKeyCreatesNewKey() $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); $this->assertStringContainsString('hex2bin:', file_get_contents($this->envPath)); - command('key:generate -prefix base64 -force'); + command('key:generate --prefix base64 --force'); $this->assertStringContainsString('successfully set.', $this->getBuffer()); $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); $this->assertStringContainsString('base64:', file_get_contents($this->envPath)); - command('key:generate -prefix hex2bin -force'); + command('key:generate --prefix hex2bin --force'); $this->assertStringContainsString('successfully set.', $this->getBuffer()); $this->assertStringContainsString(env('encryption.key'), file_get_contents($this->envPath)); $this->assertStringContainsString('hex2bin:', file_get_contents($this->envPath)); @@ -115,4 +119,53 @@ public function testDefaultShippedEnvIsMissing() $this->assertStringContainsString('Both default shipped', $this->getBuffer()); $this->assertStringContainsString('Error in setting', $this->getBuffer()); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/6838 + */ + public function testKeyGenerateWhenKeyIsMissingInDotEnvFile() + { + file_put_contents($this->envPath, ''); + + command('key:generate'); + + $this->assertStringContainsString('Application\'s new encryption key was successfully set.', $this->getBuffer()); + $this->assertSame("\nencryption.key = " . env('encryption.key'), file_get_contents($this->envPath)); + } + + public function testKeyGenerateWhenNewHexKeyIsSubsequentlyCommentedOut() + { + command('key:generate'); + $key = env('encryption.key', ''); + file_put_contents($this->envPath, str_replace( + 'encryption.key = ' . $key, + '# encryption.key = ' . $key, + file_get_contents($this->envPath), + $count + )); + $this->assertSame(1, $count, 'Failed commenting out the previously set application key.'); + + CITestStreamFilter::$buffer = ''; + command('key:generate --force'); + $this->assertStringContainsString('was successfully set.', $this->getBuffer()); + $this->assertNotSame($key, env('encryption.key', $key), 'Failed replacing the commented out key.'); + } + + public function testKeyGenerateWhenNewBase64KeyIsSubsequentlyCommentedOut() + { + command('key:generate --prefix base64'); + $key = env('encryption.key', ''); + file_put_contents($this->envPath, str_replace( + 'encryption.key = ' . $key, + '# encryption.key = ' . $key, + file_get_contents($this->envPath), + $count + )); + $this->assertSame(1, $count, 'Failed commenting out the previously set application key.'); + + CITestStreamFilter::$buffer = ''; + command('key:generate --force'); + $this->assertStringContainsString('was successfully set.', $this->getBuffer()); + $this->assertNotSame($key, env('encryption.key', $key), 'Failed replacing the commented out key.'); + } } diff --git a/tests/system/Commands/GeneratorsTest.php b/tests/system/Commands/GeneratorsTest.php index 7b19bf1a97fe..072915e0cdb1 100644 --- a/tests/system/Commands/GeneratorsTest.php +++ b/tests/system/Commands/GeneratorsTest.php @@ -21,6 +21,9 @@ */ final class GeneratorsTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/HelpCommandTest.php b/tests/system/Commands/HelpCommandTest.php index ea9d512e28d4..94c5b43a0f0b 100644 --- a/tests/system/Commands/HelpCommandTest.php +++ b/tests/system/Commands/HelpCommandTest.php @@ -21,6 +21,9 @@ */ final class HelpCommandTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/InfoCacheTest.php b/tests/system/Commands/InfoCacheTest.php index a11eff9c372e..d53c5f5bd6da 100644 --- a/tests/system/Commands/InfoCacheTest.php +++ b/tests/system/Commands/InfoCacheTest.php @@ -23,6 +23,9 @@ */ final class InfoCacheTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/MigrationGeneratorTest.php b/tests/system/Commands/MigrationGeneratorTest.php index 05d7cd0a5b19..725cbae1b3be 100644 --- a/tests/system/Commands/MigrationGeneratorTest.php +++ b/tests/system/Commands/MigrationGeneratorTest.php @@ -21,6 +21,9 @@ */ final class MigrationGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index 68de997ea7a5..f378040ab261 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -21,7 +21,11 @@ */ final class MigrationIntegrationTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; + private string $migrationFileFrom = SUPPORTPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; private string $migrationFileTo = APPPATH . 'Database/Migrations/20160428212500_Create_test_tables.php'; diff --git a/tests/system/Commands/ModelGeneratorTest.php b/tests/system/Commands/ModelGeneratorTest.php index 69c61e8be163..f15451b3e786 100644 --- a/tests/system/Commands/ModelGeneratorTest.php +++ b/tests/system/Commands/ModelGeneratorTest.php @@ -21,6 +21,9 @@ */ final class ModelGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/PublishCommandTest.php b/tests/system/Commands/PublishCommandTest.php index 4f798ef922c9..85867561e3ee 100644 --- a/tests/system/Commands/PublishCommandTest.php +++ b/tests/system/Commands/PublishCommandTest.php @@ -22,6 +22,9 @@ */ final class PublishCommandTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/RoutesTest.php index 70b5cfdc34ba..bd061ec48149 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/RoutesTest.php @@ -22,6 +22,9 @@ */ final class RoutesTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/ScaffoldGeneratorTest.php b/tests/system/Commands/ScaffoldGeneratorTest.php index 4dc298c32524..2b985b097652 100644 --- a/tests/system/Commands/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/ScaffoldGeneratorTest.php @@ -24,6 +24,9 @@ */ final class ScaffoldGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/SeederGeneratorTest.php b/tests/system/Commands/SeederGeneratorTest.php index bbb261183e02..79d6cde8fd98 100644 --- a/tests/system/Commands/SeederGeneratorTest.php +++ b/tests/system/Commands/SeederGeneratorTest.php @@ -21,6 +21,9 @@ */ final class SeederGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Commands/ValidationGeneratorTest.php b/tests/system/Commands/ValidationGeneratorTest.php index 316562751cf0..0d801947562d 100644 --- a/tests/system/Commands/ValidationGeneratorTest.php +++ b/tests/system/Commands/ValidationGeneratorTest.php @@ -21,6 +21,9 @@ */ final class ValidationGeneratorTest extends CIUnitTestCase { + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index b94bcf4cb27b..58854f27df68 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -36,6 +36,8 @@ public function testSingleServiceWithNoParamsSupplied(string $service): void $service1 = single_service($service); $service2 = single_service($service); + assert($service1 !== null); + $this->assertInstanceOf(get_class($service1), $service2); $this->assertNotSame($service1, $service2); } @@ -64,6 +66,8 @@ public function testSingleServiceWithAtLeastOneParamSupplied(string $service): v $service1 = single_service($service, ...$params); $service2 = single_service($service, ...$params); + assert($service1 !== null); + $this->assertInstanceOf(get_class($service1), $service2); $this->assertNotSame($service1, $service2); @@ -77,6 +81,9 @@ public function testSingleServiceWithAllParamsSupplied(): void $cache1 = single_service('cache', null, true); $cache2 = single_service('cache', null, true); + assert($cache1 !== null); + assert($cache2 !== null); + // Assert that even passing true as last param this will // not create a shared instance. $this->assertInstanceOf(get_class($cache1), $cache2); diff --git a/tests/system/Config/BaseConfigTest.php b/tests/system/Config/BaseConfigTest.php index 71ef095f0fcf..014deca6c344 100644 --- a/tests/system/Config/BaseConfigTest.php +++ b/tests/system/Config/BaseConfigTest.php @@ -26,7 +26,7 @@ */ final class BaseConfigTest extends CIUnitTestCase { - private $fixturesFolder; + private string $fixturesFolder; protected function setUp(): void { diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index b35ab39b5fb6..f47376ef3786 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -26,7 +26,7 @@ final class DotEnvTest extends CIUnitTestCase { private ?vfsStreamDirectory $root; private string $path; - private $fixturesFolder; + private string $fixturesFolder; protected function setUp(): void { diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index ac515816fc0c..143591aac10c 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -53,8 +53,8 @@ */ final class ServicesTest extends CIUnitTestCase { - private $config; - private $original; + private App $config; + private array $original; protected function setUp(): void { diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 3c4095044099..7aefdba65dd8 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -39,11 +39,7 @@ final class ControllerTest extends CIUnitTestCase { private App $config; - - /** - * @var Controller - */ - private $controller; + private ?Controller $controller = null; /** * Current request. diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index adf2fd35db97..300e7f7fe5e0 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -100,9 +100,6 @@ public function testCanConnectAndStoreConnection() $this->assertSame(123, $db->getConnection()); } - /** - * @group single - */ public function testCanConnectToFailoverWhenNoConnectionAvailable() { $options = $this->options; diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index c7eca5f2b9a3..273c52491c15 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -305,8 +305,6 @@ public function testSimpleBindsWithNamedBindPlaceholderElsewhere() } /** - * @group single - * * @see https://github.com/codeigniter4/CodeIgniter4/issues/201 */ public function testSimilarNamedBinds() diff --git a/tests/system/Database/Builder/LikeTest.php b/tests/system/Database/Builder/LikeTest.php index 03ba724646bb..23e7943a3b56 100644 --- a/tests/system/Database/Builder/LikeTest.php +++ b/tests/system/Database/Builder/LikeTest.php @@ -189,9 +189,6 @@ public function testOrNotLike() $this->assertSame($expectedBinds, $builder->getBinds()); } - /** - * @group single - */ public function testCaseInsensitiveLike() { $builder = new BaseBuilder('job', $this->db); diff --git a/tests/system/Database/Live/DeleteTest.php b/tests/system/Database/Live/DeleteTest.php index 9badfb6ec516..68e2a41cb60f 100644 --- a/tests/system/Database/Live/DeleteTest.php +++ b/tests/system/Database/Live/DeleteTest.php @@ -53,11 +53,6 @@ public function testDeleteWithInternalWhere() $this->dontSeeInDatabase('job', ['name' => 'Developer']); } - /** - * @group single - * - * @throws DatabaseException - */ public function testDeleteWithLimit() { $this->seeNumRecords(2, 'user', ['country' => 'US']); diff --git a/tests/system/Database/Live/EscapeTest.php b/tests/system/Database/Live/EscapeTest.php index 8aacd092ff16..53be5c307b32 100644 --- a/tests/system/Database/Live/EscapeTest.php +++ b/tests/system/Database/Live/EscapeTest.php @@ -24,7 +24,7 @@ final class EscapeTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = false; - private $char; + private string $char; protected function setUp(): void { diff --git a/tests/system/Database/Live/MetadataTest.php b/tests/system/Database/Live/MetadataTest.php index 40a16043c76a..d698f647abe5 100644 --- a/tests/system/Database/Live/MetadataTest.php +++ b/tests/system/Database/Live/MetadataTest.php @@ -11,7 +11,6 @@ namespace CodeIgniter\Database\Live; -use CodeIgniter\Database\Forge; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Config\Database; @@ -26,22 +25,22 @@ final class MetadataTest extends CIUnitTestCase { use DatabaseTestTrait; - private ?Forge $forge = null; - protected $refresh = true; - protected $seed = CITestSeeder::class; - /** - * Array of expected tables. + * The seed file used for all tests within this test case. + * + * @var string */ - private array $expectedTables; + protected $seed = CITestSeeder::class; + + private array $expectedTables = []; protected function setUp(): void { parent::setUp(); - // Prepare the array of expected tables once - $prefix = $this->db->getPrefix(); - $this->expectedTables = [ + $prefix = $this->db->getPrefix(); + + $tables = [ $prefix . 'migrations', $prefix . 'user', $prefix . 'job', @@ -55,55 +54,90 @@ protected function setUp(): void ]; if (in_array($this->db->DBDriver, ['MySQLi', 'Postgre'], true)) { - $this->expectedTables[] = $prefix . 'ci_sessions'; + $tables[] = $prefix . 'ci_sessions'; } + + sort($tables); + $this->expectedTables = $tables; } - public function testListTables() + private function createExtraneousTable(): void { - $result = $this->db->listTables(true); + $oldPrefix = $this->db->getPrefix(); + $this->db->setPrefix('tmp_'); + + Database::forge($this->DBGroup) + ->addField([ + 'name' => ['type' => 'varchar', 'constraint' => 31], + 'created_at' => ['type' => 'datetime', 'null' => true], + ]) + ->createTable('widgets'); - $this->assertSame($this->expectedTables, array_values($result)); + $this->db->setPrefix($oldPrefix); } - public function testListTablesConstrainPrefix() + private function dropExtraneousTable(): void { - $result = $this->db->listTables(true); + $oldPrefix = $this->db->getPrefix(); + $this->db->setPrefix('tmp_'); + + Database::forge($this->DBGroup)->dropTable('widgets'); - $this->assertSame($this->expectedTables, array_values($result)); + $this->db->setPrefix($oldPrefix); } - public function testConstrainPrefixIgnoresOtherTables() + public function testListTablesUnconstrainedByPrefixReturnsAllTables() { - $this->forge = Database::forge($this->DBGroup); + try { + $this->createExtraneousTable(); - // Stash the prefix and change it - $DBPrefix = $this->db->getPrefix(); - $this->db->setPrefix('tmp_'); + $tables = $this->db->listTables(); + $this->assertIsArray($tables); + $this->assertNotSame([], $tables); - // Create a table with the new prefix - $fields = [ - 'name' => [ - 'type' => 'varchar', - 'constraint' => 31, - ], - 'created_at' => [ - 'type' => 'datetime', - 'null' => true, - ], - ]; - $this->forge->addField($fields); - $this->forge->createTable('widgets'); + $expectedTables = $this->expectedTables; + $expectedTables[] = 'tmp_widgets'; - // Restore the prefix and get the tables with the original prefix - $this->db->setPrefix($DBPrefix); - $result = $this->db->listTables(true); + sort($tables); + $this->assertSame($expectedTables, array_values($tables)); + } finally { + $this->dropExtraneousTable(); + } + } - $this->assertSame($this->expectedTables, array_values($result)); + public function testListTablesConstrainedByPrefixReturnsOnlyTablesWithMatchingPrefix() + { + try { + $this->createExtraneousTable(); - // Clean up temporary table - $this->db->setPrefix('tmp_'); - $this->forge->dropTable('widgets'); - $this->db->setPrefix($DBPrefix); + $tables = $this->db->listTables(true); + $this->assertIsArray($tables); + $this->assertNotSame([], $tables); + + sort($tables); + $this->assertSame($this->expectedTables, array_values($tables)); + } finally { + $this->dropExtraneousTable(); + } + } + + public function testListTablesConstrainedByExtraneousPrefixReturnsOnlyTheExtraneousTable() + { + try { + $this->createExtraneousTable(); + + $oldPrefix = $this->db->getPrefix(); + $this->db->setPrefix('tmp_'); + + $tables = $this->db->listTables(true); + $this->assertIsArray($tables); + $this->assertNotSame([], $tables); + + sort($tables); + $this->assertSame(['tmp_widgets'], array_values($tables)); + } finally { + $this->db->setPrefix($oldPrefix); + $this->dropExtraneousTable(); + } } } diff --git a/tests/system/Database/Live/SQLite/AlterTableTest.php b/tests/system/Database/Live/SQLite/AlterTableTest.php index 60baaae441f8..4097a4a97c75 100644 --- a/tests/system/Database/Live/SQLite/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite/AlterTableTest.php @@ -22,6 +22,8 @@ /** * @group DatabaseLive * + * @requires extension sqlite3 + * * @internal */ final class AlterTableTest extends CIUnitTestCase @@ -36,12 +38,6 @@ final class AlterTableTest extends CIUnitTestCase protected $migrate = false; private Table $table; - - /** - * @var Connection - */ - protected $db; - private Forge $forge; protected function setUp(): void @@ -50,7 +46,7 @@ protected function setUp(): void $config = [ 'DBDriver' => 'SQLite3', - 'database' => 'database.db', + 'database' => ':memory:', 'DBDebug' => true, ]; diff --git a/tests/system/Database/Live/TransactionTest.php b/tests/system/Database/Live/TransactionTest.php new file mode 100644 index 000000000000..7dff9eb74329 --- /dev/null +++ b/tests/system/Database/Live/TransactionTest.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database\Live; + +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\DatabaseTestTrait; +use Config\Database; +use Exception; +use Tests\Support\Database\Seeds\CITestSeeder; + +/** + * @group DatabaseLive + * + * @internal + */ +final class TransactionTest extends CIUnitTestCase +{ + use DatabaseTestTrait; + + protected $refresh = true; + protected $seed = CITestSeeder::class; + + protected function setUp(): void + { + // Reset connection instance. + $this->db = Database::connect($this->DBGroup, false); + + parent::setUp(); + } + + /** + * Sets $DBDebug to false. + * + * WARNING: this value will persist! take care to roll it back. + */ + protected function disableDBDebug(): void + { + $this->setPrivateProperty($this->db, 'DBDebug', false); + } + + /** + * Sets $DBDebug to true. + */ + protected function enableDBDebug(): void + { + $this->setPrivateProperty($this->db, 'DBDebug', true); + } + + public function testTransStartDBDebugTrue() + { + $builder = $this->db->table('job'); + $e = null; + + try { + $this->db->transStart(); + + $jobData = [ + 'name' => 'Grocery Sales', + 'description' => 'Discount!', + ]; + $builder->insert($jobData); + + // Duplicate entry '1' for key 'PRIMARY' + $jobData = [ + 'id' => 1, + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->db->transComplete(); + } catch (Exception $e) { + // Do nothing. + + // MySQLi + // mysqli_sql_exception: Duplicate entry '1' for key 'PRIMARY' + + // SQLite3 + // ErrorException: SQLite3::exec(): UNIQUE constraint failed: db_job.id + + // Postgres + // ErrorException: pg_query(): Query failed: ERROR: duplicate key value violates unique constraint "pk_db_job" + // DETAIL: Key (id)=(1) already exists. + + // SQLSRV + // Exception: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot insert explicit + // value for identity column in table 'db_job' when IDENTITY_INSERT is set to OFF. + + // OCI8 + // ErrorException: oci_execute(): ORA-00001: unique constraint (ORACLE.pk_db_job) violated + } + + $this->assertInstanceOf(Exception::class, $e); + $this->dontSeeInDatabase('job', ['name' => 'Grocery Sales']); + } + + public function testTransStartDBDebugFalse() + { + $this->disableDBDebug(); + + $builder = $this->db->table('job'); + + $this->db->transStart(); + + $jobData = [ + 'name' => 'Grocery Sales', + 'description' => 'Discount!', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + // Duplicate entry '1' for key 'PRIMARY' + $jobData = [ + 'id' => 1, + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertFalse($this->db->transStatus()); + + $this->db->transComplete(); + + $this->dontSeeInDatabase('job', ['name' => 'Grocery Sales']); + + $this->enableDBDebug(); + } + + public function testTransStrictTrueAndDBDebugFalse() + { + $this->disableDBDebug(); + + $builder = $this->db->table('job'); + + // The first transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Grocery Sales', + 'description' => 'Discount!', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + // Duplicate entry '1' for key 'PRIMARY' + $jobData = [ + 'id' => 1, + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertFalse($this->db->transStatus()); + + $this->db->transComplete(); + + $this->dontSeeInDatabase('job', ['name' => 'Grocery Sales']); + + // The second transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertFalse($this->db->transStatus()); + + $this->db->transComplete(); + + $this->dontSeeInDatabase('job', ['name' => 'Comedian']); + + $this->enableDBDebug(); + } + + public function testTransStrictFalseAndDBDebugFalse() + { + $this->disableDBDebug(); + + $builder = $this->db->table('job'); + + $this->db->transStrict(false); + + // The first transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Grocery Sales', + 'description' => 'Discount!', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + // Duplicate entry '1' for key 'PRIMARY' + $jobData = [ + 'id' => 1, + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertFalse($this->db->transStatus()); + + $this->db->transComplete(); + + $this->dontSeeInDatabase('job', ['name' => 'Grocery Sales']); + + // The second transaction group + $this->db->transStart(); + + $jobData = [ + 'name' => 'Comedian', + 'description' => 'Theres something in your teeth', + ]; + $builder->insert($jobData); + + $this->assertTrue($this->db->transStatus()); + + $this->db->transComplete(); + + $this->seeInDatabase('job', ['name' => 'Comedian']); + + $this->enableDBDebug(); + } +} diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index d69db467cdc2..1b2e8473c47e 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -181,9 +181,7 @@ public function testUpdateWithWhereSameColumn3() } /** - * @group single - * - * @see https://github.com/codeigniter4/CodeIgniter4/issues/324 + * @see https://github.com/codeigniter4/CodeIgniter4/issues/324 */ public function testUpdatePeriods() { diff --git a/tests/system/Database/Live/WhereTest.php b/tests/system/Database/Live/WhereTest.php index 6c5cd5b81ca3..6687df7d2e9f 100644 --- a/tests/system/Database/Live/WhereTest.php +++ b/tests/system/Database/Live/WhereTest.php @@ -106,9 +106,6 @@ public function testWhereIn() $this->assertSame('Accountant', $jobs[1]->name); } - /** - * @group single - */ public function testWhereNotIn() { $jobs = $this->db->table('job') diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index e4612c1358db..28d9a2301fe5 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -22,6 +22,7 @@ use Config\Migrations; use Config\Services; use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; /** * @group DatabaseLive @@ -39,8 +40,8 @@ final class MigrationRunnerTest extends CIUnitTestCase // Use specific migration files for this test case. protected $namespace = 'Tests\Support\MigrationTestMigrations'; - private $root; - private $config; + private vfsStreamDirectory $root; + private Migrations $config; protected function setUp(): void { @@ -238,7 +239,7 @@ public function testFindMigrationsSuccessTimestamp() $mig1 = (object) [ 'version' => '2018-01-24-102301', 'name' => 'Some_migration', - 'path' => TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102301_Some_migration.php', + 'path' => realpath(TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102301_Some_migration.php'), 'class' => 'Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_some_migration', 'namespace' => 'Tests\Support\MigrationTestMigrations', ]; @@ -247,7 +248,7 @@ public function testFindMigrationsSuccessTimestamp() $mig2 = (object) [ 'version' => '2018-01-24-102302', 'name' => 'Another_migration', - 'path' => TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102302_Another_migration.php', + 'path' => realpath(TESTPATH . '_support/MigrationTestMigrations/Database/Migrations/2018-01-24-102302_Another_migration.php'), 'class' => 'Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_another_migration', 'namespace' => 'Tests\Support\MigrationTestMigrations', 'uid' => '20180124102302Tests\Support\MigrationTestMigrations\Database\Migrations\Migration_another_migration', diff --git a/tests/system/Debug/Toolbar/Collectors/HistoryTest.php b/tests/system/Debug/Toolbar/Collectors/HistoryTest.php index fc65d096e19d..4356cccf67c8 100644 --- a/tests/system/Debug/Toolbar/Collectors/HistoryTest.php +++ b/tests/system/Debug/Toolbar/Collectors/HistoryTest.php @@ -25,6 +25,10 @@ final class HistoryTest extends CIUnitTestCase private const STEP = 0.000001; private float $time; + + /** + * @var false|resource + */ private $streamFilter; protected function setUp(): void diff --git a/tests/system/Files/FileWithVfsTest.php b/tests/system/Files/FileWithVfsTest.php index e965ed1ef375..6e5af6b3974c 100644 --- a/tests/system/Files/FileWithVfsTest.php +++ b/tests/system/Files/FileWithVfsTest.php @@ -13,6 +13,7 @@ use CodeIgniter\Test\CIUnitTestCase; use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; /** * @internal @@ -22,9 +23,9 @@ final class FileWithVfsTest extends CIUnitTestCase { // For VFS stuff - private $root; - private $path; - private $start; + private ?vfsStreamDirectory $root = null; + private string $path; + private string $start; private File $file; protected function setUp(): void diff --git a/tests/system/Filters/CSRFTest.php b/tests/system/Filters/CSRFTest.php index e704167d86fa..3848ffe3e771 100644 --- a/tests/system/Filters/CSRFTest.php +++ b/tests/system/Filters/CSRFTest.php @@ -12,6 +12,9 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Response; use CodeIgniter\Test\CIUnitTestCase; /** @@ -23,9 +26,14 @@ */ final class CSRFTest extends CIUnitTestCase { - private $config; + private \Config\Filters $config; + + /** + * @var CLIRequest|IncomingRequest|null + */ private $request; - private $response; + + private ?Response $response = null; protected function setUp(): void { diff --git a/tests/system/Filters/DebugToolbarTest.php b/tests/system/Filters/DebugToolbarTest.php index 2144cf856ebd..8b98b870f008 100644 --- a/tests/system/Filters/DebugToolbarTest.php +++ b/tests/system/Filters/DebugToolbarTest.php @@ -12,6 +12,9 @@ namespace CodeIgniter\Filters; use CodeIgniter\Config\Services; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Response; use CodeIgniter\Test\CIUnitTestCase; use Config\Filters as FilterConfig; @@ -24,8 +27,12 @@ */ final class DebugToolbarTest extends CIUnitTestCase { + /** + * @var CLIRequest|IncomingRequest + */ private $request; - private $response; + + private Response $response; protected function setUp(): void { diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index d7de8e84f06c..f111f4b957f6 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -22,6 +22,7 @@ use CodeIgniter\Filters\fixtures\Multiple2; use CodeIgniter\Filters\fixtures\Role; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ConfigFromArrayTrait; @@ -48,7 +49,7 @@ final class FiltersTest extends CIUnitTestCase { use ConfigFromArrayTrait; - private $response; + private Response $response; protected function setUp(): void { diff --git a/tests/system/Filters/HoneypotTest.php b/tests/system/Filters/HoneypotTest.php index 714a35ebaf60..efde6d372dff 100644 --- a/tests/system/Filters/HoneypotTest.php +++ b/tests/system/Filters/HoneypotTest.php @@ -13,6 +13,9 @@ use CodeIgniter\Config\Services; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Response; use CodeIgniter\Test\CIUnitTestCase; use Config\Honeypot; @@ -25,10 +28,15 @@ */ final class HoneypotTest extends CIUnitTestCase { - private $config; - private $honey; + private \Config\Filters $config; + private Honeypot $honey; + + /** + * @var CLIRequest|IncomingRequest|null + */ private $request; - private $response; + + private ?Response $response = null; protected function setUp(): void { diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index 6c54df5fb66b..5ca818965033 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -21,7 +21,7 @@ */ final class JSONFormatterTest extends CIUnitTestCase { - private $jsonFormatter; + private JSONFormatter $jsonFormatter; protected function setUp(): void { diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php index b934747561e4..c642d7e77eca 100644 --- a/tests/system/Format/XMLFormatterTest.php +++ b/tests/system/Format/XMLFormatterTest.php @@ -21,7 +21,7 @@ */ final class XMLFormatterTest extends CIUnitTestCase { - private $xmlFormatter; + private XMLFormatter $xmlFormatter; protected function setUp(): void { diff --git a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php index 40864bb0a8f8..f3540379dd6c 100644 --- a/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php +++ b/tests/system/HTTP/CURLRequestDoNotShareOptionsTest.php @@ -809,6 +809,22 @@ public function testApplyBody() $this->assertSame('name=George', $request->curl_options[CURLOPT_POSTFIELDS]); } + public function testApplyBodyByOptions() + { + $request = $this->getRequest([ + 'base_uri' => 'http://www.foo.com/api/v1/', + 'delay' => 100, + ]); + + $request->setOutput('Hi there'); + $response = $request->post('answer', [ + 'body' => 'name=George', + ]); + + $this->assertSame('Hi there', $response->getBody()); + $this->assertSame('name=George', $request->curl_options[CURLOPT_POSTFIELDS]); + } + public function testBodyIsResetOnSecondRequest() { $request = $this->getRequest([ diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 36ee88863534..4cf1a03944b5 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -792,6 +792,22 @@ public function testApplyBody() $this->assertSame('name=George', $request->curl_options[CURLOPT_POSTFIELDS]); } + public function testApplyBodyByOptions() + { + $request = $this->getRequest([ + 'base_uri' => 'http://www.foo.com/api/v1/', + 'delay' => 100, + ]); + + $request->setOutput('Hi there'); + $response = $request->post('answer', [ + 'body' => 'name=George', + ]); + + $this->assertSame('Hi there', $response->getBody()); + $this->assertSame('name=George', $request->curl_options[CURLOPT_POSTFIELDS]); + } + public function testResponseHeaders() { $request = $this->getRequest([ diff --git a/tests/system/HTTP/Files/FileCollectionTest.php b/tests/system/HTTP/Files/FileCollectionTest.php index 77b7a6c2ac8c..c9d2c0123acf 100644 --- a/tests/system/HTTP/Files/FileCollectionTest.php +++ b/tests/system/HTTP/Files/FileCollectionTest.php @@ -218,9 +218,6 @@ public function testExtensionGuessing() $this->assertSame('zip', $file->guessExtension()); } - /** - * @group single - */ public function testAllReturnsValidSingleFileNestedName() { $_FILES = [ @@ -309,9 +306,6 @@ public function testHasFileWithMultipleFilesWithDifferentNames() $this->assertTrue($collection->hasFile('userfile2')); } - /** - * @group single - */ public function testHasFileWithSingleFileNestedName() { $_FILES = [ diff --git a/tests/system/HTTP/HeaderTest.php b/tests/system/HTTP/HeaderTest.php index 1e986fa48933..43ccf91b3c26 100644 --- a/tests/system/HTTP/HeaderTest.php +++ b/tests/system/HTTP/HeaderTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Test\CIUnitTestCase; +use Error; use stdClass; /** @@ -43,6 +44,28 @@ public function testHeaderStoresBasicsWithNull() $this->assertSame('', $header->getValue()); } + public function testHeaderStoresBasicWithInt() + { + $name = 'foo'; + $value = 123; + + $header = new Header($name, $value); + + $this->assertSame($name, $header->getName()); + $this->assertSame((string) $value, $header->getValue()); + } + + public function testHeaderStoresBasicWithObject() + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Object of class stdClass could not be converted to string'); + + $name = 'foo'; + $value = new stdClass(); + + new Header($name, $value); + } + public function testHeaderStoresArrayValues() { $name = 'foo'; @@ -57,12 +80,26 @@ public function testHeaderStoresArrayValues() $this->assertSame($value, $header->getValue()); } + public function testHeaderStoresArrayKeyValue() + { + $name = 'foo'; + $value = [ + 'key' => 'val', + ]; + + $header = new Header($name, $value); + + $this->assertSame($name, $header->getName()); + $this->assertSame($value, $header->getValue()); + $this->assertSame('key=val', $header->getValueLine()); + } + public function testHeaderSetters() { $name = 'foo'; $value = [ 'bar', - 'baz', + 123, ]; $header = new Header($name); @@ -74,7 +111,7 @@ public function testHeaderSetters() $header->setName($name)->setValue($value); $this->assertSame($name, $header->getName()); $this->assertSame($value, $header->getValue()); - $this->assertSame($name . ': bar, baz', (string) $header); + $this->assertSame($name . ': bar, 123', (string) $header); } public function testHeaderAppendsValueSkippedForNull() @@ -157,19 +194,6 @@ public function testHeaderLineSimple() $this->assertSame($expected, $header->getValueLine()); } - public function testHeaderLineValueNotStringOrArray() - { - $name = 'foo'; - $value = new stdClass(); - - $expected = ''; - - $header = new Header($name, $value); - - $this->assertSame($name, $header->getName()); - $this->assertSame($expected, $header->getValueLine()); - } - public function testHeaderSetValueWithNullWillMarkAsEmptyString() { $name = 'foo'; diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index ff9e105661ca..0b3566aa0d9c 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\Test\CIUnitTestCase; @@ -700,7 +701,10 @@ public function testGetIPAddressNormal() { $expected = '123.123.123.123'; $_SERVER['REMOTE_ADDR'] = $expected; - $this->request = new Request(new App()); + + $this->request = new Request(new App()); + $this->request->populateHeaders(); + $this->assertSame($expected, $this->request->getIPAddress()); // call a second time to exercise the initial conditional block in getIPAddress() $this->assertSame($expected, $this->request->getIPAddress()); @@ -709,66 +713,214 @@ public function testGetIPAddressNormal() public function testGetIPAddressThruProxy() { $expected = '123.123.123.123'; - $_SERVER['REMOTE_ADDR'] = '10.0.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $_SERVER['REMOTE_ADDR'] = '10.0.1.200'; + + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address $this->assertSame($expected, $this->request->getIPAddress()); } - public function testGetIPAddressThruProxyInvalid() + public function testGetIPAddressThruProxyIPv6() { - $expected = '123.456.23.123'; - $_SERVER['REMOTE_ADDR'] = '10.0.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; + $expected = '123.123.123.123'; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $_SERVER['REMOTE_ADDR'] = '2001:db8::2:1'; + + $config = new App(); + $config->proxyIPs = [ + '2001:db8::2:1' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // we should see the original forwarded address + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyInvalidIPAddress() + { + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.456.23.123'; + $expected = '10.0.1.200'; + $_SERVER['REMOTE_ADDR'] = $expected; + + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); // spoofed address invalid - $this->assertSame('10.0.1.200', $this->request->getIPAddress()); + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyInvalidIPAddressIPv6() + { + $_SERVER['HTTP_X_FORWARDED_FOR'] = '2001:xyz::1'; + $expected = '2001:db8::2:1'; + $_SERVER['REMOTE_ADDR'] = $expected; + + $config = new App(); + $config->proxyIPs = [ + '2001:db8::2:1' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // spoofed address invalid + $this->assertSame($expected, $this->request->getIPAddress()); } public function testGetIPAddressThruProxyNotWhitelisted() { - $expected = '123.456.23.123'; - $_SERVER['REMOTE_ADDR'] = '10.10.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; - $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $expected = '10.10.1.200'; + $_SERVER['REMOTE_ADDR'] = $expected; + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.456.23.123'; + + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); // spoofed address invalid - $this->assertSame('10.10.1.200', $this->request->getIPAddress()); + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyNotWhitelistedIPv6() + { + $expected = '2001:db8::2:2'; + $_SERVER['REMOTE_ADDR'] = $expected; + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.456.23.123'; + + $config = new App(); + $config->proxyIPs = [ + '2001:db8::2:1' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // spoofed address invalid + $this->assertSame($expected, $this->request->getIPAddress()); } public function testGetIPAddressThruProxySubnet() { $expected = '123.123.123.123'; - $_SERVER['REMOTE_ADDR'] = '192.168.5.21'; - $config = new App(); - $config->proxyIPs = ['192.168.5.0/24']; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $_SERVER['REMOTE_ADDR'] = '192.168.5.21'; + + $config = new App(); + $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address $this->assertSame($expected, $this->request->getIPAddress()); } - public function testGetIPAddressThruProxyOutofSubnet() + public function testGetIPAddressThruProxySubnetIPv6() { $expected = '123.123.123.123'; - $_SERVER['REMOTE_ADDR'] = '192.168.5.21'; - $config = new App(); - $config->proxyIPs = ['192.168.5.0/28']; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $_SERVER['REMOTE_ADDR'] = '2001:db8:1234:ffff:ffff:ffff:ffff:ffff'; + + $config = new App(); + $config->proxyIPs = ['2001:db8:1234::/48' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // we should see the original forwarded address + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyOutOfSubnet() + { + $expected = '192.168.5.21'; + $_SERVER['REMOTE_ADDR'] = $expected; + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123'; + + $config = new App(); + $config->proxyIPs = ['192.168.5.0/28' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // we should see the original forwarded address + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyOutOfSubnetIPv6() + { + $expected = '2001:db8:1235:ffff:ffff:ffff:ffff:ffff'; + $_SERVER['REMOTE_ADDR'] = $expected; + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123'; + + $config = new App(); + $config->proxyIPs = ['2001:db8:1234::/48' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address - $this->assertSame('192.168.5.21', $this->request->getIPAddress()); + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyBothIPv4AndIPv6() + { + $expected = '2001:db8:1235:ffff:ffff:ffff:ffff:ffff'; + $_SERVER['REMOTE_ADDR'] = $expected; + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123'; + + $config = new App(); + $config->proxyIPs = [ + '192.168.5.0/28' => 'X-Forwarded-For', + '2001:db8:1234::/48' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); + + // we should see the original forwarded address + $this->assertSame($expected, $this->request->getIPAddress()); + } + + public function testGetIPAddressThruProxyInvalidConfigString() + { + $this->expectException(ConfigException::class); + $this->expectExceptionMessage( + 'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.' + ); + + $config = new App(); + $config->proxyIPs = '192.168.5.0/28'; + $this->request = new Request($config); + $this->request->populateHeaders(); + + $this->request->getIPAddress(); + } + + public function testGetIPAddressThruProxyInvalidConfigArray() + { + $this->expectException(ConfigException::class); + $this->expectExceptionMessage( + 'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.' + ); + + $config = new App(); + $config->proxyIPs = ['192.168.5.0/28']; + $this->request = new Request($config); + $this->request->populateHeaders(); + + $this->request->getIPAddress(); } // @TODO getIPAddress should have more testing, to 100% code coverage diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 770b21209a26..8f96b267ad1e 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -30,12 +30,6 @@ protected function setUp(): void $this->message = new Message(); } - protected function tearDown(): void - { - $this->message = null; - unset($this->message); - } - // We can only test the headers retrieved from $_SERVER // This test might fail under apache. public function testHeadersRetrievesHeaders() @@ -246,23 +240,24 @@ public function testSetHeaderWithExistingArrayValuesAppendNullValue() public function testPopulateHeadersWithoutContentType() { - // fail path, if the CONTENT_TYPE doesn't exist $original = $_SERVER; - $_SERVER = ['HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.50']; $originalEnv = getenv('CONTENT_TYPE'); + + // fail path, if the CONTENT_TYPE doesn't exist + $_SERVER = ['HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.50']; putenv('CONTENT_TYPE'); $this->message->populateHeaders(); $this->assertNull($this->message->header('content-type')); + putenv("CONTENT_TYPE={$originalEnv}"); - $this->message->removeHeader('accept-language'); $_SERVER = $original; // restore so code coverage doesn't break } public function testPopulateHeadersWithoutHTTP() { - // fail path, if arguement does't have the HTTP_* + // fail path, if argument doesn't have the HTTP_* $original = $_SERVER; $_SERVER = [ 'USER_AGENT' => 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405', @@ -273,6 +268,7 @@ public function testPopulateHeadersWithoutHTTP() $this->assertNull($this->message->header('user-agent')); $this->assertNull($this->message->header('request-method')); + $_SERVER = $original; // restore so code coverage doesn't break } @@ -288,7 +284,7 @@ public function testPopulateHeadersKeyNotExists() $this->message->populateHeaders(); $this->assertSame('', $this->message->header('accept-charset')->getValue()); - $this->message->removeHeader('accept-charset'); + $_SERVER = $original; // restore so code coverage doesn't break } @@ -305,8 +301,7 @@ public function testPopulateHeaders() $this->assertSame('text/html; charset=utf-8', $this->message->header('content-type')->getValue()); $this->assertSame('en-us,en;q=0.50', $this->message->header('accept-language')->getValue()); - $this->message->removeHeader('content-type'); - $this->message->removeHeader('accept-language'); + $_SERVER = $original; // restore so code coverage doesn't break } } diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index a9c6b16bbe64..547b50ca8039 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -81,9 +81,6 @@ public function testNegotiateMediaSupportsStrictMatching() $this->assertSame('', $this->negotiate->media(['text/plain'], true)); } - /** - * @group single - */ public function testAcceptCharsetMatchesBasics() { $this->request->setHeader('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8'); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 6f91ad6ad93d..44c0c54624aa 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -33,8 +33,8 @@ final class RedirectResponseTest extends CIUnitTestCase */ protected $routes; - private $request; - private $config; + private MockIncomingRequest $request; + private App $config; protected function setUp(): void { diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php index 376e4f82ef16..17cf45078fe0 100644 --- a/tests/system/HTTP/RequestTest.php +++ b/tests/system/HTTP/RequestTest.php @@ -609,10 +609,15 @@ public function testGetIPAddressThruProxy() { $expected = '123.123.123.123'; $_SERVER['REMOTE_ADDR'] = '10.0.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address $this->assertSame($expected, $this->request->getIPAddress()); @@ -622,10 +627,15 @@ public function testGetIPAddressThruProxyInvalid() { $expected = '123.456.23.123'; $_SERVER['REMOTE_ADDR'] = '10.0.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + + $this->request = new Request($config); + $this->request->populateHeaders(); // spoofed address invalid $this->assertSame('10.0.1.200', $this->request->getIPAddress()); @@ -635,10 +645,15 @@ public function testGetIPAddressThruProxyNotWhitelisted() { $expected = '123.456.23.123'; $_SERVER['REMOTE_ADDR'] = '10.10.1.200'; - $config = new App(); - $config->proxyIPs = '10.0.1.200,192.168.5.0/24'; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + + $config = new App(); + $config->proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + $this->request = new Request($config); + $this->request->populateHeaders(); // spoofed address invalid $this->assertSame('10.10.1.200', $this->request->getIPAddress()); @@ -648,10 +663,12 @@ public function testGetIPAddressThruProxySubnet() { $expected = '123.123.123.123'; $_SERVER['REMOTE_ADDR'] = '192.168.5.21'; - $config = new App(); - $config->proxyIPs = ['192.168.5.0/24']; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + + $config = new App(); + $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address $this->assertSame($expected, $this->request->getIPAddress()); @@ -661,10 +678,12 @@ public function testGetIPAddressThruProxyOutofSubnet() { $expected = '123.123.123.123'; $_SERVER['REMOTE_ADDR'] = '192.168.5.21'; - $config = new App(); - $config->proxyIPs = ['192.168.5.0/28']; $_SERVER['HTTP_X_FORWARDED_FOR'] = $expected; - $this->request = new Request($config); + + $config = new App(); + $config->proxyIPs = ['192.168.5.0/28' => 'X-Forwarded-For']; + $this->request = new Request($config); + $this->request->populateHeaders(); // we should see the original forwarded address $this->assertSame('192.168.5.21', $this->request->getIPAddress()); diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 0b40abda5605..3df76be68d3a 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -28,7 +28,7 @@ */ final class ResponseTest extends CIUnitTestCase { - private $server; + private array $server; protected function setUp(): void { diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 802e6f80bf09..e1b15748ae41 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -663,8 +663,6 @@ public function testResolveRelativeURI($rel, $expected) /** * @dataProvider defaultResolutions * - * @group single - * * @param mixed $rel * @param mixed $expected */ @@ -792,8 +790,6 @@ public function testGetQueryWithStrings() /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/331 - * - * @group single */ public function testNoExtraSlashes() { diff --git a/tests/system/Helpers/CookieHelperTest.php b/tests/system/Helpers/CookieHelperTest.php index 13d900456c6b..e20ae47742f9 100755 --- a/tests/system/Helpers/CookieHelperTest.php +++ b/tests/system/Helpers/CookieHelperTest.php @@ -32,7 +32,7 @@ final class CookieHelperTest extends CIUnitTestCase { private IncomingRequest $request; - private $name; + private string $name; private string $value; private int $expire; private Response $response; diff --git a/tests/system/Helpers/NumberHelperTest.php b/tests/system/Helpers/NumberHelperTest.php index 818bf014e8f4..84f05607d4a3 100755 --- a/tests/system/Helpers/NumberHelperTest.php +++ b/tests/system/Helpers/NumberHelperTest.php @@ -123,9 +123,6 @@ public function testQuadrillions() $this->assertSame('123.5 quadrillion', number_to_amount('123,456,700,000,000,000', 1, 'en_US')); } - /** - * @group single - */ public function testCurrencyCurrentLocale() { $this->assertSame('$1,235', number_to_currency(1234.56, 'USD', 'en_US')); diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php index 024e167e2905..5b9c0c0073d8 100644 --- a/tests/system/Honeypot/HoneypotTest.php +++ b/tests/system/Honeypot/HoneypotTest.php @@ -14,6 +14,9 @@ use CodeIgniter\Config\Services; use CodeIgniter\Filters\Filters; use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\Response; use CodeIgniter\Test\CIUnitTestCase; /** @@ -25,10 +28,15 @@ */ final class HoneypotTest extends CIUnitTestCase { - private $config; - private $honeypot; + private \Config\Honeypot $config; + private Honeypot $honeypot; + + /** + * @var CLIRequest|IncomingRequest + */ private $request; - private $response; + + private Response $response; protected function setUp(): void { diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 0be7d4931490..cff2ddbd8f11 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -45,6 +45,9 @@ protected function tearDown(): void parent::tearDown(); Locale::setDefault($this->currentLocale); + + // Reset current time. + Time::setTestNow(); } public function testNewTimeNow() @@ -224,7 +227,6 @@ public function testCreateFromFormat() $time = Time::createFromFormat('F j, Y', 'January 15, 2017', 'America/Chicago'); $this->assertCloseEnoughString(date('2017-01-15 H:i:s', $now->getTimestamp()), $time->toDateTimeString()); - Time::setTestNow(); } public function testCreateFromFormatWithTimezoneString() diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index ed8f9c620856..82ffcf20bf34 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -148,12 +148,13 @@ public function testOnlyDeleted(): void } /** - * If where condition is set, beyond the value was empty (0,'', NULL, etc.), - * Exception should not be thrown because condition was explicity set + * Given an explicit empty value in the WHERE condition + * When executing a soft delete + * Then an exception should not be thrown * * @dataProvider emptyPkValues * - * @param mixed $emptyValue + * @param int|string|null $emptyValue */ public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue($emptyValue): void { @@ -167,7 +168,7 @@ public function testDontThrowExceptionWhenSoftDeleteConditionIsSetWithEmptyValue /** * @dataProvider emptyPkValues * - * @param mixed $emptyValue + * @param int|string|null $emptyValue */ public function testThrowExceptionWhenSoftDeleteParamIsEmptyValue($emptyValue): void { @@ -175,22 +176,25 @@ public function testThrowExceptionWhenSoftDeleteParamIsEmptyValue($emptyValue): $this->expectExceptionMessage('Deletes are not allowed unless they contain a "where" or "like" clause.'); $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); + $this->createModel(UserModel::class)->delete($emptyValue); } /** * @dataProvider emptyPkValues * - * @param mixed $emptyValue + * @param int|string|null $emptyValue */ public function testDontDeleteRowsWhenSoftDeleteParamIsEmpty($emptyValue): void { - $this->expectException(DatabaseException::class); - $this->expectExceptionMessage('Deletes are not allowed unless they contain a "where" or "like" clause.'); - $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); - $this->createModel(UserModel::class)->delete($emptyValue); + try { + $this->createModel(UserModel::class)->delete($emptyValue); + } catch (DatabaseException $e) { + // Do nothing. + } + $this->seeInDatabase('user', ['name' => 'Derek Jones', 'deleted_at IS NULL' => null]); } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index e5feaa27e28c..59ebe2bb26c0 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -293,7 +293,7 @@ public function testInsertWithSetAndEscape(): void $this->assertGreaterThan(0, $this->model->getInsertID()); $result = $this->model->where('name', 'Scott')->where('country', '2')->where('email', '2+2')->first(); - $this->assertCloseEnough(time(), strtotime($result->created_at)); + $this->assertNotNull($result->created_at); } /** diff --git a/tests/system/Pager/PagerTest.php b/tests/system/Pager/PagerTest.php index 47476e56001f..e47e06cd7596 100644 --- a/tests/system/Pager/PagerTest.php +++ b/tests/system/Pager/PagerTest.php @@ -30,8 +30,8 @@ */ final class PagerTest extends CIUnitTestCase { - private ?Pager $pager = null; - private $config; + private ?Pager $pager = null; + private ?PagerConfig $config = null; protected function setUp(): void { diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index 37faed7f2c50..1ddad41937c7 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -463,9 +463,6 @@ public function testSetTempDataArraySingleTTL() $this->assertLessThanOrEqual($_SESSION['__ci_vars']['baz'], $time + 200); } - /** - * @group single - */ public function testGetTestDataReturnsAll() { $session = $this->getInstance(); diff --git a/tests/system/Test/ControllerTestTraitTest.php b/tests/system/Test/ControllerTestTraitTest.php index 84892479fee9..de8e8fbc4777 100644 --- a/tests/system/Test/ControllerTestTraitTest.php +++ b/tests/system/Test/ControllerTestTraitTest.php @@ -13,10 +13,12 @@ use App\Controllers\Home; use App\Controllers\NeverHeardOfIt; +use CodeIgniter\Controller; use CodeIgniter\Log\Logger; use CodeIgniter\Test\Mock\MockLogger as LoggerConfig; use Config\App; use Config\Services; +use Exception; use Tests\Support\Controllers\Popcorn; /** @@ -241,4 +243,20 @@ public function testRedirectRoute() ->execute('toindex'); $this->assertTrue($result->isRedirect()); } + + public function testUsesRequestBody() + { + $this->controller = new class () extends Controller { + public function throwsBody(): void + { + throw new Exception($this->request->getBody()); + } + }; + $this->controller->initController($this->request, $this->response, $this->logger); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('banana'); + + $this->withBody('banana')->execute('throwsBody'); + } } diff --git a/tests/system/Test/TestCaseTest.php b/tests/system/Test/TestCaseTest.php index 219102e33b8c..130e4a840a7c 100644 --- a/tests/system/Test/TestCaseTest.php +++ b/tests/system/Test/TestCaseTest.php @@ -25,7 +25,7 @@ final class TestCaseTest extends CIUnitTestCase { /** - * @var bool|resource + * @var false|resource|null */ private $stream_filter; diff --git a/tests/system/Test/TestResponseTest.php b/tests/system/Test/TestResponseTest.php index 371e21942908..84275808a71b 100644 --- a/tests/system/Test/TestResponseTest.php +++ b/tests/system/Test/TestResponseTest.php @@ -13,7 +13,6 @@ use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\Response; -use CodeIgniter\HTTP\ResponseInterface; use Config\App; use Config\Services; use PHPUnit\Framework\AssertionFailedError; @@ -26,11 +25,7 @@ final class TestResponseTest extends CIUnitTestCase { private ?TestResponse $testResponse = null; - - /** - * @var ResponseInterface - */ - private $response; + private Response $response; protected function setUp(): void { diff --git a/tests/system/Throttle/ThrottleTest.php b/tests/system/Throttle/ThrottleTest.php index 93049f0edf32..4bff58c01f51 100644 --- a/tests/system/Throttle/ThrottleTest.php +++ b/tests/system/Throttle/ThrottleTest.php @@ -107,9 +107,6 @@ public function testRemove() $this->assertTrue($throttler->check('127.0.0.1', 1, MINUTE)); } - /** - * @group single - */ public function testDecrementsValues() { $throttler = new Throttler($this->cache); diff --git a/tests/system/Typography/TypographyTest.php b/tests/system/Typography/TypographyTest.php index ef920760126c..d72e127c9a5b 100644 --- a/tests/system/Typography/TypographyTest.php +++ b/tests/system/Typography/TypographyTest.php @@ -20,7 +20,7 @@ */ final class TypographyTest extends CIUnitTestCase { - private $typography; + private Typography $typography; protected function setUp(): void { diff --git a/tests/system/View/CellTest.php b/tests/system/View/CellTest.php index c72dec491597..8a1b692a1c3f 100644 --- a/tests/system/View/CellTest.php +++ b/tests/system/View/CellTest.php @@ -23,7 +23,7 @@ */ final class CellTest extends CIUnitTestCase { - private $cache; + private MockCache $cache; private Cell $cell; protected function setUp(): void diff --git a/tests/system/View/DecoratorsTest.php b/tests/system/View/DecoratorsTest.php index 51f74e78e99d..95b848d9f8a2 100644 --- a/tests/system/View/DecoratorsTest.php +++ b/tests/system/View/DecoratorsTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\View; +use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Config\Factories; use CodeIgniter\Config\Services; use CodeIgniter\Test\CIUnitTestCase; @@ -25,9 +26,9 @@ */ final class DecoratorsTest extends CIUnitTestCase { - private $loader; - private $viewsDir; - private $config; + private FileLocator $loader; + private string $viewsDir; + private ?\Config\View $config = null; protected function setUp(): void { diff --git a/tests/system/View/ParserFilterTest.php b/tests/system/View/ParserFilterTest.php index 153569bc6d63..74ed82e82931 100644 --- a/tests/system/View/ParserFilterTest.php +++ b/tests/system/View/ParserFilterTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\View; +use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Config\Services; use CodeIgniter\Test\CIUnitTestCase; use Config\View; @@ -22,9 +23,9 @@ */ final class ParserFilterTest extends CIUnitTestCase { - private $loader; - private $viewsDir; - private $config; + private FileLocator $loader; + private string $viewsDir; + private View $config; protected function setUp(): void { diff --git a/tests/system/View/ParserPluginTest.php b/tests/system/View/ParserPluginTest.php index 9495ee90dec5..3c8c781d0edc 100644 --- a/tests/system/View/ParserPluginTest.php +++ b/tests/system/View/ParserPluginTest.php @@ -64,9 +64,9 @@ public function testMailto() */ public function testMailtoWithDashAndParenthesis() { - $template = '{+ mailto email=foo-bar@example.com title="Scilly (the Great)" +}'; + $template = '{+ mailto email=foo-bar@example.com title="Online español test level" +}'; - $this->assertSame(mailto('foo-bar@example.com', 'Scilly (the Great)'), $this->parser->renderString($template)); + $this->assertSame(mailto('foo-bar@example.com', 'Online español test level'), $this->parser->renderString($template)); } public function testSafeMailto() diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index ab3bb8e65bc1..79bd21b4efe3 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -744,9 +744,6 @@ public function testParseRuns() $this->assertSame($result, $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testCanAddAndRemovePlugins() { $this->parser->addPlugin('first', static fn ($str) => $str); @@ -762,9 +759,6 @@ public function testCanAddAndRemovePlugins() $this->assertArrayNotHasKey('first', $setParsers); } - /** - * @group parserplugins - */ public function testParserPluginNoMatches() { $template = 'hit:it'; @@ -772,9 +766,6 @@ public function testParserPluginNoMatches() $this->assertSame('hit:it', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserPluginNoParams() { $this->parser->addPlugin('hit:it', static fn ($str) => str_replace('here', 'Hip to the Hop', $str), true); @@ -784,9 +775,6 @@ public function testParserPluginNoParams() $this->assertSame(' stuff Hip to the Hop ', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserPluginClosure() { $config = $this->config; @@ -799,9 +787,6 @@ public function testParserPluginClosure() $this->assertSame('Hello, world', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserPluginParams() { $this->parser->addPlugin('growth', static function ($str, array $params) { @@ -822,9 +807,6 @@ public function testParserPluginParams() $this->assertSame(' 2 4 6 8', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserSingleTag() { $this->parser->addPlugin('hit:it', static fn () => 'Hip to the Hop', false); @@ -834,9 +816,6 @@ public function testParserSingleTag() $this->assertSame('Hip to the Hop', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserSingleTagWithParams() { $this->parser->addPlugin('hit:it', static fn (array $params = []) => "{$params['first']} to the {$params['last']}", false); @@ -846,9 +825,6 @@ public function testParserSingleTagWithParams() $this->assertSame('foo to the bar', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserSingleTagWithSingleParams() { $this->parser->addPlugin('hit:it', static fn (array $params = []) => "{$params[0]} to the {$params[1]}", false); @@ -858,9 +834,6 @@ public function testParserSingleTagWithSingleParams() $this->assertSame('foo to the bar', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserSingleTagWithQuotedParams() { $this->parser->addPlugin('count', static function (array $params = []) { @@ -878,9 +851,6 @@ public function testParserSingleTagWithQuotedParams() $this->assertSame('0. foo bar 1. baz 2. foo bar ', $this->parser->renderString($template)); } - /** - * @group parserplugins - */ public function testParserSingleTagWithNamedParams() { $this->parser->addPlugin('read_params', static function (array $params = []) { diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 96f695acc92f..537740231f66 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -11,6 +11,7 @@ namespace CodeIgniter\View; +use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Config\Services; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; @@ -24,9 +25,9 @@ */ final class ViewTest extends CIUnitTestCase { - private $loader; - private $viewsDir; - private $config; + private FileLocator $loader; + private string $viewsDir; + private \Config\View $config; protected function setUp(): void { diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 7ed43a6d970a..27896bd83847 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.2.11 v4.2.10 v4.2.9 v4.2.8 diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst index fb9ed875d637..1d0ef82d4682 100644 --- a/user_guide_src/source/changelogs/v4.2.0.rst +++ b/user_guide_src/source/changelogs/v4.2.0.rst @@ -60,6 +60,8 @@ Behavior Changes Enhancements ************ +.. _v420-new-improved-auto-routing: + New Improved Auto Routing ========================= @@ -93,6 +95,7 @@ Database - Added the class ``CodeIgniter\Database\RawSql`` which expresses raw SQL strings. - :ref:`select() `, :ref:`where() `, :ref:`like() `, :ref:`join() ` accept the ``CodeIgniter\Database\RawSql`` instance. - ``DBForge::addField()`` default value raw SQL string support. See :ref:`forge-addfield-default-value-rawsql`. +- SQLite3 has a new Config item ``foreignKeys`` that enables foreign key constraints. Helpers and Functions ===================== @@ -118,6 +121,7 @@ Commands Others ====== +- Added ``$this->validateData()`` in Controller. See :ref:`controller-validatedata`. - Content Security Policy (CSP) enhancements - Added the configs ``$scriptNonceTag`` and ``$styleNonceTag`` in ``Config\ContentSecurityPolicy`` to customize the CSP placeholders (``{csp-script-nonce}`` and ``{csp-style-nonce}``) - Added the config ``$autoNonce`` in ``Config\ContentSecurityPolicy`` to disable the CSP placeholder replacement diff --git a/user_guide_src/source/changelogs/v4.2.11.rst b/user_guide_src/source/changelogs/v4.2.11.rst new file mode 100644 index 000000000000..98e66a8ad7a0 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.2.11.rst @@ -0,0 +1,32 @@ +Version 4.2.11 +############## + +Release Date: December 21, 2022 + +**4.2.11 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 2 + +SECURITY +******** + +- *Attackers may spoof IP address when using proxy* was fixed. See the `Security advisory GHSA-ghw3-5qvm-3mqc `_ for more information. +- *Potential Session Handlers Vulnerability* was fixed. See the `Security advisory GHSA-6cq5-8cj7-g558 `_ for more information. + +BREAKING +******** + +- The ``Config\App::$proxyIPs`` value format has been changed. See :ref:`Upgrading Guide `. +- The key of the session data record for :ref:`sessions-databasehandler-driver`, + :ref:`sessions-memcachedhandler-driver` and :ref:`sessions-redishandler-driver` + has changed. See :ref:`Upgrading Guide `. + +Bugs Fixed +********** + +- Fixed a ``FileLocator::locateFile()`` bug where a similar namespace name could be replaced by another, causing a failure to find a file that exists. +- Fixed a ``RedisHandler`` session class to use the correct config when used with a socket connection. + +See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/cli/cli_controllers.rst b/user_guide_src/source/cli/cli_controllers.rst index c4230091de7c..31ccc7b9cf2c 100644 --- a/user_guide_src/source/cli/cli_controllers.rst +++ b/user_guide_src/source/cli/cli_controllers.rst @@ -41,8 +41,8 @@ works exactly like a normal route definition: For more information, see the :ref:`Routes ` page. -.. warning:: If you enable :ref:`auto-routing` and place the command file in **app/Controllers**, - anyone could access the command with the help of auto-routing via HTTP. +.. warning:: If you enable :ref:`auto-routing-legacy` and place the command file in **app/Controllers**, + anyone could access the command with the help of :ref:`auto-routing-legacy` via HTTP. Run via CLI =========== diff --git a/user_guide_src/source/cli/cli_generators.rst b/user_guide_src/source/cli/cli_generators.rst index 1ce2a52b0dd1..d50665fb6e04 100644 --- a/user_guide_src/source/cli/cli_generators.rst +++ b/user_guide_src/source/cli/cli_generators.rst @@ -13,7 +13,7 @@ etc. You can also scaffold a complete set of files with just one command. Introduction ************ -All built-in generators reside under the ``Generators`` namespace when listed using ``php spark list``. +All built-in generators reside under the ``Generators`` group when listed using ``php spark list``. To view the full description and usage information on a particular generator, use the command:: > php spark help @@ -28,7 +28,7 @@ where ```` will be replaced with the command to check. .. note:: Working on modules? Code generation will set the root namespace to a default of ``APP_NAMESPACE``. Should you need to have the generated code elsewhere in your module namespace, make sure to set - the ``--namespace`` option in your command, e.g., ``php spark make:model blog --namespace Acme\Blog``. + the ``--namespace`` option in your command, e.g., ``php spark make:model blog --namespace Acme\\Blog``. .. warning:: Make sure when setting the ``--namespace`` option that the supplied namespace is a valid namespace defined in your ``$psr4`` array in ``Config\Autoload`` or defined in your composer autoload @@ -269,12 +269,12 @@ Running this in your terminal:: > php spark make:scaffold user -will create the following classes: +will create the following files: -(1) ``App\Controllers\User``; -(2) ``App\Models\User``; -(3) ``App\Database\Migrations\_User``; and -(4) ``App\Database\Seeds\User``. +(1) **app/Controllers/User.php** +(2) **app/Models/User.php** +(3) **app/Database/Migrations/_User.php** and +(4) **app/Database/Seeds/User.php** To include an ``Entity`` class in the scaffolded files, just include the ``--return entity`` to the command and it will be passed to the model generator. diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index 395c133742b1..dff3a92e017b 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -36,6 +36,8 @@ On the other hand, Services have code to create instances, so it can create a co that needs other services or class instances. When you get a service, Services require a service name, not a class name, so the returned instance can be changed without changing the client code. +.. _factories-example: + Example ======= diff --git a/user_guide_src/source/concepts/mvc.rst b/user_guide_src/source/concepts/mvc.rst index 3840547de494..27d53c0758bd 100644 --- a/user_guide_src/source/concepts/mvc.rst +++ b/user_guide_src/source/concepts/mvc.rst @@ -51,7 +51,7 @@ Views are generally stored in **app/Views**, but can quickly become unwieldy if CodeIgniter does not enforce any type of organization, but a good rule of thumb would be to create a new directory in the **Views** directory for each controller. Then, name views by the method name. This makes them very easy to find later on. For example, a user's profile might be displayed in a controller named ``User``, and a method named ``profile``. -You might store the view file for this method in **app/Views/User/Profile.php**. +You might store the view file for this method in **app/Views/user/profile.php**. That type of organization works great as a base habit to get into. At times you might need to organize it differently. That's not a problem. As long as CodeIgniter can find the file, it can display it. diff --git a/user_guide_src/source/concepts/services.rst b/user_guide_src/source/concepts/services.rst index 0e20441faee2..a3dc9fabb74d 100644 --- a/user_guide_src/source/concepts/services.rst +++ b/user_guide_src/source/concepts/services.rst @@ -106,14 +106,14 @@ CodeIgniter's classes provide an interface that they adhere to. When you want to core classes, you only need to ensure you meet the requirements of the interface and you know that the classes are compatible. -For example, the ``RouterCollection`` class implements the ``RouterCollectionInterface``. When you +For example, the ``RouteCollection`` class implements the ``RouteCollectionInterface``. When you want to create a replacement that provides a different way to create routes, you just need to -create a new class that implements the ``RouterCollectionInterface``: +create a new class that implements the ``RouteCollectionInterface``: .. literalinclude:: services/006.php -Finally, modify **app/Config/Services.php** to create a new instance of ``MyRouter`` -instead of ``CodeIgniter\Router\RouterCollection``: +Finally, add the ``routes()`` method to **app/Config/Services.php** to create a new instance of ``MyRouteCollection`` +instead of ``CodeIgniter\Router\RouteCollection``: .. literalinclude:: services/007.php diff --git a/user_guide_src/source/concepts/services/006.php b/user_guide_src/source/concepts/services/006.php index c352a4365710..98781fecd795 100644 --- a/user_guide_src/source/concepts/services/006.php +++ b/user_guide_src/source/concepts/services/006.php @@ -4,7 +4,7 @@ use CodeIgniter\Router\RouteCollectionInterface; -class MyRouter implements RouteCollectionInterface +class MyRouteCollection implements RouteCollectionInterface { // Implement required methods here. } diff --git a/user_guide_src/source/concepts/services/007.php b/user_guide_src/source/concepts/services/007.php index 296d8445fefc..10616d39fb50 100644 --- a/user_guide_src/source/concepts/services/007.php +++ b/user_guide_src/source/concepts/services/007.php @@ -6,10 +6,10 @@ class Services extends BaseService { + // ... + public static function routes() { - return new \App\Router\MyRouter(); + return new \App\Router\MyRouteCollection(static::locator(), config('Modules')); } - - // ... } diff --git a/user_guide_src/source/concepts/services/008.php b/user_guide_src/source/concepts/services/008.php index 5843b28ab5e2..2b3f198b6eaf 100644 --- a/user_guide_src/source/concepts/services/008.php +++ b/user_guide_src/source/concepts/services/008.php @@ -6,10 +6,10 @@ class Services extends BaseService { + // ... + public static function renderer($viewPath = APPPATH . 'views/') { return new \CodeIgniter\View\View($viewPath); } - - // ... } diff --git a/user_guide_src/source/concepts/services/010.php b/user_guide_src/source/concepts/services/010.php index b74d9c470888..79c0a5b31b8d 100644 --- a/user_guide_src/source/concepts/services/010.php +++ b/user_guide_src/source/concepts/services/010.php @@ -6,14 +6,14 @@ class Services extends BaseService { - public static function routes($getShared = false) + // ... + + public static function routes($getShared = true) { - if (! $getShared) { - return new \CodeIgniter\Router\RouteCollection(); + if ($getShared) { + return static::getSharedInstance('routes'); } - return static::getSharedInstance('routes'); + return new \App\Router\MyRouteCollection(static::locator(), config('Modules')); } - - // ... } diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index ee30ae01eb29..b5c4bcbc8c12 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -24,7 +24,7 @@ version = '4.2' # The full version, including alpha/beta/rc tags. -release = '4.2.10' +release = '4.2.11' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/database/configuration.rst b/user_guide_src/source/database/configuration.rst index 0e37f90073b3..a72e645231cf 100644 --- a/user_guide_src/source/database/configuration.rst +++ b/user_guide_src/source/database/configuration.rst @@ -6,6 +6,9 @@ Database Configuration :local: :depth: 2 +.. note:: + See :ref:`requirements-supported-databases` for currently supported database drivers. + *********** Config File *********** @@ -110,12 +113,12 @@ Explanation of Values: =============== =========================================================================================================== **dsn** The DSN connect string (an all-in-one configuration sequence). **hostname** The hostname of your database server. Often this is 'localhost'. -**username** The username used to connect to the database. -**password** The password used to connect to the database. +**username** The username used to connect to the database. (``SQLite3`` does not use this.) +**password** The password used to connect to the database. (``SQLite3`` does not use this.) **database** The name of the database you want to connect to. .. note:: CodeIgniter doesn't support dots (``.``) in the database, table, and column names. -**DBDriver** The database type. e.g.,: ``MySQLi``, ``Postgres``, etc. The case must match the driver name +**DBDriver** The database driver name. e.g.,: ``MySQLi``, ``Postgres``, etc. The case must match the driver name **DBPrefix** An optional table prefix which will added to the table name when running :doc:`Query Builder ` queries. This permits multiple CodeIgniter installations to share one database. @@ -126,10 +129,10 @@ Explanation of Values: **swapPre** A default table prefix that should be swapped with ``DBPrefix``. This is useful for distributed applications where you might run manually written queries, and need the prefix to still be customizable by the end user. -**schema** The database schema, default value varies by driver. Used by ``Postgres`` and ``SQLSRV`` drivers. +**schema** The database schema, default value varies by driver. (Used by ``Postgres`` and ``SQLSRV``.) **encrypt** Whether or not to use an encrypted connection. - ``SQLSRV`` drivers accept true/false - ``MySQLi`` drivers accept an array with the following options: + ``SQLSRV`` driver accepts true/false + ``MySQLi`` driver accepts an array with the following options: * ``ssl_key`` - Path to the private key file * ``ssl_cert`` - Path to the public key certificate file * ``ssl_ca`` - Path to the certificate authority file @@ -138,10 +141,8 @@ Explanation of Values: * ``ssl_verify`` - true/false; Whether to verify the server certificate or not (``MySQLi`` only) **compress** Whether or not to use client compression (``MySQLi`` only). **strictOn** true/false (boolean) - Whether to force "Strict Mode" connections, good for ensuring strict SQL - while developing an application. -**port** The database port number. To use this value you have to add a line to the database config array. - - .. literalinclude:: configuration/009.php + while developing an application (``MySQLi`` only). +**port** The database port number. **foreignKeys** true/false (boolean) - Whether or not to enable Foreign Key constraint (``SQLite3`` only). .. important:: SQLite3 Foreign Key constraint is disabled by default. @@ -149,8 +150,7 @@ Explanation of Values: To enforce Foreign Key constraint, set this config item to true. =============== =========================================================================================================== -.. note:: Depending on what database platform you are using (MySQL, PostgreSQL, - etc.) not all values will be needed. For example, when using SQLite you +.. note:: Depending on what database driver you are using (``MySQLi``, ``Postgres``, + etc.) not all values will be needed. For example, when using ``SQLite3`` you will not need to supply a username or password, and the database name - will be the path to your database file. The information above assumes - you are using MySQL. + will be the path to your database file. diff --git a/user_guide_src/source/database/configuration/009.php b/user_guide_src/source/database/configuration/009.php deleted file mode 100644 index 06eecbdfd59c..000000000000 --- a/user_guide_src/source/database/configuration/009.php +++ /dev/null @@ -1,6 +0,0 @@ - 5432, -]; diff --git a/user_guide_src/source/database/queries.rst b/user_guide_src/source/database/queries.rst index 53da79ed4500..e9b92021b478 100644 --- a/user_guide_src/source/database/queries.rst +++ b/user_guide_src/source/database/queries.rst @@ -191,7 +191,7 @@ that query multiple times with new sets of data. This eliminates the possibility passed to the database in a different format than the query itself. When you need to run the same query multiple times it can be quite a bit faster, too. However, to use it for every query can have major performance hits, since you're calling out to the database twice as often. Since the Query Builder and Database connections already handle escaping the data -for you, the safety aspect is already taken care of for you. There will be times, though, when you need to ability +for you, the safety aspect is already taken care of for you. There will be times, though, when you need the ability to optimize the query by running a prepared statement, or prepared query. Preparing the Query diff --git a/user_guide_src/source/database/query_builder/103.php b/user_guide_src/source/database/query_builder/103.php index ffe869181fc7..9ec4d277f385 100644 --- a/user_guide_src/source/database/query_builder/103.php +++ b/user_guide_src/source/database/query_builder/103.php @@ -1,11 +1,10 @@ db->table('users')->select('id', 'name'); -$builder = $this->db->table('users')->select('id', 'name'); - -$builder->union($union)->limit(10)->get(); +$builder = $db->table('users')->select('id, name')->limit(10); +$union = $db->table('groups')->select('id, name'); +$builder->union($union)->get(); /* * Produces: * SELECT * FROM (SELECT `id`, `name` FROM `users` LIMIT 10) uwrp0 - * UNION SELECT * FROM (SELECT `id`, `name` FROM `users`) uwrp1 + * UNION SELECT * FROM (SELECT `id`, `name` FROM `groups`) uwrp1 */ diff --git a/user_guide_src/source/database/query_builder/104.php b/user_guide_src/source/database/query_builder/104.php index 11c716dea999..571dd61c976a 100644 --- a/user_guide_src/source/database/query_builder/104.php +++ b/user_guide_src/source/database/query_builder/104.php @@ -1,9 +1,9 @@ db->table('users')->select('id', 'name')->orderBy('id', 'DESC')->limit(5); -$builder = $this->db->table('users')->select('id', 'name')->orderBy('id', 'ASC')->limit(5)->union($union); +$union = $db->table('users')->select('id, name')->orderBy('id', 'DESC')->limit(5); +$builder = $db->table('users')->select('id, name')->orderBy('id', 'ASC')->limit(5)->union($union); -$this->db->newQuery()->fromSubquery($builder, 'q')->orderBy('id', 'DESC')->get(); +$db->newQuery()->fromSubquery($builder, 'q')->orderBy('id', 'DESC')->get(); /* * Produces: * SELECT * FROM ( diff --git a/user_guide_src/source/database/transactions.rst b/user_guide_src/source/database/transactions.rst index 958a87a21e0a..285bd8c0da35 100644 --- a/user_guide_src/source/database/transactions.rst +++ b/user_guide_src/source/database/transactions.rst @@ -36,13 +36,13 @@ Running Transactions ==================== To run your queries using transactions you will use the -``$this->db->transStart()`` and ``$this->db->transComplete()`` functions as +``$this->db->transStart()`` and ``$this->db->transComplete()`` methods as follows: .. literalinclude:: transactions/001.php -You can run as many queries as you want between the start/complete -functions and they will all be committed or rolled back based on the success +You can run as many queries as you want between the ``transStart()``/``transComplete()`` +methods and they will all be committed or rolled back based on the success or failure of any given query. Strict Mode @@ -50,7 +50,7 @@ Strict Mode By default, CodeIgniter runs all transactions in Strict Mode. When strict mode is enabled, if you are running multiple groups of transactions, if -one group fails all groups will be rolled back. If strict mode is +one group fails all subsequent groups will be rolled back. If strict mode is disabled, each group is treated independently, meaning a failure of one group will not affect any others. @@ -61,9 +61,11 @@ Strict Mode can be disabled as follows: Managing Errors =============== -If you have error reporting enabled in your Config/Database.php file -you'll see a standard error message if the commit was unsuccessful. If -debugging is turned off, you can manage your own errors like this: +When you have ``DBDebug`` true in your **app/Config/Database.php** file, +if a query error occurs, all the queries will be rolled backed, and an exception +will be thrown. So you'll see a standard error page. + +If the ``DBDebug`` is false, you can manage your own errors like this: .. literalinclude:: transactions/003.php @@ -84,14 +86,15 @@ Test Mode You can optionally put the transaction system into "test mode", which will cause your queries to be rolled back -- even if the queries produce a valid result. To use test mode simply set the first parameter in the -``$this->db->transStart()`` function to true: +``$this->db->transStart()`` method to true: .. literalinclude:: transactions/005.php Running Transactions Manually ============================= -If you would like to run transactions manually you can do so as follows: +When you have ``DBDebug`` false in your **app/Config/Database.php** file, and +if you would like to run transactions manually you can do so as follows: .. literalinclude:: transactions/006.php diff --git a/user_guide_src/source/dbmgmt/forge.rst b/user_guide_src/source/dbmgmt/forge.rst index a6d75b9d7c7e..0d78b94f9c65 100644 --- a/user_guide_src/source/dbmgmt/forge.rst +++ b/user_guide_src/source/dbmgmt/forge.rst @@ -179,7 +179,7 @@ you may add them directly in forge: .. literalinclude:: forge/012.php -You can specify the desired action for the "on delete" and "on update" properties of the constraint: +You can specify the desired action for the "on update" and "on delete" properties of the constraint: .. literalinclude:: forge/013.php diff --git a/user_guide_src/source/dbmgmt/forge/013.php b/user_guide_src/source/dbmgmt/forge/013.php index 33aa485380ee..377a47c014b4 100644 --- a/user_guide_src/source/dbmgmt/forge/013.php +++ b/user_guide_src/source/dbmgmt/forge/013.php @@ -3,5 +3,8 @@ $forge->addForeignKey('users_id', 'users', 'id', 'CASCADE', 'CASCADE'); // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +$forge->addForeignKey('users_id', 'users', 'id', '', 'CASCADE'); +// gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE + $forge->addForeignKey(['users_id', 'users_name'], 'users', ['id', 'name'], 'CASCADE', 'CASCADE'); // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`, `users_name`) REFERENCES `users`(`id`, `name`) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/user_guide_src/source/general/helpers.rst b/user_guide_src/source/general/helpers.rst index 4113f6cf91ff..684ca021f70e 100644 --- a/user_guide_src/source/general/helpers.rst +++ b/user_guide_src/source/general/helpers.rst @@ -20,8 +20,8 @@ functions. CodeIgniter does not load Helper Files by default, so the first step in using a Helper is to load it. Once loaded, it becomes globally available -in your :doc:`controller ` and -:doc:`views `. +in your :doc:`controller <../incoming/controllers>` and +:doc:`views <../outgoing/views>`. Helpers are typically stored in your **system/Helpers**, or **app/Helpers** directory. CodeIgniter will look first in your @@ -32,6 +32,8 @@ global **system/Helpers** directory. Loading a Helper ================ +.. note:: The URL helper is always loaded so you do not need to load it yourself. + Loading a helper file is quite simple using the following method: .. literalinclude:: helpers/001.php @@ -44,22 +46,33 @@ For example, to load the **Cookie Helper** file, which is named .. literalinclude:: helpers/002.php +.. note:: The Helper loading method above does not return a value, so + don't try to assign it to a variable. Just use it as shown. + +Loading Multiple Helpers +------------------------ + If you need to load more than one helper at a time, you can pass an array of file names in and all of them will be loaded: .. literalinclude:: helpers/003.php +Loading in a Controller +----------------------- + A helper can be loaded anywhere within your controller methods (or even within your View files, although that's not a good practice), as -long as you load it before you use it. You can load your helpers in your +long as you load it before you use it. + +You can load your helpers in your controller constructor so that they become available automatically in -any function, or you can load a helper in a specific function that needs +any method, or you can load a helper in a specific method that needs it. -.. note:: The Helper loading method above does not return a value, so - don't try to assign it to a variable. Just use it as shown. +However if you want to load in your controller constructor, you can use the ``$helpers`` +property in Controller instead. See :ref:`Controllers `. -.. note:: The URL helper is always loaded so you do not need to load it yourself. +.. _helpers-loading-from-non-standard-locations: Loading from Non-standard Locations ----------------------------------- @@ -73,13 +86,17 @@ sub-directory named **Helpers**. An example will help understand this. For this example, assume that we have grouped together all of our Blog-related code into its own namespace, ``Example\Blog``. The files exist on our server at -**/Modules/Blog/**. So, we would put our Helper files for the blog module in -**/Modules/Blog/Helpers/**. A **blog_helper** file would be at -**/Modules/Blog/Helpers/blog_helper.php**. Within our controller we could +**Modules/Blog/**. So, we would put our Helper files for the blog module in +**Modules/Blog/Helpers/**. A **blog_helper** file would be at +**Modules/Blog/Helpers/blog_helper.php**. Within our controller we could use the following command to load the helper for us: .. literalinclude:: helpers/004.php +You can also use the following way: + +.. literalinclude:: helpers/007.php + .. note:: The functions within files loaded this way are not truly namespaced. The namespace is simply used as a convenient way to locate the files. @@ -89,7 +106,7 @@ Using a Helper Once you've loaded the Helper File containing the function you intend to use, you'll call it the way you would a standard PHP function. -For example, to create a link using the ``anchor()`` function in one of +For example, to create a link using the :php:func:`anchor()` function in one of your view files you would do this: .. literalinclude:: helpers/005.php @@ -119,7 +136,7 @@ functions: .. literalinclude:: helpers/006.php -The ``helper()`` method will scan through all PSR-4 namespaces defined in **app/Config/Autoload.php** +The ``helper()`` function will scan through all PSR-4 namespaces defined in **app/Config/Autoload.php** and load in ALL matching helpers of the same name. This allows any module's helpers to be loaded, as well as any helpers you've created specifically for this application. The load order is as follows: @@ -131,5 +148,5 @@ is as follows: Now What? ========= -In the Table of Contents, you'll find a list of all the available :doc:`Helpers `. +In the Table of Contents, you'll find a list of all the available :doc:`Helpers <../helpers/index>`. Browse each one to see what they do. diff --git a/user_guide_src/source/general/helpers/007.php b/user_guide_src/source/general/helpers/007.php new file mode 100644 index 000000000000..fc698c8e0b37 --- /dev/null +++ b/user_guide_src/source/general/helpers/007.php @@ -0,0 +1,3 @@ +` +The core element of the modules functionality comes from the :doc:`PSR-4 compatible autoloading <../concepts/autoloader>` that CodeIgniter uses. While any code can use the PSR-4 autoloader and namespaces, the primary way to take full advantage of -modules is to namespace your code and add it to **app/Config/Autoload.php**, in the ``psr4`` section. +modules is to namespace your code and add it to **app/Config/Autoload.php**, in the ``$psr4`` property. For example, let's say we want to keep a simple blog module that we can re-use between applications. We might create folder with our company name, Acme, to store all of our modules within. We will put it right alongside our **app** directory in the main project root:: - /acme // New modules directory - /app - /public - /system - /tests - /writable + acme/ // New modules directory + app/ + public/ + system/ + tests/ + writable/ -Open **app/Config/Autoload.php** and add the ``Acme\Blog`` namespace to the ``psr4`` array property: +Open **app/Config/Autoload.php** and add the ``Acme\Blog`` namespace to the ``$psr4`` array property: .. literalinclude:: modules/001.php @@ -40,19 +40,19 @@ and become comfortable with their use. Several file types will be scanned for au A common directory structure within a module will mimic the main application folder:: - /acme - /Blog - /Config - /Controllers - /Database - /Migrations - /Seeds - /Helpers - /Language - /en - /Libraries - /Models - /Views + acme/ + Blog/ + Config/ + Controllers/ + Database/ + Migrations/ + Seeds/ + Helpers/ + Language/ + en/ + Libraries/ + Models/ + Views/ Of course, there is nothing forcing you to use this exact structure, and you should organize it in the manner that best suits your module, leaving out directories you don't need, creating new directories for Entities, Interfaces, @@ -81,10 +81,11 @@ Many times, you will need to specify the full namespace to files you want to inc configured to make integrating modules into your applications simpler by automatically discovering many different file types, including: -- :doc:`Events ` -- :doc:`Registrars ` -- :doc:`Route files ` -- :doc:`Services ` +- :doc:`Events <../extending/events>` +- :doc:`Filters <../incoming/filters>` +- :doc:`Registrars <./configuration>` +- :doc:`Route files <../incoming/routing>` +- :doc:`Services <../concepts/services>` This is configured in the file **app/Config/Modules.php**. @@ -116,7 +117,7 @@ by editing the ``$discoverInComposer`` variable in ``Config\Modules.php``: .. literalinclude:: modules/004.php ================== -Working With Files +Working with Files ================== This section will take a look at each of the file types (controllers, views, language files, etc) and how they can @@ -126,7 +127,7 @@ guide, but is being reproduced here so that it's easier to grasp how all of the Routes ====== -By default, :doc:`routes ` are automatically scanned for within modules. It can be turned off in +By default, :doc:`routes <../incoming/routing>` are automatically scanned for within modules. It can be turned off in the **Modules** config file, described above. .. note:: Since the files are being included into the current scope, the ``$routes`` instance is already defined for you. @@ -138,7 +139,7 @@ In that case, see :ref:`routing-priority`. Filters ======= -By default, :doc:`filters ` are automatically scanned for within modules. +By default, :doc:`filters <../incoming/filters>` are automatically scanned for within modules. It can be turned off in the **Modules** config file, described above. .. note:: Since the files are being included into the current scope, the ``$filters`` instance is already defined for you. @@ -175,7 +176,7 @@ Config files are automatically discovered whenever using the ``config()`` functi .. note:: ``config()`` finds the file in **app/Config/** when there is a class with the same shortname, even if you specify a fully qualified class name like ``config(\Acme\Blog\Config\Blog::class)``. - This is because ``config()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`factories-options` for more information. + This is because ``config()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`Factories Example ` for more information. Migrations ========== @@ -194,11 +195,13 @@ is provided. If calling on the CLI, you will need to provide double backslashes: Helpers ======= -Helpers will be located automatically from defined namespaces when using the ``helper()`` method, as long as it +Helpers will be automatically discovered within defined namespaces when using the ``helper()`` function, as long as it is within the namespaces **Helpers** directory: .. literalinclude:: modules/009.php +You can specify namespaces. See :ref:`helpers-loading-from-non-standard-locations` for details. + Language Files ============== @@ -215,10 +218,18 @@ Libraries are always instantiated by their fully-qualified class name, so no spe Models ====== -Models are always instantiated by their fully-qualified class name, so no special access is provided: +If you instantiate models with ``new`` keyword by their fully-qualified class names, no special access is provided: .. literalinclude:: modules/011.php +Model files are automatically discovered whenever using the :php:func:`model()` function that is always available. + +.. note:: We don't recommend you use the same short classname in modules. + +.. note:: ``model()`` finds the file in **app/Models/** when there is a class with the same shortname, + even if you specify a fully qualified class name like ``model(\Acme\Blog\Model\PostModel::class)``. + This is because ``model()`` is a wrapper for the ``Factories`` class which uses ``preferApp`` by default. See :ref:`Factories Example ` for more information. + Views ===== diff --git a/user_guide_src/source/general/modules/006.php b/user_guide_src/source/general/modules/006.php index 663705d523a6..3c3ce7c29abc 100644 --- a/user_guide_src/source/general/modules/006.php +++ b/user_guide_src/source/general/modules/006.php @@ -1,4 +1,4 @@ get('blog', 'Acme\Blog\Controllers\Blog::index'); +$routes->get('blog', '\Acme\Blog\Controllers\Blog::index'); diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index fef689fb2da1..b9eaf738575c 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -35,21 +35,26 @@ Included Properties The CodeIgniter's Controller provides these properties. -**Request Object** +Request Object +============== The application's main :doc:`Request Instance ` is always available as a class property, ``$this->request``. -**Response Object** +Response Object +=============== The application's main :doc:`Response Instance ` is always available as a class property, ``$this->response``. -**Logger Object** +Logger Object +============= An instance of the :doc:`Logger <../general/logging>` class is available as a class property, ``$this->logger``. +.. _controllers-helpers: + Helpers ======= @@ -79,7 +84,7 @@ modify this by passing the duration (in seconds) as the first parameter: .. _controller-validate: -Validating data +Validating Data *************** $this->validate() @@ -90,21 +95,34 @@ The method accepts an array of rules in the first parameter, and in the optional second parameter, an array of custom error messages to display if the items are not valid. Internally, this uses the controller's ``$this->request`` instance to get the data to be validated. + +.. warning:: + The ``validate()`` method uses :ref:`Validation::withRequest() ` method. + It validates data from :ref:`$request->getJSON() ` + or :ref:`$request->getRawInput() ` + or :ref:`$request->getVar() `. + Which data is used depends on the request. Remember that an attacker is free to send any request to + the server. + The :doc:`Validation Library docs ` have details on rule and message array formats, as well as available rules: .. literalinclude:: controllers/004.php If you find it simpler to keep the rules in the configuration file, you can replace -the ``$rules`` array with the name of the group as defined in ``Config\Validation.php``: +the ``$rules`` array with the name of the group as defined in **app/Config/Validation.php**: .. literalinclude:: controllers/005.php .. note:: Validation can also be handled automatically in the model, but sometimes it's easier to do it in the controller. Where is up to you. +.. _controller-validatedata: + $this->validateData() ===================== +.. versionadded:: 4.2.0 + Sometimes you may want to check the controller method parameters or other custom data. In that case, you can use the ``$this->validateData()`` method. The method accepts an array of data to validate in the first parameter: @@ -135,6 +153,10 @@ Auto Routing (Improved) Since v4.2.0, the new more secure Auto Routing has been introduced. +.. note:: If you are familiar with Auto Routing, which was enabled by default + from CodeIgniter 3 through 4.1.x, you can see the differences in + :ref:`ChangeLog v4.2.0 `. + This section describes the functionality of the new auto-routing. It automatically routes an HTTP request, and executes the corresponding controller method without route definitions. @@ -295,7 +317,7 @@ your **app/Config/Routes.php** file. CodeIgniter also permits you to map your URIs using its :ref:`Defined Route Routing `.. -.. _controller-auto-routing: +.. _controller-auto-routing-legacy: Auto Routing (Legacy) ********************* diff --git a/user_guide_src/source/incoming/controllers/006.php b/user_guide_src/source/incoming/controllers/006.php index 1ae78e5791ff..555a8c98d570 100644 --- a/user_guide_src/source/incoming/controllers/006.php +++ b/user_guide_src/source/incoming/controllers/006.php @@ -17,7 +17,9 @@ public function product(int $id) ]; if (! $this->validateData($data, $rule)) { - // ... + return view('store/product', [ + 'errors' => $this->validator->getErrors(), + ]); } // ... diff --git a/user_guide_src/source/incoming/filters.rst b/user_guide_src/source/incoming/filters.rst index b0fda865fe66..fd68bf748dc4 100644 --- a/user_guide_src/source/incoming/filters.rst +++ b/user_guide_src/source/incoming/filters.rst @@ -71,7 +71,7 @@ This file contains four properties that allow you to configure exactly when the .. Warning:: It is recommended that you should always add ``*`` at the end of a URI in the filter settings. Because a controller method might be accessible by different URLs than you think. - For example, when auto-routing is enabled, if you have ``Blog::index``, + For example, when :ref:`auto-routing-legacy` is enabled, if you have ``Blog::index``, it can be accessible with ``blog``, ``blog/index``, and ``blog/index/1``, etc. $aliases @@ -127,8 +127,8 @@ specify the method name in lowercase. It's value would be an array of filters to In addition to the standard HTTP methods, this also supports one special case: 'cli'. The 'cli' method would apply to all requests that were run from the command line. -.. Warning:: If you use ``$methods`` filters, you should :ref:`disable auto-routing ` - because auto-routing permits any HTTP method to access a controller. +.. Warning:: If you use ``$methods`` filters, you should :ref:`disable Auto Routing (Legacy) ` + because :ref:`auto-routing-legacy` permits any HTTP method to access a controller. Accessing the controller with a method you don't expect could bypass the filter. $filters diff --git a/user_guide_src/source/incoming/incomingrequest.rst b/user_guide_src/source/incoming/incomingrequest.rst index 2ca9a9c4c236..d665167f824c 100644 --- a/user_guide_src/source/incoming/incomingrequest.rst +++ b/user_guide_src/source/incoming/incomingrequest.rst @@ -1,5 +1,6 @@ +##################### IncomingRequest Class -********************* +##################### The IncomingRequest class provides an object-oriented representation of an HTTP request from a client, like a browser. It extends from, and has access to all the methods of the :doc:`Request ` and :doc:`Message ` @@ -10,7 +11,7 @@ classes, in addition to the methods listed below. :depth: 2 Accessing the Request ---------------------- +********************* An instance of the request class already populated for you if the current class is a descendant of ``CodeIgniter\Controller`` and can be accessed as a class property: @@ -28,7 +29,7 @@ the controller, where you can save it as a class property: .. literalinclude:: incomingrequest/003.php Determining Request Type ------------------------- +************************ A request could be of several types, including an AJAX request or a request from the command line. This can be checked with the ``isAJAX()`` and ``isCLI()`` methods: @@ -43,7 +44,12 @@ You can check the HTTP method that this request represents with the ``method()`` .. literalinclude:: incomingrequest/005.php -By default, the method is returned as a lower-case string (i.e., 'get', 'post', etc). You can get an +By default, the method is returned as a lower-case string (i.e., ``'get'``, ``'post'``, etc). + +.. note:: The functionality to convert the return value to lower case is deprecated. + It will be removed in the future version, and this method will be PSR-7 equivalent. + +You can get an uppercase version by wrapping the call in ``strtoupper()``:: // Returns 'GET' @@ -54,11 +60,16 @@ You can also check if the request was made through and HTTPS connection with the .. literalinclude:: incomingrequest/006.php Retrieving Input ----------------- +**************** + +You can retrieve input from ``$_SERVER``, ``$_GET``, ``$_POST``, and ``$_ENV`` through the Request object. +The data is not automatically filtered and returns the raw input data as passed in the request. -You can retrieve input from $_SERVER, $_GET, $_POST, and $_ENV through the Request object. -The data is not automatically filtered and returns the raw input data as passed in the request. The main -advantages to using these methods instead of accessing them directly ($_POST['something']), is that they +.. note:: It is bad practice to use global variables. Basically, it should be avoided + and it is recommended to use methods of the Request object. + +The main +advantages to using these methods instead of accessing them directly (``$_POST['something']``), is that they will return null if the item doesn't exist, and you can have the data filtered. This lets you conveniently use data without having to test whether an item exists first. In other words, normally you might do something like this: @@ -69,28 +80,41 @@ With CodeIgniter's built-in methods you can simply do this: .. literalinclude:: incomingrequest/008.php -The ``getVar()`` method will pull from $_REQUEST, so will return any data from $_GET, $POST, or $_COOKIE. While this +.. _incomingrequest-getting-data: + +Getting Data +============ + +The ``getVar()`` method will pull from ``$_REQUEST``, so will return any data from ``$_GET``, ``$POST``, or ``$_COOKIE`` (depending on php.ini `request-order `_). + +.. note:: If the incoming request has a ``Content-Type`` header set to ``application/json``, + the ``getVar()`` method returns the JSON data instead of ``$_REQUEST`` data. + +While this is convenient, you will often need to use a more specific method, like: * ``$request->getGet()`` * ``$request->getPost()`` -* ``$request->getServer()`` * ``$request->getCookie()`` +* ``$request->getServer()`` +* ``$request->getEnv()`` -In addition, there are a few utility methods for retrieving information from either $_GET or $_POST, while +In addition, there are a few utility methods for retrieving information from either ``$_GET`` or ``$_POST``, while maintaining the ability to control the order you look for it: -* ``$request->getPostGet()`` - checks $_POST first, then $_GET -* ``$request->getGetPost()`` - checks $_GET first, then $_POST +* ``$request->getPostGet()`` - checks ``$_POST`` first, then ``$_GET`` +* ``$request->getGetPost()`` - checks ``$_GET`` first, then ``$_POST`` -**Getting JSON data** +.. _incomingrequest-getting-json-data: -You can grab the contents of php://input as a JSON stream with ``getJSON()``. +Getting JSON Data +================= + +You can grab the contents of ``php://input`` as a JSON stream with ``getJSON()``. .. note:: This has no way of checking if the incoming data is valid JSON or not, you should only use this method if you know that you're expecting JSON. - .. literalinclude:: incomingrequest/009.php By default, this will return any objects in the JSON data as objects. If you want that converted to associative @@ -99,30 +123,31 @@ arrays, pass in ``true`` as the first parameter. The second and third parameters match up to the ``depth`` and ``options`` arguments of the `json_decode `_ PHP function. -If the incoming request has a ``CONTENT_TYPE`` header set to "application/json", you can also use ``getVar()`` to get +If the incoming request has a ``Content-Type`` header set to ``application/json``, you can also use ``getVar()`` to get the JSON stream. Using ``getVar()`` in this way will always return an object. -**Get Specific Data from JSON** +Getting Specific Data from JSON +=============================== You can get a specific piece of data from a JSON stream by passing a variable name into ``getVar()`` for the data that you want or you can use "dot" notation to dig into the JSON to get data that is not on the root level. - .. literalinclude:: incomingrequest/010.php - If you want the result to be an associative array instead of an object, you can use ``getJsonVar()`` instead and pass true in the second parameter. This function can also be used if you can't guarantee that the incoming request will have the -correct ``CONTENT_TYPE`` header. - +correct ``Content-Type`` header. .. literalinclude:: incomingrequest/011.php -.. note:: See the documentation for ``dot_array_search()`` in the ``Array`` helper for more information on "dot" notation. +.. note:: See the documentation for :php:func:`dot_array_search()` in the ``Array`` helper for more information on "dot" notation. + +.. _incomingrequest-retrieving-raw-data: -**Retrieving Raw data (PUT, PATCH, DELETE)** +Retrieving Raw Data (PUT, PATCH, DELETE) +======================================== -Finally, you can grab the contents of php://input as a raw stream with ``getRawInput()``: +Finally, you can grab the contents of ``php://input`` as a raw stream with ``getRawInput()``: .. literalinclude:: incomingrequest/012.php @@ -130,7 +155,8 @@ This will retrieve data and convert it to an array. Like this: .. literalinclude:: incomingrequest/013.php -**Filtering Input Data** +Filtering Input Data +==================== To maintain security of your application, you will want to filter all input as you access it. You can pass the type of filter to use as the second parameter of any of these methods. The native ``filter_var()`` @@ -145,7 +171,7 @@ All of the methods mentioned above support the filter type passed in as the seco exception of ``getJSON()``. Retrieving Headers ------------------- +****************** You can get access to any header that was sent with the request with the ``headers()`` method, which returns an array of all headers, with the key as the name of the header, and the value is an instance of @@ -171,7 +197,7 @@ If you need the entire header, with the name and values in a single string, simp .. literalinclude:: incomingrequest/019.php The Request URL ---------------- +*************** You can retrieve a :doc:`URI ` object that represents the current URI for this request through the ``$request->getUri()`` method. You can cast this object as a string to get a full URL for the current request: @@ -189,7 +215,7 @@ functions use, so this is a helpful way to "spoof" an incoming request for testi .. literalinclude:: incomingrequest/022.php Uploaded Files --------------- +************** Information about all uploaded files can be retrieved through ``$request->getFiles()``, which returns an array of ``CodeIgniter\HTTP\Files\UploadedFile`` instance. This helps to ease the pain of working with uploaded files, @@ -211,7 +237,7 @@ multi-file upload, based on the filename given in the HTML file input: .. note:: The files here correspond to ``$_FILES``. Even if a user just clicks submit button of a form and does not upload any file, the file will still exist. You can check that the file was actually uploaded by the ``isValid()`` method in UploadedFile. See :ref:`verify-a-file` for more details. Content Negotiation -------------------- +******************* You can easily negotiate content types with the request through the ``negotiate()`` method: @@ -220,7 +246,7 @@ You can easily negotiate content types with the request through the ``negotiate( See the :doc:`Content Negotiation ` page for more details. Class Reference -=============== +*************** .. note:: In addition to the methods listed here, this class inherits the methods from the :doc:`Request Class ` and the :doc:`Message Class `. @@ -276,7 +302,7 @@ The methods provided by the parent classes that are available are: `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_REQUEST if no parameters supplied, otherwise the REQUEST value if found, or null if not + :returns: ``$_REQUEST`` if no parameters supplied, otherwise the REQUEST value if found, or null if not :rtype: mixed|null The first parameter will contain the name of the REQUEST item you are looking for: @@ -315,7 +341,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_GET if no parameters supplied, otherwise the GET value if found, or null if not + :returns: ``$_GET`` if no parameters supplied, otherwise the GET value if found, or null if not :rtype: mixed|null This method is identical to ``getVar()``, only it fetches GET data. @@ -327,7 +353,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_POST if no parameters supplied, otherwise the POST value if found, or null if not + :returns: ``$_POST`` if no parameters supplied, otherwise the POST value if found, or null if not :rtype: mixed|null This method is identical to ``getVar()``, only it fetches POST data. @@ -339,7 +365,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_POST and $_GET combined if no parameters specified (prefer POST value on conflict), + :returns: ``$_POST`` and ``$_GET`` combined if no parameters specified (prefer POST value on conflict), otherwise looks for POST value, if nothing found looks for GET value, if no value found returns null :rtype: mixed|null @@ -359,7 +385,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_GET and $_POST combined if no parameters specified (prefer GET value on conflict), + :returns: ``$_GET`` and ``$_POST`` combined if no parameters specified (prefer GET value on conflict), otherwise looks for GET value, if nothing found looks for POST value, if no value found returns null :rtype: mixed|null @@ -380,7 +406,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_COOKIE if no parameters supplied, otherwise the COOKIE value if found or null if not + :returns: ``$_COOKIE`` if no parameters supplied, otherwise the COOKIE value if found or null if not :rtype: mixed This method is identical to ``getPost()`` and ``getGet()``, only it fetches cookie data: @@ -403,7 +429,7 @@ The methods provided by the parent classes that are available are: found `here `__. :param int $flags: Flags to apply. A list of flags can be found `here `__. - :returns: $_SERVER item value if found, null if not + :returns: ``$_SERVER`` item value if found, null if not :rtype: mixed This method is identical to the ``getPost()``, ``getGet()`` and ``getCookie()`` diff --git a/user_guide_src/source/incoming/incomingrequest/022.php b/user_guide_src/source/incoming/incomingrequest/022.php index a726fcf2d5cf..723d81e3a3a3 100644 --- a/user_guide_src/source/incoming/incomingrequest/022.php +++ b/user_guide_src/source/incoming/incomingrequest/022.php @@ -7,7 +7,9 @@ final class MyMenuTest extends CIUnitTestCase public function testActiveLinkUsesCurrentUrl() { service('request')->setPath('users/list'); + $menu = new MyMenu(); + $this->assertTrue('users/list', $menu->getActiveLink()); } } diff --git a/user_guide_src/source/incoming/request.rst b/user_guide_src/source/incoming/request.rst index 153eef5f78fb..0b6128a4bd19 100644 --- a/user_guide_src/source/incoming/request.rst +++ b/user_guide_src/source/incoming/request.rst @@ -28,9 +28,8 @@ Class Reference .. literalinclude:: request/001.php - .. important:: This method takes into account the ``App->proxyIPs`` setting and will - return the reported HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP, HTTP_X_CLIENT_IP, or - HTTP_X_CLUSTER_CLIENT_IP address for the allowed IP address. + .. important:: This method takes into account the ``Config\App::$proxyIPs`` setting and will + return the reported client IP address by the HTTP header for the allowed IP address. .. php:method:: isValidIP($ip[, $which = '']) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index aa47dc2e1264..6ce4ce319d1e 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -32,13 +32,42 @@ If you expect a GET request, you use the ``get()`` method: .. literalinclude:: routing/001.php A route takes the URI path (``/``) on the left, and maps it to the controller and method (``Home::index``) on the right, -along with any parameters that should be passed to the controller. The controller and method should +along with any parameters that should be passed to the controller. + +The controller and method should be listed in the same way that you would use a static method, by separating the class -and its method with a double-colon, like ``Users::list``. If that method requires parameters to be +and its method with a double-colon, like ``Users::list``. + +If that method requires parameters to be passed to it, then they would be listed after the method name, separated by forward-slashes: .. literalinclude:: routing/002.php +Examples +======== + +Here are a few basic routing examples. + +A URL containing the word **journals** in the first segment will be mapped to the ``\App\Controllers\Blogs`` class, +and the default method, which is usually ``index()``: + +.. literalinclude:: routing/006.php + +A URL containing the segments **blog/joe** will be mapped to the ``\App\Controllers\Blogs`` class and the ``users()`` method. +The ID will be set to ``34``: + +.. literalinclude:: routing/007.php + +A URL with **product** as the first segment, and anything in the second will be mapped to the ``\App\Controllers\Catalog`` class +and the ``productLookup()`` method: + +.. literalinclude:: routing/008.php + +A URL with **product** as the first segment, and a number in the second will be mapped to the ``\App\Controllers\Catalog`` class +and the ``productLookupByID()`` method passing in the match as a variable to the method: + +.. literalinclude:: routing/009.php + HTTP verbs ========== @@ -99,31 +128,6 @@ Placeholders Description .. note:: ``{locale}`` cannot be used as a placeholder or other part of the route, as it is reserved for use in :doc:`localization `. -Examples -======== - -Here are a few basic routing examples. - -A URL containing the word **journals** in the first segment will be mapped to the ``\App\Controllers\Blogs`` class, -and the default method, which is usually ``index()``: - -.. literalinclude:: routing/006.php - -A URL containing the segments **blog/joe** will be mapped to the ``\App\Controllers\Blogs`` class and the ``users()`` method. -The ID will be set to ``34``: - -.. literalinclude:: routing/007.php - -A URL with **product** as the first segment, and anything in the second will be mapped to the ``\App\Controllers\Catalog`` class -and the ``productLookup()`` method: - -.. literalinclude:: routing/008.php - -A URL with **product** as the first segment, and a number in the second will be mapped to the ``\App\Controllers\Catalog`` class -and the ``productLookupByID()`` method passing in the match as a variable to the method: - -.. literalinclude:: routing/009.php - Note that a single ``(:any)`` will match multiple segments in the URL if present. For example the route: .. literalinclude:: routing/010.php @@ -133,6 +137,9 @@ Controller should take into account the maximum parameters: .. literalinclude:: routing/011.php +.. important:: Do not put any placeholder after ``(:any)``. Because the number of + parameters passed to the controller method may change. + If matching multiple segments is not the intended behavior, ``(:segment)`` should be used when defining the routes. With the examples URLs from above: @@ -188,7 +195,7 @@ is allowed, as are back-references. .. literalinclude:: routing/018.php -In the above example, a URI similar to **products/shirts/123** would instead call the ``show`` method +In the above example, a URI similar to **products/shirts/123** would instead call the ``show()`` method of the ``Products`` controller class, with the original first and second segment passed as arguments to it. With regular expressions, you can also catch a segment containing a forward slash (``/``), which would usually @@ -202,7 +209,7 @@ redirect them back to the same page after they log in, you may find this example For those of you who don't know regular expressions and want to learn more about them, `regular-expressions.info `_ might be a good starting point. -.. important:: Note: You can also mix and match wildcards with regular expressions. +.. note:: You can also mix and match placeholders with regular expressions. Closures ======== @@ -354,8 +361,8 @@ available from the command line: .. note:: It is recommended to use Spark Commands for CLI scripts instead of calling controllers via CLI. See the :doc:`../cli/cli_commands` page for detailed information. -.. warning:: If you enable :ref:`auto-routing` and place the command file in **app/Controllers**, - anyone could access the command with the help of auto-routing via HTTP. +.. warning:: If you enable :ref:`auto-routing-legacy` and place the command file in **app/Controllers**, + anyone could access the command with the help of Auto Routing (Legacy) via HTTP. Global Options ============== @@ -379,8 +386,8 @@ The value for the filter can be a string or an array of strings: See :doc:`Controller filters ` for more information on setting up filters. .. Warning:: If you set filters to routes in **app/Config/Routes.php** - (not in **app/Config/Filters.php**), it is recommended to disable auto-routing. - When auto-routing is enabled, it may be possible that a controller can be accessed + (not in **app/Config/Filters.php**), it is recommended to disable Auto Routing (Legacy). + When :ref:`auto-routing-legacy` is enabled, it may be possible that a controller can be accessed via a different URL than the configured route, in which case the filter you specified to the route will not be applied. See :ref:`use-defined-routes-only` to disable auto-routing. @@ -578,6 +585,10 @@ Auto Routing (Improved) Since v4.2.0, the new more secure Auto Routing has been introduced. +.. note:: If you are familiar with Auto Routing, which was enabled by default + from CodeIgniter 3 through 4.1.x, you can see the differences in + :ref:`ChangeLog v4.2.0 `. + When no defined route is found that matches the URI, the system will attempt to match that URI against the controllers and methods when Auto Routing is enabled. .. important:: For security reasons, if a controller is used in the defined routes, Auto Routing (Improved) does not route to the controller. @@ -661,7 +672,7 @@ In this example, if the user were to visit **example.com/products**, and a ``Pro .. note:: You cannot access the controller with the URI of the default method name. In the example above, you can access **example.com/products**, but if you access **example.com/products/listall**, it will be not found. -.. _auto-routing: +.. _auto-routing-legacy: Auto Routing (Legacy) ********************* @@ -705,7 +716,7 @@ Consider this URI:: In the above example, CodeIgniter would attempt to find a controller named **Helloworld.php** and executes ``index()`` method with passing ``'1'`` as the first argument. -See :ref:`Auto Routing (Legacy) in Controllers ` for more info. +See :ref:`Auto Routing (Legacy) in Controllers ` for more info. Configuration Options (Legacy) ============================== @@ -725,7 +736,7 @@ The default controller is also used when no matching route has been found, and t in the controllers directory. For example, if the user visits **example.com/admin**, if a controller was found at **app/Controllers/Admin/Home.php**, it would be used. -See :ref:`Auto Routing in Controllers ` for more info. +See :ref:`Auto Routing (Legacy) in Controllers ` for more info. Default Method (Legacy) ----------------------- diff --git a/user_guide_src/source/installation/installing_composer.rst b/user_guide_src/source/installation/installing_composer.rst index 3cb0c42d0781..fc3fbe5c5c7b 100644 --- a/user_guide_src/source/installation/installing_composer.rst +++ b/user_guide_src/source/installation/installing_composer.rst @@ -72,12 +72,13 @@ Read the :doc:`upgrade instructions `, and check Breaking Changes and Pros ---- -Simple installation; easy to update +Simple installation; easy to update. Cons ---- -You still need to check for ``app/Config`` changes after updating +You still need to check for file changes in the **project space** +(root, app, public, writable) after updating. Structure --------- @@ -155,12 +156,13 @@ Read the :doc:`upgrade instructions `, and check Breaking Changes and Pros ---- -Relatively simple installation; easy to update +Relatively simple installation; easy to update. Cons ---- -You still need to check for ``app/Config`` changes after updating +You still need to check for file changes in the **project space** +(root, app, public, writable) after updating. Structure --------- diff --git a/user_guide_src/source/installation/installing_manual.rst b/user_guide_src/source/installation/installing_manual.rst index 82b13b1066e3..9ae1403b6cbd 100644 --- a/user_guide_src/source/installation/installing_manual.rst +++ b/user_guide_src/source/installation/installing_manual.rst @@ -44,12 +44,12 @@ Read the :doc:`upgrade instructions `, and check Breaking Changes and Pros ==== -Download and run +Download and run. Cons ==== -You are responsible for merge conflicts when updating +You are responsible for merge conflicts when updating. Structure ========= diff --git a/user_guide_src/source/installation/troubleshooting.rst b/user_guide_src/source/installation/troubleshooting.rst index 40043971728a..426769938a49 100644 --- a/user_guide_src/source/installation/troubleshooting.rst +++ b/user_guide_src/source/installation/troubleshooting.rst @@ -24,9 +24,9 @@ I have to include index.php in my URL ------------------------------------- If a URL like ``/mypage/find/apple`` doesn't work, but the similar -URL ``/index.php/mypage/find/apple`` does, that sounds like your ``.htaccess`` rules +URL ``/index.php/mypage/find/apple`` does, that sounds like your **.htaccess** rules (for Apache) are not set up properly, or the ``mod_rewrite`` extension -in Apache's ``httpd.conf`` is commented out. +in Apache's **httpd.conf** is commented out. Only the default page loads --------------------------- @@ -37,7 +37,7 @@ REQUEST_URI variable needed to serve search-engine friendly URLs. As a first step, open your **app/Config/App.php** file and look for the URI Protocol information. It will recommend that you try a couple of alternate settings. If it still doesn't work after you've tried this -you'll need to force CodeIgniter to add a question mark to your URLs. To +you'll need to force CodeIgniter to add a question mark (``?``) to your URLs. To do this open your **app/Config/App.php** file and change this: .. literalinclude:: troubleshooting/001.php @@ -59,7 +59,7 @@ The tutorial gives 404 errors everywhere :( ------------------------------------------- You can't follow the tutorial using PHP's built-in web server. -It doesn't process the `.htaccess` file needed to route +It doesn't process the **.htaccess** file needed to route requests properly. The solution: use Apache to serve your site, or else the built-in @@ -88,7 +88,7 @@ CodeIgniter Error Logs CodeIgniter logs error messages, according to the settings in **app/Config/Logger.php**. -You can adjust the error threshold to see more or fewer messages. +You can adjust the error threshold to see more or fewer messages. See :ref:`Logging ` for details. -The default configuration has daily log files stored in `writable/logs`. +The default configuration has daily log files stored in **writable/logs**. It would be a good idea to check them if things aren't working the way you expect! diff --git a/user_guide_src/source/installation/troubleshooting/002.php b/user_guide_src/source/installation/troubleshooting/002.php index 719aabac2f65..688791f44285 100644 --- a/user_guide_src/source/installation/troubleshooting/002.php +++ b/user_guide_src/source/installation/troubleshooting/002.php @@ -9,5 +9,6 @@ class App extends BaseConfig // ... public $indexPage = 'index.php?'; + // ... } diff --git a/user_guide_src/source/installation/upgrade_415.rst b/user_guide_src/source/installation/upgrade_415.rst index 21c3fa44d94f..ae10e90f6874 100644 --- a/user_guide_src/source/installation/upgrade_415.rst +++ b/user_guide_src/source/installation/upgrade_415.rst @@ -56,8 +56,8 @@ If you want the same behavior as the previous version, set the CSRF filter like Protecting **GET** method needs only when you use ``form_open()`` auto-generation of CSRF field. -.. 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. +.. Warning:: In general, if you use ``$methods`` filters, you should :ref:`disable Auto Routing (Legacy) ` + because :ref:`auto-routing-legacy` permits any HTTP method to access a controller. Accessing the controller with a method you don't expect could bypass the filter. CURLRequest header change diff --git a/user_guide_src/source/installation/upgrade_4211.rst b/user_guide_src/source/installation/upgrade_4211.rst new file mode 100644 index 000000000000..bc5dbc51d1cc --- /dev/null +++ b/user_guide_src/source/installation/upgrade_4211.rst @@ -0,0 +1,72 @@ +############################### +Upgrading from 4.2.10 to 4.2.11 +############################### + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +Breaking Changes +**************** + +.. _upgrade-4211-proxyips: + +Config\\App::$proxyIPs +====================== + +The config value format has been changed. Now you must set your proxy IP address and the HTTP header name for the client IP address pair as an array:: + + public $proxyIPs = [ + '10.0.1.200' => 'X-Forwarded-For', + '192.168.5.0/24' => 'X-Forwarded-For', + ]; + +``ConfigException`` will be thrown for old format config value. + +.. _upgrade-4211-session-key: + +Session Handler Key Changes +=========================== + +The key of the session data record for :ref:`sessions-databasehandler-driver`, +:ref:`sessions-memcachedhandler-driver` and :ref:`sessions-redishandler-driver` +has changed. Therefore, any existing session data will be invalidated after +the upgrade if you are using these session handlers. + +- When using ``DatabaseHandler``, the ``id`` column value in the session table + now contains the session cookie name (``Config\App::$sessionCookieName``). +- When using ``MemcachedHandler`` or ``RedisHandler``, the key value contains + the session cookie name (``Config\App::$sessionCookieName``). + +There is maximum length for the ``id`` column and Memcached key (250 bytes). +If the following values exceed those maximum length, the session will not work properly. + +- the session cookie name, delimiter, and session id (32 characters by default) + when using ``DatabaseHandler`` +- the prefix (``ci_session``), session cookie name, delimiters, and session id + when using ``MemcachedHandler`` + +Project Files +************* + +Version ``4.2.11`` did not alter any executable code in project files. + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +* app/Config/App.php +* app/Config/Autoload.php +* app/Config/Logger.php +* app/Config/Toolbar.php +* app/Views/welcome_message.php +* composer.json +* phpunit.xml.dist diff --git a/user_guide_src/source/installation/upgrade_4xx.rst b/user_guide_src/source/installation/upgrade_4xx.rst index b0c3d3dea9be..6eadd080a02c 100644 --- a/user_guide_src/source/installation/upgrade_4xx.rst +++ b/user_guide_src/source/installation/upgrade_4xx.rst @@ -58,7 +58,7 @@ Application Structure Routing ======= -- The Auto Routing is disabled by default. If you want to use the Auto Routing in the same way as CI3, you need to enable :ref:`auto-routing`. +- The Auto Routing is disabled by default. If you want to use the Auto Routing in the same way as CI3, you need to enable :ref:`auto-routing-legacy`. - CI4 also has an optional new more secure :ref:`auto-routing-improved`. Model, View and Controller diff --git a/user_guide_src/source/installation/upgrade_routing.rst b/user_guide_src/source/installation/upgrade_routing.rst index 669b00dc2751..0fc9fe5251d3 100644 --- a/user_guide_src/source/installation/upgrade_routing.rst +++ b/user_guide_src/source/installation/upgrade_routing.rst @@ -21,11 +21,12 @@ What has been changed Upgrade Guide ============= -1. If you use the Auto Routing in the same way as CI3, you need to enable :ref:`auto-routing`. -2. You have to change the syntax of each routing line and append it in **app/Config/Routes.php**. For example: +1. If you use the Auto Routing in the same way as CI3, you need to enable :ref:`auto-routing-legacy`. +2. The placeholder ``(:any)`` in CI3 will be ``(:segment)`` in CI4. +3. You have to change the syntax of each routing line and append it in **app/Config/Routes.php**. For example: - ``$route['journals'] = 'blogs';`` to ``$routes->add('journals', 'Blogs::index');``. This would map to the ``index()`` method in the ``Blogs`` controller. - - ``$route['product/(:any)'] = 'catalog/product_lookup';`` to ``$routes->add('product/(:any)', 'Catalog::productLookup');`` + - ``$route['product/(:any)'] = 'catalog/product_lookup';`` to ``$routes->add('product/(:segment)', 'Catalog::productLookup');`` - ``$route['login/(.+)'] = 'auth/login/$1';`` to ``$routes->add('login/(.+)', 'Auth::login/$1');`` Code Example diff --git a/user_guide_src/source/installation/upgrade_routing/001.php b/user_guide_src/source/installation/upgrade_routing/001.php index 4bc53ace4321..1378517e258d 100644 --- a/user_guide_src/source/installation/upgrade_routing/001.php +++ b/user_guide_src/source/installation/upgrade_routing/001.php @@ -2,14 +2,13 @@ namespace Config; -// Create a new instance of our RouteCollection class. -$routes = Services::routes(); +// ... -// Load the system's routing file first, so that the app and ENVIRONMENT -// can override as needed. -if (file_exists(SYSTEMPATH . 'Config/Routes.php')) { - require SYSTEMPATH . 'Config/Routes.php'; -} +/* + * -------------------------------------------------------------------- + * Route Definitions + * -------------------------------------------------------------------- + */ // ... @@ -21,4 +20,6 @@ $routes->add('posts/update', 'Posts::update'); $routes->add('drivers/create', 'Drivers::create'); $routes->add('drivers/update', 'Drivers::update'); -$routes->add('posts/(:any)', 'Posts::view/$1'); +$routes->add('posts/(:segment)', 'Posts::view/$1'); + +// ... diff --git a/user_guide_src/source/installation/upgrade_routing/ci3sample/001.php b/user_guide_src/source/installation/upgrade_routing/ci3sample/001.php index 31875312f013..c4facdd8ff03 100644 --- a/user_guide_src/source/installation/upgrade_routing/ci3sample/001.php +++ b/user_guide_src/source/installation/upgrade_routing/ci3sample/001.php @@ -1,12 +1,14 @@ `_ version 7.4 or newer is required, with the -`*intl* extension `_ and `*mbstring* extension `_ -installed. +.. contents:: + :local: + :depth: 2 + +*************************** +PHP and Required Extensions +*************************** + +`PHP `_ version 7.4 or newer is required, with the following PHP extensions are enabled: + + - `intl `_ + - `mbstring `_ + - `json `_ + +*********************** +Optional PHP Extensions +*********************** The following PHP extensions should be enabled on your server: - - ``php-json`` - - ``php-mysqlnd`` (if you use MySQL) - - ``php-xml`` + - `mysqlnd `_ (if you use MySQL) + - `curl `_ (if you use :doc:`CURLRequest `) + - `imagick `_ (if you use :doc:`Image ` class ImageMagickHandler) + - `gd `_ (if you use :doc:`Image ` class GDHandler) + - `simplexml `_ (if you format XML) + +The following PHP extensions are required when you use a Cache server: + + - `memcache `_ (if you use :doc:`Cache ` class MemcachedHandler with Memcache) + - `memcached `_ (if you use :doc:`Cache ` class MemcachedHandler with Memcached) + - `redis `_ (if you use :doc:`Cache ` class RedisHandler) + +The following PHP extensions are required when you use PHPUnit: + + - `dom `_ (if you use :doc:`TestResponse ` class) + - `libxml `_ (if you use :doc:`TestResponse ` class) + - `xdebug `_ (if you use ``CIUnitTestCase::assertHeaderEmitted()``) + +.. _requirements-supported-databases: -In order to use the :doc:`CURLRequest `, you will need -`libcurl `_ installed. +******************* +Supported Databases +******************* A database is required for most web application programming. Currently supported databases are: diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index cbec85970755..7756fafe8490 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -147,8 +147,8 @@ It is also possible to enable the CSRF filter only for specific methods: .. literalinclude:: security/009.php -.. Warning:: If you use ``$methods`` filters, you should :ref:`disable auto-routing ` - because auto-routing permits any HTTP method to access a controller. +.. Warning:: If you use ``$methods`` filters, you should :ref:`disable Auto Routing (Legacy) ` + because :ref:`auto-routing-legacy` permits any HTTP method to access a controller. Accessing the controller with a method you don't expect could bypass the filter. HTML Forms diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index cf9fb7c19a1f..136a958072a8 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -358,8 +358,8 @@ same way: unusable during the same request after you destroy the session. You may also use the ``stop()`` method to completely kill the session -by removing the old session_id, destroying all data, and destroying -the cookie that contained the session id: +by removing the old session ID, destroying all data, and destroying +the cookie that contained the session ID: .. literalinclude:: sessions/038.php @@ -390,26 +390,35 @@ all of the options and their effects. You'll find the following Session related preferences in your **app/Config/App.php** file: -============================== ============================================ ================================================= ============================================================================================ -Preference Default Options Description -============================== ============================================ ================================================= ============================================================================================ -**sessionDriver** CodeIgniter\\Session\\Handlers\\FileHandler CodeIgniter\\Session\\Handlers\\FileHandler The session storage driver to use. - CodeIgniter\\Session\\Handlers\\DatabaseHandler - CodeIgniter\\Session\\Handlers\\MemcachedHandler - CodeIgniter\\Session\\Handlers\\RedisHandler - CodeIgniter\\Session\\Handlers\\ArrayHandler -**sessionCookieName** ci_session [A-Za-z\_-] characters only The name used for the session cookie. -**sessionExpiration** 7200 (2 hours) Time in seconds (integer) The number of seconds you would like the session to last. - If you would like a non-expiring session (until browser is closed) set the value to zero: 0 -**sessionSavePath** null None Specifies the storage location, depends on the driver being used. -**sessionMatchIP** false true/false (boolean) Whether to validate the user's IP address when reading the session cookie. - Note that some ISPs dynamically changes the IP, so if you want a non-expiring session you - will likely set this to false. -**sessionTimeToUpdate** 300 Time in seconds (integer) This option controls how often the session class will regenerate itself and create a new - session ID. Setting it to 0 will disable session ID regeneration. -**sessionRegenerateDestroy** false true/false (boolean) Whether to destroy session data associated with the old session ID when auto-regenerating - the session ID. When set to false, the data will be later deleted by the garbage collector. -============================== ============================================ ================================================= ============================================================================================ +============================== ================== =========================== ============================================================ +Preference Default Options Description +============================== ================== =========================== ============================================================ +**sessionDriver** FileHandler::class FileHandler::class The session storage driver to use. + DatabaseHandler::class All the session drivers are located in the + MemcachedHandler::class ``CodeIgniter\Session\Handlers\`` namespace. + RedisHandler::class + ArrayHandler::class +**sessionCookieName** ci_session [A-Za-z\_-] characters only The name used for the session cookie. + The value will be included in the key of the + Database/Memcached/Redis session records. So, set the value + so that it does not exceed the maximum length of the key. +**sessionExpiration** 7200 (2 hours) Time in seconds (integer) The number of seconds you would like the session to last. + If you would like a non-expiring session (until browser is + closed) set the value to zero: 0 +**sessionSavePath** null None Specifies the storage location, depends on the driver being + used. +**sessionMatchIP** false true/false (boolean) Whether to validate the user's IP address when reading the + session cookie. Note that some ISPs dynamically changes the IP, + so if you want a non-expiring session you will likely set this + to false. +**sessionTimeToUpdate** 300 Time in seconds (integer) This option controls how often the session class will + regenerate itself and create a new session ID. Setting it to 0 + will disable session ID regeneration. +**sessionRegenerateDestroy** false true/false (boolean) Whether to destroy session data associated with the old + session ID when auto-regenerating + the session ID. When set to false, the data will be later + deleted by the garbage collector. +============================== ================== =========================== ============================================================ .. note:: As a last resort, the Session library will try to fetch PHP's session related INI settings, as well as legacy CI settings such as @@ -498,9 +507,9 @@ permissions will probably break your application. Instead, you should do something like this, depending on your environment :: - mkdir //Writable/sessions/ - chmod 0700 //Writable/sessions/ - chown www-data //Writable/sessions/ + > mkdir //writable/sessions/ + > chmod 0700 //writable/sessions/ + > chown www-data //writable/sessions/ Bonus Tip --------- @@ -518,6 +527,8 @@ In addition, if performance is your only concern, you may want to look into using `tmpfs `_, (warning: external resource), which can make your sessions blazing fast. +.. _sessions-databasehandler-driver: + DatabaseHandler Driver ====================== @@ -561,6 +572,10 @@ For PostgreSQL:: CREATE INDEX "ci_sessions_timestamp" ON "ci_sessions" ("timestamp"); +.. note:: The ``id`` value contains the session cookie name (``Config\App::$sessionCookieName``) + and the session ID and a delimiter. It should be increased as needed, for example, + when using long session IDs. + You will also need to add a PRIMARY KEY **depending on your 'sessionMatchIP' setting**. The examples below work both on MySQL and PostgreSQL:: @@ -595,6 +610,8 @@ when it generates the code. done processing session data if you're having performance issues. +.. _sessions-redishandler-driver: + RedisHandler Driver =================== @@ -631,6 +648,8 @@ sufficient: .. literalinclude:: sessions/041.php +.. _sessions-memcachedhandler-driver: + MemcachedHandler Driver ======================= diff --git a/user_guide_src/source/libraries/sessions/039.php b/user_guide_src/source/libraries/sessions/039.php index 3aedc2f047af..ae2941cc39ce 100644 --- a/user_guide_src/source/libraries/sessions/039.php +++ b/user_guide_src/source/libraries/sessions/039.php @@ -6,7 +6,9 @@ class App extends BaseConfig { - public $sessionDriver = 'CodeIgniter\Session\Handlers\DatabaseHandler'; + // ... + public $sessionDriver = 'CodeIgniter\Session\Handlers\DatabaseHandler'; + // ... public $sessionSavePath = 'ci_sessions'; // ... } diff --git a/user_guide_src/source/libraries/sessions/040.php b/user_guide_src/source/libraries/sessions/040.php index f645887ec2be..a9d3bd4f44d0 100644 --- a/user_guide_src/source/libraries/sessions/040.php +++ b/user_guide_src/source/libraries/sessions/040.php @@ -6,6 +6,7 @@ class App extends BaseConfig { + // ... public $sessionDBGroup = 'groupName'; // ... } diff --git a/user_guide_src/source/libraries/sessions/041.php b/user_guide_src/source/libraries/sessions/041.php index f0aed919a816..bbec5e664844 100644 --- a/user_guide_src/source/libraries/sessions/041.php +++ b/user_guide_src/source/libraries/sessions/041.php @@ -6,7 +6,9 @@ class App extends BaseConfig { - public $sessionDiver = 'CodeIgniter\Session\Handlers\RedisHandler'; + // ... + public $sessionDiver = 'CodeIgniter\Session\Handlers\RedisHandler'; + // ... public $sessionSavePath = 'tcp://localhost:6379'; // ... } diff --git a/user_guide_src/source/libraries/sessions/042.php b/user_guide_src/source/libraries/sessions/042.php index cad56413151c..e5da9ddeb355 100644 --- a/user_guide_src/source/libraries/sessions/042.php +++ b/user_guide_src/source/libraries/sessions/042.php @@ -6,7 +6,9 @@ class App extends BaseConfig { - public $sessionDriver = 'CodeIgniter\Session\Handlers\MemcachedHandler'; + // ... + public $sessionDriver = 'CodeIgniter\Session\Handlers\MemcachedHandler'; + // ... public $sessionSavePath = 'localhost:11211'; // ... } diff --git a/user_guide_src/source/libraries/sessions/043.php b/user_guide_src/source/libraries/sessions/043.php index e5fab8602236..7174dcd76973 100644 --- a/user_guide_src/source/libraries/sessions/043.php +++ b/user_guide_src/source/libraries/sessions/043.php @@ -6,6 +6,8 @@ class App extends BaseConfig { + // ... + // localhost will be given higher priority (5) here, // compared to 192.0.2.1 with a weight of 1. public $sessionSavePath = 'localhost:11211:5,192.0.2.1:11211:1'; diff --git a/user_guide_src/source/libraries/throttler.rst b/user_guide_src/source/libraries/throttler.rst index 49d4bb4e1917..3a7d7bda6df0 100644 --- a/user_guide_src/source/libraries/throttler.rst +++ b/user_guide_src/source/libraries/throttler.rst @@ -73,8 +73,8 @@ Next, we assign it to all POST requests made on the site: .. literalinclude:: throttler/004.php -.. Warning:: If you use ``$methods`` filters, you should :ref:`disable auto-routing ` - because auto-routing permits any HTTP method to access a controller. +.. Warning:: If you use ``$methods`` filters, you should :ref:`disable Auto Routing (Legacy) ` + because :ref:`auto-routing-legacy` permits any HTTP method to access a controller. Accessing the controller with a method you don't expect could bypass the filter. And that's all there is to it. Now all POST requests made on the site will have to be rate limited. diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 8bf0c074fc08..ec7f237d7172 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -276,6 +276,8 @@ To give a labeled error message you can set up as: .. literalinclude:: validation/007.php +.. _validation-withrequest: + withRequest() ============= @@ -286,6 +288,15 @@ data to be validated: .. literalinclude:: validation/008.php +.. note:: This method gets JSON data from + :ref:`$request->getJSON() ` + when the request is a JSON request (``Content-Type: application/json``), + or gets Raw data from + :ref:`$request->getRawInput() ` + when the request is a PUT, PATCH, DELETE request and + is not HTML form post (``Content-Type: multipart/form-data``), + or gets data from :ref:`$request->getVar() `. + Working with Validation *********************** diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 6dbdb0a96951..7cf9f34ec29b 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -53,7 +53,10 @@ that extends ``CodeIgniter\Model``: This empty class provides convenient access to the database connection, the Query Builder, and a number of additional convenience methods. -Should you need additional setup in your model you may extend the ``initialize()`` function +initialize() +============ + +Should you need additional setup in your model you may extend the ``initialize()`` method which will be run immediately after the Model's constructor. This allows you to perform extra steps without repeating the constructor parameters, for example extending other models: @@ -76,7 +79,7 @@ configuration file. Configuring Your Model ====================== -The model class has a few configuration options that can be set to allow the class' methods +The model class has some configuration options that can be set to allow the class' methods to work seamlessly for you. The first two are used by all of the CRUD methods to determine what table to use and how we can find the required records: @@ -121,6 +124,8 @@ qualified name of a class** that can be used with the Result object's ``getCusto method. Using the special ``::class`` constant of the class will allow most IDEs to auto-complete the name and allow functions like refactoring to better understand your code. +.. _model-use-soft-deletes: + $useSoftDeletes --------------- @@ -146,49 +151,60 @@ potential mass assignment vulnerabilities. .. note:: The ``$primaryKey`` field should never be an allowed field. +Dates +----- + $useTimestamps --------------- +^^^^^^^^^^^^^^ This boolean value determines whether the current date is automatically added to all inserts and updates. If true, will set the current time in the format specified by ``$dateFormat``. This -requires that the table have columns named **created_at** and **updated_at** in the appropriate +requires that the table have columns named **created_at**, **updated_at** and **deleted_at** in the appropriate data type. +$dateFormat +^^^^^^^^^^^ + +This value works with ``$useTimestamps`` and ``$useSoftDeletes`` to ensure that the correct type of +date value gets inserted into the database. By default, this creates DATETIME values, but +valid options are: ``'datetime'``, ``'date'``, or ``'int'`` (a PHP timestamp). Using **useSoftDeletes** or +**useTimestamps** with an invalid or missing **dateFormat** will cause an exception. + $createdField -------------- +^^^^^^^^^^^^^ Specifies which database field to use for data record create timestamp. Leave it empty to avoid updating it (even if ``$useTimestamps`` is enabled). $updatedField -------------- +^^^^^^^^^^^^^ Specifies which database field should use for keep data record update timestamp. Leave it empty to avoid update it (even ``$useTimestamps`` is enabled). -$dateFormat ------------ +$deletedField +^^^^^^^^^^^^^ -This value works with ``$useTimestamps`` and ``$useSoftDeletes`` to ensure that the correct type of -date value gets inserted into the database. By default, this creates DATETIME values, but -valid options are: ``'datetime'``, ``'date'``, or ``'int'`` (a PHP timestamp). Using **useSoftDeletes** or -useTimestamps with an invalid or missing dateFormat will cause an exception. +Specifies which database field to use for soft deletions. See :ref:`model-use-soft-deletes`. + +Validation +---------- $validationRules ----------------- +^^^^^^^^^^^^^^^^ Contains either an array of validation rules as described in :ref:`validation-array` or a string containing the name of a validation group, as described in the same section. Described in more detail below. $validationMessages -------------------- +^^^^^^^^^^^^^^^^^^^ Contains an array of custom error messages that should be used during validation, as described in :ref:`validation-custom-errors`. Described in more detail below. $skipValidation ---------------- +^^^^^^^^^^^^^^^ Whether validation should be skipped during all **inserts** and **updates**. The default value is ``false``, meaning that data will always attempt to be validated. This is @@ -198,7 +214,7 @@ this model will never validate. .. _clean-validation-rules: $cleanValidationRules ---------------------- +^^^^^^^^^^^^^^^^^^^^^ Whether validation rules should be removed that do not exist in the passed data. This is used in **updates**. @@ -210,28 +226,35 @@ You can also change the value by the ``cleanRules()`` method. .. note:: Prior to v4.2.7, ``$cleanValidationRules`` did not work due to a bug. +Callbacks +--------- + +$allowCallbacks +^^^^^^^^^^^^^^^ + +Whether the callbacks defined below should be used. + $beforeInsert -------------- +^^^^^^^^^^^^^ $afterInsert ------------- +^^^^^^^^^^^^ $beforeUpdate -------------- +^^^^^^^^^^^^^ $afterUpdate ------------- +^^^^^^^^^^^^ +$beforeFind +^^^^^^^^^^^ $afterFind ----------- +^^^^^^^^^^ +$beforeDelete +^^^^^^^^^^^^^ $afterDelete ------------- +^^^^^^^^^^^^ These arrays allow you to specify callback methods that will be run on the data at the time specified in the property name. -$allowCallbacks ---------------- - -Whether the callbacks defined above should be used. - -Working With Data +Working with Data ***************** Finding Data @@ -254,8 +277,8 @@ of just one: .. literalinclude:: model/007.php -If no parameters are passed in, will return all rows in that model's table, effectively acting -like ``findAll()``, though less explicit. +.. note:: If no parameters are passed in, ``find()`` will return all rows in that model's table, + effectively acting like ``findAll()``, though less explicit. findColumn() ------------ @@ -264,7 +287,7 @@ Returns null or an indexed array of column values: .. literalinclude:: model/008.php -``$column_name`` should be a name of single column else you will get the DataException. +``$column_name`` should be a name of single column else you will get the ``DataException``. findAll() --------- @@ -292,7 +315,7 @@ Returns the first row in the result set. This is best used in combination with t withDeleted() ------------- -If ``$useSoftDeletes`` is true, then the **find*()** methods will not return any rows where 'deleted_at IS NOT NULL'. +If ``$useSoftDeletes`` is true, then the **find*()** methods will not return any rows where ``deleted_at IS NOT NULL``. To temporarily override this, you can use the ``withDeleted()`` method prior to calling the **find*()** method. .. literalinclude:: model/013.php @@ -332,6 +355,8 @@ of the columns in a ``$table``, while the array's values are the values to save .. literalinclude:: model/016.php +.. important:: If the ``$primaryKey`` field is set to ``null`` then the update will affect all records in the table. + Multiple records may be updated with a single call by passing an array of primary keys as the first parameter: .. literalinclude:: model/017.php @@ -546,33 +571,6 @@ testing, migrations, or seeds. In these cases, you can turn the protection on or .. literalinclude:: model/042.php -Working With Query Builder -========================== - -You can get access to a shared instance of the Query Builder for that model's database connection any time you -need it: - -.. literalinclude:: model/043.php - -This builder is already set up with the model's ``$table``. If you need access to another table -you can pass it in as a parameter, but be aware that this will not return a shared instance: - -.. literalinclude:: model/044.php - -You can also use Query Builder methods and the Model's CRUD methods in the same chained call, allowing for -very elegant use: - -.. literalinclude:: model/045.php - -.. important:: The Model does not provide a perfect interface to the Query Builder. - The Model and the Query Builder are separate classes with different purposes. - They should not be expected to return the same data. - For example, if you need to get the compiledInsert you should do so directly on the builder instance. - -.. note:: You can also access the model's database connection seamlessly: - - .. literalinclude:: model/046.php - Runtime Return Type Changes =========================== @@ -609,6 +607,57 @@ This is best used during cronjobs, data exports, or other large tasks. .. literalinclude:: model/049.php +Working with Query Builder +************************** + +Getting Query Builder for the Model's Table +=========================================== + +CodeIgniter Model has one instance of the Query Builder for that model's database connection. +You can get access to the **shared** instance of the Query Builder any time you need it: + +.. literalinclude:: model/043.php + +This builder is already set up with the model's ``$table``. + +.. note:: Once you get the Query Builder instance, you can call methods of the + :doc:`Query Builder <../database/query_builder>`. + However, since Query Builder is not a Model, you cannot call methods of the Model. + +Getting Query Builder for Another Table +======================================= + +If you need access to another table, you can get another instance of the Query Builder. +Pass the table name in as a parameter, but be aware that this will **not** return +a shared instance: + +.. literalinclude:: model/044.php + +Mixing Methods of Query Builder and Model +========================================= + +You can also use Query Builder methods and the Model's CRUD methods in the same chained call, allowing for +very elegant use: + +.. literalinclude:: model/045.php + +In this case, it operates on the shared instance of the Query Builder held by the model. + +.. important:: The Model does not provide a perfect interface to the Query Builder. + The Model and the Query Builder are separate classes with different purposes. + They should not be expected to return the same data. + +If the Query Builder returns a result, it is returned as is. +In that case, the result may be different from the one returned by the model's method +and may not be what was expected. The model's events are not triggered. + +To prevent unexpected behavior, do not use Query Builder methods that return results +and specify the model's method at the end of the method chaining. + +.. note:: You can also access the model's database connection seamlessly: + + .. literalinclude:: model/046.php + Model Events ************ diff --git a/user_guide_src/source/models/model/001.php b/user_guide_src/source/models/model/001.php index 06ef67b15a7d..22f30bd5e5ee 100644 --- a/user_guide_src/source/models/model/001.php +++ b/user_guide_src/source/models/model/001.php @@ -1,15 +1,15 @@ where('active', 1) - ->findAll(); +$users = $userModel->where('active', 1)->findAll(); diff --git a/user_guide_src/source/models/model/012.php b/user_guide_src/source/models/model/012.php index f00b70258b05..0f48babbab4a 100644 --- a/user_guide_src/source/models/model/012.php +++ b/user_guide_src/source/models/model/012.php @@ -1,4 +1,3 @@ where('deleted', 0) - ->first(); +$user = $userModel->where('deleted', 0)->first(); diff --git a/user_guide_src/source/testing/controllers.rst b/user_guide_src/source/testing/controllers.rst index 113c5ef2b66e..130364260cba 100644 --- a/user_guide_src/source/testing/controllers.rst +++ b/user_guide_src/source/testing/controllers.rst @@ -54,7 +54,7 @@ for details. withConfig($config) ------------------- -Allows you to pass in a modified version of **Config\App.php** to test with different settings: +Allows you to pass in a modified version of **app/Config/App.php** to test with different settings: .. literalinclude:: controllers/005.php diff --git a/user_guide_src/source/tutorial/conclusion.rst b/user_guide_src/source/tutorial/conclusion.rst index 94b8ba5e3f2d..ac5300159a48 100644 --- a/user_guide_src/source/tutorial/conclusion.rst +++ b/user_guide_src/source/tutorial/conclusion.rst @@ -21,4 +21,4 @@ If you still have questions about the framework or your own CodeIgniter code, you can: - Check out our `Forum `_ -- Check out our `Slack `_ +- 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 99d28abf3cdb..fa8469385654 100644 --- a/user_guide_src/source/tutorial/create_news_items.rst +++ b/user_guide_src/source/tutorial/create_news_items.rst @@ -1,6 +1,10 @@ Create News Items ################# +.. contents:: + :local: + :depth: 2 + You now know how you can read data from a database using CodeIgniter, but you haven't written any information to the database yet. In this section, you'll expand your news controller and model created earlier to include @@ -18,8 +22,8 @@ Open the **app/Config/Filters.php** file and update the ``$methods`` property li It configures the CSRF filter to be enabled for all **POST** requests. You can read more about the CSRF protection in :doc:`Security ` library. -.. 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. +.. Warning:: In general, if you use ``$methods`` filters, you should :ref:`disable Auto Routing (Legacy) ` + because :ref:`auto-routing-legacy` 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 diff --git a/user_guide_src/source/tutorial/create_news_items/004.php b/user_guide_src/source/tutorial/create_news_items/004.php index 8a0905770c82..4687bd9abd67 100644 --- a/user_guide_src/source/tutorial/create_news_items/004.php +++ b/user_guide_src/source/tutorial/create_news_items/004.php @@ -6,6 +6,6 @@ $routes->get('news/(:segment)', 'News::view/$1'); $routes->get('news', 'News::index'); $routes->get('pages', 'Pages::index'); -$routes->get('(:any)', 'Pages::view/$1'); +$routes->get('(:segment)', 'Pages::view/$1'); // ... diff --git a/user_guide_src/source/tutorial/index.rst b/user_guide_src/source/tutorial/index.rst index c0a03bf1c1dd..e14841c3544c 100644 --- a/user_guide_src/source/tutorial/index.rst +++ b/user_guide_src/source/tutorial/index.rst @@ -2,6 +2,10 @@ Build Your First Application ############################ +.. contents:: + :local: + :depth: 2 + Overview ******** @@ -110,13 +114,20 @@ This means that your application works and you can start making changes to it. Debugging ********* -Now that you're in development mode, you'll see a toolbar on the bottom of your application. +Debug Toolbar +============= + +Now that you're in development mode, you'll see the CodeIgniter flame on the right bottom of your application. Click it and you'll see the debug toolbar. + This toolbar contains a number of helpful items that you can reference during development. This will never show in production environments. Clicking any of the tabs along the bottom brings up additional information. Clicking the X on the right of the toolbar minimizes it to a small square with the CodeIgniter flame on it. If you click that the toolbar will show again. +Error Pages +=========== + In addition to this, CodeIgniter has some helpful error pages when you hit exceptions or other errors in your program. Open up ``app/Controllers/Home.php`` and change some line to generate an error (removing a semi-colon or brace should do the trick!). You will be diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/tutorial/news_section.rst index ae76a8488fb4..447887ea88db 100644 --- a/user_guide_src/source/tutorial/news_section.rst +++ b/user_guide_src/source/tutorial/news_section.rst @@ -1,6 +1,10 @@ News Section ############ +.. contents:: + :local: + :depth: 2 + 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 @@ -10,12 +14,12 @@ Create a Database to Work with ****************************** The CodeIgniter installation assumes that you have set up an appropriate -database, as outlined in the :doc:`requirements `. +database, as outlined in the :ref:`requirements `. In this tutorial, we provide SQL code for a MySQL database, and we also assume that you have a suitable client for issuing database commands (mysql, MySQL Workbench, or phpMyAdmin). -You need to create a database that can be used for this tutorial, +You need to create a database ``ci4tutorial`` 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):: @@ -50,7 +54,7 @@ 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 in :doc:`../database/configuration`:: database.default.hostname = localhost database.default.database = ci4tutorial @@ -80,7 +84,7 @@ library. This will make the database class available through the Now that the database and a model have been set up, you'll need a method to get all of our posts from our database. To do this, the database abstraction layer that is included with CodeIgniter - -:doc:`Query Builder <../database/query_builder>` - is used in the ``CodeIgnite\Model``. This makes it +:doc:`Query Builder <../database/query_builder>` - is used in the ``CodeIgniter\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 diff --git a/user_guide_src/source/tutorial/news_section/008.php b/user_guide_src/source/tutorial/news_section/008.php index 4ba41638ef7f..6a75bd79b466 100644 --- a/user_guide_src/source/tutorial/news_section/008.php +++ b/user_guide_src/source/tutorial/news_section/008.php @@ -5,6 +5,6 @@ $routes->get('news/(:segment)', 'News::view/$1'); $routes->get('news', 'News::index'); $routes->get('pages', 'Pages::index'); -$routes->get('(:any)', 'Pages::view/$1'); +$routes->get('(:segment)', 'Pages::view/$1'); // ... diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/tutorial/static_pages.rst index 00a384e3a9fb..8ed72bf9fba8 100644 --- a/user_guide_src/source/tutorial/static_pages.rst +++ b/user_guide_src/source/tutorial/static_pages.rst @@ -1,6 +1,10 @@ Static Pages ############ +.. contents:: + :local: + :depth: 2 + .. note:: This tutorial assumes you've downloaded CodeIgniter and :doc:`installed the framework <../installation/index>` in your development environment. @@ -144,11 +148,11 @@ arguments. More information about routing can be found in the URI Routing :doc:`documentation `. -Here, the second rule in the ``$routes`` object matches GET request -to the URI path ``/pages`` maps the ``index()`` method of the ``Pages`` class. +Here, the second rule in the ``$routes`` object matches a GET request +to the URI path ``/pages``, and it maps to 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 +The third rule in the ``$routes`` object matches a GET request to a URI segment +using the placeholder ``(:segment)``, and passes the parameter to the ``view()`` method of the ``Pages`` class. Running the App diff --git a/user_guide_src/source/tutorial/static_pages/004.php b/user_guide_src/source/tutorial/static_pages/004.php index a48a8caf18cb..e88803a96c66 100644 --- a/user_guide_src/source/tutorial/static_pages/004.php +++ b/user_guide_src/source/tutorial/static_pages/004.php @@ -1,4 +1,4 @@ get('pages', 'Pages::index'); -$routes->get('(:any)', 'Pages::view/$1'); +$routes->get('(:segment)', 'Pages::view/$1');