diff --git a/.dockerignore b/.dockerignore index 42dc9d58b..92bd5419d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,23 +1,57 @@ -node_modules -tmp +# Version control +.git +.gitignore + +# Sensitive config (user may mount their own) app/Config/Email.php + +# Build artifacts +node_modules/ +dist/ +tmp/ *.patch patches/ + +# IDE and editor files .idea/ -git-svn-diff.py -*.bash +.vscode/ .swp +*.swp .buildpath .project -.settings/* -.git -dist/ -node_modules/ -*.swp +.settings/ + +# Development tools and configs +tests/ +phpunit.xml +.php-cs-fixer.* +phpstan.neon +*.bash +git-svn-diff.py + +# Documentation +*.md +!LICENSE +branding/ + +# Build configs (not needed at runtime) +composer.json +composer.lock +package.json +package-lock.json +gulpfile.js +.env +.env.* +.dockerignore + +# Temporary and backup files *.rej *.orig *~ *.~ *.log -app/writable/session/* -!app/writable/session/index.html + +# CI +.github/ +.github/workflows/ +build/ diff --git a/.env.example b/.env.example index 17298ab69..d055638c1 100644 --- a/.env.example +++ b/.env.example @@ -2,62 +2,116 @@ # ENVIRONMENT #-------------------------------------------------------------------- -CI_ENVIRONMENT = production +CI_ENVIRONMENT=production + +#-------------------------------------------------------------------- +# SECURITY: ALLOWED HOSTNAMES +#-------------------------------------------------------------------- +# CRITICAL: Whitelist of allowed hostnames to prevent Host Header +# Injection attacks (GHSA-jchf-7hr6-h4f3). +# +# REQUIRED IN PRODUCTION: Application will fail to start if not configured. +# In development, falls back to 'localhost' with an error log. +# +# Configure with comma-separated list of domains/subdomains: +# app.allowedHostnames='yourdomain.com,www.yourdomain.com' +# +# Or via environment variable (useful for Docker/Compose): +# ALLOWED_HOSTNAMES=yourdomain.com,www.yourdomain.com +# +# For local development: +# app.allowedHostnames='localhost' +# +# Note: Do not include protocol (http/https) or port numbers. +app.allowedHostnames='' #-------------------------------------------------------------------- # DATABASE #-------------------------------------------------------------------- -database.default.hostname = 'localhost' -database.default.database = 'ospos' -database.default.username = 'admin' -database.default.password = 'pointofsale' -database.default.DBDriver = 'MySQLi' -database.default.DBPrefix = 'ospos_' +database.default.hostname='localhost' +database.default.database='ospos' +database.default.username='admin' +database.default.password='pointofsale' +database.default.DBDriver='MySQLi' +database.default.DBPrefix='ospos_' -database.development.hostname = 'localhost' -database.development.database = 'ospos' -database.development.username = 'admin' -database.development.password = 'pointofsale' -database.development.DBDriver = 'MySQLi' -database.development.DBPrefix = 'ospos_' +database.development.hostname='localhost' +database.development.database='ospos' +database.development.username='admin' +database.development.password='pointofsale' +database.development.DBDriver='MySQLi' +database.development.DBPrefix='ospos_' -database.tests.hostname = 'localhost' -database.tests.database = 'ospos' -database.tests.username = 'admin' -database.tests.password = 'pointofsale' -database.tests.DBDriver = 'MySQLi' -database.tests.DBPrefix = 'ospos_' +database.tests.hostname='localhost' +database.tests.database='ospos_test' +database.tests.username='admin' +database.tests.password='pointofsale' +database.tests.DBDriver='MySQLi' +database.tests.DBPrefix='ospos_' #-------------------------------------------------------------------- # ENCRYPTION #-------------------------------------------------------------------- -encryption.key = '' +# Leave blank and the application auto-generates a unique key on first use. +# For Docker/Compose, pass it via the ENCRYPTION_KEY env var instead: +# docker run -e ENCRYPTION_KEY="$(openssl rand -hex 32)" opensourcepos +# ENCRYPTION_KEY is read as a fallback when encryption.key is empty, so no +# shared key needs to be committed or baked into the shipped image. +encryption.key='' +# Persistent secret for HMAC-hashing login-throttle cache keys. Left blank and +# provisioned on startup (php spark env:provision); independent of encryption.key. +# For Docker/Compose, pass it via the THROTTLE_KEY env var instead: +# docker run -e THROTTLE_KEY="$(openssl rand -hex 32)" opensourcepos +# THROTTLE_KEY is read as a fallback when throttle.key is empty, so no +# shared secret needs to be committed or baked into the shipped image. +throttle.key='' #-------------------------------------------------------------------- # LOGGER -# - 0 = Disables logging, Error logging TURNED OFF -# - 1 = Emergency Messages - System is unusable -# - 2 = Alert Messages - Action Must Be Taken Immediately -# - 3 = Critical Messages - Application component unavailable, unexpected exception. -# - 4 = Runtime Errors - Don't need immediate action, but should be monitored. -# - 5 = Warnings - Exceptional occurrences that are not errors. -# - 6 = Notices - Normal but significant events. -# - 7 = Info - Interesting events, like user logging in, etc. -# - 8 = Debug - Detailed debug information. -# - 9 = All Messages +# - 0=Disables logging, Error logging TURNED OFF +# - 1=Emergency Messages - System is unusable +# - 2=Alert Messages - Action Must Be Taken Immediately +# - 3=Critical Messages - Application component unavailable, unexpected exception. +# - 4=Runtime Errors - Don't need immediate action, but should be monitored. +# - 5=Warnings - Exceptional occurrences that are not errors. +# - 6=Notices - Normal but significant events. +# - 7=Info - Interesting events, like user logging in, etc. +# - 8=Debug - Detailed debug information. +# - 9=All Messages #-------------------------------------------------------------------- -logger.threshold = 0 -app.db_log_enabled = false +logger.threshold=0 +app.db_log_enabled=false #-------------------------------------------------------------------- # HONEYPOT #-------------------------------------------------------------------- -honeypot.hidden = true -honeypot.label = 'Fill This Field' -honeypot.name = 'honeypot' -honeypot.template = '' -honeypot.container = '
{template}
' +honeypot.hidden=true +honeypot.label='Fill This Field' +honeypot.name='honeypot' +honeypot.template='' +honeypot.container='
{template}
' + +#-------------------------------------------------------------------- +# SECURITY: DISALLOW PASSWORD CHANGE +#-------------------------------------------------------------------- +# When true, disables the "change password" feature for all employees. +# Useful when passwords are managed by an external system (e.g. SSO/LDAP). +# +# DISALLOW_PASSWORD_CHANGE=false + +DISALLOW_PASSWORD_CHANGE=false + +#-------------------------------------------------------------------- +# SECURITY: DISALLOW GRANT CHANGE +#-------------------------------------------------------------------- +# When true, disables changing an employee's grants for all employees. +# New employees cannot be created with grants while this is enabled. +# Useful for demo deployments. +# +# DISALLOW_GRANT_CHANGE=false + +DISALLOW_GRANT_CHANGE=false diff --git a/.github/ISSUE_TEMPLATE/bug report.yml b/.github/ISSUE_TEMPLATE/bug report.yml index c026d3144..fddd1ddcc 100644 --- a/.github/ISSUE_TEMPLATE/bug report.yml +++ b/.github/ISSUE_TEMPLATE/bug report.yml @@ -1,121 +1,187 @@ -name: Bug Report -description: File a bug report -title: "[Bug]: " -labels: ["bug", "triage"] -projects: ["ospos/3", "ospos/4"] -assignees: - - none -body: - - type: markdown - attributes: - value: | - Bug reports indicate that something is not working as intended. - Please include as much detail as possible and submit a separate bug report for each problem. - Do not include personal identifying information such as email addresses or encryption keys. - - type: textarea - id: bug-description - attributes: - label: Bug Description? - description: Describe the problem that you are seeing - placeholder: "Describe the problem that you are seeing" - validations: - required: true - - type: textarea - id: steps-reproduce - attributes: - label: Steps to Reproduce? - description: List the steps to reproduce this issue - placeholder: "Steps to Reproduce" - validations: - required: true - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior? - description: Tell us what did you expect to happen? - placeholder: "Expected Behavior" - validations: - required: true - - type: dropdown - id: ospos-version - attributes: - label: OpensourcePOS Version - description: What version of our software are you running? - options: - - development (unreleased) - - opensourcepos 3.4.1 - - opensourcepos 3.4.0 - - opensourcepos 3.3.9 - - opensourcepos 3.3.8 - - opensourcepos 3.3.7 - default: 0 - validations: - required: true - - type: dropdown - id: php-version - attributes: - label: Php version - description: What version of Php? - options: - - Php 7.2 - - Php 7.3 - - Php 7.4 - - Php 8.1 - - Php 8.2 - - Php 8.3 - - Php 8.4 - default: 0 - validations: - required: true - - type: dropdown - id: browsers - attributes: - label: What browsers are you seeing the problem on? - multiple: true - options: - - Firefox - - Chrome - - Safari - - Microsoft Edge - - Other - - type: input - id: server - attributes: - label: Server Operating System and version - description: "Server Operating System " - placeholder: "Server Operating System " - validations: - required: true - - type: input - id: database - attributes: - label: Database Management System and version - description: "Database Management System" - placeholder: "Database Management" - validations: - required: true - - type: input - id: webserver - attributes: - label: Web Server and version - description: "Web Server and version " - placeholder: "Web Server and version " - validations: - required: true - - type: textarea - id: servers - attributes: - label: System Information Report (optional) - description: Copy and paste from OSPOS > Configuration > Setup & Conf > Setup & Conf? - placeholder: System Information Report - value: "System Information Report" - validations: - required: true - - type: checkboxes - id: terms - attributes: - label: Unmodified copy of OpensourcePOS - description: By submitting this issue you agree this copy has not been modified - options: - - label: I agree this copy has not been modified - required: true +name: šŸ› Bug Report +description: File a bug report to help us improve +title: "[Bug]: " +labels: ["bug", "triage"] +projects: ["ospos/3", "ospos/4"] +assignees: [] +body: + # ───────────────────────────────────────────────────────────────────────────── + # INTRODUCTION + # ───────────────────────────────────────────────────────────────────────────── + - type: markdown + attributes: + value: | + ## Thanks for taking the time to fill out this bug report! 🐜 + + Bug reports help us identify and fix issues. Please provide as much detail as possible. + + > āš ļø **Important:** Submit a separate bug report for each problem you encounter. + > + > 🚫 Do not include personal identifying information such as email addresses or encryption keys. + + # ───────────────────────────────────────────────────────────────────────────── + # PROBLEM DESCRIPTION + # ───────────────────────────────────────────────────────────────────────────── + - type: textarea + id: bug-description + attributes: + label: šŸ› Bug Description + description: A clear and concise description of what the bug is. + placeholder: | + Example: When I try to print a receipt, the application crashes + with an error message saying "Unable to connect to printer". + validations: + required: true + + - type: textarea + id: steps-reproduce + attributes: + label: šŸ“‹ Steps to Reproduce + description: Detailed steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: āœ… Expected Behavior + description: A clear and concise description of what you expected to happen. + placeholder: | + Example: The receipt should print successfully without any errors. + validations: + required: true + + # ───────────────────────────────────────────────────────────────────────────── + # ENVIRONMENT DETAILS + # ───────────────────────────────────────────────────────────────────────────── + - type: dropdown + id: ospos-version + attributes: + label: šŸ“¦ OpenSourcePOS Version + description: What version of our software are you running? + options: + - development (unreleased) + - OpenSourcePOS 3.4.2 + - OpenSourcePOS 3.4.1 + - OpenSourcePOS 3.4.0 + - OpenSourcePOS 3.3.9 + - OpenSourcePOS 3.3.8 + default: 0 + validations: + required: true + + - type: dropdown + id: php-version + attributes: + label: šŸ”§ PHP Version + description: What version of PHP are you running? + options: + - PHP 8.4 + - PHP 8.3 + - PHP 8.2 + - PHP 8.1 + - PHP 7.4 + - Other + default: 0 + validations: + required: true + + - type: dropdown + id: browsers + attributes: + label: 🌐 Browser(s) + description: What browser(s) are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Other + + - type: input + id: server + attributes: + label: šŸ–„ļø Server Operating System + description: What server OS and version are you running? + placeholder: "e.g., Ubuntu 22.04, CentOS 7, Windows Server 2022" + validations: + required: true + + - type: input + id: database + attributes: + label: šŸ—„ļø Database + description: What database management system and version are you using? + placeholder: "e.g., MySQL 8.0, MariaDB 10.11, Percona 8.0" + validations: + required: true + + - type: input + id: webserver + attributes: + label: šŸŒ Web Server + description: What web server and version are you using? + placeholder: "e.g., Apache 2.4, Nginx 1.24, Caddy 2.7" + validations: + required: true + + # ───────────────────────────────────────────────────────────────────────────── + # ADDITIONAL INFORMATION + # ───────────────────────────────────────────────────────────────────────────── + - type: textarea + id: system-info + attributes: + label: šŸ“Š System Information Report + description: | + Copy and paste the system information from OSPOS: + + **Navigation:** Configuration → Setup & Conf → System Info + placeholder: | + Paste the System Information Report here... + render: text + validations: + required: true + + - type: textarea + id: logs + attributes: + label: šŸ“œ Relevant Log Output + description: | + Please copy and paste any relevant log output. + + **Log locations:** + - OSPOS logs: `writable/logs/` + - Web server logs: `/var/log/apache2/` or `/var/log/nginx/` + - PHP logs: Check your `php.ini` for `error_log` location + placeholder: | + Paste log output here... + render: shell + + - type: textarea + id: screenshots + attributes: + label: šŸ“ø Screenshots + description: If applicable, add screenshots to help explain your problem. + placeholder: Drag and drop images here... + + # ───────────────────────────────────────────────────────────────────────────── + # CONFIRMATION + # ───────────────────────────────────────────────────────────────────────────── + - type: checkboxes + id: terms + attributes: + label: āœ“ Confirmation + description: Please confirm the following before submitting + options: + - label: I certify that this is an unmodified copy of OpenSourcePOS + required: true + - label: I have searched existing issues to ensure this bug has not already been reported + required: true + - label: I have provided all the information requested above + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 028fa0b3d..16714eb90 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,63 +1,136 @@ -name: ✨ Feature Request -description: Suggest an idea for this project -title: "[Feature]: " -labels: ["enhancement"] -assignees: ["none"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this feature request! šŸ¤— - Please make sure this feature request hasn't been already submitted by someone by looking through other open/closed issues. 😃 - - - type: dropdown - attributes: - multiple: false - label: Type of Feature - description: Select the type of feature request. - options: - - "✨ New Feature" - - "šŸ“ Documentation" - - "šŸŽØ Style and UI" - - "šŸ”Ø Code Refactor" - - "⚔ Performance Improvements" - - "āœ… New Test" - validations: - required: true - - - type: dropdown - id: ospos-version - attributes: - label: OpensourcePOS Version - description: What version of our software are you running? - options: - - opensourcepos 3.3.9 - - opensourcepos 3.3.8 - - opensourcepos 3.3.7 - default: 0 - validations: - required: true - - - type: textarea - id: description - attributes: - label: Description - description: Give us a brief description of the feature or enhancement you would like - validations: - required: true - - - type: textarea - id: additional-information - attributes: - label: Additional Information - description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc. - - - type: checkboxes - id: terms - attributes: - label: Verify you searched open requests in OpensourcePOS - description: By submitting this request you agree that you have searched Open Requests in the Tracker - options: - - label: I agree I have searched Open Requests - required: true - +name: ✨ Feature Request +description: Suggest an idea or enhancement for this project +title: "[Feature]: " +labels: ["enhancement"] +assignees: [] +body: + # ───────────────────────────────────────────────────────────────────────────── + # INTRODUCTION + # ───────────────────────────────────────────────────────────────────────────── + - type: markdown + attributes: + value: | + ## Thanks for suggesting a new feature! šŸ’” + + We appreciate you taking the time to help improve OpenSourcePOS. + + > šŸ“‹ **Before submitting:** Please search [existing feature requests](https://github.com/opensourcepos/opensourcepos/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) to ensure your idea hasn't already been suggested. + + # ───────────────────────────────────────────────────────────────────────────── + # FEATURE DETAILS + # ───────────────────────────────────────────────────────────────────────────── + - type: dropdown + id: feature-type + attributes: + label: šŸ·ļø Feature Type + description: What type of feature are you requesting? + options: + - "✨ New Feature" + - "šŸ“ Documentation Improvement" + - "šŸŽØ UI/UX Enhancement" + - "šŸ”Ø Code Refactoring" + - "⚔ Performance Improvement" + - "āœ… New Test Coverage" + - "šŸ”Œ Plugin/Integration" + default: 0 + validations: + required: true + + - type: dropdown + id: ospos-version + attributes: + label: šŸ“¦ OpenSourcePOS Version + description: What version are you currently running? + options: + - development (unreleased) + - OpenSourcePOS 3.4.2 + - OpenSourcePOS 3.4.1 + - OpenSourcePOS 3.4.0 + - OpenSourcePOS 3.3.9 + - OpenSourcePOS 3.3.8 + default: 0 + validations: + required: true + + - type: textarea + id: problem-statement + attributes: + label: šŸŽÆ Problem Statement + description: | + Is your feature request related to a problem? Please describe. + + A clear description of what the problem is. Ex: I'm always frustrated when [...] + placeholder: | + Example: I always have to manually calculate taxes for different regions, + which is time-consuming and error-prone. + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: šŸ’” Proposed Solution + description: A clear and concise description of what you want to happen. + placeholder: | + Example: Add an automatic tax calculation feature that: + - Detects the customer's region + - Applies the correct tax rate + - Generates a tax report automatically + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: šŸ”„ Alternatives Considered + description: A clear description of any alternative solutions or features you've considered. + placeholder: | + Example: I considered using an external tax service, but it would be + better to have this integrated directly into OpenSourcePOS. + + # ───────────────────────────────────────────────────────────────────────────── + # ADDITIONAL INFORMATION + # ───────────────────────────────────────────────────────────────────────────── + - type: textarea + id: additional-context + attributes: + label: šŸ“Ž Additional Context + description: | + Add any other context, screenshots, mockups, or references about the feature request here. + + **Helpful additions:** + - Links to similar features in other software + - Mockups or diagrams + - Code examples + - Documentation references + placeholder: | + Any other relevant information, links, or screenshots... + + - type: textarea + id: acceptance-criteria + attributes: + label: āœ… Acceptance Criteria + description: | + (Optional) Define what "done" looks like for this feature. + + Format: **Given** [context], **When** [action], **Then** [outcome] + placeholder: | + Given a customer is selected from region X + When the sale is completed + Then the tax rate for region X is automatically applied + And the tax amount is correctly calculated + And a tax entry is logged in the report + + # ───────────────────────────────────────────────────────────────────────────── + # CONFIRMATION + # ───────────────────────────────────────────────────────────────────────────── + - type: checkboxes + id: terms + attributes: + label: āœ“ Confirmation + description: Please confirm before submitting + options: + - label: I have searched existing feature requests to ensure this is not a duplicate + required: true + - label: I have provided a clear problem statement and proposed solution + required: true \ No newline at end of file diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..e4adb1f83 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,63 @@ +# GitHub Actions + +This document describes the CI/CD workflows for OSPOS. + +## Build and Release Workflow (`.github/workflows/build-release.yml`) + +### Build Process +- Setup PHP 8.2 with required extensions +- Setup Node.js 20 +- Install composer dependencies +- Install npm dependencies +- Build frontend assets with Gulp + +### Docker Images +- Build and push `opensourcepos` Docker image for multiple architectures (linux/amd64, linux/arm64) +- On `master`: tagged `master` and `` +- On other branches: tagged `-` +- On a semver tag (e.g. `3.4.2`): tagged `` and `latest` +- The version number is never stamped onto `master`/branch builds — it only appears on tag releases +- Pushed to Docker Hub + +### Releases +- Create distribution archives (tar.gz, zip) +- Create/update GitHub "unstable" release on master branch only + +## Required Secrets + +To use this workflow, you need to add the following secrets to your repository: + +1. **DOCKER_USERNAME** - Docker Hub username for pushing images +2. **DOCKER_PASSWORD** - Docker Hub password/token for pushing images + +### How to add secrets + +1. Go to your repository on GitHub +2. Click **Settings** → **Secrets and variables** → **Actions** +3. Click **New repository secret** +4. Add `DOCKER_USERNAME` and `DOCKER_PASSWORD` + +The `GITHUB_TOKEN` is automatically provided by GitHub Actions. + +## Workflow Triggers + +- **Push to master** - Runs build, Docker push (`master` + `` tags), and creates/updates the `unstable` release +- **Push to other branches** - Runs build and Docker push (`-` tag) +- **Push a semver tag** (e.g. `3.4.2`) - Runs build and Docker push (`` + `latest` tags) +- **Pull requests** - Runs build only (PHPUnit tests run in parallel via phpunit.yml); no Docker push + +## Existing Workflows + +This repository also has these workflows: +- `.github/workflows/main.yml` - PHP linting with PHP-CS-Fixer +- `.github/workflows/phpunit.yml` - PHPUnit tests (runs on all PHP versions 8.1-8.4) +- `.github/workflows/php-linter.yml` - PHP linting + +## Testing + +PHPUnit tests are run separately via `.github/workflows/phpunit.yml` on every push and pull request, testing against PHP 8.1, 8.2, 8.3, and 8.4. + +To test the build workflow: +1. Add the required secrets +2. Push to master or create a PR +3. Monitor the Actions tab in GitHub \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 000000000..2aa1e3b96 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,313 @@ +name: Build and Release + +on: + push: + branches: + - '**' + tags: + - '[0-9]+.[0-9]+.[0-9]+' + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-22.04 + + outputs: + version: ${{ steps.version.outputs.version }} + version-tag: ${{ steps.version.outputs.version-tag }} + short-sha: ${{ steps.version.outputs.short-sha }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: intl, mbstring, mysqli, gd, bcmath, zip + coverage: none + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get composer cache directory + run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ env.COMPOSER_CACHE_FILES_DIR }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Get npm cache directory + run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV + + - name: Cache npm dependencies + uses: actions/cache@v4 + with: + path: ${{ env.NPM_CACHE_DIR }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install composer dependencies + run: composer install --no-dev --optimize-autoloader + + - name: Install npm dependencies + run: npm ci + + - name: Install gulp globally + run: npm install -g gulp-cli + + - name: Get version info + id: version + run: | + VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g") + BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///' | tr '/' '_') + TAG=$(echo "${GITHUB_TAG:-$BRANCH}" | tr '/' '_') + SHORT_SHA=$(git rev-parse --short=6 HEAD) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version-tag=$VERSION-$BRANCH-$SHORT_SHA" >> $GITHUB_OUTPUT + echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + env: + GITHUB_TAG: ${{ github.ref_name }} + + - name: Validate release tag + if: startsWith(github.ref, 'refs/tags/') + run: | + # The ZIP below is named from App.php, while the draft-release job + # globs by the tag name. A manually-pushed tag that does not match + # App.php would silently produce a draft release with no archive, so + # fail fast instead. + if [ "${GITHUB_REF_NAME}" != "${{ steps.version.outputs.version }}" ]; then + echo "::error::tag ${GITHUB_REF_NAME} does not match App.php version ${{ steps.version.outputs.version }}" + exit 1 + fi + + - name: Create .env file + run: | + cp .env.example .env + sed -i 's/production/development/g' .env + + - name: Update commit hash + run: | + SHORT_SHA="${{ steps.version.outputs.short-sha }}" + sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$SHORT_SHA'/g" app/Config/OSPOS.php + + - name: Build frontend assets + run: npm run build + + - name: Create distribution archives + run: | + set -euo pipefail + gulp compress + VERSION="${{ steps.version.outputs.version }}" + SHORT_SHA="${{ steps.version.outputs.short-sha }}" + mv dist/opensourcepos.tar "dist/opensourcepos.$VERSION.$SHORT_SHA.tar" + mv dist/opensourcepos.zip "dist/opensourcepos.$VERSION.$SHORT_SHA.zip" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ steps.version.outputs.short-sha }} + path: dist/ + retention-days: 7 + + - name: Upload build context for Docker + uses: actions/upload-artifact@v4 + with: + name: build-context-${{ steps.version.outputs.short-sha }} + path: | + . + !.git + !node_modules + include-hidden-files: true + retention-days: 1 + + docker: + name: Build Docker Image + runs-on: ubuntu-22.04 + needs: build + if: github.event_name == 'push' + + steps: + - name: Download build context + uses: actions/download-artifact@v4 + with: + name: build-context-${{ needs.build.outputs.short-sha }} + path: . + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Determine Docker tags + id: tags + run: | + REGISTRY="${{ secrets.DOCKER_USERNAME }}/opensourcepos" + SHA="${{ needs.build.outputs.short-sha }}" + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" + echo "tags=${REGISTRY}:${VERSION},${REGISTRY}:latest" >> "$GITHUB_OUTPUT" + elif [[ "$GITHUB_REF" == refs/heads/master ]]; then + echo "tags=${REGISTRY}:master,${REGISTRY}:${SHA}" >> "$GITHUB_OUTPUT" + else + BRANCH="${GITHUB_REF#refs/heads/}" + BRANCH="$(printf '%s' "$BRANCH" | LC_ALL=C tr -c 'A-Za-z0-9_.-' '_')" + BRANCH="${BRANCH:0:$((128 - ${#SHA} - 1))}" + [[ "$BRANCH" == [-.]* ]] && BRANCH="_${BRANCH:1}" + echo "tags=${REGISTRY}:${BRANCH}-${SHA}" >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_REF: ${{ github.ref }} + + - name: Build and push Docker images + uses: docker/build-push-action@v5 + with: + context: . + target: ospos + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.tags.outputs.tags }} + + unstable-release: + name: Create Unstable Release + needs: build + runs-on: ubuntu-22.04 + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist-${{ needs.build.outputs.short-sha }} + path: dist/ + + - name: Get version info + id: version + run: | + VERSION="${{ needs.build.outputs.version }}" + SHORT_SHA=$(git rev-parse --short=6 HEAD) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT + + - name: Create/Update unstable release + uses: softprops/action-gh-release@v2 + with: + tag_name: unstable + name: Unstable OpenSourcePOS + body: | + This is a build of the latest master which might contain bugs. Use at your own risk. + + Check the releases section for the latest official release. + files: | + dist/opensourcepos.${{ steps.version.outputs.version }}.${{ steps.version.outputs.short-sha }}.zip + prerelease: true + draft: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + official-release: + name: Create Official Release (Draft) + needs: [build, docker] + runs-on: ubuntu-22.04 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist-${{ needs.build.outputs.short-sha }} + path: dist/ + + - name: Build release notes + run: | + VERSION="${GITHUB_REF_NAME}" + # Extract this version's changelog section (header + entries) from CHANGELOG.md. + SECTION=$(awk -v v="$VERSION" ' + $0 ~ "^## \\[" v "\\] - " {insec=1; print; next} + insec && $0 ~ "^## \\[" {insec=0; next} + insec {print} + ' CHANGELOG.md) + if [ -z "$SECTION" ]; then + echo "WARNING: no changelog section found for $VERSION" + SECTION="Changelog section for ${VERSION} is not present in CHANGELOG.md." + fi + { + echo "## Upgrade instructions" + echo "" + echo "**Docker:**" + echo "" + echo '```bash' + echo "docker pull ${{ secrets.DOCKER_USERNAME }}/opensourcepos:${VERSION}" + echo "docker compose -f docker-compose.nginx.yml up -d" + echo '```' + echo "" + echo "**Existing installation:** download one of the archives below, extract it over your current install, and run any new database migrations." + echo "" + echo "---" + echo "" + echo "$SECTION" + } > /tmp/release_notes.md + echo "=== release notes (first 12 lines) ===" + head -12 /tmp/release_notes.md + env: + GITHUB_REF_NAME: ${{ github.ref_name }} + + - name: Create draft release + run: | + VERSION="${GITHUB_REF_NAME}" + ZIP=$(ls dist/opensourcepos."${VERSION}".*.zip 2>/dev/null | head -1) + if [ -n "$ZIP" ]; then + gh release create "$VERSION" \ + --draft \ + --title "OpenSourcePOS ${VERSION}" \ + --notes-file /tmp/release_notes.md \ + "$ZIP" + else + gh release create "$VERSION" \ + --draft \ + --title "OpenSourcePOS ${VERSION}" \ + --notes-file /tmp/release_notes.md + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 6218fde3a..000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '21 12 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ā„¹ļø Command-line programs to run using the OS shell. - # šŸ“š https://git.io/JvXDl - - # āœļø If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/delete-unstable-release.yml b/.github/workflows/delete-unstable-release.yml index c10399599..c4247ed7c 100644 --- a/.github/workflows/delete-unstable-release.yml +++ b/.github/workflows/delete-unstable-release.yml @@ -8,15 +8,25 @@ on: jobs: delete_unstable_release: runs-on: ubuntu-latest - steps: - - name: "Delete last unstable release" - uses: sgpublic/delete-release-action@v1.2 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} - with: - release-drop: false - release-drop-tag: false - pre-release-drop: true - pre-release-keep-count: -1 - pre-release-drop-tag: true - + steps: + - name: "Delete the unstable release (matched by name)" + env: + TOKEN: ${{ secrets.TOKEN }} + RELEASE_NAME: "Unstable OpenSourcePOS" + TAG_NAME: "unstable" + run: | + set -euo pipefail + api="https://api.github.com/repos/${GITHUB_REPOSITORY}" + # Resolve the release id from its exact name so only this one release + # is ever touched; drafts and every other release are left alone. + id=$(curl -fsSL -H "Authorization: Bearer ${TOKEN}" "${api}/releases?per_page=100" \ + | jq -r --arg n "${RELEASE_NAME}" '.[] | select(.name==$n) | .id // empty') + if [ -z "${id}" ]; then + echo "No release named '${RELEASE_NAME}' found; nothing to do." + exit 0 + fi + curl -fsSL -X DELETE -H "Authorization: Bearer ${TOKEN}" "${api}/releases/${id}" >/dev/null + echo "Deleted release '${RELEASE_NAME}' (id=${id})" + curl -fsSL -X DELETE -H "Authorization: Bearer ${TOKEN}" "${api}/git/refs/tags/${TAG_NAME}" >/dev/null \ + || echo "Tag '${TAG_NAME}' already gone; skipping." + echo "Deleted tag '${TAG_NAME}'" diff --git a/.github/workflows/deploy-core.yml b/.github/workflows/deploy-core.yml new file mode 100644 index 000000000..65824837a --- /dev/null +++ b/.github/workflows/deploy-core.yml @@ -0,0 +1,219 @@ +name: Deploy Core + +on: + workflow_call: + inputs: + image_tag: + description: 'Docker image tag to deploy' + type: string + required: true + sha: + description: 'Git commit SHA to deploy' + type: string + required: true + description: + description: 'Deployment description' + type: string + required: true + pr_number: + description: 'Pull request number (optional)' + type: string + required: false + outputs: + deployment_id: + description: 'GitHub deployment ID' + value: ${{ jobs.deploy.outputs.deployment_id }} + status: + description: 'Deployment status (success/failure)' + value: ${{ jobs.deploy.outputs.status }} + +concurrency: + group: deploy-staging + cancel-in-progress: false + +permissions: + contents: read + deployments: write + +jobs: + deploy: + name: Deploy to staging + runs-on: ubuntu-latest + + environment: + name: staging + url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }} + deployment: false + + outputs: + deployment_id: ${{ steps.deployment.outputs.deployment_id }} + status: ${{ steps.webhook.outputs.status }} + + steps: + - name: Create GitHub Deployment + id: deployment + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ inputs.image_tag }} + REF_SHA: ${{ inputs.sha }} + DESCRIPTION: ${{ inputs.description }} + run: | + set -euo pipefail + + DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \ + -X POST \ + -f ref="${REF_SHA}" \ + -f environment="staging" \ + -f description="${DESCRIPTION}" \ + -F auto_merge=false \ + -F required_contexts[] \ + --jq '.id') + + if [ -z "$DEPLOYMENT_ID" ]; then + echo "::error::Failed to create deployment" + exit 1 + fi + + echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT" + echo "Created deployment: $DEPLOYMENT_ID" + + - name: Set deployment status to in_progress + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="in_progress" \ + -f description="Deployment in progress..." \ + -f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + + - name: Trigger deployment webhook + id: webhook + env: + DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }} + DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }} + DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }} + IMAGE_TAG: ${{ inputs.image_tag }} + REF_SHA: ${{ inputs.sha }} + DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -euo pipefail + + if [ -z "$DEPLOY_WEBHOOK_URL" ]; then + echo "::error::DEPLOY_WEBHOOK_URL secret is not configured" + echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings" + echo "status=failure" >> "$GITHUB_OUTPUT" + exit 1 + fi + + REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}" + REPO_NAMESPACE="${REPO_NAME%%/*}" + REPO_SHORT_NAME="${REPO_NAME#*/}" + PUSHED_AT=$(date +%s) + + if [ -n "$PR_NUMBER" ]; then + PAYLOAD=$(jq -n \ + --arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --argjson pushed_at "$PUSHED_AT" \ + --arg pusher "$GITHUB_ACTOR" \ + --arg tag "$IMAGE_TAG" \ + --arg repo_name "$REPO_NAME" \ + --arg name "$REPO_SHORT_NAME" \ + --arg namespace "$REPO_NAMESPACE" \ + --arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \ + --arg deployment_id "$DEPLOYMENT_ID" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg sha "$REF_SHA" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg actor "$GITHUB_ACTOR" \ + --argjson pr_number "$PR_NUMBER" \ + '{ + callback_url: $callback_url, + push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag}, + repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"}, + github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number} + }') + else + PAYLOAD=$(jq -n \ + --arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + --argjson pushed_at "$PUSHED_AT" \ + --arg pusher "$GITHUB_ACTOR" \ + --arg tag "$IMAGE_TAG" \ + --arg repo_name "$REPO_NAME" \ + --arg name "$REPO_SHORT_NAME" \ + --arg namespace "$REPO_NAMESPACE" \ + --arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \ + --arg deployment_id "$DEPLOYMENT_ID" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg sha "$REF_SHA" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg actor "$GITHUB_ACTOR" \ + '{ + callback_url: $callback_url, + push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag}, + repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"}, + github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor} + }') + fi + + echo "Sending webhook..." + echo "Image: ${IMAGE_TAG}" + echo "Environment: staging" + + HEADERS=(-H "Content-Type: application/json") + + if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then + SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //') + HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE") + echo "Using HMAC-SHA256 signature verification" + else + echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed" + echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings" + fi + + HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \ + -o response.txt -w "%{http_code}" \ + -X POST \ + "${HEADERS[@]}" \ + -d "$PAYLOAD" \ + "$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000" + + echo "Response code: $HTTP_CODE" + if [ -s response.txt ]; then + cat response.txt + fi + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "status=success" >> "$GITHUB_OUTPUT" + else + echo "status=failure" >> "$GITHUB_OUTPUT" + fi + + - name: Set deployment status + if: always() + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ inputs.image_tag }} + run: | + set -euo pipefail + + STATE="${{ steps.webhook.outputs.status }}" + + if [ "$STATE" = "success" ]; then + DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" \ + '"Deployed image \($tag) to staging"') + + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="success" \ + -f description="$DESCRIPTION" + else + gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \ + -X POST \ + -f state="failure" \ + -f description="Deployment failed" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml new file mode 100644 index 000000000..ec9751698 --- /dev/null +++ b/.github/workflows/deploy-pr.yml @@ -0,0 +1,79 @@ +name: PR Deploy + +on: + pull_request_review: + types: [submitted] + +concurrency: + group: staging-deploy + cancel-in-progress: false + +permissions: + contents: read + deployments: write + pull-requests: write + +jobs: + prepare: + name: Prepare deployment + runs-on: ubuntu-latest + if: > + github.event.review.state == 'approved' && + github.event.pull_request.head.repo.full_name == github.repository + outputs: + image_tag: ${{ steps.image.outputs.tag }} + sha: ${{ github.event.pull_request.head.sha }} + pr_number: ${{ github.event.pull_request.number }} + + steps: + - name: Checkout PR + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Get image tag + id: image + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_SHA: ${{ github.event.pull_request.head.sha }} + run: | + IMAGE_TAG="pr-${PR_NUMBER}-${PR_SHA:0:7}" + echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" + + deploy: + name: Deploy to staging + needs: prepare + uses: ./.github/workflows/deploy-core.yml + with: + image_tag: ${{ needs.prepare.outputs.image_tag }} + sha: ${{ needs.prepare.outputs.sha }} + description: Deploy PR #${{ needs.prepare.outputs.pr_number }} to staging + pr_number: ${{ needs.prepare.outputs.pr_number }} + secrets: inherit + + comment: + name: Comment deployment status + needs: [prepare, deploy] + if: always() && needs.prepare.result == 'success' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} + PR_NUMBER: ${{ needs.prepare.outputs.pr_number }} + REF_SHA: ${{ needs.prepare.outputs.sha }} + STATUS: ${{ needs.deploy.outputs.status }} + + steps: + - name: Comment on PR + run: | + if [ "$STATUS" = "success" ]; then + BODY=$(jq -nr --arg tag "$IMAGE_TAG" --arg sha "$REF_SHA" --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '"āœ… **Staging deployment completed**\n\nšŸ”— **URL**: https://dev.opensourcepos.org\nšŸ“¦ **Image Tag**: `\($tag)`\nšŸ”Ø **Commit**: \($sha)\n\nView logs: \($url)"') + else + BODY=$(jq -nr --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + '"āŒ **Staging deployment failed**\n\nCheck the [workflow logs](\($url)) for details."') + fi + + gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + -X POST \ + -f body="$BODY" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..a8d9d6af3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Docker image tag to deploy (e.g., v3.4.0, latest)' + required: true + default: 'latest' + +permissions: + contents: read + deployments: write + +jobs: + deploy: + name: Deploy to staging + uses: ./.github/workflows/deploy-core.yml + with: + image_tag: ${{ inputs.image_tag }} + sha: ${{ github.sha }} + description: Deploy image ${{ inputs.image_tag }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6dd73190..4d823dc3f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,6 @@ jobs: fail-fast: false matrix: php-version: - - '8.1' - '8.2' - '8.3' - '8.4' diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index f74dd4fb0..000000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: opencode - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - opencode: - if: | - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - pull-requests: read - issues: read - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - with: - model: anthropic/claude-3-haiku-20240307 \ No newline at end of file diff --git a/.github/workflows/php-linter.yml b/.github/workflows/php-linter.yml index c27f5535c..fb94c5b86 100644 --- a/.github/workflows/php-linter.yml +++ b/.github/workflows/php-linter.yml @@ -12,14 +12,6 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - name: PHP Lint 8.0 - uses: dbfx/github-phplint/8.0@master - with: - folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\"" - - name: PHP Lint 8.1 - uses: dbfx/github-phplint/8.1@master - with: - folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\"" - name: PHP Lint 8.2 uses: dbfx/github-phplint/8.2@master with: diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index fe7b3ff48..b4c69e499 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -34,7 +34,6 @@ jobs: fail-fast: false matrix: php-version: - - '8.1' - '8.2' - '8.3' - '8.4' @@ -69,17 +68,13 @@ jobs: - name: Install npm dependencies run: npm install - - name: Build database.sql - run: npm run gulp build-database - - name: Start MariaDB run: | docker run -d --name mysql \ - -e MYSQL_ROOT_PASSWORD=root \ - -e MYSQL_DATABASE=ospos \ + -e MYSQL_ROOT_PASSWORD=root \ + -e MYSQL_DATABASE=ospos_test \ -e MYSQL_USER=admin \ -e MYSQL_PASSWORD=pointofsale \ - -v $PWD/app/Database/database.sql:/docker-entrypoint-initdb.d/database.sql \ -p 3306:3306 \ mariadb:10.5 # Wait for MariaDB to be ready @@ -89,6 +84,13 @@ jobs: done echo "MariaDB is ready!" + # Grant admin the CREATE/DROP privileges it needs to drop and + # recreate the test database at the start of each test class. + # Scoped to ospos_test only — never global *.* — so a compromised + # test process cannot alter or drop unrelated schemas. + docker exec mysql mysql -u root -proot \ + --execute="GRANT CREATE, DROP ON ospos_test.* TO 'admin'@'%'; FLUSH PRIVILEGES;" + - name: Get composer cache directory run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV @@ -107,11 +109,36 @@ jobs: - name: Create .env file run: cp .env.example .env + - name: Provision per-run encryption key + # Generates a unique key for this run and exports it as a real OS env + # var (ENCRYPTION_KEY), which the app's Encryption config reads as a + # fallback after the normal .env lookup. This is supported explicitly, + # so no shared key is committed or shipped. + run: | + KEY=$(openssl rand -hex 32) + printf 'ENCRYPTION_KEY=%s\n' "$KEY" >> "$GITHUB_ENV" + + - name: Provision per-run throttle key + # checkThrottleEncryption() is a read-only guard that throws unless a + # throttle.key is present (app/Helpers/security_helper.php). It is + # normally minted at container startup via `php spark env:provision`, + # so the test environment must provide one. + # + # We write it into the .env file (replacing the empty placeholder + # copied from .env.example) rather than exporting an OS env var: + # CodeIgniter's env() resolves $_ENV first, and DotEnv populates + # $_ENV['throttle.key']='' from .env, which would shadow any OS var + # before getenv() is ever consulted. + run: | + KEY=$(openssl rand -hex 32) + sed -i "s/^throttle\.key=''/throttle.key='$KEY'/" .env + grep -Eq "^throttle\.key=.+" .env + - name: Run PHPUnit tests env: CI_ENVIRONMENT: testing MYSQL_HOST_NAME: 127.0.0.1 - run: composer test -- --log-junit test-results/junit.xml + run: composer test -- --no-coverage --log-junit test-results/junit.xml - name: Upload test results uses: actions/upload-artifact@v4 @@ -123,4 +150,4 @@ jobs: - name: Stop MariaDB if: always() - run: docker stop mysql && docker rm mysql \ No newline at end of file + run: docker stop mysql && docker rm mysql diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c9f306ce8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,165 @@ +name: Release + +# Cuts the current release (changelog + tag + draft release) and, optionally, +# bumps App.php to the next dev version. One workflow, full release cycle. +# +# Flow: +# 1. Release current version: +# - generate git-cliff changelog section (base..HEAD) +# - insert it into CHANGELOG.md + GFM compare refs +# - commit + push the changelog +# - tag the current version and push the tag with the repo PAT +# (a PAT push triggers build-release.yml, whose official-release job +# creates the DRAFT GitHub Release with the changelog + assets) +# 2. (optional) bump to the next dev version: +# - update App.php / package.json / docker-compose / issue templates +# - commit + push + +on: + workflow_dispatch: + inputs: + next_bump: + description: 'After cutting this release, bump App.php to the next dev version' + required: true + type: choice + options: + - patch + - minor + - major + - skip + default: 'patch' + +permissions: + contents: write + +jobs: + release: + name: Cut release and bump next version + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Use the repo PAT (an admin token) so the changelog + bump pushes + # below can update the protected master branch (enforce_admins is off). + # GITHUB_TOKEN cannot, because master requires a PR review. + token: ${{ secrets.TOKEN }} + + - name: Install git-cliff + run: | + V=2.14.2 + curl -sLSfL "https://github.com/orhun/git-cliff/releases/download/v${V}/git-cliff-${V}-x86_64-unknown-linux-musl.tar.gz" -o /tmp/cliff.tar.gz + tar xzf /tmp/cliff.tar.gz -C /tmp + mv "/tmp/git-cliff-${V}/git-cliff" /usr/local/bin/git-cliff + chmod +x /usr/local/bin/git-cliff + git cliff --version + + - name: Determine version + id: version + run: | + APP_VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g") + TARGET="$APP_VERSION" + if ! [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: target '$TARGET' is not in X.Y.Z form"; exit 1 + fi + BASE=$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) + if [ -z "$BASE" ]; then + echo "ERROR: no previous release tag found"; exit 1 + fi + if [ "$BASE" = "$TARGET" ]; then + echo "ERROR: target $TARGET equals base $BASE; nothing to release"; exit 1 + fi + if ! printf '%s\n%s\n' "$BASE" "$TARGET" | sort -VC 2>/dev/null; then + echo "ERROR: target $TARGET is not greater than base $BASE"; exit 1 + fi + echo "target=$TARGET" >> "$GITHUB_OUTPUT" + echo "base=$BASE" >> "$GITHUB_OUTPUT" + echo "Releasing $TARGET (previous release: $BASE)" + + - name: Generate changelog section + run: | + git cliff --config cliff.toml \ + --tag "${{ steps.version.outputs.target }}" \ + --output /tmp/section.md \ + "${{ steps.version.outputs.base }}..HEAD" + sed -i '/./,$!d' /tmp/section.md + echo "Generated $(grep -c '^- ' /tmp/section.md) entries" + echo "=== first 5 ===" + head -5 /tmp/section.md + + - name: Insert into CHANGELOG.md + run: | + TARGET="${{ steps.version.outputs.target }}" + BASE="${{ steps.version.outputs.base }}" + sed -i "/^\[unreleased\]:/a [${TARGET}]: https://github.com/opensourcepos/opensourcepos/compare/${BASE}...${TARGET}" CHANGELOG.md + sed -i "s|^\[unreleased\]:.*|\[unreleased\]: https://github.com/opensourcepos/opensourcepos/compare/${TARGET}...HEAD|" CHANGELOG.md + LINE=$(grep -nE '^## \[[0-9]+\.' CHANGELOG.md | head -1 | cut -d: -f1) + printf '%s\n\n' "$(cat /tmp/section.md)" > /tmp/section_final.md + sed -i "$((LINE-1))r /tmp/section_final.md" CHANGELOG.md + echo "Inserted $TARGET section before line $LINE" + + - name: Commit changelog + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + git commit -m "docs: add ${{ steps.version.outputs.target }} changelog" + git push origin HEAD + + - name: Tag and push (PAT triggers build-release.yml) + run: | + TAG="${{ steps.version.outputs.target }}" + git tag "$TAG" + # Push with the repo PAT (not GITHUB_TOKEN) so the tag-push event + # triggers build-release.yml, whose official-release job creates the + # draft release with the changelog and assets. + git push "https://x-access-token:${RELEASE_PAT}@github.com/opensourcepos/opensourcepos.git" "refs/tags/$TAG" + echo "Released $TAG — draft release will be created by build-release.yml" + env: + RELEASE_PAT: ${{ secrets.TOKEN }} + + - name: Bump to next dev version (optional) + if: ${{ inputs.next_bump != 'skip' }} + run: | + CURRENT="${{ steps.version.outputs.target }}" + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + case "${{ inputs.next_bump }}" in + major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + minor) MINOR=$((MINOR + 1)); PATCH=0 ;; + patch) PATCH=$((PATCH + 1)) ;; + *) echo "ERROR: unknown bump type ${{ inputs.next_bump }}"; exit 1 ;; + esac + NEXT="${MAJOR}.${MINOR}.${PATCH}" + echo "Bumping to next dev version $NEXT" + + sed -i "s/public string \\\$application_version = '[^']*';/public string \\\$application_version = '$NEXT';/" app/Config/App.php + sed -i "s/\"version\": \"[^\"]*\",/\"version\": \"$NEXT\",/" package.json + # Keep package-lock.json in sync: bump only the @opensourcepos/opensourcepos + # package version (top-level + packages.""), leave dependency versions alone. + awk -v v="$NEXT" ' + /"name": "@opensourcepos\/opensourcepos"/ { expect=1 } + expect && /"version": / { sub(/"version": "[^"]*"/, "\"version\": \"" v "\""); expect=0 } + { print } + ' package-lock.json > .package-lock.tmp && mv .package-lock.tmp package-lock.json + # Update the "latest X.Y version" README line only when major.minor changes + NEW_MM=$(echo "$NEXT" | cut -d. -f1,2) + OLD_MM=$(echo "$CURRENT" | cut -d. -f1,2) + if [ "$NEW_MM" != "$OLD_MM" ]; then + sed -i "s/The latest \`[0-9]*\.[0-9]*\` version/The latest \`${NEW_MM}\` version/" README.md + fi + + echo "=== version refs after bump ===" + grep "application_version" app/Config/App.php + grep '"version"' package.json | head -1 + grep -A1 '"name": "@opensourcepos/opensourcepos"' package-lock.json + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add app/Config/App.php package.json package-lock.json + [ "$NEW_MM" != "$OLD_MM" ] && git add README.md + git commit -m "chore: bump version to $NEXT" + git push origin HEAD diff --git a/.github/workflows/update-issue-templates.yml b/.github/workflows/update-issue-templates.yml deleted file mode 100644 index f101a6264..000000000 --- a/.github/workflows/update-issue-templates.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Update Issue Templates - -on: - release: - types: [published] - workflow_dispatch: - schedule: - - cron: '0 0 * * 0' - -jobs: - update-templates: - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Fetch releases and update templates - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Fetch releases from GitHub API - RELEASES=$(gh api repos/${{ github.repository }}/releases --jq '.[].tag_name' | head -n 10) - - # Create temporary file with options - OPTIONS_FILE=$(mktemp) - echo " - development (unreleased)" >> "$OPTIONS_FILE" - while IFS= read -r release; do - echo " - opensourcepos $release" >> "$OPTIONS_FILE" - done <<< "$RELEASES" - - update_template() { - local template="$1" - local template_path=".github/ISSUE_TEMPLATE/$template" - - # Find the line numbers for the OpensourcePOS Version dropdown - start_line=$(grep -n "label: OpensourcePOS Version" "$template_path" | cut -d: -f1) - - if [ -z "$start_line" ]; then - echo "Could not find OpensourcePOS Version in $template" - return 1 - fi - - # Find the options section and default line - options_start=$((start_line + 3)) - default_line=$(grep -n "default:" "$template_path" | awk -F: -v opts="$options_start" '$1 > opts {print $1; exit}') - - # Create new template file - head -n $((options_start - 1)) "$template_path" > "${template_path}.new" - cat "$OPTIONS_FILE" >> "${template_path}.new" - tail -n +$default_line "$template_path" >> "${template_path}.new" - mv "${template_path}.new" "$template_path" - - echo "Updated $template" - } - - update_template "bug report.yml" - update_template "feature_request.yml" - - - name: Commit and push changes - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add .github/ISSUE_TEMPLATE/*.yml - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "Update issue templates with latest releases [skip ci]" - git push - fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ef8696fa..6d066cb9e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ $RECYCLE.BIN/ .com.apple.timemachine.donotpresent # Other +build/ generate_languages.php dist docs @@ -75,6 +76,8 @@ system/ *~ *.~ .env +.env.lock +.env.tmp.* auth.json *.png @@ -85,5 +88,5 @@ auth.json /writable/logs/*.log /writable/debugbar/*.json /app/Database/database.sql -/writable/cache/settings +/writable/cache/* /.env.bak diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 908983cfd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,54 +0,0 @@ -sudo: required - -branches: - except: - - unstable - - weblate -services: - - docker - -dist: jammy -language: node_js -node_js: - - 20 -script: - - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - - docker run --rm -u $(id -u) -v $(pwd):/app opensourcepos/composer:ci4 composer install - - version=$(grep application_version app/Config/App.php | sed "s/.*=\s'\(.*\)';/\1/g") - - cp .env.example .env && sed -i 's/production/development/g' .env - - sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$rev'/g" app/Config/OSPOS.php - - echo "$version-$branch-$rev" - - npm version "$version-$branch-$rev" --force || true - - sed -i 's/opensourcepos.tar.gz/opensourcepos.$version.tgz/g' package.json - - npm ci && npm install -g gulp && npm run build - - docker build . --target ospos -t ospos - - docker build . --target ospos_test -t ospos_test - - docker run --rm ospos_test /app/vendor/bin/phpunit --testdox - - docker build app/Database/ -t "jekkos/opensourcepos:sql-$TAG" -env: - global: - - BRANCH=$(echo ${TRAVIS_BRANCH} | sed s/feature\\///) - - TAG=$(echo "${TRAVIS_TAG:-$BRANCH}" | tr '/' '-') - - date=`date +%Y%m%d%H%M%S` && branch=${TRAVIS_BRANCH} && rev=`git rev-parse --short=6 HEAD` -after_success: - - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" && docker tag "ospos:latest" - "jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:sql-$TAG" - - gulp compress - - mv dist/opensourcepos.tar.gz "dist/opensourcepos.$version.$rev.tgz" - - mv dist/opensourcepos.zip "dist/opensourcepos.$version.$rev.zip" -deploy: - - provider: releases - edge: true - file: dist/opensourcepos.$version.$rev.zip - name: "Unstable OpensourcePos" - overwrite: true - release_notes: "This is a build of the latest master which might contain bugs. Use at your own risk. Check releases section for the latest official release" - prerelease: true - tag_name: unstable - user: jekkos - - api_key: - secure: "KOukL8IFf/uL/BjMyCSKjf2vylydjcWqgEx0eMqFCg3nZ4ybMaOwPORRthIfyT72/FvGX/aoxxEn0uR/AEtb+hYQXHmNS+kZdX72JCe8LpGuZ7FJ5X+Eo9mhJcsmS+smd1sC95DySSc/GolKPo+0WtJYONY/xGCLxm+9Ay4HREg=" - - on: - branch: master diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..d423d2acd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +# Agent Instructions + +This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase. + +## Code Style + +- **PSR-12** enforced via PHP-CS-Fixer (config: `.php-cs-fixer.no-header.php`) +- Follow PHP CodeIgniter 4 coding standards +- `camelCase` for variables and methods; `PascalCase` for classes; `UPPER_CASE` for constants +- When editing existing code containing non-PSR-compliant local variable names, refactor those variable names to `camelCase` as part of the edit +- All newly written code (variables, classes, functions) must use PSR-compliant naming, regardless of surrounding code style +- PHP 8.2+ features acceptable (named arguments, enums, readonly properties) +- Write PHP 8.2+ compatible code with proper type declarations +- Always import classes, functions, and constants with a `use` statement at the top of the file instead of referencing them inline via fully-qualified name (e.g. `use Config\Database;` then `Database::connect()`, not `\Config\Database::connect()`) +- No useless comments or docblocks: if the code speaks for itself, explanatory comments are unnecessary. Do not add comments or docblocks that merely restate what the code already makes clear (e.g. `@param array $items` next to `array $items`, or a comment repeating a method's name) — only comment on non-obvious rationale, constraints, or behavior +- Views in `app/Views/errors/html/` are excluded from the fixer +- Run fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php` +- **JavaScript**: use `const` for variables that are never reassigned, `let` for variables that are. Never use `var`. + +## Development + +- Create a new git worktree for each issue, based on the latest state of `origin/master` +- Commit fixes to the worktree and push to the remote + +## Testing + +- Run PHPUnit tests: `composer test` +- Tests must pass before submitting changes +- **One test file per class under test.** A controller, model, library, or helper gets exactly one test file covering all of its behavior — `Item_kits.php` → `tests/Controllers/Item_kitsTest.php` (or `Item_kitsControllerTest.php`, matching this codebase's existing `*ControllerTest.php` suffix for controllers), `Sale_lib.php` → `tests/Libraries/Sale_libTest.php`, etc. Do not create feature- or endpoint-scoped test files alongside a class's main test file (e.g. no `Item_kitsBarcodeTest.php` next to `Item_kitsControllerTest.php`) — add the new test methods to the existing file for that class instead. If no test file exists yet for the class, create the one canonical file rather than a narrowly-scoped one. +- **Name test methods as `test{MethodUnderTest}_{Behavior}`.** `PascalCase` the method under test, then an underscore, then a short behavior phrase — `testPostSave_RejectsNegativeTaxPercent`, `testSaveLocale_AcceptsValidReferenceCodeMinMax`. Start the behavior with a verb: `Accepts`/`Requires` for valid or boundary cases, `Rejects` for invalid ones. The name should read as a self-documenting sentence and needs no docblock. + +## Build + +- Install dependencies: `composer install && npm install` +- Build assets: `npm run build` or `gulp` + +## Conventions + +- Controllers go in `app/Controllers/` +- Models go in `app/Models/` +- Views go in `app/Views/` +- Database migrations in `app/Database/Migrations/` +- Use CodeIgniter 4 framework patterns and helpers +- Sanitize user input; escape output using `esc()` helper + +## Localization + +- When adding new keys to language files, add the key to all `app/Language/*/` variants +- **New keys must be inserted in alphabetical order** within the language array +- Non-English files must use an empty string (`''`) as the value when no translation is provided — CodeIgniter automatically falls back to the default (`en`) language. This applies only when a translation genuinely isn't available yet. +- **When explicitly asked to translate a phrase for a non-English language file, always provide the actual translation** — never leave the value as an empty string, and never leave source English text in a non-English language file +- Never copy English text from a neighboring key as a value for a non-English language file, even if that neighboring key is already untranslated — evaluate each key independently +- Only `app/Language/en/` and `app/Language/en-GB/` should contain English strings +- When translating a string containing placeholders (e.g. `{filePath}`, `{reason}`) or literal filenames/keys (e.g. `throttle.key`, `.env`), keep that placeholder or filename text unchanged and untranslated — move it to whatever position is grammatically correct in the translated sentence +- Plugin language files (`app/Plugins/*/Language/`) follow the same localization rules as `app/Language/` +- Use `'` to encapsulate key and string values. If the value contains `'` then it should be escaped as `\'` +- Align the `=>` of a newly inserted key with the `=>` column already used by the rest of the file, if that file pads keys to a fixed column (not all do — some files have no padding at all). If the new key is longer than the widest existing key and would push the alignment column further right, reformat the whole file to the new wider column rather than leaving only the new line at a different width + +## Security + +- Never commit secrets, credentials, or `.env` files +- Use parameterized queries to prevent SQL injection +- Validate and sanitize all user input +- Never reference security advisory IDs (CVE, GHSA, etc.) in code, comments, commit messages, docblocks, documentation, or URLs — treat them the same as secrets. They act as a roadmap for attackers researching the exact exploit a fix addresses. diff --git a/BUILD.md b/BUILD.md index b3858453c..57dd3efc4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -14,6 +14,7 @@ The build process uses the build tools "npm" and "gulp" to piece everything toge ## Prerequisites +- Install Node.js 20 or later (the build fails on Node 18 and earlier - one of the license reporting dependencies requires newer JavaScript regex features) - Install the latest version of NPM (tested using version 9.4.2) - Install the latest version of Composer (tested using composer 2.5.1) diff --git a/CHANGELOG.md b/CHANGELOG.md index e79764d70..890a137b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...HEAD +[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.2...HEAD [3.4.2]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...3.4.2 [3.4.1]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...3.4.1 [3.4.0]: https://github.com/opensourcepos/opensourcepos/compare/3.3.9...3.4.0 @@ -34,10 +34,273 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## [3.4.0] - 2025-02-06 +## [3.4.2] - 2026-09-30 +- Fix writable folder permission check (#4270) (#4273) by @jekkos +- Extended payment delete fix (#4274) by @jekkos +- Upgrade github workflow (#3708) (#4280) by @jekkos +- Fix typo in writeable (#4270) by @jekkos +- Fix migration 20250522000000 (#4284) by @jekkos +- Upgrade to ci 4.6.2 (#4296) (#4298) by @jekkos +- Fix barcode generation in items (#4270) by @jekkos +- Allow empty tax category id (#4285) (#4288) by @jekkos +- Fix security incident email address (#4298) by @jekkos +- Fix item kits update (#4294) by @jekkos +- Revert toast message sanitization (#4302) by @jekkos +- Fix for suspended sales (#4283) (#4303) by @jekkos +- Fix reference to uploads folder (#4270) (#4286) by @jekkos +- Add generic try/catch in import (#4302) by @jekkos +- Bump jspdf from 3.0.1 to 3.0.2 (#4309) by @dependabot[bot] +- Fix mount path for uploads (#4308) by @jekkos +- Add transactions to missing config keys migration. (#4318) by @Joe Williams +- [Feature] Add logging to migrations (#4327) by @Joe Williams +- Clean up docker compose setup (#4308) by @jekkos +- Fix tax configuration pages (#4331) by @jekkos +- Update SECURITY.md contact (#4335) by @jekkos +- Add server side validation for password (#4335) by @jekkos +- Add env variable to disallow pwd change (#4325) by @jekkos +- Add recent releases to issue template (#4317) by @jekkos +- Add DOMpurify + fix XSS (#4341) by @jekkos +- Fix attachment cid (#4314) by @jekkos +- Add DOMPurify to JS includes (#4341) by @jekkos +- Allow anonymous giftcard creation (#4278) by @jekkos +- Fix toast notifications in config (#4341) (#4343) by @jekkos +- Fix creation of date attribute value (#4310) (#4344) by @jekkos +- Fix wrong migration script location (#4285) by @jekkos +- Escape return_policy in receipt + invoice (#4349) by @jekkos +- Fix for detailed suppliers report (#4351) by @jekkos +- Add show/hide cost price & profit feature - in reports #4130 (#4350) by @BhojKamal +- Fix travis build after merge (#4130) by @jekkos +- Add equals as permitted URI character (#4329) by @Chathura Dilushanka +- Fix multiple XSS vulnerabilities (#3965) (#4356) by @jekkos +- Bump lodash from 4.17.21 to 4.17.23 (#4369) by @dependabot[bot] +- Bump jspdf and jspdf-autotable (#4373) by @dependabot[bot] +- Fix XSS vulnerabilities in invoices + receipts (#3965) (#4363) by @jekkos +- Fix XSS vulnerability in attributes (#3965) by @jekkos +- Fix XSS vulnerability in register (#3965) by @jekkos +- Fix XSS vulnerability in register (#3965) by @jekkos +- Fix XSS vulnerabilities in invoice_email.php view by @jekkos +- Fix permission bypass in Reports submodule access control (#4389) by @jekkos +- Use Content-Type application/json for AJAX responses (#4357) by @jekkos +- Language Array Key Typo Fix (#4371) by @Lucas Lyimo +- Fix: Refresh session language for employee after update. (#4245) by @jekkos +- Fix Docker image upload by replacing slashes in TAG by @jekkos +- Fix broken object-level authorization in Employees controller (CVE-worthy) (#4391) by @jekkos +- Bump dompurify from 3.3.1 to 3.3.2 (#4402) by @dependabot[bot] +- Fix incorrect argument types in migration round_number() methods (#4403) by @jekkos +- dd validation for invalid stock locations in CSV import (#4399) by @jekkos +- fix(security): whitelist and validate invoice template types (#4393) by @jekkos +- Fix second-order SQL injection in currency_symbol config (#4390) by @jekkos +- Add row-level authorization to password change endpoints (#4401) by @jekkos +- Fix: Handle image filenames with spaces in thumbnails by @jekkos +- Fix: Sanitize image filenames to prevent thumbnail display issues (#4372) by @jekkos +- Add migration to fix existing image filenames with spaces (#4372) by @jekkos +- Refactor: Move ADMIN_MODULES to constants, rename methods to camelCase by @jekkos +- Fix SQL injection in custom attribute search by @Ollama +- Fix stored XSS vulnerability in item descriptions by @Ollama +- Fix stored XSS vulnerabilities in employee permissions and customer data by @Ollama +- Fix: Preserve CHECKBOX attribute state when adding attributes (#4385) by @jekkos +- Fix payment type becoming null when editing sales by @Ollama +- Fix broken SQL injection fix - use havingLike() instead of having() with named params by @Ollama +- Fix mass assignment vulnerability in bulk edit (GHSA-49mq-h2g4-grr9) by @Ollama +- Sync language files (#3468) by @Ollama +- Add workflow to auto-update issue templates with releases by @Ollama +- Update SECURITY.md with published security advisories by @Ollama +- Bump jspdf from 4.1.0 to 4.2.0 (#4383) by @dependabot[bot] +- Add filter persistence for table views via URL query string (#4400) by @jekkos +- Fix filter persistence javascript issues (#4400) by @jekkos +- Fix PHPUnit test configuration for database connectivity (#4430) by @jekkos +- Fix IDOR vulnerability in password change (GHSA-mcc2-8rp2-q6ch) (#4427) by @jekkos +- Fix XSS vulnerability in tax invoice view (#4432) by @jekkos +- Fix permission bypass in Sales.getManage() access control (#4428) by @jekkos +- Update SECURITY.md with published security advisories (#4431) by @jekkos +- Fix SQL injection in suggestions column configuration (#4421) by @jekkos +- Fix PHPUnit environment variables not being set (#4434) by @jekkos +- Fix DECIMAL attribute not respecting locale format (#4422) by @jekkos +- Fix stored XSS vulnerability in Attribute Definitions (GHSA-rvfg-ww4r-rwqf) (#4429) by @jekkos +- Fix: Host Header Injection vulnerability (GHSA-jchf-7hr6-h4f3) by @Ollama +- Fix stored XSS in gcaptcha_site_key on login page by @Ollama +- Fix stored XSS via stock location name by @Ollama +- Fix Token_lib::render() for PHP 8.4 compatibility by @Ollama +- Use CIUnitTestCase for consistency with other tests by @Ollama +- Fix: Pass parameter to generate() and add composite format tests by @Ollama +- Fix strftime directives handling and tighten test assertions by @Ollama +- Add AGENTS.md with coding guidelines for AI agents by @Ollama +- Fix: Add Debit Card filter to Daily Sales and Takings by @Ollama +- Fix Taxes Summary Report totals not matching row values by @Ollama +- Add unit tests for Taxes Summary Report calculations by @Ollama +- Fix rounding consistency and update tests per review feedback by @Ollama +- Rewrite tests to use database integration testing by @Ollama +- Add seed data to tests for proper integration testing by @Ollama +- Fix: Restrict employee selection in expenses and receivings forms by @Ollama +- Fix review comments: remove redundant loop and add XSS escaping by @Ollama +- Bump jspdf from 4.2.0 to 4.2.1 by @dependabot[bot] +- Bump picomatch from 2.3.1 to 2.3.2 (#4451) by @dependabot[bot] +- fix: Clear sale session after completing sale by @Ollama +- fix: Remove redundant clear_mode() calls by @Ollama +- Translate missing strings in multiple languages by @Ollama +- Fix translation issues from code review by @Ollama +- Remove English fallbacks from non-English translations by @Ollama +- Add Calendar.php translations for missing languages by @Ollama +- feat: migrate CI from Travis to GitHub Actions with enhancements by @Ollama +- refactor: remove tables.sql and constraints.sql (#4447) by @Ollama +- refactor: remove build-database gulp task (#4447) by @Ollama +- refactor: optimize Docker image size by @Ollama +- fix: remove duplicate phpunit.xml that prevented tests from running by @Ollama +- fix: Use file-based session until database is migrated by @Ollama +- feat: Improve migration UX on login page by @Ollama +- Disable opencode workflow + run docker build by @jekkos +- Fix negative price/quantity/discount validation (GHSA-wv3j-pp8r-7q43) (#4450) by @Nozomu Sasaki (Paul) +- fix(ci): replace / with _ in branch names for Docker tags by @Ollama +- fix(security): prevent command injection in sendmail path configuration by @Ollama +- fix(security): prevent SQL injection in tax controller sort columns by @Ollama +- feat: add release workflow with automated version bumping by @Ollama +- refactor: simplify release workflow to version bump only by @Ollama +- fix: address review comments by @Ollama +- fix: address all review comments and restore issue template version update by @Ollama +- fix: Tax Rate form not loading due to router service failure (#4479) by @jekkos +- fix: Handle empty database on fresh install (#4467) by @jekkos +- Fix: Improve allowedHostnames .env configuration and fail-fast in production (#4482) by @jekkos +- [Feature]: Case-sensitive attribute updates and CSV Import attribute deletion capability (#4384) by @objecttothis +- fix: change docker image tag to master by @jekkos +- Update to CodeIgniter 4.7.2 (#4485) by @objecttothis +- Bump lodash from 4.17.23 to 4.18.1 (#4462) by @dependabot[bot] +- [Fix]: Add missing return statements to Sales Controller functions by @Ollama +- Encourage users to star the project by @objecttothis +- Bump dompurify from 3.3.2 to 3.4.0 (#4512) by @dependabot[bot] +- fix: propagate attribute definition failures in postSaveGeneral() (#4509) by @jekkos +- fix: Escape dynamic output and fix CSS property in barcode_sheet.php (#4501) by @jekkos +- Fix CRC currency reverting to EUR/LAK in locale config (#4511) by @jekkos +- fix: Add missing $img_tag variable in Sales::getSendPdf() (#4515) by @jekkos +- fix: Language dropdown not displaying saved language correctly (#4518) by @jekkos +- fix: Scope orWhere clauses in Item::exists() and Item::get_item_id() (#4520) by @jekkos +- fix: Update calendar translations (#4498) by @jekkos +- fix: Catch mysqli_sql_exception in DB fallback handlers for fresh Docker installs (#4525) by @jekkos +- fix(home): improve internal data type handling for user identification in auth process by @enricodelarosa +- Assignable Keyboard Shortcuts Updates (#4532) by @WShells +- chore: miscellaneous updates and improvements (#4530) by @BudsieBuds +- chore(deps): bump minimatch from 3.1.2 to 3.1.5 (#4536) by @dependabot[bot] +- chore: sync project files to match upstream templates (#4537) by @BudsieBuds +- fix(ci): include hidden files in Docker build context (#4543) by @jekkos +- feat: add ALLOWED_HOSTNAMES environment variable support for Docker/Compose (#4544) by @jekkos +- fix(docker): correct permissions and fix migration barcode_type error (#4546) by @jekkos +- docs: Update SECURITY.md with disclosure process (#4549) by @jekkos +- feat: Bank transfer and wallet payment option added #4540 (#4547) by @BhojKamal +- fix(security): Path traversal vulnerability in getPicThumb (#4545) by @jekkos +- fix(security): SQL injection and path traversal vulnerabilities (#4539) by @jekkos +- fix: Capture CSV import failures in save_tax_data and save_inventory_quantities (#4507) by @jekkos +- fix: validate attributeId > 0 in saveAttributeLink() (#4508) by @jekkos +- feat: Add deployment workflow with approval gates (#4522) by @jekkos +- Bugfixes to get Migration working on MySQL and MariaDB (#4551) by @objecttothis +- Bugfix: Sale search in register not handling trailing space properly (#4557) by @objecttothis +- fix: cast string returns to int in MY_Migration (#4560) by @jekkos +- Add fallback for allowedHostnames environment variable (#4565) by @objecttothis +- fix: Allow searching by Sale ID in Takings/Daily Sales view (#4569) by @jekkos +- Add Guards to Database Migration (#4571) by @objecttothis +- fix: tax rate inputs blank with comma-decimal locales (#4555) by @jekkos +- fix(security): Fix DOMPDF RCE and customer email sanitization (#4568) by @jekkos +- Fix overly lenient date validation (#4574) by @objecttothis +- fix(security): Escape attribute value in register by @jekkos +- chore(deps): bump dompurify from 3.4.0 to 3.4.11 (#4578) by @dependabot[bot] +- Bugfix: Fix problems with migration UI in login (#4589) by @objecttothis +- Forgotten commit from login migration branch (#4592) by @objecttothis +- Feature: Payment reference code (#4587) by @objecttothis +- fix(giftcard): correct return type and rename getGiftcardId method (#4600) by @objecttothis +- chore(deps): bump dompurify from 3.4.11 to 3.4.12 (#4602) by @dependabot[bot] +- bugfix(reports): crash on detailed sales report when sale has multiple payments with reference codes (#4599) by @objecttothis +- style(models): normalize quote style in SQL GROUP_CONCAT expression (#4608) by @objecttothis +- chore(deps): upgrade dompdf from v2.0.8 to v3.1.6 (#4610) by @objecttothis +- chore(deps): bump brace-expansion (#4614) by @dependabot[bot] +- chore(deps): add xlsx via SheetJS CDN and upgrade tableexport plugin (#4615) by @objecttothis +- chore(deps): bump lodash.template from 4.5.0 to 4.18.1 (#4616) by @objecttothis +- fix(login): skip auth validation on new install to allow migration (#4609) by @objecttothis +- fix: Wrap postSave() in single transaction for atomicity (#4506) by @jekkos +- refactor: Replace var with let/const in JavaScript files (#4503) by @jekkos +- fix: get_definition_by_name() returns single row instead of multi-dimensional array (#4452) (#4464) by @Jonathan Chang +- fix(config): validate theme param to prevent XSS via invalid theme (#4620) by @objecttothis +- fix(sales): enforce server-side authorization for price changes (#4631) by @objecttothis +- fix(sales): escape quote number in email template to prevent XSS (#4625) by @objecttothis +- refactor(migrations): rename execute_script to executeScript across all migrations (#4611) by @objecttothis +- fix(auth): validate gcaptcha before password to prevent bypass (#4618) by @objecttothis +- refactor: apply PSR-12 naming to Attribute definition methods (#4624) by @Rayan Abdul Cader +- fix(security): sanitize filenames and escape logo path in config (#4630) by @objecttothis +- fix: use db_connect() for item save transactions (#4636) by @richardmilles +- fix(xss): remove redundant escaping that double-encoded item attribute values (#4628) by @objecttothis +- chore(deps): bump codeigniter4/framework from 4.7.2 to 4.7.4 (#4638) by @dependabot[bot] +- chore(deps): bump dompurify from 3.4.12 to 3.4.13 (#4639) by @dependabot[bot] +- Reject item CSV imports whose header row is missing required columns (#4597) by @Sai Asish Y +- fix(items): validate item_number and skip receiving quantity default for temp items (#4621) by @objecttothis +- Feature: CodeIgniter Throttler (#4619) by @objecttothis +- Bugfix: Resolve Race Condition in Rewards and Gift Card Spending (#4640) by @objecttothis +- hotfix(auth): hash throttler keys to improve security (#4646) by @objecttothis +- fix(items): add explicit sentinel value for clearing supplier in bulk edit (#4617) by @objecttothis +- Codeigniter changes between 4.7.2 and 4.7.4 (#4650) by @objecttothis +- Hotfix: Fix CI3 database migration caused by regression (#4649) by @objecttothis +- fix(sales): gate per-record endpoints behind reports_sales grant (#4627) by @objecttothis +- fix(email): update method call to camelCase for PSR-12 compliance (#4659) by @objecttothis +- Feature admin account safeguards (#4657) by @objecttothis +- Ensure payload data is escaped to prevent XSS (#4664) by @objecttothis +- fix(sales): enforce reports_sales grant on search endpoint (#4673) by @objecttothis +- fix(reports, home): resolve double-URL-decoding bypass for method grants (#4660) (#4666) by @objecttothis +- Bugfix tax names (#4677) by @objecttothis +- feat(validation, tests): add `valid_path_strict` rule and integrate into mailpath validation (#4684) by @objecttothis +- fix(sales): harden payment validation and gift card handling by @objecttothis +- fix: prevent duplicate items when editing imported rows (#4634) by @richardmilles +- fix(barcode): resolve string interpolation issue in barcode display html (#4692) by @Vighnesh Nilajakar +- fix(sales): gate getSearch behind reports_sales grant by @jekkos +- bugfix(sales): reject non-negative gift-card amount_tendered (#4674) by @jekkos +- fix(sales): harden unsuspend with auth, status gating, and null safety by @objecttothis +- fix(tests): resolve all phpunit failures — clean-DB suite green (#4626) (#4691) by @jekkos +- fix(licenses): guard malformed data, parallelize gulp tasks, require Node 20 by @objecttothis +- fix(validation): broaden sendmail path regex, expand i18n, strip advisory IDs by @objecttothis +- fix(security): handle special characters in `.env` key values and improve insertion logic (#4656) by @objecttothis +- fix(locale): validate language_code against known locales to block path traversal (#4704) by @jekkos +- Fix GHSA-frx7-c5vv-m3mr: recompute cashup total server-side and force owner identity (#4706) by @jekkos +- feat(security): add THROTTLE_KEY env-var fallback for throttle.key (#4707) by @jekkos +- fix(i18n): translate remaining English labels in Swiss German Items.php (#4701) by @Rayan Abdul Cader +- fix(ci): stop stamping app version onto master and branch Docker tags (#4709) by @jekkos +- chore(deps): bump fflate from 0.8.2 to 0.8.3 (#4690) by @dependabot[bot] +- fix(i18n): swap print_delay_autoreturn number/required messages in 5 locales (#4699) by @Rayan Abdul Cader +- chore(release): unified git-cliff release workflow (changelog + tag + optional bump) (#4711) by @jekkos +- fix(release): push changelog/bump to master via admin PAT (GITHUB_TOKEN blocked by branch protection) by @jekkos +- docs: add 3.4.2 changelog by @github-actions[bot] +- chore: bump version to 3.4.3 by @github-actions[bot] +- fix(security): HTML-escape attribute dropdown option labels in items attributes view (#4715) by @jekkos +- fix(release): keep package-lock.json version in sync on bump (#4718) by @jekkos +- fix(security): strip all HTML tags from $.notify alert messages (#4716) by @jekkos +- chore: strip advisory IDs from code comments and changelog (#4720) by @jekkos +- fix(security): report unwritable .env.lock, make throttle limits configurable (#4714) by @jekkos +- chore: reset 3.4.2 (undo premature 3.4.3 bump + stale changelog) for re-cut by @jekkos + +## [3.4.1] - 2025-06-05 +- Feature: PSR-12 Compliant Indentation by @objecttothis in ([#4196](https://github.com/opensourcepos/opensourcepos/pull/4196)) +- Add .env to dist zip by @jekkos in ([#4199](https://github.com/opensourcepos/opensourcepos/pull/4199)) +- Add CI4 coding standards linter ([#3708](https://github.com/opensourcepos/opensourcepos/issues/3708)) by @jekkos in ([#4198](https://github.com/opensourcepos/opensourcepos/pull/4198)) +- Bump canvg from 3.0.10 to 3.0.11 by @dependabot in ([#4189](https://github.com/opensourcepos/opensourcepos/pull/4189)) +- Bump jspdf and jspdf-autotable by @dependabot in ([#4190](https://github.com/opensourcepos/opensourcepos/pull/4190)) +- Feature bump ci to 4.6.0 by @objecttothis in ([#4197](https://github.com/opensourcepos/opensourcepos/pull/4197)) +- Add Kurdish language option to UI by @BudsieBuds in ([#4210](https://github.com/opensourcepos/opensourcepos/pull/4210)) +- Convert language ku to ckb by @BudsieBuds in ([#4211](https://github.com/opensourcepos/opensourcepos/pull/4211)) +- Fix PHP 8.4 errors by @BudsieBuds in ([#4215](https://github.com/opensourcepos/opensourcepos/pull/4215)) +- Add default bootstrap to themes by @BudsieBuds in ([#4219](https://github.com/opensourcepos/opensourcepos/pull/4219)) +- Update language names by @BudsieBuds in ([#4218](https://github.com/opensourcepos/opensourcepos/pull/4218)) +- Update install docs by @BudsieBuds in ([#4217](https://github.com/opensourcepos/opensourcepos/pull/4217)) +- Convert menu icons to SVG by @BudsieBuds in ([#4220](https://github.com/opensourcepos/opensourcepos/pull/4220)) +- Enhance license handling by @BudsieBuds in ([#4223](https://github.com/opensourcepos/opensourcepos/pull/4223)) +- Fix datetime rendering ([#4226](https://github.com/opensourcepos/opensourcepos/issues/4226)) by @jekkos in ([#4227](https://github.com/opensourcepos/opensourcepos/pull/4227)) +- Fix datetime rendering by @jekkos in ([#4228](https://github.com/opensourcepos/opensourcepos/pull/4228)) +- Fix null error when sending by email a receipt of a sale that has no invoice by @diego-ramos in ([#4229](https://github.com/opensourcepos/opensourcepos/pull/4229)) +- Update Receivings.php to save form. by @odiea in ([#4231](https://github.com/opensourcepos/opensourcepos/pull/4231)) +- Update Cashups.php for ajax cashup total to work. by @odiea in ([#4238](https://github.com/opensourcepos/opensourcepos/pull/4238)) +- Coding style updates for PSR-12 compliance & improved readability by @BudsieBuds in ([#4204](https://github.com/opensourcepos/opensourcepos/pull/4204)) +- Fix Codeigniter disallowed characters error with payment types that have accents by @diego-ramos in ([#4232](https://github.com/opensourcepos/opensourcepos/pull/4232)) +- Fixed broken escape string for success & warning messages by @Franchovy in ([#4253](https://github.com/opensourcepos/opensourcepos/pull/4253)) +- Bugfix constraint migration fix by @objecttothis in ([#4230](https://github.com/opensourcepos/opensourcepos/pull/4230)) +- Fix item number lookup in sales/receivings ([#4212](https://github.com/opensourcepos/opensourcepos/issues/4212)) by @jekkos in ([#4250](https://github.com/opensourcepos/opensourcepos/pull/4250)) + +## [3.4.0] - 2025-03-23 - Translation updates (Spanish, Indonesian, Swedish, Urdu, Chinese, Thai, French, Dutch) -- PHP 8.x support +- PHP `8.x` support - Security fixes (XSS, SQLi) - Migration to Gulp as buildsystem - Decimal validation fix diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 10e1acd6c..302dae0b9 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,98 +1,85 @@ -Contributor Covenant Code of Conduct -Our Pledge -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. -Our Standards -Examples of behavior that contributes to a positive environment for our -community include: +[comment]: # (Contributor Covenant 2.1 - from https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, -and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall -community +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of -any kind +* The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others’ private information, such as a physical or email address, -without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a -professional setting +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting -Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. -Scope -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. -Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. -All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. -Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: -1. Correction -Community Impact: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. -Consequence: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. -2. Warning -Community Impact: A violation through a single incident or series of -actions. -Consequence: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. -3. Temporary Ban -Community Impact: A serious violation of community standards, including -sustained inappropriate behavior. -Consequence: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. -4. Permanent Ban -Community Impact: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. -Consequence: A permanent ban from any sort of public interaction within the -community. -Attribution -This Code of Conduct is adapted from the Contributor Covenant, -version 2.1, available at -https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. -Community Impact Guidelines were inspired by -Mozilla’s code of conduct enforcement ladder. -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +## Enforcement Responsibilities +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/Dockerfile b/Dockerfile index 376b0834d..4180c9259 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,23 @@ FROM php:8.2-apache AS ospos LABEL maintainer="jekkos" -RUN apt update && apt-get install -y libicu-dev libgd-dev -RUN a2enmod rewrite -RUN docker-php-ext-install mysqli bcmath intl gd +RUN apt-get update && apt-get install -y --no-install-recommends \ + libicu-dev \ + libgd-dev \ + && docker-php-ext-install mysqli bcmath intl gd \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && a2enmod rewrite + RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini WORKDIR /app -COPY . /app -RUN ln -s /app/*[^public] /var/www && rm -rf /var/www/html && ln -nsf /app/public /var/www/html -RUN chmod -R 770 /app/writable/uploads /app/writable/logs /app/writable/cache && chown -R www-data:www-data /app - -FROM ospos AS ospos_test - -COPY --from=composer /usr/bin/composer /usr/bin/composer - -RUN apt-get install -y libzip-dev wget git -RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O /bin/wait-for-it.sh && chmod +x /bin/wait-for-it.sh -RUN docker-php-ext-install zip -RUN composer install -d/app -#RUN sed -i 's/backupGlobals="true"/backupGlobals="false"/g' /app/tests/phpunit.xml -WORKDIR /app/tests - -CMD ["/app/vendor/phpunit/phpunit/phpunit", "/app/test/helpers"] +COPY --chown=www-data:www-data . /app +RUN chmod 750 /app/writable/logs /app/writable/uploads /app/writable/cache /app/public/uploads /app/public/uploads/item_pics \ + && chmod 640 /app/writable/uploads/importCustomers.csv \ + && ln -s /app/*[^public] /var/www \ + && rm -rf /var/www/html \ + && ln -nsf /app/public /var/www/html FROM ospos AS ospos_dev diff --git a/INSTALL.md b/INSTALL.md index 3aeeca927..eb75cc034 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,27 +1,67 @@ ## Server Requirements -- PHP version `8.1` to `8.4` are supported, PHP version `≤7.4` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section. +- PHP version `8.2` to `8.4` are supported, PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section. - MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance. - Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP). - Raspberry PI based installations proved to work, see [wiki page here](). - For Windows based installations please read [the wiki](https://github.com/opensourcepos/opensourcepos/wiki). There are closed issues about this subject, as this topic has been covered a lot. +## Security Configuration + +### Allowed Hostnames (REQUIRED for Production) + +āš ļø **CRITICAL**: OpenSourcePOS validates the Host header to prevent Host Header Injection attacks. **You MUST configure `app.allowedHostnames` for production deployments. If not configured, the application will fail to start.** + +**Add to your `.env` file:** + +```bash +# Comma-separated list of allowed hostnames (no protocols or ports) +app.allowedHostnames = 'yourdomain.com,www.yourdomain.com' +``` + +**For local development:** + +```bash +app.allowedHostnames = 'localhost' +``` + +**If you see this error at startup:** + +```text +RuntimeException: Security: allowedHostnames is not configured. +``` + +**Solution**: Add `app.allowedHostnames` to your `.env` file with your domain(s). + +**Why this matters:** +- Prevents Host Header Injection attacks +- Ensures URLs are generated with the correct domain +- Fixes issue #4480: .env configuration now works via comma-separated values + +### HTTPS Behind Proxy + +If your installation is behind a proxy with SSL offloading, set: +``` +FORCE_HTTPS = true +``` + ## Local install -First of all, if you're seeing the message `system folder missing` after launching your browser, or cannot find `database.sql`, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps. +First of all, if you're seeing the message `system folder missing` after launching your browser, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps. 1. Download the a [pre-release for a specific branch](https://github.com/opensourcepos/opensourcepos/releases) or the latest stable [from GitHub here](https://github.com/opensourcepos/opensourcepos/releases). A repository clone will not work unless know how to build the project. 2. Create/locate a new MySQL database to install Open Source Point of Sale into. -3. Execute the file `app/Database/database.sql` to create the tables needed. -4. Unzip and upload Open Source Point of Sale files to the web-server. -5. Open `.env` file and modify credentials to connect to your database if needed. (First copy .env.example to .env and update) +3. Unzip and upload Open Source Point of Sale files to the web-server. +4. If `.env` does not exist, copy `.env.example` to `.env`. +5. Open `.env` and modify credentials to connect to your database if needed. +6. The database schema will be automatically created when you first access the application. Migrations run automatically on fresh installs. 7. Go to your install `public` dir via the browser. 8. Log in using - Username: admin - Password: pointofsale 9. If everything works, then set the `CI_ENVIRONMENT` variable to `production` in the .env file -9. Enjoy! -10. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment. +10. Enjoy! +11. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment. ## Local install using Docker @@ -31,7 +71,15 @@ Docker runs natively on Mac and Linux. Windows requires WSL2 to be installed. Pl **Be aware that this setup is not suited for production usage! Change the default passwords in the compose file before exposing the containers publicly.** -Start the containers using the following command +First create a **regular `.env` file** in the project root (a missing one is not +auto-created as a file — see the compose `create_host_path: false` setting). Copy +the shipped example: + +``` + cp .env.example .env +``` + +Then start the containers: ``` docker-compose up diff --git a/README.md b/README.md index 755c73f60..ecd695b8f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

-Build Status +Build Status Join the chat at https://app.gitter.im Project Version Translation Status @@ -102,11 +102,11 @@ NOTE: If you're running non-release code, please make sure you always run the la - If you have suhosin installed and face an issue with CSRF, please make sure you read [issue #1492](https://github.com/opensourcepos/opensourcepos/issues/1492). -- PHP `≄ 8.1` is required to run this app. +- PHP `≄ 8.2` is required to run this app. ## šŸƒ Keep the Machine Running -If you like our project, please consider buying us a coffee through the button below so we can keep adding features. +If you like our project, please consider buying us a coffee through the button below so we can keep adding features. Please star the project if you like it! [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MUN6AEG7NY6H8)\ Or refer to the [FUNDING.yml](.github/FUNDING.yml) file. @@ -137,7 +137,7 @@ Any person or company found breaching the license agreement might find a bunch o ## šŸ™ Credits -|

DigitalOcean
|
JetBrains
|
Travis CI
| +|
DigitalOcean
|
JetBrains
|
GitHub
| | --- | --- | --- | -|
DigitalOcean Logo
|
IntelliJ IDEA Logo
|
Travis CI Logo
| -| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [Travis CI](https://www.travis-ci.com/) for providing a free continuous integration service for open source projects. | +|
DigitalOcean Logo
|
IntelliJ IDEA Logo
|
GitHub Actions Logo
| +| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. | diff --git a/SECURITY.md b/SECURITY.md index 771524f48..ab58ee26e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,8 +5,9 @@ - [Supported Versions](#supported-versions) - [Security Advisories](#security-advisories) - [Reporting a Vulnerability](#reporting-a-vulnerability) + - [Disclosure Process](#disclosure-process) - + # Security Policy @@ -21,26 +22,116 @@ We release patches for security vulnerabilities. ## Security Advisories -The following security vulnerabilities have been published: - -### High Severity - -| CVE | Vulnerability | CVSS | Published | Fixed In | Credit | -|-----|--------------|------|-----------|----------|--------| -| [CVE-2025-68434](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-wjm4-hfwg-5w5r) | CSRF leading to Admin Creation | 8.8 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos | -| [CVE-2025-68147](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-xgr7-7pvw-fpmh) | Stored XSS in Return Policy | 8.1 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos | -| [CVE-2025-66924](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-gv8j-f6gq-g59m) | Stored XSS in Item Kits | 7.2 | 2026-03-04 | 3.4.2 | @hungnqdz, @omkaryepre | - -### Medium Severity - -| CVE | Vulnerability | CVSS | Published | Fixed In | Credit | -|-----|--------------|------|-----------|----------|--------| -| [CVE-2025-68658](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-32r8-8r9r-9chw) | Stored XSS in Company Name | 4.3 | 2026-01-13 | 3.4.2 | @hungnqdz | - -For a complete list including draft advisories, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories). +For a complete list of published and draft security advisories with CVE details, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories). ## Reporting a Vulnerability -Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**. +**Option 1: GitHub Security Advisory (Preferred)** -You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. \ No newline at end of file +1. Create a draft security advisory directly on GitHub: + - Go to https://github.com/opensourcepos/opensourcepos/security/advisories + - Click "New draft security advisory" + - Fill in the vulnerability details using our [template below](#vulnerability-template) + - Submit as **draft** (not published) + +2. Notify us for triage: + - Send an email to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)** with: + - Subject: `[GHSA] Brief description of vulnerability` + - Link to the draft advisory + - Brief summary + +**Option 2: Email Report** + +Send vulnerability details to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**. + +You will receive a response within 48 hours. Confirmed vulnerabilities will be patched within a few days depending on complexity. + +## Disclosure Process + +### Timeline + +| Step | Timeline | Action | +|------|----------|--------| +| 1. Report received | Day 0 | We acknowledge within 48 hours | +| 2. Triage & confirmation | Day 1-3 | We validate the vulnerability | +| 3. Fix development | Day 3-7 | We develop and test the fix | +| 4. Patch release | Day 7-10 | We release a security patch | +| 5. CVE request | Day 7-14 | We request CVE from GitHub (if applicable) | +| 6. Advisory published | Day 14 | We publish the advisory with credit | +| 7. Public disclosure | Day 14+ | Full disclosure after patch release | + +### CVE Process + +**We request CVE identifiers through GitHub's security advisory system.** This is the preferred and easiest method: + +1. After we confirm and fix the vulnerability, we'll request a CVE through GitHub +2. GitHub coordinates with MITRE on our behalf +3. The CVE is automatically linked to the advisory +4. You'll be credited as the reporter in the published advisory + +**Already have a CVE?** If you've already obtained a CVE from another source (e.g., VulDB, CVE.MITRE.ORG), please include it in your report or advisory. We'll update our advisory to reference the existing CVE. + +### No Bug Bounty Program + +**Important:** Open Source Point of Sale does not offer a bug bounty program. + +- All security research and vulnerability triage is done on a **voluntary basis** in our free time +- We do not offer monetary rewards for vulnerability reports +- We do credit reporters in published advisories (unless anonymity is requested) +- We greatly appreciate the security research community's efforts to help improve project security + +### Security Best Practices for Researchers + +- **Do not** access, modify, or delete data that doesn't belong to you +- **Do not** perform denial of service attacks +- **Do not** publicly disclose vulnerabilities before we've had time to fix them +- **Do** provide sufficient information to reproduce the vulnerability +- **Do** allow us reasonable time to fix before public disclosure +- **Do** report through official channels (GitHub advisories or email) + +### Vulnerability Template + +When creating a draft advisory, please include: + +``` +## Summary +[Brief description of the vulnerability] + +## Impact +- **Confidentiality:** [High/Medium/Low - what data can be exposed] +- **Integrity:** [High/Medium/Low - what can be modified] +- **Availability:** [High/Medium/Low - service disruption potential] +- **Privilege Required:** [None/Low/High - authentication level needed] +- **CVSS v3.1:** [Score] ([Vector string]) + +## Details +[Technical details about the vulnerability] + +**Affected Code:** +```php +// Path to affected file and vulnerable code +``` + +**Attack Vector:** +[How an attacker can exploit this] + +## Proof of Concept +```bash +# Steps to reproduce +``` + +## Patch +[Suggested fix or approach] + +## Affected Versions +- OpenSourcePOS X.Y.Z and earlier + +## Credit +[Your GitHub username or preferred name] +``` + +--- + +**Thank you to all security researchers who have contributed to making Open Source Point of Sale more secure.** Your voluntary efforts help protect thousands of users worldwide and contribute to a safer, more trustworthy free and open-source software ecosystem. We deeply appreciate your responsible disclosure and the time you invest in improving our project. + +If you've reported a vulnerability and would like to discuss CVE coordination or have questions about the process, please reach out to us at [jeroen@steganos.dev](mailto:jeroen@steganos.dev). \ No newline at end of file diff --git a/app/Commands/EnvProvision.php b/app/Commands/EnvProvision.php new file mode 100644 index 000000000..987b1d552 --- /dev/null +++ b/app/Commands/EnvProvision.php @@ -0,0 +1,126 @@ +key ?? ''); + + if ($key !== '' && strlen($key) >= 64) { + CLI::write('encryption.key : CI4 key already present', 'green'); + CLI::newLine(); + + return; + } + + $converter = new CI3SecretConverter(); + + if ($key !== '' && strlen($key) < 64) { + // DB read, safe outside the .env lock. + $plain = $converter->decryptAll($key); + $hasData = $this->anyNonEmpty($plain); + + // Backup -> rotate -> re-encrypt -> verify -> persist under a single .env lock. + rotateEncryptionKeyTransaction($key, static function () use ($plain, $hasData, $converter): void { + $encrypted = $converter->encryptAll($plain); + + if (array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) { + throw new RuntimeException('Failed to verify converted encryption data.'); + } + + if ($hasData) { + $converter->saveAll($encrypted); + } + }); + + CLI::write('encryption.key : rotated CI3 -> CI4 key', 'green'); + if ($hasData) { + CLI::write('legacy secrets : converted and verified to CI4 cipher', 'green'); + } + } else { + // Fresh key (no old key to decrypt): a single atomic write suffices; + // the transaction wrapper still serialises it against other workers. + rotateEncryptionKeyTransaction(null, static function (): void {}); + + CLI::write('encryption.key : new CI4 key generated', 'green'); + + if ($this->legacySecretsPresent()) { + CLI::write('legacy secrets : WARNING - stored CI3 secrets found but no CI3 key to decrypt them; they could not be recovered', 'yellow'); + } + } + + CLI::newLine(); + CLI::write('env:provision complete.', 'green'); + CLI::newLine(); + } + + private function anyNonEmpty(array $plain): bool + { + foreach ($plain as $value) { + if ((string) $value !== '') { + return true; + } + } + + return false; + } + + private function legacySecretsPresent(): bool + { + try { + $appConfig = model('Appconfig'); + } catch (Exception $e) { + return false; + } + + foreach (CI3SecretConverter::LEGACY_KEYS as $col) { + try { + if ($appConfig->get_value($col) !== '') { + return true; + } + } catch (Exception $e) { + return false; + } + } + + return false; + } +} diff --git a/app/Config/App.php b/app/Config/App.php index 257d1e786..de204b036 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -12,7 +12,7 @@ class App extends BaseConfig * * @var string */ - public string $application_version = '3.4.2'; + public string $application_version = '3.4.3'; /** * This is the commit hash for the version you are currently using. @@ -58,9 +58,9 @@ class App extends BaseConfig * Allowed Hostnames in the Site URL other than the hostname in the baseURL. * If you want to accept multiple Hostnames, set this. * - * E.g., - * When your site URL ($baseURL) is 'http://example.com/', and your site - * also accepts 'http://media.example.com/' and 'http://accounts.example.com/': + * Or via environment variable (useful for Docker/Compose): + * ALLOWED_HOSTNAMES=example.com,www.example.com + * * ['media.example.com', 'accounts.example.com'] * * @var list @@ -117,7 +117,7 @@ class App extends BaseConfig | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! | */ - public string $permittedURIChars = 'a-z 0-9~%.:_\-='; + public string $permittedURIChars = 'a-z 0-9~%.:_\-'; /** * -------------------------------------------------------------------------- @@ -278,14 +278,97 @@ class App extends BaseConfig * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ * @see http://www.w3.org/TR/CSP/ */ - public bool $CSPEnabled = false; // TODO: Currently CSP3 tags are not supported so enabling this causes problems with script-src-elem, style-src-attr and style-src-elem + public bool $CSPEnabled = false; public function __construct() { parent::__construct(); + + // Solution for CodeIgniter 4 limitation: arrays cannot be set from .env + // See: https://github.com/codeigniter4/CodeIgniter4/issues/7311 + $envAllowedHostnames = $this->getEnvString('ALLOWED_HOSTNAMES') + ?? $this->getEnvString('app.allowedHostnames'); + + if ($envAllowedHostnames !== null) { + $this->allowedHostnames = array_values(array_filter( + array_map('trim', explode(',', $envAllowedHostnames)), + static fn (string $hostname): bool => $hostname !== '' + )); + } + $this->https_on = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_ENV['FORCE_HTTPS']) && $_ENV['FORCE_HTTPS'] == 'true'); + + $host = $this->getValidHost(); $this->baseURL = $this->https_on ? 'https' : 'http'; - $this->baseURL .= '://' . ((isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : 'localhost') . '/'; + $this->baseURL .= '://' . $host . '/'; $this->baseURL .= str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); } + + /** + * Validates and returns a trusted hostname. + * + * Security: Prevents Host Header Injection attacks + * by validating the HTTP_HOST against a whitelist of allowed hostnames. + * + * In production: Fails fast if allowedHostnames is not configured. + * In development: Allows localhost fallback with an error log. + * + * @return string A validated hostname + * @throws \RuntimeException If allowedHostnames is not configured in production + */ + private function getValidHost(): string + { + $httpHost = $_SERVER['HTTP_HOST'] ?? 'localhost'; + + // Determine environment + // CodeIgniter's test bootstrap sets $_SERVER['CI_ENVIRONMENT'] = 'testing' + // Check $_SERVER first, then $_ENV, then fall back to 'production' + $environment = $_SERVER['CI_ENVIRONMENT'] ?? $_ENV['CI_ENVIRONMENT'] ?? getenv('CI_ENVIRONMENT') ?: 'production'; + + if (empty($this->allowedHostnames)) { + $errorMessage = + 'Security: allowedHostnames is not configured. ' . + 'Host header injection protection is disabled. ' . + 'Set app.allowedHostnames in your .env file or ALLOWED_HOSTNAMES environment variable. ' . + 'Example: app.allowedHostnames = "example.com,www.example.com" ' . + 'Received Host: ' . $httpHost; + + // Production: Fail explicitly to prevent silent security vulnerabilities + // Testing and development: Allow localhost fallback + if ($environment === 'production') { + throw new \RuntimeException($errorMessage); + } + + log_message('error', $errorMessage . ' Using localhost fallback (development only).'); + return 'localhost'; + } + + if (in_array($httpHost, $this->allowedHostnames, true)) { + return $httpHost; + } + + // Host not in whitelist - use first configured hostname as fallback + log_message('warning', + 'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' . + 'Using fallback: ' . $this->allowedHostnames[0] + ); + + return $this->allowedHostnames[0]; + } + + private function getEnvString(string $key): ?string + { + $value = env($key); + + if (is_string($value) && trim($value) !== '') { + return $value; + } + + $raw = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key); + if (is_string($raw) && trim($raw) !== '') { + return $raw; + } + + return null; + } } diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 87c815f8f..3ad1105cb 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -17,8 +17,6 @@ use CodeIgniter\Config\AutoloadConfig; * * NOTE: This class is required prior to Autoloader instantiation, * and does not extend BaseConfig. - * - * @immutable */ class Autoload extends AutoloadConfig { @@ -41,8 +39,7 @@ class Autoload extends AutoloadConfig */ public $psr4 = [ APP_NAMESPACE => APPPATH, - 'Config' => APPPATH . 'Config', - 'dompdf' => APPPATH . 'ThirdParty/dompdf/src' + 'Config' => APPPATH . 'Config' ]; /** diff --git a/app/Config/Boot/testing.php b/app/Config/Boot/testing.php index 02fd04a2b..40b6ca83c 100644 --- a/app/Config/Boot/testing.php +++ b/app/Config/Boot/testing.php @@ -1,23 +1,38 @@ + * + * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect + */ + public array $shareConnectionOptions = [ + CURL_LOCK_DATA_CONNECT, + CURL_LOCK_DATA_DNS, + ]; + /** * -------------------------------------------------------------------------- * CURLRequest Share Options diff --git a/app/Config/Cache.php b/app/Config/Cache.php index a05ca78c1..0e0dfca9e 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -3,6 +3,7 @@ namespace Config; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Cache\Handlers\ApcuHandler; use CodeIgniter\Cache\Handlers\DummyHandler; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\Cache\Handlers\MemcachedHandler; @@ -78,7 +79,7 @@ class Cache extends BaseConfig * Your file storage preferences can be specified below, if you are using * the File driver. * - * @var array + * @var array{storePath?: string, mode?: int} */ public array $file = [ 'storePath' => WRITEPATH . 'cache/', @@ -95,7 +96,7 @@ class Cache extends BaseConfig * * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached * - * @var array + * @var array{host?: string, port?: int, weight?: int, raw?: bool} */ public array $memcached = [ 'host' => '127.0.0.1', @@ -108,17 +109,28 @@ class Cache extends BaseConfig * ------------------------------------------------------------------------- * Redis settings * ------------------------------------------------------------------------- + * * Your Redis server can be specified below, if you are using * the Redis or Predis drivers. * - * @var array + * @var array{ + * host?: string, + * password?: string|null, + * port?: int, + * timeout?: int, + * async?: bool, + * persistent?: bool, + * database?: int + * } */ public array $redis = [ - 'host' => '127.0.0.1', - 'password' => null, - 'port' => 6379, - 'timeout' => 0, - 'database' => 0, + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + 'async' => false, // specific to Predis and ignored by the native Redis extension + 'persistent' => false, + 'database' => 0, ]; /** @@ -132,6 +144,7 @@ class Cache extends BaseConfig * @var array> */ public array $validHandlers = [ + 'apcu' => ApcuHandler::class, 'dummy' => DummyHandler::class, 'file' => FileHandler::class, 'memcached' => MemcachedHandler::class, @@ -158,4 +171,28 @@ class Cache extends BaseConfig * @var bool|list */ public $cacheQueryString = false; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Status Codes + * -------------------------------------------------------------------------- + * + * HTTP status codes that are allowed to be cached. Only responses with + * these status codes will be cached by the PageCache filter. + * + * Default: [] - Cache all status codes (backward compatible) + * + * Recommended: [200] - Only cache successful responses + * + * You can also use status codes like: + * [200, 404, 410] - Cache successful responses and specific error codes + * [200, 201, 202, 203, 204] - All 2xx successful responses + * + * WARNING: Using [] may cache temporary error pages (404, 500, etc). + * Consider restricting to [200] for production applications to avoid + * caching errors that should be temporary. + * + * @var list + */ + public array $cacheStatusCodes = []; } diff --git a/app/Config/Constants.php b/app/Config/Constants.php index 83c74b400..ccd5296cf 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -84,9 +84,12 @@ defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest a /** * Global Constants. */ -const NEW_ENTRY = -1; -const ACTIVE = 0; -const DELETED = 1; +const NEW_ENTRY = -1; +const ACTIVE = 0; +const DELETED = 1; +const INSUFFICIENT_GIFTCARD_BALANCE = -2; +const INSUFFICIENT_REWARD_POINTS = -3; +const INSUFFICIENT_STOCK = -4; /** * Attribute Related Constants. diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php index 5a46774af..bdf7c2ba1 100644 --- a/app/Config/ContentSecurityPolicy.php +++ b/app/Config/ContentSecurityPolicy.php @@ -30,6 +30,11 @@ class ContentSecurityPolicy extends BaseConfig */ public ?string $reportURI = null; + /** + * Specifies a reporting endpoint to which violation reports ought to be sent. + */ + public ?string $reportTo = null; + /** * Instructs user agents to rewrite URL schemes, changing * HTTP to HTTPS. This directive is for websites with @@ -38,12 +43,12 @@ class ContentSecurityPolicy extends BaseConfig public bool $upgradeInsecureRequests = false; // ------------------------------------------------------------------------- - // Sources allowed + // CSP DIRECTIVES SETTINGS // NOTE: once you set a policy to 'none', it cannot be further restricted // ------------------------------------------------------------------------- /** - * Will default to self if not overridden + * Will default to `'self'` if not overridden * * @var list|string|null */ @@ -64,6 +69,21 @@ class ContentSecurityPolicy extends BaseConfig 'www.google.com www.gstatic.com' ]; + /** + * Specifies valid sources for JavaScript + false, 'selected_printer' => 'takings_printer']) ?>