mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
6 Commits
issue-4474
...
feature/tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80fdf2b7b5 | ||
|
|
6d02eb12c2 | ||
|
|
0c3574423c | ||
|
|
6fea9c3abd | ||
|
|
0547d3ba60 | ||
|
|
a18dde4bbe |
29
.env.example
29
.env.example
@@ -7,20 +7,31 @@ CI_ENVIRONMENT = production
|
||||
#--------------------------------------------------------------------
|
||||
# SECURITY: ALLOWED HOSTNAMES
|
||||
#--------------------------------------------------------------------
|
||||
# CRITICAL: Whitelist of allowed hostnames to prevent Host Header
|
||||
# IMPORTANT: 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.
|
||||
# If not configured, the application will default to 'localhost',
|
||||
# which may break functionality in production.
|
||||
#
|
||||
# Configure with comma-separated list of domains/subdomains:
|
||||
# app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||
# Configure this with all domains/subdomains that host your application:
|
||||
# - Primary domain
|
||||
# - WWW subdomain (if used)
|
||||
# - Any alternative domains
|
||||
#
|
||||
# For local development:
|
||||
# app.allowedHostnames = 'localhost'
|
||||
# Examples:
|
||||
# Single domain:
|
||||
# app.allowedHostnames.0 = 'example.com'
|
||||
#
|
||||
# Note: Do not include protocol (http/https) or port numbers.
|
||||
app.allowedHostnames = ''
|
||||
# Multiple domains:
|
||||
# app.allowedHostnames.0 = 'example.com'
|
||||
# app.allowedHostnames.1 = 'www.example.com'
|
||||
# app.allowedHostnames.2 = 'demo.opensourcepos.org'
|
||||
#
|
||||
# For localhost development:
|
||||
# app.allowedHostnames.0 = 'localhost'
|
||||
#
|
||||
# Note: Do not include the protocol (http/https) or port number.
|
||||
#app.allowedHostnames.0 = ''
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# DATABASE
|
||||
|
||||
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
@@ -1,187 +1,121 @@
|
||||
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
|
||||
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
|
||||
|
||||
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,136 +1,63 @@
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
14
.github/workflows/build-release.yml
vendored
14
.github/workflows/build-release.yml
vendored
@@ -2,6 +2,10 @@ name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
@@ -76,8 +80,8 @@ jobs:
|
||||
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 '/' '_')
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///')
|
||||
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
|
||||
@@ -153,9 +157,9 @@ jobs:
|
||||
- name: Determine Docker tags
|
||||
id: tags
|
||||
run: |
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '_')
|
||||
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '-')
|
||||
if [ "$BRANCH" = "master" ]; then
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:master" >> $GITHUB_OUTPUT
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:latest" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -211,4 +215,4 @@ jobs:
|
||||
prerelease: true
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.1'
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
|
||||
33
.github/workflows/opencode.yml
vendored
Normal file
33
.github/workflows/opencode.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
8
.github/workflows/php-linter.yml
vendored
8
.github/workflows/php-linter.yml
vendored
@@ -12,6 +12,14 @@ 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:
|
||||
|
||||
3
.github/workflows/phpunit.yml
vendored
3
.github/workflows/phpunit.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.1'
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
@@ -118,4 +119,4 @@ jobs:
|
||||
|
||||
- name: Stop MariaDB
|
||||
if: always()
|
||||
run: docker stop mysql && docker rm mysql
|
||||
run: docker stop mysql && docker rm mysql
|
||||
172
.github/workflows/release.yml
vendored
172
.github/workflows/release.yml
vendored
@@ -1,172 +0,0 @@
|
||||
name: Release Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- minor
|
||||
- major
|
||||
- patch
|
||||
default: 'minor'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: Prepare Release
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
CURRENT_VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
- name: Calculate new version
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION="${{ steps.current_version.outputs.current_version }}"
|
||||
VERSION_TYPE="${{ github.event.inputs.version_type }}"
|
||||
|
||||
# Parse current version
|
||||
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
|
||||
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
|
||||
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
|
||||
|
||||
# Bump version based on type
|
||||
case $VERSION_TYPE in
|
||||
major)
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
;;
|
||||
minor)
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
;;
|
||||
patch)
|
||||
PATCH=$((PATCH + 1))
|
||||
;;
|
||||
esac
|
||||
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "previous_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "New version: $NEW_VERSION (was: $CURRENT_VERSION, type: $VERSION_TYPE)"
|
||||
|
||||
- name: Update version in App.php
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
sed -i "s/public string \\\$application_version = '[^']*';/public string \\\$application_version = '$NEW_VERSION';/" app/Config/App.php
|
||||
echo "Updated app/Config/App.php"
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
sed -i "s/\"version\": \"[^\"]*\",/\"version\": \"$NEW_VERSION\",/" package.json
|
||||
echo "Updated package.json"
|
||||
|
||||
- name: Update version in docker-compose.nginx.yml
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
sed -i "s/jekkos\/opensourcepos:[^ ]*/jekkos\/opensourcepos:$NEW_VERSION/" docker-compose.nginx.yml
|
||||
echo "Updated docker-compose.nginx.yml"
|
||||
|
||||
- name: Update version in README.md
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
# Extract major.minor for the "latest X.Y version" text
|
||||
MAJOR_MINOR=$(echo "$NEW_VERSION" | cut -d. -f1,2)
|
||||
sed -i "s/The latest \`[0-9]*\.[0-9]*\` version/The latest \`${MAJOR_MINOR}\` version/" README.md
|
||||
echo "Updated README.md with version ${MAJOR_MINOR}"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
PREVIOUS_VERSION="${{ steps.version.outputs.previous_version }}"
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
|
||||
# Get commits since last version
|
||||
if git rev-parse "$PREVIOUS_VERSION" >/dev/null 2>&1; then
|
||||
COMMITS=$(git log "$PREVIOUS_VERSION"..HEAD --pretty=format:"- %s" --no-merges)
|
||||
else
|
||||
COMMITS=$(git log --pretty=format:"- %s" --no-merges -50)
|
||||
fi
|
||||
|
||||
# Create changelog entry
|
||||
CHANGELOG_FILE="CHANGELOG.md"
|
||||
|
||||
# Create the new version comparison link
|
||||
NEW_LINK="[${NEW_VERSION}]: https://github.com/opensourcepos/opensourcepos/compare/${PREVIOUS_VERSION}...${NEW_VERSION}"
|
||||
|
||||
# Insert new link after [unreleased] line
|
||||
sed -i "/^\[unreleased\]/a $NEW_LINK" "$CHANGELOG_FILE"
|
||||
|
||||
# Update [unreleased] link to start from new version
|
||||
sed -i "s|^\[unreleased\]: .*|\[unreleased\]: https://github.com/opensourcepos/opensourcepos/compare/${NEW_VERSION}...HEAD|" "$CHANGELOG_FILE"
|
||||
|
||||
# Create version header and content using temp file to avoid sed issues with special characters
|
||||
VERSION_DATE=$(date +%Y-%m-%d)
|
||||
VERSION_HEADER="## [$NEW_VERSION] - $VERSION_DATE"
|
||||
|
||||
# Create temp file with changelog entry
|
||||
TMP_FILE=$(mktemp)
|
||||
{
|
||||
echo ""
|
||||
echo "$VERSION_HEADER"
|
||||
echo ""
|
||||
echo "$COMMITS"
|
||||
} > "$TMP_FILE"
|
||||
|
||||
# Insert after Unreleased header
|
||||
sed -i "/^## \[Unreleased\]/r $TMP_FILE" "$CHANGELOG_FILE"
|
||||
rm "$TMP_FILE"
|
||||
|
||||
echo "Updated CHANGELOG.md"
|
||||
echo "Changelog entries:"
|
||||
echo "$COMMITS"
|
||||
|
||||
- name: Update version in issue templates
|
||||
run: |
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
|
||||
# Calculate version to remove (keep 5 versions)
|
||||
PREVIOUS_VERSION="${{ steps.version.outputs.previous_version }}"
|
||||
|
||||
# Bug report template - insert new version after development (unreleased)
|
||||
BUG_TEMPLATE=".github/ISSUE_TEMPLATE/bug report.yml"
|
||||
sed -i "/- development (unreleased)/a\\ - OpenSourcePOS ${NEW_VERSION}" "$BUG_TEMPLATE"
|
||||
# Remove the oldest version (5th version from the end)
|
||||
sed -i "/OpenSourcePOS 3\\.3\\.7/d" "$BUG_TEMPLATE"
|
||||
echo "Updated $BUG_TEMPLATE"
|
||||
|
||||
# Feature request template - insert new version after development (unreleased)
|
||||
FEATURE_TEMPLATE=".github/ISSUE_TEMPLATE/feature_request.yml"
|
||||
sed -i "/- development (unreleased)/a\\ - OpenSourcePOS ${NEW_VERSION}" "$FEATURE_TEMPLATE"
|
||||
# Remove the oldest version (5th version from the end)
|
||||
sed -i "/OpenSourcePOS 3\\.3\\.7/d" "$FEATURE_TEMPLATE"
|
||||
echo "Updated $FEATURE_TEMPLATE"
|
||||
|
||||
- name: Commit version bump
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||
|
||||
git add app/Config/App.php package.json docker-compose.nginx.yml CHANGELOG.md README.md .github/ISSUE_TEMPLATE/
|
||||
git commit -m "chore: release version $NEW_VERSION"
|
||||
git push origin HEAD
|
||||
72
.github/workflows/update-issue-templates.yml
vendored
Normal file
72
.github/workflows/update-issue-templates.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
3
Dockerfile.test
Normal file
3
Dockerfile.test
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM php:8.4-cli
|
||||
RUN apt-get update && apt-get install -y libicu-dev && docker-php-ext-install intl
|
||||
WORKDIR /app
|
||||
38
INSTALL.md
38
INSTALL.md
@@ -1,6 +1,6 @@
|
||||
## Server Requirements
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).
|
||||
@@ -8,36 +8,26 @@
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### Allowed Hostnames (REQUIRED for Production)
|
||||
### Allowed Hostnames (Required for Production)
|
||||
|
||||
⚠️ **CRITICAL**: OpenSourcePOS validates the Host header to prevent Host Header Injection attacks (GHSA-jchf-7hr6-h4f3). **You MUST configure `app.allowedHostnames` for production deployments. If not configured, the application will fail to start.**
|
||||
OpenSourcePOS validates the Host header against a whitelist to prevent Host Header Injection attacks (GHSA-jchf-7hr6-h4f3). **You must configure this for production deployments.**
|
||||
|
||||
**Add to your `.env` file:**
|
||||
Add the following to your `.env` file:
|
||||
|
||||
```bash
|
||||
# Comma-separated list of allowed hostnames (no protocols or ports)
|
||||
app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||
```
|
||||
app.allowedHostnames.0 = 'yourdomain.com'
|
||||
app.allowedHostnames.1 = 'www.yourdomain.com'
|
||||
```
|
||||
|
||||
**For local development:**
|
||||
|
||||
```bash
|
||||
app.allowedHostnames = 'localhost'
|
||||
**For local development**, use:
|
||||
```
|
||||
app.allowedHostnames.0 = '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 (GHSA-jchf-7hr6-h4f3)
|
||||
- Ensures URLs are generated with the correct domain
|
||||
- Security advisory: https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-jchf-7hr6-h4f3
|
||||
- Fixes issue #4480: .env configuration now works via comma-separated values
|
||||
If `allowedHostnames` is not configured:
|
||||
1. A security warning will be logged
|
||||
2. The application will fall back to 'localhost' as the hostname
|
||||
3. This means URLs generated by the application (links, redirects, etc.) will point to 'localhost'
|
||||
|
||||
### HTTPS Behind Proxy
|
||||
|
||||
|
||||
@@ -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.2` is required to run this app.
|
||||
- PHP `≥ 8.1` 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. Please star the project if you like it!
|
||||
If you like our project, please consider buying us a coffee through the button below so we can keep adding features.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MUN6AEG7NY6H8)\
|
||||
Or refer to the [FUNDING.yml](.github/FUNDING.yml) file.
|
||||
|
||||
@@ -55,13 +55,21 @@ class App extends BaseConfig
|
||||
public string $baseURL; // Defined in the constructor
|
||||
|
||||
/**
|
||||
* 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/':
|
||||
* ['media.example.com', 'accounts.example.com']
|
||||
* Allowed Hostnames for the Site URL.
|
||||
*
|
||||
* Security: This is used to validate the HTTP Host header to prevent
|
||||
* Host Header Injection attacks. If the Host header doesn't match
|
||||
* an entry in this list, the request will use the first allowed hostname.
|
||||
*
|
||||
* IMPORTANT: This MUST be configured for production deployments.
|
||||
* If empty, the application will fall back to 'localhost'.
|
||||
*
|
||||
* Configure via .env file:
|
||||
* app.allowedHostnames.0 = 'example.com'
|
||||
* app.allowedHostnames.1 = 'www.example.com'
|
||||
*
|
||||
* For local development:
|
||||
* app.allowedHostnames.0 = 'localhost'
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
@@ -117,7 +125,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,24 +286,13 @@ 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;
|
||||
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 function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
|
||||
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
|
||||
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||
if ($envAllowedHostnames !== false && trim($envAllowedHostnames) !== '') {
|
||||
$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 .= '://' . $host . '/';
|
||||
@@ -304,40 +301,23 @@ class App extends BaseConfig
|
||||
|
||||
/**
|
||||
* Validates and returns a trusted hostname.
|
||||
*
|
||||
*
|
||||
* Security: Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
|
||||
* 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 =
|
||||
log_message('warning',
|
||||
'Security: allowedHostnames is not configured. ' .
|
||||
'Host header injection protection is disabled. ' .
|
||||
'Set app.allowedHostnames in your .env file. ' .
|
||||
'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).');
|
||||
'Please set app.allowedHostnames in your .env file. ' .
|
||||
'Received Host: ' . $httpHost
|
||||
);
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
@@ -345,8 +325,7 @@ class App extends BaseConfig
|
||||
return $httpHost;
|
||||
}
|
||||
|
||||
// Host not in whitelist - use first configured hostname as fallback
|
||||
log_message('warning',
|
||||
log_message('warning',
|
||||
'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' .
|
||||
'Using fallback: ' . $this->allowedHostnames[0]
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ use CodeIgniter\Config\AutoloadConfig;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Autoload extends AutoloadConfig
|
||||
{
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* The environment testing is reserved for PHPUnit testing. It has special
|
||||
* conditions built into the framework at various places to assist with that.
|
||||
* You can’t use it for your development.
|
||||
*/
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ERROR DISPLAY
|
||||
| ERROR DISPLAY
|
||||
|--------------------------------------------------------------------------
|
||||
| In development, we want to show as many errors as possible to help
|
||||
| make sure they don't make it to production. And save us hours of
|
||||
| painful debugging.
|
||||
*/
|
||||
*/
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG BACKTRACES
|
||||
| DEBUG BACKTRACES
|
||||
|--------------------------------------------------------------------------
|
||||
| If true, this constant will tell the error screens to display debug
|
||||
| backtraces along with the other error information. If you would
|
||||
| prefer to not see this, set this value to false.
|
||||
*/
|
||||
*/
|
||||
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG MODE
|
||||
| DEBUG MODE
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug mode is an experimental flag that can allow changes throughout
|
||||
| the system. It's not widely used currently, and may not survive
|
||||
| release of the framework.
|
||||
*/
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
*/
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
@@ -6,22 +6,6 @@ use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class CURLRequest extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* CURLRequest Share Connection Options
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Share connection options between requests.
|
||||
*
|
||||
* @var list<int>
|
||||
*
|
||||
* @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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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;
|
||||
@@ -79,7 +78,7 @@ class Cache extends BaseConfig
|
||||
* Your file storage preferences can be specified below, if you are using
|
||||
* the File driver.
|
||||
*
|
||||
* @var array{storePath?: string, mode?: int}
|
||||
* @var array<string, int|string|null>
|
||||
*/
|
||||
public array $file = [
|
||||
'storePath' => WRITEPATH . 'cache/',
|
||||
@@ -96,7 +95,7 @@ class Cache extends BaseConfig
|
||||
*
|
||||
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||
*
|
||||
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
|
||||
* @var array<string, bool|int|string>
|
||||
*/
|
||||
public array $memcached = [
|
||||
'host' => '127.0.0.1',
|
||||
@@ -109,28 +108,17 @@ class Cache extends BaseConfig
|
||||
* -------------------------------------------------------------------------
|
||||
* Redis settings
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Your Redis server can be specified below, if you are using
|
||||
* the Redis or Predis drivers.
|
||||
*
|
||||
* @var array{
|
||||
* host?: string,
|
||||
* password?: string|null,
|
||||
* port?: int,
|
||||
* timeout?: int,
|
||||
* async?: bool,
|
||||
* persistent?: bool,
|
||||
* database?: int
|
||||
* }
|
||||
* @var array<string, int|string|null>
|
||||
*/
|
||||
public array $redis = [
|
||||
'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,
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'database' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -144,7 +132,6 @@ class Cache extends BaseConfig
|
||||
* @var array<string, class-string<CacheInterface>>
|
||||
*/
|
||||
public array $validHandlers = [
|
||||
'apcu' => ApcuHandler::class,
|
||||
'dummy' => DummyHandler::class,
|
||||
'file' => FileHandler::class,
|
||||
'memcached' => MemcachedHandler::class,
|
||||
@@ -171,28 +158,4 @@ class Cache extends BaseConfig
|
||||
* @var bool|list<string>
|
||||
*/
|
||||
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<int>
|
||||
*/
|
||||
public array $cacheStatusCodes = [];
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ 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
|
||||
@@ -43,12 +38,12 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
public bool $upgradeInsecureRequests = false;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CSP DIRECTIVES SETTINGS
|
||||
// Sources allowed
|
||||
// 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>|string|null
|
||||
*/
|
||||
@@ -69,21 +64,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
'www.google.com www.gstatic.com'
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies valid sources for JavaScript <script> elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $scriptSrcElem = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for JavaScript inline event
|
||||
* handlers and JavaScript URLs.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $scriptSrcAttr = 'self';
|
||||
|
||||
/**
|
||||
* Lists allowed stylesheets' URLs.
|
||||
*
|
||||
@@ -96,21 +76,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
'https://fonts.googleapis.com',
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies valid sources for stylesheets <link> elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $styleSrcElem = 'self';
|
||||
|
||||
/**
|
||||
* Specifies valid sources for stylesheets inline
|
||||
* style attributes and `<style>` elements.
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $styleSrcAttr = 'self';
|
||||
|
||||
/**
|
||||
* Defines the origins from which images can be loaded.
|
||||
*
|
||||
@@ -204,11 +169,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
*/
|
||||
public $manifestSrc;
|
||||
|
||||
/**
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $workerSrc = [];
|
||||
|
||||
/**
|
||||
* Limits the kinds of plugins a page may invoke.
|
||||
*
|
||||
@@ -224,17 +184,17 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
public $sandbox;
|
||||
|
||||
/**
|
||||
* Nonce placeholder for style tags.
|
||||
* Nonce tag for style
|
||||
*/
|
||||
public string $styleNonceTag = '{csp-style-nonce}';
|
||||
|
||||
/**
|
||||
* Nonce placeholder for script tags.
|
||||
* Nonce tag for script
|
||||
*/
|
||||
public string $scriptNonceTag = '{csp-script-nonce}';
|
||||
|
||||
/**
|
||||
* Replace nonce tag automatically?
|
||||
* Replace nonce tag automatically
|
||||
*/
|
||||
public bool $autoNonce = true;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class Cookie extends BaseConfig
|
||||
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
||||
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
||||
*
|
||||
* @var ''|'Lax'|'None'|'Strict'
|
||||
* @phpstan-var 'None'|'Lax'|'Strict'|''
|
||||
*/
|
||||
public string $samesite = 'Lax';
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ class Database extends Config
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'numberNative' => false,
|
||||
'foundRows' => false,
|
||||
'dateFormat' => [
|
||||
'date' => 'Y-m-d',
|
||||
'datetime' => 'Y-m-d H:i:s',
|
||||
@@ -57,27 +55,26 @@ class Database extends Config
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public array $tests = [
|
||||
'DSN' => '',
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'admin',
|
||||
'password' => 'pointofsale',
|
||||
'database' => 'ospos',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => 'ospos_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => (ENVIRONMENT !== 'production'),
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_general_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'foreignKeys' => true,
|
||||
'busyTimeout' => 1000,
|
||||
'synchronous' => null,
|
||||
'dateFormat' => [
|
||||
'DSN' => '',
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'admin',
|
||||
'password' => 'pointofsale',
|
||||
'database' => 'ospos',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => 'ospos_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => (ENVIRONMENT !== 'production'),
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_general_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'foreignKeys' => true,
|
||||
'busyTimeout' => 1000,
|
||||
'dateFormat' => [
|
||||
'date' => 'Y-m-d',
|
||||
'datetime' => 'Y-m-d H:i:s',
|
||||
'time' => 'H:i:s',
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
class DocTypes
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -30,11 +30,6 @@ class Email extends BaseConfig
|
||||
*/
|
||||
public string $SMTPHost = 'mail.mxserver.com';
|
||||
|
||||
/**
|
||||
* Which SMTP authentication method to use: login, plain
|
||||
*/
|
||||
public string $SMTPAuthMethod = 'login';
|
||||
|
||||
/**
|
||||
* SMTP Username
|
||||
*/
|
||||
|
||||
@@ -23,23 +23,6 @@ class Encryption extends BaseConfig
|
||||
*/
|
||||
public string $key = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Previous Encryption Keys
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* When rotating encryption keys, add old keys here to maintain ability
|
||||
* to decrypt data encrypted with previous keys. Encryption always uses
|
||||
* the current $key. Decryption tries current key first, then falls back
|
||||
* to previous keys if decryption fails.
|
||||
*
|
||||
* In .env file, use comma-separated string:
|
||||
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
|
||||
*
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $previousKeys = '';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Encryption Driver to Use
|
||||
|
||||
@@ -65,15 +65,12 @@ class Filters extends BaseFilters
|
||||
* List of filter aliases that are always
|
||||
* applied before and after every request.
|
||||
*
|
||||
* @var array{
|
||||
* before: array<string, array{except: list<string>|string}>|list<string>,
|
||||
* after: array<string, array{except: list<string>|string}>|list<string>
|
||||
* }
|
||||
* @var array<string, array<string, array<string, string>>>|array<string, list<string>>
|
||||
*/
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'honeypot',
|
||||
'csrf' => ['except' => 'login|migrate'],
|
||||
'csrf' => ['except' => 'login'],
|
||||
'invalidchars',
|
||||
],
|
||||
'after' => [
|
||||
@@ -103,7 +100,7 @@ class Filters extends BaseFilters
|
||||
* before or after URI patterns.
|
||||
*
|
||||
* Example:
|
||||
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||
* isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
|
||||
@@ -61,13 +61,4 @@ class Format extends BaseConfig
|
||||
'application/xml' => 0,
|
||||
'text/xml' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Maximum depth for JSON encoding.
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* This value determines how deep the JSON encoder will traverse nested structures.
|
||||
*/
|
||||
public int $jsonEncodeDepth = 512;
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
class Hostnames
|
||||
{
|
||||
// List of known two-part TLDs for subdomain extraction
|
||||
public const TWO_PART_TLDS = [
|
||||
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
|
||||
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
|
||||
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
|
||||
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
|
||||
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
|
||||
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
|
||||
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
|
||||
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
|
||||
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
|
||||
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
|
||||
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
|
||||
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
|
||||
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
|
||||
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
|
||||
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
|
||||
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
|
||||
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
|
||||
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
|
||||
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
|
||||
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
|
||||
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
|
||||
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
|
||||
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
|
||||
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
|
||||
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
|
||||
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
|
||||
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
|
||||
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
|
||||
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
|
||||
];
|
||||
}
|
||||
@@ -16,8 +16,6 @@ class Images extends BaseConfig
|
||||
/**
|
||||
* The path to the image library.
|
||||
* Required for ImageMagick, GraphicsMagick, or NetPBM.
|
||||
*
|
||||
* @deprecated 4.7.0 No longer used.
|
||||
*/
|
||||
public string $libraryPath = '/usr/local/bin/convert';
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Log\Handlers\FileHandler;
|
||||
use CodeIgniter\Log\Handlers\HandlerInterface;
|
||||
|
||||
class Logger extends BaseConfig
|
||||
{
|
||||
@@ -74,7 +73,7 @@ class Logger extends BaseConfig
|
||||
* Handlers are executed in the order defined in this array, starting with
|
||||
* the handler on top and continuing down.
|
||||
*
|
||||
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
|
||||
* @var array<class-string, array<string, int|list<string>|string>>
|
||||
*/
|
||||
public array $handlers = [
|
||||
/*
|
||||
|
||||
@@ -47,19 +47,4 @@ class Migrations extends BaseConfig
|
||||
* - Y_m_d_His_
|
||||
*/
|
||||
public string $timestampFormat = 'YmdHis_';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Enable/Disable Migration Lock
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* Locking is disabled by default.
|
||||
*
|
||||
* When enabled, it will prevent multiple migration processes
|
||||
* from running at the same time by using a lock mechanism.
|
||||
*
|
||||
* This is useful in production environments to avoid conflicts
|
||||
* or race conditions during concurrent deployments.
|
||||
*/
|
||||
public bool $lock = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* Mimes
|
||||
*
|
||||
* This file contains an array of mime types. It is used by the
|
||||
* Upload class to help identify allowed file types.
|
||||
*
|
||||
@@ -13,6 +15,8 @@ namespace Config;
|
||||
*
|
||||
* When working with mime types, please make sure you have the ´fileinfo´
|
||||
* extension enabled to reliably detect the media types.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Mimes
|
||||
{
|
||||
@@ -478,16 +482,13 @@ class Mimes
|
||||
'application/sla',
|
||||
'application/vnd.ms-pki.stl',
|
||||
'application/x-navistyle',
|
||||
'model/stl',
|
||||
'application/octet-stream',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Attempts to determine the best mime type for the given file extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* @return array|string|null The mime type found, or none if unable to determine.
|
||||
* @return string|null The mime type found, or none if unable to determine.
|
||||
*/
|
||||
public static function guessTypeFromExtension(string $extension): array|string|null
|
||||
{
|
||||
@@ -523,7 +524,7 @@ class Mimes
|
||||
}
|
||||
|
||||
// Reverse check the mime type list if no extension was proposed.
|
||||
// This search is order-sensitive!
|
||||
// This search is order sensitive!
|
||||
foreach (static::$mimes as $ext => $types) {
|
||||
if (in_array($type, (array) $types, true)) {
|
||||
return $ext;
|
||||
|
||||
@@ -9,6 +9,8 @@ use CodeIgniter\Modules\Modules as BaseModules;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Modules extends BaseModules
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Config;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* This class holds the configuration options stored from the database so that on launch those settings can be cached
|
||||
@@ -35,21 +34,11 @@ class OSPOS extends BaseConfig
|
||||
if ($cache) {
|
||||
$this->settings = decode_array($cache);
|
||||
} else {
|
||||
try {
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (DatabaseException $e) {
|
||||
// Database table doesn't exist yet (migrations haven't run)
|
||||
// Return empty settings to allow migration page to display
|
||||
$this->settings = [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home'
|
||||
];
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +50,4 @@ class OSPOS extends BaseConfig
|
||||
$this->cache->delete('settings');
|
||||
$this->set_settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Config;
|
||||
* NOTE: This class does not extend BaseConfig for performance reasons.
|
||||
* So you cannot replace the property values with Environment Variables.
|
||||
*
|
||||
* WARNING: Do not use these options when running the app in the Worker Mode.
|
||||
* @immutable
|
||||
*/
|
||||
class Optimize
|
||||
{
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Config;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Paths
|
||||
{
|
||||
@@ -75,16 +77,4 @@ class Paths
|
||||
* is used when no value is provided to `Services::renderer()`.
|
||||
*/
|
||||
public string $viewDirectory = __DIR__ . '/../Views';
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* ENVIRONMENT DIRECTORY NAME
|
||||
* ---------------------------------------------------------------
|
||||
*
|
||||
* This variable must contain the name of the directory where
|
||||
* the .env file is located.
|
||||
* Please consider security implications when changing this
|
||||
* value - the directory should not be publicly accessible.
|
||||
*/
|
||||
public string $envDirectory = __DIR__ . '/../../';
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ $routes->setDefaultController('Login');
|
||||
$routes->get('/', 'Login::index');
|
||||
$routes->get('login', 'Login::index');
|
||||
$routes->post('login', 'Login::index');
|
||||
$routes->post('migrate', 'Login::migrate');
|
||||
|
||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
@@ -96,15 +96,6 @@ class Routing extends BaseRouting
|
||||
*/
|
||||
public bool $autoRoute = true;
|
||||
|
||||
/**
|
||||
* If TRUE, the system will look for attributes on controller
|
||||
* class and methods that can run before and after the
|
||||
* controller/method.
|
||||
*
|
||||
* If FALSE, will ignore any attributes.
|
||||
*/
|
||||
public bool $useControllerAttributes = true;
|
||||
|
||||
/**
|
||||
* For Defined Routes.
|
||||
* If TRUE, will enable the use of the 'prioritize' option
|
||||
|
||||
@@ -13,9 +13,9 @@ class Security extends BaseConfig
|
||||
*
|
||||
* Protection Method for Cross Site Request Forgery protection.
|
||||
*
|
||||
* @var string 'cookie' or 'session'
|
||||
* @var string|false 'cookie', 'session', or false
|
||||
*/
|
||||
public string $csrfProtection = 'session';
|
||||
public string|false $csrfProtection = 'session';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Config;
|
||||
|
||||
use App\Libraries\MY_Language;
|
||||
use Locale;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
@@ -39,11 +38,9 @@ class Services extends BaseService
|
||||
/**
|
||||
* Responsible for loading the language string translations.
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @param bool $getShared
|
||||
* @return MY_Language
|
||||
*/
|
||||
public static function language(?string $locale = null, bool $getShared = true): MY_Language
|
||||
public static function language(?string $locale = null, bool $getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('language', $locale)->setLocale($locale);
|
||||
@@ -58,12 +55,12 @@ class Services extends BaseService
|
||||
// Use '?:' for empty string check
|
||||
$locale = $locale ?: $requestLocale;
|
||||
|
||||
return new MY_Language($locale);
|
||||
return new \App\Libraries\MY_Language($locale);
|
||||
}
|
||||
|
||||
private static HTMLPurifier $htmlPurifier;
|
||||
private static $htmlPurifier;
|
||||
|
||||
public static function htmlPurifier($getShared = true): object
|
||||
public static function htmlPurifier($getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('htmlPurifier');
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Session\Handlers\BaseHandler;
|
||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||
use CodeIgniter\Session\Handlers\FileHandler;
|
||||
|
||||
class Session extends BaseConfig
|
||||
{
|
||||
@@ -126,23 +124,4 @@ class Session extends BaseConfig
|
||||
* seconds.
|
||||
*/
|
||||
public int $lockMaxRetries = 300;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if ($this->driver === DatabaseHandler::class) {
|
||||
try {
|
||||
$db = Database::connect();
|
||||
|
||||
if (!$db->tableExists($this->savePath)) {
|
||||
$this->driver = FileHandler::class;
|
||||
$this->savePath = WRITEPATH . 'session';
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
$this->driver = FileHandler::class;
|
||||
$this->savePath = WRITEPATH . 'session';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,29 +119,4 @@ class Toolbar extends BaseConfig
|
||||
public array $watchedExtensions = [
|
||||
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
|
||||
];
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Ignored HTTP Headers
|
||||
* --------------------------------------------------------------------------
|
||||
*
|
||||
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
|
||||
* HTML response. This is correct for full page loads, but it breaks requests
|
||||
* that expect only a clean HTML fragment.
|
||||
*
|
||||
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
|
||||
* manage navigation on the client side. Injecting the Debug Toolbar into their
|
||||
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
|
||||
* (such as infinite loops or "Maximum call stack size exceeded").
|
||||
*
|
||||
* Any request containing one of the following headers is treated as a
|
||||
* client-managed or partial request, and the Debug Toolbar injection is skipped.
|
||||
*
|
||||
* @var array<string, string|null>
|
||||
*/
|
||||
public array $disableOnHeaders = [
|
||||
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
|
||||
'HX-Request' => 'true', // HTMX requests
|
||||
'X-Up-Version' => null, // Unpoly partial requests
|
||||
];
|
||||
}
|
||||
|
||||
@@ -230,13 +230,9 @@ class UserAgents extends BaseConfig
|
||||
*/
|
||||
public array $robots = [
|
||||
'googlebot' => 'Googlebot',
|
||||
'google-pagerenderer' => 'Google Page Renderer',
|
||||
'google-read-aloud' => 'Google Read Aloud',
|
||||
'google-safety' => 'Google Safety Bot',
|
||||
'msnbot' => 'MSNBot',
|
||||
'baiduspider' => 'Baiduspider',
|
||||
'bingbot' => 'Bing',
|
||||
'bingpreview' => 'BingPreview',
|
||||
'slurp' => 'Inktomi Slurp',
|
||||
'yahoo' => 'Yahoo',
|
||||
'ask jeeves' => 'Ask Jeeves',
|
||||
@@ -252,11 +248,5 @@ class UserAgents extends BaseConfig
|
||||
'ia_archiver' => 'Alexa Crawler',
|
||||
'MJ12bot' => 'Majestic-12',
|
||||
'Uptimebot' => 'Uptimebot',
|
||||
'duckduckbot' => 'DuckDuckBot',
|
||||
'sogou' => 'Sogou Spider',
|
||||
'exabot' => 'Exabot',
|
||||
'bot' => 'Generic Bot',
|
||||
'crawler' => 'Generic Crawler',
|
||||
'spider' => 'Generic Spider',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -135,19 +135,4 @@ class OSPOSRules
|
||||
{
|
||||
return parse_decimals($candidate) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a locale-aware decimal value is non-negative (>= 0).
|
||||
*
|
||||
* @param string $candidate
|
||||
* @param string|null $error
|
||||
* @return bool
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function nonNegativeDecimal(string $candidate, ?string &$error = null): bool
|
||||
{
|
||||
$value = parse_decimals($candidate);
|
||||
|
||||
return $value !== false && $value >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,21 +59,4 @@ class View extends BaseView
|
||||
* @var list<class-string<ViewDecoratorInterface>>
|
||||
*/
|
||||
public array $decorators = [];
|
||||
|
||||
/**
|
||||
* Subdirectory within app/Views for namespaced view overrides.
|
||||
*
|
||||
* Namespaced views will be searched in:
|
||||
*
|
||||
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
|
||||
*
|
||||
* This allows application-level overrides for package or module views
|
||||
* without modifying vendor source files.
|
||||
*
|
||||
* Examples:
|
||||
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
|
||||
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
|
||||
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
|
||||
*/
|
||||
public string $appOverridesFolder = 'overrides';
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* This configuration controls how CodeIgniter behaves when running
|
||||
* in worker mode (with FrankenPHP).
|
||||
*/
|
||||
class WorkerMode
|
||||
{
|
||||
/**
|
||||
* Persistent Services
|
||||
*
|
||||
* List of service names that should persist across requests.
|
||||
* These services will NOT be reset between requests.
|
||||
*
|
||||
* Services not in this list will be reset for each request to prevent
|
||||
* state leakage.
|
||||
*
|
||||
* Recommended persistent services:
|
||||
* - `autoloader`: PSR-4 autoloading configuration
|
||||
* - `locator`: File locator
|
||||
* - `exceptions`: Exception handler
|
||||
* - `commands`: CLI commands registry
|
||||
* - `codeigniter`: Main application instance
|
||||
* - `superglobals`: Superglobals wrapper
|
||||
* - `routes`: Router configuration
|
||||
* - `cache`: Cache instance
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public array $persistentServices = [
|
||||
'autoloader',
|
||||
'locator',
|
||||
'exceptions',
|
||||
'commands',
|
||||
'codeigniter',
|
||||
'superglobals',
|
||||
'routes',
|
||||
'cache',
|
||||
];
|
||||
|
||||
/**
|
||||
* Reset Event Listeners
|
||||
*
|
||||
* List of event names whose listeners should be removed between requests.
|
||||
* Use this if you register event listeners inside other event callbacks
|
||||
* (rather than at the top level of Config/Events.php), which would cause
|
||||
* them to accumulate across requests in worker mode.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public array $resetEventListeners = [];
|
||||
|
||||
/**
|
||||
* Force Garbage Collection
|
||||
*
|
||||
* Whether to force garbage collection after each request.
|
||||
* Helps prevent memory leaks at a small performance cost.
|
||||
*/
|
||||
public bool $forceGarbageCollection = true;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ class Attributes extends Secure_Controller
|
||||
|
||||
$definition_name = $definition_data['definition_name'];
|
||||
|
||||
if ($this->attribute->saveDefinition($definition_data, $definition_id)) {
|
||||
if ($this->attribute->save_definition($definition_data, $definition_id)) {
|
||||
// New definition
|
||||
if ($definition_id == NO_DEFINITION_ID) {
|
||||
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));
|
||||
|
||||
@@ -3,46 +3,56 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\CLIRequest;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
* BaseController provides a convenient place for loading components
|
||||
* and performing functions that are needed by all your controllers.
|
||||
*
|
||||
* Extend this class in any new controllers:
|
||||
* ```
|
||||
* class Home extends BaseController
|
||||
* ```
|
||||
*
|
||||
* For security, be sure to declare any new methods as protected or private.
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
*/
|
||||
abstract class BaseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Instance of the main Request object.
|
||||
*
|
||||
* @var CLIRequest|IncomingRequest
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* An array of helpers to be loaded automatically upon
|
||||
* class instantiation. These helpers will be available
|
||||
* to all other controllers that extend BaseController.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $helpers = [];
|
||||
|
||||
/**
|
||||
* Be sure to declare properties for any property fetch you initialized.
|
||||
* The creation of dynamic property is deprecated in PHP 8.2.
|
||||
*/
|
||||
|
||||
// protected $session;
|
||||
|
||||
/**
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param LoggerInterface $logger
|
||||
* @return void
|
||||
*/
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||||
{
|
||||
// Load here all helpers you want to be available in your controllers that extend BaseController.
|
||||
// Caution: Do not put the this below the parent::initController() call below.
|
||||
// $this->helpers = ['form', 'url'];
|
||||
|
||||
// Caution: Do not edit this line.
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
||||
// Preload any models, libraries, etc, here.
|
||||
// $this->session = service('session');
|
||||
|
||||
// E.g.: $this->session = service('session');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveGeneral(): ResponseInterface
|
||||
{
|
||||
$batchSaveData = [
|
||||
$batch_save_data = [
|
||||
'theme' => $this->request->getPost('theme'),
|
||||
'login_form' => $this->request->getPost('login_form'),
|
||||
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
|
||||
@@ -398,19 +398,19 @@ class Config extends Secure_Controller
|
||||
|
||||
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
|
||||
|
||||
if ($batchSaveData['category_dropdown']) {
|
||||
$definitionData['definition_name'] = 'ospos_category';
|
||||
$definitionData['definition_flags'] = 0;
|
||||
$definitionData['definition_type'] = 'DROPDOWN';
|
||||
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||
$definitionData['deleted'] = 0;
|
||||
if ($batch_save_data['category_dropdown'] == 1) {
|
||||
$definition_data['definition_name'] = 'ospos_category';
|
||||
$definition_data['definition_flags'] = 0;
|
||||
$definition_data['definition_type'] = 'DROPDOWN';
|
||||
$definition_data['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||
$definition_data['deleted'] = 0;
|
||||
|
||||
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
|
||||
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
|
||||
$this->attribute->save_definition($definition_data, CATEGORY_DEFINITION_ID);
|
||||
} elseif ($batch_save_data['category_dropdown'] == NO_DEFINITION_ID) {
|
||||
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||
}
|
||||
|
||||
$success = $this->appconfig->batch_save($batchSaveData);
|
||||
$success = $this->appconfig->batch_save($batch_save_data);
|
||||
|
||||
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
||||
}
|
||||
@@ -504,24 +504,9 @@ class Config extends Secure_Controller
|
||||
$password = $this->encrypter->encrypt($this->request->getPost('smtp_pass'));
|
||||
}
|
||||
|
||||
$protocol = $this->request->getPost('protocol');
|
||||
$mailpath = $this->request->getPost('mailpath');
|
||||
|
||||
// Validate mailpath: required for sendmail, optional for others but must be safe if provided
|
||||
$isMailpathRequired = ($protocol === 'sendmail');
|
||||
$isMailpathProvided = !empty($mailpath);
|
||||
$isMailpathValid = $isMailpathProvided && preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $mailpath);
|
||||
|
||||
if (($isMailpathRequired && !$isMailpathProvided) || ($isMailpathProvided && !$isMailpathValid)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Config.mailpath_invalid')
|
||||
]);
|
||||
}
|
||||
|
||||
$batch_save_data = [
|
||||
'protocol' => $protocol,
|
||||
'mailpath' => $mailpath,
|
||||
'protocol' => $this->request->getPost('protocol'),
|
||||
'mailpath' => $this->request->getPost('mailpath'),
|
||||
'smtp_host' => $this->request->getPost('smtp_host'),
|
||||
'smtp_user' => $this->request->getPost('smtp_user'),
|
||||
'smtp_pass' => $password,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\MY_Migration;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
@@ -35,12 +34,12 @@ class Home extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the "change employee password" form
|
||||
* Load "change employee password" form
|
||||
*
|
||||
* @param int $employeeId
|
||||
* @return ResponseInterface|string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY)
|
||||
{
|
||||
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
|
||||
$currentPersonId = $loggedInEmployee->person_id;
|
||||
@@ -82,7 +81,7 @@ class Home extends Secure_Controller
|
||||
if ($this->employee->check_password($this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS), $this->request->getPost('current_password'))) {
|
||||
// Validate password length BEFORE hashing
|
||||
$new_password = $this->request->getPost('password');
|
||||
|
||||
|
||||
if (strlen($new_password) < 8) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
@@ -90,7 +89,7 @@ class Home extends Secure_Controller
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$employee_data = [
|
||||
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'password' => password_hash($new_password, PASSWORD_DEFAULT),
|
||||
@@ -125,4 +124,4 @@ class Home extends Secure_Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Controllers;
|
||||
|
||||
use App\Libraries\Barcode_lib;
|
||||
use App\Libraries\Item_lib;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\Item;
|
||||
@@ -13,6 +14,7 @@ use App\Models\Item_taxes;
|
||||
use App\Models\Stock_location;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Tax_category;
|
||||
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Images\Handlers\BaseHandler;
|
||||
use CodeIgniter\HTTP\DownloadResponse;
|
||||
@@ -71,7 +73,7 @@ class Items extends Secure_Controller
|
||||
$this->session->set('allow_temp_items', 0);
|
||||
|
||||
$data['table_headers'] = get_items_manage_table_headers();
|
||||
|
||||
|
||||
// Restore stock_location from URL or session
|
||||
$stockLocation = $this->request->getGet('stock_location', FILTER_SANITIZE_NUMBER_INT);
|
||||
$data['stock_location'] = $stockLocation
|
||||
@@ -506,7 +508,7 @@ class Items extends Secure_Controller
|
||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||
|
||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||
$values = &$data['definition_values'][$definition_id];
|
||||
$values['attribute_id'] = $attribute_id;
|
||||
@@ -542,7 +544,7 @@ class Items extends Secure_Controller
|
||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||
|
||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||
$values = &$data['definition_values'][$definition_id];
|
||||
$values['attribute_id'] = $attribute_id;
|
||||
@@ -663,13 +665,10 @@ class Items extends Secure_Controller
|
||||
|
||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
|
||||
// Wrap the entire save sequence in a single transaction for atomicity
|
||||
$this->db->transBegin();
|
||||
if ($this->item->save_value($item_data, $item_id)) {
|
||||
$success = true;
|
||||
$new_item = false;
|
||||
|
||||
$success = $this->item->save_value($item_data, $item_id);
|
||||
$new_item = false;
|
||||
|
||||
if ($success) {
|
||||
if ($item_id === NEW_ENTRY) {
|
||||
$item_id = $item_data['item_id'];
|
||||
$new_item = true;
|
||||
@@ -693,7 +692,7 @@ class Items extends Secure_Controller
|
||||
|
||||
$tax_name_index++;
|
||||
}
|
||||
$success = $success && $this->item_taxes->save_value($items_taxes_data, $item_id);
|
||||
$success &= $this->item_taxes->save_value($items_taxes_data, $item_id);
|
||||
}
|
||||
|
||||
// Save item quantity
|
||||
@@ -714,7 +713,7 @@ class Items extends Secure_Controller
|
||||
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
|
||||
|
||||
if ($item_quantity->quantity != $updated_quantity || $new_item) {
|
||||
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
||||
$success &= $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
||||
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
@@ -725,25 +724,25 @@ class Items extends Secure_Controller
|
||||
'trans_inventory' => $updated_quantity - $item_quantity->quantity
|
||||
];
|
||||
|
||||
$success = $success && $this->inventory->insert($inv_data, false);
|
||||
$success &= $this->inventory->insert($inv_data, false);
|
||||
}
|
||||
}
|
||||
$success = $success && $this->saveItemAttributes($item_id);
|
||||
$this->saveItemAttributes($item_id);
|
||||
|
||||
if ($success && $upload_success) {
|
||||
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
||||
|
||||
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $item_id]);
|
||||
} else {
|
||||
$message = $upload_success ? lang('Items.error_adding_updating') . ' ' . $item_data['name'] : strip_tags($upload_data['error']);
|
||||
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $item_id]);
|
||||
}
|
||||
} else {
|
||||
$message = lang('Items.error_adding_updating') . ' ' . $item_data['name'];
|
||||
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => NEW_ENTRY]);
|
||||
}
|
||||
|
||||
// Check all success conditions before committing
|
||||
if ($success && $upload_success) {
|
||||
$this->db->transCommit();
|
||||
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
||||
|
||||
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $item_id]);
|
||||
}
|
||||
|
||||
// Rollback on failure
|
||||
$this->db->transRollback();
|
||||
$message = $upload_success ? lang('Items.error_adding_updating') . ' ' . $item_data['name'] : strip_tags($upload_data['error']);
|
||||
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $item_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -778,7 +777,7 @@ class Items extends Secure_Controller
|
||||
|
||||
$filename = $file->getClientName();
|
||||
$info = pathinfo($filename);
|
||||
|
||||
|
||||
// Sanitize filename to remove problematic characters like spaces
|
||||
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
|
||||
|
||||
@@ -941,7 +940,7 @@ class Items extends Secure_Controller
|
||||
*/
|
||||
public function getGenerateCsvFile(): DownloadResponse
|
||||
{
|
||||
helper('importfile');
|
||||
helper('importfile_helper');
|
||||
$name = 'import_items.csv';
|
||||
$allowed_locations = $this->stock_location->get_allowed_locations();
|
||||
$allowed_attributes = $this->attribute->get_definition_names();
|
||||
@@ -960,13 +959,14 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports items from a CSV formatted file.
|
||||
* Imports items from CSV formatted file.
|
||||
* @return ResponseInterface
|
||||
* @throws ReflectionException
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postImportCsvFile(): ResponseInterface
|
||||
{
|
||||
helper('importfile');
|
||||
helper('importfile_helper');
|
||||
try {
|
||||
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
|
||||
@@ -975,33 +975,33 @@ class Items extends Secure_Controller
|
||||
set_time_limit(240);
|
||||
|
||||
$failCodes = [];
|
||||
$csvRows = get_csv_file($_FILES['file_path']['tmp_name']);
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||
$attributeDefinitionNames = $this->attribute->get_definition_names();
|
||||
$csv_rows = get_csv_file($_FILES['file_path']['tmp_name']);
|
||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$allowed_stock_locations = $this->stock_location->get_allowed_locations();
|
||||
$attribute_definition_names = $this->attribute->get_definition_names();
|
||||
|
||||
unset($attributeDefinitionNames[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
||||
unset($attribute_definition_names[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
||||
|
||||
$attributeData = [];
|
||||
$attribute_data = [];
|
||||
|
||||
foreach ($attributeDefinitionNames as $definitionName) {
|
||||
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
|
||||
foreach ($attribute_definition_names as $definition_name) {
|
||||
$attribute_data[$definition_name] = $this->attribute->get_definition_by_name($definition_name)[0];
|
||||
|
||||
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
|
||||
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
|
||||
if ($attribute_data[$definition_name]['definition_type'] === DROPDOWN) {
|
||||
$attribute_data[$definition_name]['dropdown_values'] = $this->attribute->get_definition_values($attribute_data[$definition_name]['definition_id']);
|
||||
}
|
||||
}
|
||||
$db = db_connect();
|
||||
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
|
||||
|
||||
foreach ($csvRows as $key => $row) {
|
||||
$isFailedRow = false;
|
||||
$itemId = (int)$row['Id'];
|
||||
$isUpdate = ($itemId > 0);
|
||||
$itemData = [
|
||||
'item_id' => $itemId,
|
||||
foreach ($csv_rows as $key => $row) {
|
||||
$is_failed_row = false;
|
||||
$item_id = (int)$row['Id'];
|
||||
$is_update = ($item_id > 0);
|
||||
$item_data = [
|
||||
'item_id' => $item_id,
|
||||
'name' => $row['Item Name'],
|
||||
'description' => filter_var($row['Description'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'description' => $row['Description'],
|
||||
'category' => $row['Category'],
|
||||
'cost_price' => $row['Cost Price'],
|
||||
'unit_price' => $row['Unit Price'],
|
||||
@@ -1011,26 +1011,25 @@ class Items extends Secure_Controller
|
||||
'pic_filename' => $row['Image']
|
||||
];
|
||||
|
||||
if (!empty($row['Supplier ID'])) {
|
||||
$itemData['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
||||
if (!empty($row['supplier ID'])) {
|
||||
$item_data['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
||||
}
|
||||
|
||||
if ($isUpdate) {
|
||||
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? null : $row['Allow Alt Description'];
|
||||
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? null : $row['Item has Serial Number'];
|
||||
if ($is_update) {
|
||||
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? null : $row['Allow Alt Description'];
|
||||
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? null : $row['Item has Serial Number'];
|
||||
} else {
|
||||
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? '0' : '1';
|
||||
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? '0' : '1';
|
||||
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? '0' : '1';
|
||||
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? '0' : '1';
|
||||
}
|
||||
|
||||
if (!empty($row['Barcode'])) {
|
||||
$itemData['item_number'] = $row['Barcode'];
|
||||
$isFailedRow = $this->item->item_number_exists($itemData['item_number'], $itemId);
|
||||
if (!empty($row['Barcode']) && !$is_update) {
|
||||
$item_data['item_number'] = $row['Barcode'];
|
||||
$is_failed_row = $this->item->item_number_exists($item_data['item_number']);
|
||||
}
|
||||
|
||||
if (!$isFailedRow) {
|
||||
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||
$isFailedRow = $this->validateCSVData($row, $itemData, $allowedStockLocations, $attributeDefinitionNames, $attributeData);
|
||||
if (!$is_failed_row) {
|
||||
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
|
||||
if (!empty($invalidLocations)) {
|
||||
$isFailedRow = true;
|
||||
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
|
||||
@@ -1038,35 +1037,28 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
// Remove false, null, '' and empty strings but keep 0
|
||||
$itemData = array_filter($itemData, function ($value) {
|
||||
$item_data = array_filter($item_data, function ($value) {
|
||||
return $value !== null && strlen($value);
|
||||
});
|
||||
|
||||
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
|
||||
$this->save_tax_data($row, $itemData);
|
||||
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||
$csvAttributeValues = $this->extractAttributeData($row);
|
||||
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
|
||||
if ($isFailedRow) {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
|
||||
continue;
|
||||
}
|
||||
if (!$is_failed_row && $this->item->save_value($item_data, $item_id)) {
|
||||
$this->save_tax_data($row, $item_data);
|
||||
$this->save_inventory_quantities($row, $item_data, $allowed_stock_locations, $employee_id);
|
||||
$is_failed_row = $this->save_attribute_data($row, $item_data, $attribute_data); // TODO: $is_failed_row never gets used after this.
|
||||
|
||||
if ($isUpdate) {
|
||||
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
|
||||
if ($is_update) {
|
||||
$item_data = array_merge($item_data, get_object_vars($this->item->get_info_by_id_or_number($item_id)));
|
||||
}
|
||||
} else {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow. This item was not imported.");
|
||||
$failed_row = $key + 2;
|
||||
$failCodes[] = $failed_row;
|
||||
log_message('error', "CSV Item import failed on line $failed_row. This item was not imported.");
|
||||
}
|
||||
|
||||
unset($csvRows[$key]);
|
||||
unset($csv_rows[$key]);
|
||||
}
|
||||
|
||||
$csvRows = null;
|
||||
$csv_rows = null;
|
||||
|
||||
if (count($failCodes) > 0) {
|
||||
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
|
||||
@@ -1074,7 +1066,6 @@ class Items extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message]);
|
||||
} else {
|
||||
$db->transCommit();
|
||||
$this->attribute->deleteOrphanedValues();
|
||||
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
|
||||
}
|
||||
@@ -1088,20 +1079,6 @@ class Items extends Secure_Controller
|
||||
|
||||
}
|
||||
|
||||
private function extractAttributeData(array $row): array
|
||||
{
|
||||
$attributeData = [];
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
if (str_starts_with($key, 'attribute_')) {
|
||||
$definitionName = substr($key, 10);
|
||||
$attributeData[$definitionName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $attributeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that stock location columns in CSV row are valid locations
|
||||
*
|
||||
@@ -1130,99 +1107,87 @@ class Items extends Secure_Controller
|
||||
* Checks the entire line of data in an import file for errors
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $itemData
|
||||
* @param array $allowedStockLocations
|
||||
* @param array $definitionNames
|
||||
* @param array $attributeData
|
||||
* @param array $item_data
|
||||
* @param array $allowed_locations
|
||||
* @param array $definition_names
|
||||
* @param array $attribute_data
|
||||
* @return bool Returns false if all data checks out and true when there is an error in the data
|
||||
*/
|
||||
private function validateCSVData(array $row, array $itemData, array $allowedStockLocations, array $definitionNames, array $attributeData): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
|
||||
private function data_error_check(array $row, array $item_data, array $allowed_locations, array $definition_names, array $attribute_data): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
|
||||
{
|
||||
$itemId = $row['Id'];
|
||||
$isUpdate = (bool)$itemId;
|
||||
$item_id = $row['Id'];
|
||||
$is_update = (bool)$item_id;
|
||||
|
||||
// Check for empty required fields
|
||||
$valuesToCheckForEmpty = [
|
||||
'name' => $itemData['name'],
|
||||
'category' => $itemData['category'],
|
||||
'unit_price' => $itemData['unit_price']
|
||||
$check_for_empty = [
|
||||
'name' => $item_data['name'],
|
||||
'category' => $item_data['category'],
|
||||
'unit_price' => $item_data['unit_price']
|
||||
];
|
||||
|
||||
foreach ($valuesToCheckForEmpty as $key => $value) {
|
||||
if (($value === null || $value === '') && !$isUpdate) {
|
||||
foreach ($check_for_empty as $key => $val) {
|
||||
if (empty($val) && !$is_update) {
|
||||
log_message('error', "Empty required value in $key.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isUpdate) {
|
||||
$itemData['cost_price'] = empty($itemData['cost_price']) ? 0 : $itemData['cost_price']; // Allow for zero wholesale price
|
||||
if (!$is_update) {
|
||||
$item_data['cost_price'] = empty($item_data['cost_price']) ? 0 : $item_data['cost_price']; // Allow for zero wholesale price
|
||||
} else {
|
||||
if (!$this->item->exists($itemId)) {
|
||||
log_message('error', "non-existent item_id: '$itemId' when either existing item_id or no item_id is required.");
|
||||
if (!$this->item->exists($item_id)) {
|
||||
log_message('error', "non-existent item_id: '$item_id' when either existing item_id or no item_id is required.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Build array of fields to check for numerics
|
||||
$valuesToCheckForNumeric = [
|
||||
'cost_price' => $itemData['cost_price'],
|
||||
'unit_price' => $itemData['unit_price'],
|
||||
'reorder_level' => $itemData['reorder_level'],
|
||||
$check_for_numeric_values = [
|
||||
'cost_price' => $item_data['cost_price'],
|
||||
'unit_price' => $item_data['unit_price'],
|
||||
'reorder_level' => $item_data['reorder_level'],
|
||||
'supplier_id' => $row['Supplier ID'],
|
||||
'Tax 1 Percent' => $row['Tax 1 Percent'],
|
||||
'Tax 2 Percent' => $row['Tax 2 Percent']
|
||||
];
|
||||
|
||||
foreach ($allowedStockLocations as $location_name) {
|
||||
$valuesToCheckForNumeric[] = $row["location_$location_name"];
|
||||
foreach ($allowed_locations as $location_name) {
|
||||
$check_for_numeric_values[] = $row["location_$location_name"];
|
||||
}
|
||||
|
||||
// Check for non-numeric values which require numeric
|
||||
foreach ($valuesToCheckForNumeric as $key => $value) {
|
||||
foreach ($check_for_numeric_values as $key => $value) {
|
||||
if (!is_numeric($value) && !empty($value)) {
|
||||
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check stock locations
|
||||
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
|
||||
if (!empty($invalidLocations)) {
|
||||
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check Attribute Data
|
||||
foreach ($definitionNames as $definitionName) {
|
||||
$attributeColumn = "attribute_$definitionName";
|
||||
if (array_key_exists($attributeColumn, $row) && $row[$attributeColumn] != '') {
|
||||
$definitionType = $attributeData[$definitionName]['definition_type'];
|
||||
$attributeValue = $row[$attributeColumn];
|
||||
foreach ($definition_names as $definition_name) {
|
||||
if (!empty($row["attribute_$definition_name"])) {
|
||||
$definition_type = $attribute_data[$definition_name]['definition_type'];
|
||||
$attribute_value = $row["attribute_$definition_name"];
|
||||
|
||||
if (strcasecmp($attributeValue, '_DELETE_') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($definitionType) {
|
||||
switch ($definition_type) {
|
||||
case DROPDOWN:
|
||||
$dropdownValues = $attributeData[$definitionName]['dropdown_values'];
|
||||
$dropdownValues[] = '';
|
||||
$dropdown_values = $attribute_data[$definition_name]['dropdown_values'];
|
||||
$dropdown_values[] = '';
|
||||
|
||||
if (!empty($attributeValue) && !in_array($attributeValue, $dropdownValues)) {
|
||||
log_message('error', "Value: '$attributeValue' is not an acceptable DROPDOWN value");
|
||||
if (!empty($attribute_value) && !in_array($attribute_value, $dropdown_values)) {
|
||||
log_message('error', "Value: '$attribute_value' is not an acceptable DROPDOWN value");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case DECIMAL:
|
||||
if (!is_numeric($attributeValue) && !empty($attributeValue)) {
|
||||
log_message('error', "'$attributeValue' is not an acceptable DECIMAL value");
|
||||
if (!is_numeric($attribute_value) && !empty($attribute_value)) {
|
||||
log_message('error', "'$attribute_value' is not an acceptable DECIMAL value");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case DATE:
|
||||
if (!valid_date($attributeValue) && !empty($attributeValue)) {
|
||||
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
|
||||
if (!valid_date($attribute_value) && !empty($attribute_value)) {
|
||||
log_message('error', "'$attribute_value' is not an acceptable DATE value. The value must match the set locale.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@@ -1233,6 +1198,59 @@ class Items extends Secure_Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves attribute data found in the CSV import.
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $item_data
|
||||
* @param array $definitions
|
||||
* @return bool
|
||||
*/
|
||||
private function save_attribute_data(array $row, array $item_data, array $definitions): bool
|
||||
{
|
||||
foreach ($definitions as $definition) {
|
||||
$attribute_name = $definition['definition_name'];
|
||||
$attribute_value = $row["attribute_$attribute_name"];
|
||||
|
||||
// Create attribute value
|
||||
if (!empty($attribute_value) || $attribute_value === '0') {
|
||||
if ($definition['definition_type'] === CHECKBOX) {
|
||||
$checkbox_is_unchecked = (strcasecmp($attribute_value, 'false') === 0 || $attribute_value === '0');
|
||||
$attribute_value = $checkbox_is_unchecked ? '0' : '1';
|
||||
|
||||
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
|
||||
} elseif (!empty($attribute_value)) {
|
||||
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$attribute_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the attribute_value and attribute_link if necessary
|
||||
*/
|
||||
private function store_attribute_value(string $value, array $attribute_data, int $item_id)
|
||||
{
|
||||
$attribute_id = $this->attribute->attributeValueExists($value, $attribute_data['definition_type']);
|
||||
|
||||
$this->attribute->deleteAttributeLinks($item_id, $attribute_data['definition_id']);
|
||||
|
||||
if (!$attribute_id) {
|
||||
$attribute_id = $this->attribute->saveAttributeValue($value, $attribute_data['definition_id'], $item_id, false, $attribute_data['definition_type']);
|
||||
} elseif (!$this->attribute->saveAttributeLink($item_id, $attribute_data['definition_id'], $attribute_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $attribute_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves inventory quantities for the row in the appropriate stock locations.
|
||||
*
|
||||
@@ -1326,11 +1344,10 @@ class Items extends Secure_Controller
|
||||
* Saves item attributes for a given item.
|
||||
*
|
||||
* @param int $itemId The item for which attributes need to be saved to.
|
||||
* @return bool Returns true when item attributes are successfully saved and false on error.
|
||||
* @return void
|
||||
*/
|
||||
public function saveItemAttributes(int $itemId): bool
|
||||
public function saveItemAttributes(int $itemId): void
|
||||
{
|
||||
$success = true;
|
||||
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
|
||||
$attributeIds = $this->request->getPost('attribute_ids');
|
||||
|
||||
@@ -1342,18 +1359,16 @@ class Items extends Secure_Controller
|
||||
switch ($definitionType) {
|
||||
case DROPDOWN:
|
||||
$attributeId = $attributeValue;
|
||||
$success = $success && $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
||||
break;
|
||||
case DECIMAL:
|
||||
$attributeValue = parse_decimals($attributeValue);
|
||||
// no break
|
||||
// Fall through to save the attribute value
|
||||
default:
|
||||
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
|
||||
$success = $success && ($attributeId > 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $success && $this->attribute->deleteOrphanedValues();
|
||||
$this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Controllers;
|
||||
use App\Libraries\MY_Migration;
|
||||
use App\Models\Employee;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Model;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -37,7 +36,6 @@ class Login extends BaseController
|
||||
|
||||
$data = [
|
||||
'has_errors' => false,
|
||||
'is_new_install' => !(MY_Migration::get_current_version()),
|
||||
'is_latest' => $migration->is_latest(),
|
||||
'latest_version' => $migration->get_latest_migration(),
|
||||
'gcaptcha_enabled' => $gcaptcha_enabled,
|
||||
@@ -73,28 +71,4 @@ class Login extends BaseController
|
||||
|
||||
return redirect()->to('home');
|
||||
}
|
||||
|
||||
public function migrate(): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$migration = new MY_Migration(config('Migrations'));
|
||||
$migration->migrate_to_ci4();
|
||||
|
||||
set_time_limit(3600);
|
||||
$migration->setNamespace('App')->latest();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => 'Migration completed successfully'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Migration failed: ' . $e->getMessage());
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage()
|
||||
])->setStatusCode(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +190,11 @@ class Receivings extends Secure_Controller
|
||||
/**
|
||||
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
|
||||
*
|
||||
* @param int|string|null $item_id
|
||||
* @param string|int|null $item_id
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postEditItem(int|string|null $item_id): string
|
||||
public function postEditItem($item_id): string
|
||||
{
|
||||
$data = [];
|
||||
|
||||
@@ -242,7 +242,7 @@ class Receivings extends Secure_Controller
|
||||
}
|
||||
|
||||
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
|
||||
|
||||
|
||||
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
|
||||
|
||||
@@ -280,10 +280,8 @@ class Receivings extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $receiving_id
|
||||
* @param bool $update_inventory
|
||||
* @return ResponseInterface
|
||||
* @throws ReflectionException
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
|
||||
{
|
||||
|
||||
@@ -425,7 +425,7 @@ class Sales extends Secure_Controller
|
||||
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
|
||||
$new_giftcard_value = max($new_giftcard_value, 0);
|
||||
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
|
||||
$new_giftcard_value = to_currency($new_giftcard_value);
|
||||
$new_giftcard_value = str_replace('$', '\$', to_currency($new_giftcard_value));
|
||||
$data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]);
|
||||
$amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num));
|
||||
|
||||
@@ -582,21 +582,12 @@ class Sales extends Secure_Controller
|
||||
$data = [];
|
||||
|
||||
$rules = [
|
||||
'price' => 'trim|required|decimal_locale|nonNegativeDecimal',
|
||||
'price' => 'trim|required|decimal_locale',
|
||||
'quantity' => 'trim|required|decimal_locale',
|
||||
'discount' => 'trim|permit_empty|decimal_locale|nonNegativeDecimal',
|
||||
'discount' => 'trim|permit_empty|decimal_locale',
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'price' => [
|
||||
'nonNegativeDecimal' => lang('Sales.negative_price_invalid'),
|
||||
],
|
||||
'discount' => [
|
||||
'nonNegativeDecimal' => lang('Sales.negative_discount_invalid'),
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->validate($rules, $messages)) {
|
||||
if ($this->validate($rules)) {
|
||||
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$price = parse_decimals($this->request->getPost('price'));
|
||||
@@ -605,38 +596,20 @@ class Sales extends Secure_Controller
|
||||
$discount = $discount_type
|
||||
? parse_quantity($this->request->getPost('discount'))
|
||||
: parse_decimals($this->request->getPost('discount'));
|
||||
$discount = $discount ?: 0;
|
||||
|
||||
// Return mode legitimately uses negative quantities for refunds
|
||||
if ($this->sale_lib->get_mode() != 'return' && $quantity < 0) {
|
||||
$data['error'] = lang('Sales.negative_quantity_invalid');
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
// Business logic: discount bounds depend on discount_type and item values
|
||||
if ($discount_type == PERCENT && $discount > 100) {
|
||||
$data['error'] = lang('Sales.discount_percent_exceeds_100');
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
if ($discount_type == FIXED && bccomp((string)$discount, bcmul((string)abs($quantity), (string)$price, 2), 2) > 0) {
|
||||
$data['error'] = lang('Sales.discount_exceeds_item_total');
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
$item_location = $this->request->getPost('location', FILTER_SANITIZE_NUMBER_INT);
|
||||
$discounted_total = $this->request->getPost('discounted_total') != ''
|
||||
? parse_decimals($this->request->getPost('discounted_total') ?? '')
|
||||
: null;
|
||||
|
||||
|
||||
$this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total);
|
||||
|
||||
$this->sale_lib->empty_payments();
|
||||
|
||||
$data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location);
|
||||
} else {
|
||||
$errors = $this->validator->getErrors();
|
||||
$data['error'] = $errors ? reset($errors) : lang('Sales.error_editing_item');
|
||||
$data['error'] = lang('Sales.error_editing_item');
|
||||
}
|
||||
|
||||
return $this->_reload($data);
|
||||
@@ -750,12 +723,6 @@ class Sales extends Secure_Controller
|
||||
$data['cash_amount_due'] = $totals['cash_amount_due'];
|
||||
$data['non_cash_amount_due'] = $totals['amount_due'];
|
||||
|
||||
// Prevent negative total sales (fraud/theft vector) - returns can have negative totals for legitimate refunds
|
||||
if ($this->sale_lib->get_mode() != 'return' && bccomp($totals['total'], '0') < 0) {
|
||||
$data['error'] = lang('Sales.negative_total_invalid');
|
||||
return $this->_reload($data);
|
||||
}
|
||||
|
||||
if ($data['cash_mode']) { // TODO: Convert this to ternary notation
|
||||
$data['amount_due'] = $totals['cash_amount_due'];
|
||||
} else {
|
||||
@@ -796,7 +763,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
|
||||
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['invoice_number'] = $invoice_number;
|
||||
$data['sale_status'] = COMPLETED;
|
||||
@@ -817,11 +784,10 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/' . $invoice_view, $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
} elseif ($this->sale_lib->is_work_order_mode()) {
|
||||
@@ -841,7 +807,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
|
||||
$data['error'] = lang('Sales.work_order_number_duplicate');
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['work_order_number'] = $work_order_number;
|
||||
$data['sale_status'] = SUSPENDED;
|
||||
@@ -854,8 +820,9 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/work_order', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} elseif ($this->sale_lib->is_quote_mode()) {
|
||||
$data['sales_quote'] = lang('Sales.quote');
|
||||
@@ -869,7 +836,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
|
||||
$data['error'] = lang('Sales.quote_number_duplicate');
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['quote_number'] = $quote_number;
|
||||
$data['sale_status'] = SUSPENDED;
|
||||
@@ -881,8 +848,9 @@ class Sales extends Secure_Controller
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/quote', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} else {
|
||||
// Save the data to the sales table
|
||||
@@ -901,11 +869,10 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1695,11 +1662,10 @@ class Sales extends Secure_Controller
|
||||
$this->item->update_item_number($item_id, $item_number);
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['item_number'] = $item_number;
|
||||
}
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1718,12 +1684,11 @@ class Sales extends Secure_Controller
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['name'] = $name;
|
||||
}
|
||||
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1742,12 +1707,11 @@ class Sales extends Secure_Controller
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['description'] = $description;
|
||||
}
|
||||
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ class Tax_categories extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_categories_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_category_id');
|
||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$tax_categories = $this->tax_category->search($search, $limit, $offset, $sort, $order);
|
||||
|
||||
@@ -50,7 +50,7 @@ class Tax_codes extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_code_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_code');
|
||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$tax_codes = $this->tax_code->search($search, $limit, $offset, $sort, $order);
|
||||
|
||||
@@ -43,7 +43,7 @@ class Tax_jurisdictions extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_jurisdictions_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'jurisdiction_id');
|
||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$tax_jurisdictions = $this->tax_jurisdiction->search($search, $limit, $offset, $sort, $order);
|
||||
|
||||
@@ -81,7 +81,7 @@ class Taxes extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_rates_manage_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_rate_id');
|
||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$tax_rates = $this->tax->search($search, $limit, $offset, $sort, $order);
|
||||
|
||||
@@ -20,7 +20,7 @@ class Migration_database_optimizations extends Migration
|
||||
|
||||
$attribute = model(Attribute::class);
|
||||
|
||||
$attribute->deleteOrphanedValues();
|
||||
$attribute->delete_orphaned_values();
|
||||
|
||||
$this->migrate_duplicate_attribute_values(DECIMAL);
|
||||
$this->migrate_duplicate_attribute_values(DATE);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Seeds;
|
||||
|
||||
use CodeIgniter\Database\Seeder;
|
||||
use Config\Database;
|
||||
|
||||
class TestDatabaseBootstrapSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
throw new \RuntimeException('TestDatabaseBootstrapSeeder can only run in the testing environment.');
|
||||
}
|
||||
|
||||
$config = config('Database');
|
||||
$group = $config->tests;
|
||||
$dbName = $group['database'];
|
||||
|
||||
if ($dbName === '' || !str_contains(strtolower($dbName), 'test')) {
|
||||
throw new \RuntimeException("Refusing to reset non-test database: {$dbName}");
|
||||
}
|
||||
|
||||
$serverConn = Database::connect([
|
||||
'hostname' => $group['hostname'],
|
||||
'username' => $group['username'],
|
||||
'password' => $group['password'],
|
||||
'DBDriver' => $group['DBDriver'],
|
||||
'database' => null,
|
||||
'charset' => $group['charset'] ?? 'utf8mb4',
|
||||
'DBCollat' => $group['DBCollat'] ?? 'utf8mb4_general_ci',
|
||||
], false);
|
||||
|
||||
$serverConn->query("DROP DATABASE IF EXISTS `{$dbName}`");
|
||||
$serverConn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}`");
|
||||
}
|
||||
}
|
||||
@@ -36,26 +36,21 @@ class Db_log
|
||||
private function generate_message(): string
|
||||
{
|
||||
$db = Database::connect();
|
||||
$lastQuery = $db->getLastQuery();
|
||||
|
||||
if ($lastQuery === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$affectedRows = $db->affectedRows();
|
||||
$executionTime = $this->convert_time($lastQuery->getDuration());
|
||||
$last_query = $db->getLastQuery();
|
||||
$affected_rows = $db->affectedRows();
|
||||
$execution_time = $this->convert_time($last_query->getDuration());
|
||||
|
||||
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
|
||||
. "\n" . $lastQuery->getQuery()
|
||||
. "\n Affected rows: $affectedRows"
|
||||
. "\n Execution Time: " . $executionTime['time'] . ' ' . $executionTime['unit'];
|
||||
. "\n" . $last_query->getQuery()
|
||||
. "\n Affected rows: $affected_rows"
|
||||
. "\n Execution Time: " . $execution_time['time'] . ' ' . $execution_time['unit'];
|
||||
|
||||
$longQuery = ($executionTime['unit'] === 's') && ($executionTime['time'] > 0.5);
|
||||
if ($longQuery) {
|
||||
$long_query = ($execution_time['unit'] === 's') && ($execution_time['time'] > 0.5);
|
||||
if ($long_query) {
|
||||
$message .= ' [LONG RUNNING QUERY]';
|
||||
}
|
||||
|
||||
return $this->config->db_log_only_long && !$longQuery ? '' : $message;
|
||||
return $this->config->db_log_only_long && !$long_query ? '' : $message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Events;
|
||||
|
||||
use App\Libraries\MY_Migration;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||
use CodeIgniter\Session\Handlers\FileHandler;
|
||||
use CodeIgniter\Session\Session;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -21,47 +19,38 @@ class Load_config
|
||||
{
|
||||
public Session $session;
|
||||
|
||||
/**
|
||||
* Loads configuration from database into App CI config and then applies those settings
|
||||
*/
|
||||
public function load_config(): void
|
||||
{
|
||||
// Migrations
|
||||
$migration_config = config('Migrations');
|
||||
$migration = new MY_Migration($migration_config);
|
||||
|
||||
$this->session = session();
|
||||
|
||||
// Database Configuration
|
||||
$config = config(OSPOS::class);
|
||||
|
||||
if (!$migration->is_latest()) {
|
||||
$this->session->destroy();
|
||||
}
|
||||
|
||||
$this->setDefaultLanguage($config);
|
||||
// Language
|
||||
$language_exists = file_exists('../app/Language/' . current_language_code());
|
||||
|
||||
if (current_language_code() == null || current_language() == null || !$language_exists) { // TODO: current_language() is undefined
|
||||
$config->settings['language'] = 'english';
|
||||
$config->settings['language_code'] = 'en';
|
||||
}
|
||||
|
||||
$language = Services::language();
|
||||
$language->setLocale(current_language_code());
|
||||
$language->setLocale($config->settings['language_code']);
|
||||
|
||||
// Time Zone
|
||||
date_default_timezone_set($config->settings['timezone'] ?? ini_get('date.timezone'));
|
||||
|
||||
bcscale(max(2, totals_decimals() + tax_decimals()));
|
||||
}
|
||||
|
||||
private function setDefaultLanguage(OSPOS $config): void
|
||||
{
|
||||
$languageCode = $config->settings['language_code'] ?? null;
|
||||
|
||||
if (empty($config->settings) || $languageCode === null) {
|
||||
$config->settings['language'] = 'english';
|
||||
$config->settings['language_code'] = 'en';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->languageExists($languageCode)) {
|
||||
$config->settings['language'] = 'english';
|
||||
$config->settings['language_code'] = 'en';
|
||||
}
|
||||
}
|
||||
|
||||
private function languageExists(string $languageCode): bool
|
||||
{
|
||||
return file_exists(APPPATH . 'Language/' . $languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Translates the attribute type to the corresponding database column name.
|
||||
*
|
||||
* Maps attribute type constants to their corresponding attribute_values table columns.
|
||||
* Defaults to 'attribute_value' for TEXT, DROPDOWN and CHECKBOX attribute types.
|
||||
*
|
||||
* @param string $input The attribute type constant (DATE, DECIMAL, etc.)
|
||||
* @return string The database column name for storing this attribute type
|
||||
*/
|
||||
function getAttributeDataType(string $input): string
|
||||
{
|
||||
$columnMap = [
|
||||
DATE => 'attribute_date',
|
||||
DECIMAL => 'attribute_decimal',
|
||||
];
|
||||
|
||||
return $columnMap[$input] ?? 'attribute_value';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the provided data type is an allowed attribute value type.
|
||||
*
|
||||
* @param string $dataType
|
||||
* @return void
|
||||
*/
|
||||
function validateAttributeValueType(string $dataType): void
|
||||
{
|
||||
$attributeValueTypes = ['attribute_value', 'attribute_decimal', 'attribute_date'];
|
||||
|
||||
if (!in_array($dataType, $attributeValueTypes, true)) {
|
||||
throw new InvalidArgumentException('Invalid data type');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @param array $stock_locations
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
*/
|
||||
|
||||
function generate_import_items_csv(array $stock_locations, array $attributes): string
|
||||
{
|
||||
$csv_headers = pack('CCC', 0xef, 0xbb, 0xbf); // Encode the Byte-Order Mark (BOM) so that UTF-8 File headers display properly in Microsoft Excel
|
||||
|
||||
@@ -22,7 +22,9 @@ function current_language_code(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config->language_code ?? DEFAULT_LANGUAGE_CODE;
|
||||
$language_code = $config['language_code'];
|
||||
|
||||
return empty($language_code) ? DEFAULT_LANGUAGE_CODE : $language_code;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +45,9 @@ function current_language(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config->language ?? DEFAULT_LANGUAGE_CODE;
|
||||
$language = $config['language'];
|
||||
|
||||
return empty($language) ? DEFAULT_LANGUAGE : $language;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,54 +11,56 @@ function check_encryption(): bool
|
||||
$old_key = config('Encryption')->key;
|
||||
|
||||
if ((empty($old_key)) || (strlen($old_key) < 64)) {
|
||||
// Create Key
|
||||
$encryption = new Encryption();
|
||||
$key = bin2hex($encryption->createKey());
|
||||
config('Encryption')->key = $key;
|
||||
|
||||
// Write to .env
|
||||
$config_path = ROOTPATH . '.env';
|
||||
$new_config_path = WRITEPATH . '/backup/.env';
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
|
||||
$backup_folder = WRITEPATH . '/backup';
|
||||
|
||||
if (!file_exists($backup_folder)) {
|
||||
@mkdir($backup_folder, 0750, true);
|
||||
if (!file_exists($backup_folder) && !mkdir($backup_folder)) {
|
||||
log_message('error', 'Could not create backup folder');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($config_path)) {
|
||||
$example_path = ROOTPATH . '.env.example';
|
||||
if (file_exists($example_path)) {
|
||||
@copy($example_path, $config_path);
|
||||
} else {
|
||||
@file_put_contents($config_path, "# OSPOS Configuration\n\n");
|
||||
}
|
||||
@chmod($config_path, 0640);
|
||||
if (!copy($config_path, $backup_path)) {
|
||||
log_message('error', "Unable to copy $config_path to $backup_path");
|
||||
}
|
||||
|
||||
if (file_exists($config_path)) {
|
||||
@copy($config_path, $backup_path);
|
||||
@chmod($backup_path, 0640);
|
||||
@chmod($config_path, 0640);
|
||||
// Copy to backup
|
||||
@chmod($config_path, 0660);
|
||||
@chmod($backup_path, 0660);
|
||||
|
||||
$config_file = file_get_contents($config_path);
|
||||
$config_file = file_get_contents($config_path);
|
||||
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
|
||||
|
||||
if (strpos($config_file, 'encryption.key') !== false) {
|
||||
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
|
||||
} else {
|
||||
$config_file .= "\nencryption.key = '$key'\n";
|
||||
}
|
||||
|
||||
if (!empty($old_key)) {
|
||||
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
||||
$insertion_point = stripos($config_file, 'encryption.key');
|
||||
if ($insertion_point !== false) {
|
||||
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@file_put_contents($config_path, $config_file);
|
||||
@chmod($config_path, 0640);
|
||||
|
||||
log_message('info', "Updated encryption key in $config_path");
|
||||
if (!empty($old_key)) {
|
||||
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
||||
$insertion_point = stripos($config_file, 'encryption.key');
|
||||
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
||||
}
|
||||
|
||||
$handle = @fopen($config_path, 'w+');
|
||||
|
||||
if (empty($handle)) {
|
||||
log_message('error', "Unable to open $config_path for updating");
|
||||
return false;
|
||||
}
|
||||
|
||||
@chmod($config_path, 0660);
|
||||
$write_failed = !fwrite($handle, $config_file);
|
||||
fclose($handle);
|
||||
|
||||
if ($write_failed) {
|
||||
log_message('error', "Unable to write to $config_path for updating.");
|
||||
return false;
|
||||
}
|
||||
log_message('info', "File $config_path has been updated.");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -72,14 +74,23 @@ function abort_encryption_conversion(): void
|
||||
$config_path = ROOTPATH . '.env';
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
|
||||
if (!file_exists($backup_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($config_path, 0640);
|
||||
$config_file = file_get_contents($backup_path);
|
||||
@file_put_contents($config_path, $config_file);
|
||||
log_message('info', "Restored $config_path from backup");
|
||||
|
||||
$handle = @fopen($config_path, 'w+');
|
||||
|
||||
if (empty($handle)) {
|
||||
log_message('error', "Unable to open $config_path to undo encryption conversion");
|
||||
} else {
|
||||
@chmod($config_path, 0660);
|
||||
$write_failed = !fwrite($handle, $config_file);
|
||||
fclose($handle);
|
||||
|
||||
if ($write_failed) {
|
||||
log_message('error', "Unable to write to $config_path to undo encryption conversion.");
|
||||
return;
|
||||
}
|
||||
log_message('info', "File $config_path has been updated to undo encryption conversion");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,9 +99,13 @@ function abort_encryption_conversion(): void
|
||||
function remove_backup(): void
|
||||
{
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
if (!file_exists($backup_path)) {
|
||||
if (! file_exists($backup_path)) {
|
||||
return;
|
||||
}
|
||||
@unlink($backup_path);
|
||||
log_message('info', "Removed $backup_path");
|
||||
if (!unlink($backup_path)) {
|
||||
log_message('error', "Unable to remove $backup_path.");
|
||||
return;
|
||||
}
|
||||
log_message('info', "File $backup_path has been removed");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use App\Models\Employee;
|
||||
use App\Models\Item_taxes;
|
||||
use App\Models\Tax_category;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\Session\Session;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -578,8 +577,8 @@ function item_kit_headers(): array
|
||||
['item_kit_number' => lang('Item_kits.item_kit_number')],
|
||||
['name' => lang('Item_kits.name')],
|
||||
['description' => lang('Item_kits.description')],
|
||||
['total_cost_price' => lang('Items.cost_price'), 'sortable' => false],
|
||||
['total_unit_price' => lang('Items.unit_price'), 'sortable' => false]
|
||||
['total_cost_price' => lang('Items.cost_price'), 'sortable' => FALSE],
|
||||
['total_unit_price' => lang('Items.unit_price'), 'sortable' => FALSE]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -655,7 +654,7 @@ function expand_attribute_values(array $definition_names, array $row): array
|
||||
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||
if (isset($indexed_values[$definition_id])) {
|
||||
$raw_value = $indexed_values[$definition_id];
|
||||
|
||||
|
||||
// Format DECIMAL attributes according to locale
|
||||
if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) {
|
||||
$attribute_values["$definition_id"] = to_decimals($raw_value);
|
||||
@@ -743,7 +742,7 @@ function get_expense_category_manage_table_headers(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html data row for the expense category
|
||||
* Gets the html data row for the expenses category
|
||||
*/
|
||||
function get_expense_category_data_row(object $expense_category): array
|
||||
{
|
||||
@@ -842,7 +841,7 @@ function get_expenses_data_last_row(object $expense): array
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expense payments summary
|
||||
* Get the expenses payments summary
|
||||
*/
|
||||
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
|
||||
{
|
||||
@@ -934,22 +933,22 @@ function get_controller(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores filter values from the URL query string.
|
||||
*
|
||||
* @param IncomingRequest $request The request object
|
||||
* Restores filter values from URL query string.
|
||||
*
|
||||
* @param CodeIgniter\HTTP\IncomingRequest $request The request object
|
||||
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
|
||||
*/
|
||||
function restoreTableFilters(IncomingRequest $request): array
|
||||
function restoreTableFilters($request): array
|
||||
{
|
||||
$startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
|
||||
return array_filter([
|
||||
'start_date' => $startDate ?: null,
|
||||
'end_date' => $endDate ?: null,
|
||||
'selected_filters' => $urlFilters ?? []
|
||||
], function ($value) {
|
||||
], function($value) {
|
||||
return $value !== null && $value !== [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +143,8 @@ function get_tax_rates_manage_table_headers(): string
|
||||
*/
|
||||
function get_tax_rates_data_row($tax_rates_row): array
|
||||
{
|
||||
$controller_name = 'taxes';
|
||||
$router = service('router');
|
||||
$controller_name = strtolower($router->controllerName());
|
||||
|
||||
return [
|
||||
'tax_rate_id' => $tax_rates_row->tax_rate_id,
|
||||
|
||||
@@ -7,7 +7,7 @@ if (!function_exists('base64url_encode')) {
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
function base64url_encode(string $data): string
|
||||
function base64url_encode($data)
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
@@ -20,7 +20,7 @@ if (!function_exists('base64url_decode')) {
|
||||
* @param string $data
|
||||
* @return string|false
|
||||
*/
|
||||
function base64url_decode(string $data): false|string
|
||||
function base64url_decode($data)
|
||||
{
|
||||
$remainder = strlen($data) % 4;
|
||||
if ($remainder) {
|
||||
@@ -28,4 +28,4 @@ if (!function_exists('base64url_decode')) {
|
||||
}
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "الجميع",
|
||||
'columns' => "أعمدة",
|
||||
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||
'refresh' => "إعادة تحميل",
|
||||
'rows_per_page' => "{0} صف بالصفحة",
|
||||
'toggle' => "تغيير",
|
||||
"all" => "الجميع",
|
||||
"columns" => "أعمدة",
|
||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
||||
"refresh" => "إعادة تحميل",
|
||||
"rows_per_page" => "{0} صف بالصفحة",
|
||||
"toggle" => "تغيير",
|
||||
];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"su" => "أحد",
|
||||
"mo" => "اثنين",
|
||||
"tu" => "ثلاثاء",
|
||||
"we" => "أربعاء",
|
||||
"th" => "خميس",
|
||||
"fr" => "جمعة",
|
||||
"sa" => "سبت",
|
||||
"sun" => "الأحد",
|
||||
"mon" => "الاثنين",
|
||||
"tue" => "الثلاثاء",
|
||||
"wed" => "الأربعاء",
|
||||
"thu" => "الخميس",
|
||||
"fri" => "الجمعة",
|
||||
"sat" => "السبت",
|
||||
"sunday" => "الأحد",
|
||||
"monday" => "الاثنين",
|
||||
"tuesday" => "الثلاثاء",
|
||||
"wednesday" => "الأربعاء",
|
||||
"thursday" => "الخميس",
|
||||
"friday" => "الجمعة",
|
||||
"saturday" => "السبت",
|
||||
"jan" => "يناير",
|
||||
"feb" => "فبراير",
|
||||
"mar" => "مارس",
|
||||
"apr" => "أبريل",
|
||||
"may" => "مايو",
|
||||
"jun" => "يونيو",
|
||||
"jul" => "يوليو",
|
||||
"aug" => "أغسطس",
|
||||
"sep" => "سبتمبر",
|
||||
"oct" => "أكتوبر",
|
||||
"nov" => "نوفمبر",
|
||||
"dec" => "ديسمبر",
|
||||
"january" => "يناير",
|
||||
"february" => "فبراير",
|
||||
"march" => "مارس",
|
||||
"april" => "أبريل",
|
||||
"mayl" => "مايو",
|
||||
"june" => "يونيو",
|
||||
"july" => "يوليو",
|
||||
"august" => "أغسطس",
|
||||
"september" => "سبتمبر",
|
||||
"october" => "أكتوبر",
|
||||
"november" => "نوفمبر",
|
||||
"december" => "ديسمبر",
|
||||
];
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "يمين",
|
||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||
"security_issue" => "تحذير من ثغرة أمنية",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "دخول",
|
||||
"logout" => "تسجيل خروج",
|
||||
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "كلمة السر",
|
||||
"required_username" => "",
|
||||
"username" => "اسم المستخدم",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "الموظف",
|
||||
"entry" => "ادخال",
|
||||
"error_editing_item" => "خطاء فى تحرير الصنف",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "بحث/مسح باركود صنف",
|
||||
"find_or_scan_item_or_receipt" => "بحث/مسح باركود صنف أو ايصال",
|
||||
"giftcard" => "بطاقة هدية",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "الكل",
|
||||
'columns' => "أعمدة",
|
||||
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||
'refresh' => "إعادة تحميل",
|
||||
'rows_per_page' => "{0} صف بالصفحة",
|
||||
'toggle' => "تغيير",
|
||||
"all" => "الكل",
|
||||
"columns" => "أعمدة",
|
||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
||||
"refresh" => "إعادة تحميل",
|
||||
"rows_per_page" => "{0} صف بالصفحة",
|
||||
"toggle" => "تغيير",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "يمين",
|
||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||
"security_issue" => "تحذير من ثغرة أمنية",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "دخول",
|
||||
"logout" => "تسجيل خروج",
|
||||
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "كلمة السر",
|
||||
"required_username" => "خانة أسم المستخدم مطلوبة.",
|
||||
"username" => "اسم المستخدم",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "الموظف",
|
||||
"entry" => "ادخال",
|
||||
"error_editing_item" => "خطاء فى تعديل المادة",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "بحث/مسح باركود المادة",
|
||||
"find_or_scan_item_or_receipt" => "بحث/مسح باركود المادة أو الايصال",
|
||||
"giftcard" => "بطاقة هدية",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "hamısı",
|
||||
'columns' => "Sütunlar",
|
||||
'hide_show_pagination' => "Gizlət/Göstər səhifənin nömrələnməsin",
|
||||
'loading' => "Lütfən gözləyin, səhifə yüklənir",
|
||||
'page_from_to' => "Göstər {0} bundan {1} buna {2} kimi",
|
||||
'refresh' => "Yenilə",
|
||||
'rows_per_page' => "{0} yazı səhifədə",
|
||||
'toggle' => "Keçid",
|
||||
"all" => "hamısı",
|
||||
"columns" => "Sütunlar",
|
||||
"hide_show_pagination" => "Gizlət/Göstər səhifənin nömrələnməsin",
|
||||
"loading" => "Lütfən gözləyin, səhifə yüklənir...",
|
||||
"page_from_to" => "Göstər {0} bundan {1} buna {2} kimi",
|
||||
"refresh" => "Yenilə",
|
||||
"rows_per_page" => "{0} yazı səhifədə",
|
||||
"toggle" => "Keçid",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "Konfiqurasiya ugursuz oldu saxlanilmadi",
|
||||
"sales_invoice_format" => "Satış Fatura Formatı",
|
||||
"sales_quote_format" => "Satış Sitat Formati",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "Konfiqurasiya uğurla saxlanıldı.",
|
||||
"saved_unsuccessfully" => "Konfiqurasiyanı saxlamq mümkün olmadı.",
|
||||
"security_issue" => "Təhlükəsizlik açığı xəbərdarlığı",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "Giriş",
|
||||
"logout" => "Çıxış",
|
||||
"migration_needed" => "{0} -ə daxil olandan sonra verilənlər bazası miqrasiyası başlayacaq.",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "Şifrə",
|
||||
"required_username" => "",
|
||||
"username" => "İstifadəçi",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "Əməkdaş",
|
||||
"entry" => "Daxil",
|
||||
"error_editing_item" => "XƏTA Malın redaktəsində",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "Malın axtarışı",
|
||||
"find_or_scan_item_or_receipt" => "Tapmaq skan etmək və ya kvitansiya",
|
||||
"giftcard" => "Hədiyyə Kartı",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "Всичко/и",
|
||||
'columns' => "Колони",
|
||||
'hide_show_pagination' => "Скриване / Показване на страници",
|
||||
'loading' => "Зареждане, моля изчакайте",
|
||||
'page_from_to' => "Показани са {0} до {1} от {2} реда",
|
||||
'refresh' => "Опресняване",
|
||||
'rows_per_page' => "{0} редове на страница",
|
||||
'toggle' => "Щифт",
|
||||
"all" => "Всичко/и",
|
||||
"columns" => "Колони",
|
||||
"hide_show_pagination" => "Скриване / Показване на страници",
|
||||
"loading" => "Зареждане, моля изчакайте...",
|
||||
"page_from_to" => "Показани са {0} до {1} от {2} реда",
|
||||
"refresh" => "Опресняване",
|
||||
"rows_per_page" => "{0} редове на страница",
|
||||
"toggle" => "Щифт",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "Right",
|
||||
"sales_invoice_format" => "Sales Invoice Format",
|
||||
"sales_quote_format" => "Sales Quote Format",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "Configuration save successful.",
|
||||
"saved_unsuccessfully" => "Configuration save failed.",
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "Login",
|
||||
"logout" => "",
|
||||
"migration_needed" => "",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "Password",
|
||||
"required_username" => "",
|
||||
"username" => "Username",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "Служител",
|
||||
"entry" => "Вход",
|
||||
"error_editing_item" => "Грешка при редактирането на елемента",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "Намерете или сканирайте елемента",
|
||||
"find_or_scan_item_or_receipt" => "Намерете или сканирайте елемент или разпис",
|
||||
"giftcard" => "Gift Карта",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "Sve",
|
||||
'columns' => "Kolone",
|
||||
'hide_show_pagination' => "Sakrij / prikaži paginaciju",
|
||||
'loading' => "Učitavanje sačekajte",
|
||||
'page_from_to' => "Prikazivanje {0} do {1} od {2} redova",
|
||||
'refresh' => "Osvježi",
|
||||
'rows_per_page' => "{0} redova po stranici",
|
||||
'toggle' => "Promijeni prikaz",
|
||||
"all" => "Sve",
|
||||
"columns" => "Kolone",
|
||||
"hide_show_pagination" => "Sakrij / prikaži paginaciju",
|
||||
"loading" => "Učitavanje sačekajte...",
|
||||
"page_from_to" => "Prikazivanje {0} do {1} od {2} redova",
|
||||
"refresh" => "Osvježi",
|
||||
"rows_per_page" => "{0} redova po stranici",
|
||||
"toggle" => "Promijeni prikaz",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "Desno",
|
||||
"sales_invoice_format" => "Format fakture",
|
||||
"sales_quote_format" => "Format navedene prodaje",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "Konfiguracija je uspješno snimljena.",
|
||||
"saved_unsuccessfully" => "Konfiguracija nije uspješno snimljena.",
|
||||
"security_issue" => "Upozorenje o sigurnosnoj ranjivosti",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "Prijava",
|
||||
"logout" => "Odjava",
|
||||
"migration_needed" => "Migracija baze podataka na {0} će početi nakon prijavljivanja.",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "Lozinka",
|
||||
"required_username" => "",
|
||||
"username" => "Korisničko ime",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "Zaposlenik",
|
||||
"entry" => "Ulaz",
|
||||
"error_editing_item" => "Greška pri uređivanju artikla",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "Pronađi/Skeniraj artikal",
|
||||
"find_or_scan_item_or_receipt" => "Pronađi/Skeniraj artikal ili priznanicu",
|
||||
"giftcard" => "Poklon kartica",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "هەموو",
|
||||
'columns' => "ستنوونەکان",
|
||||
'hide_show_pagination' => "شاردنەوە/پێشاندانی لاپەڕەسازی",
|
||||
'loading' => "بارکردن، تکایە چاوەڕوان بن",
|
||||
'page_from_to' => "پیشاندانی {0} بۆ {1} لە {2} ڕیزەکان",
|
||||
'refresh' => "ڕفرێش",
|
||||
'rows_per_page' => "{0} ڕیز بۆ هەر لاپەڕەیەک",
|
||||
'toggle' => "دوگمە",
|
||||
"all" => "هەموو",
|
||||
"columns" => "ستنوونەکان",
|
||||
"hide_show_pagination" => "شاردنەوە/پێشاندانی لاپەڕەسازی",
|
||||
"loading" => "بارکردن، تکایە چاوەڕوان بن...",
|
||||
"page_from_to" => "پیشاندانی {0} بۆ {1} لە {2} ڕیزەکان",
|
||||
"refresh" => "ڕفرێش",
|
||||
"rows_per_page" => "{0} ڕیز بۆ هەر لاپەڕەیەک",
|
||||
"toggle" => "دوگمە",
|
||||
];
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
'login' => "چوونەژوورەوە",
|
||||
'logout' => "چوونەدەرەوە",
|
||||
'migration_needed' => "گواستنەوەی داتابەیس بۆ {0} دوای چوونەژوورەوە دەست پێدەکات.",
|
||||
'migration_required' => "",
|
||||
'migration_auth_message' => "",
|
||||
'migration_initializing' => "",
|
||||
'migration_running' => "",
|
||||
'migration_complete' => "",
|
||||
'migration_complete_login' => "",
|
||||
'migration_failed' => "",
|
||||
'migration_error_connection' => "",
|
||||
'migration_complete_redirect' => "",
|
||||
'password' => "وشەی نهێنی",
|
||||
'required_username' => "خانەی ناوی بەکارهێنەر پێویستە.",
|
||||
'username' => "ناوی بەکارهێنەر",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
'employee' => "فەرمانبەر",
|
||||
'entry' => "تۆمار",
|
||||
'error_editing_item' => "هەڵە لە دەستکاریکردنی ئایتم",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
'find_or_scan_item' => "دۆزینەوە یان سکانکردنی ئایتم",
|
||||
'find_or_scan_item_or_receipt' => "دۆزینەوە یان سکانکردنی ئایتم یان پسوڵە",
|
||||
'giftcard' => "کارتی دیاری",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "Vše",
|
||||
'columns' => "Sloupce",
|
||||
'hide_show_pagination' => "Zobrazit/skrýt stránkování",
|
||||
'loading' => "Nahrávám, prosím počkejte",
|
||||
'page_from_to' => "Zobrazeno {0} až {1} z {2} řádků",
|
||||
'refresh' => "Obnovit",
|
||||
'rows_per_page' => "{0} řádků na stránku",
|
||||
'toggle' => "Přepnout",
|
||||
"all" => "Vše",
|
||||
"columns" => "Sloupce",
|
||||
"hide_show_pagination" => "Zobrazit/skrýt stránkování",
|
||||
"loading" => "Nahrávám, prosím počkejte...",
|
||||
"page_from_to" => "Zobrazeno {0} až {1} z {2} řádků",
|
||||
"refresh" => "Obnovit",
|
||||
"rows_per_page" => "{0} řádků na stránku",
|
||||
"toggle" => "Přepnout",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "",
|
||||
"sales_invoice_format" => "",
|
||||
"sales_quote_format" => "",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "",
|
||||
"saved_unsuccessfully" => "",
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "Login",
|
||||
"logout" => "",
|
||||
"migration_needed" => "",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "Heslo",
|
||||
"required_username" => "",
|
||||
"username" => "Uživatelské jméno",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "Prodávající",
|
||||
"entry" => "Záznam",
|
||||
"error_editing_item" => "Chyba při úpravě položky",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "Najít nebo skenovat položku",
|
||||
"find_or_scan_item_or_receipt" => "Najít nebo skenovat položku či účtenku",
|
||||
"giftcard" => "Dárkový poukaz",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "Alle",
|
||||
'columns' => "Kolonner",
|
||||
'hide_show_pagination' => "Gem/Vis sideinddeling",
|
||||
'loading' => "Indlæser, vent venligst",
|
||||
'page_from_to' => "Viser {0} to {1} af {2} rækker",
|
||||
'refresh' => "Opdater",
|
||||
'rows_per_page' => "{0} rækker per side",
|
||||
'toggle' => "Skift",
|
||||
"all" => "Alle",
|
||||
"columns" => "Kolonner",
|
||||
"hide_show_pagination" => "Gem/Vis sideinddeling",
|
||||
"loading" => "Indlæser, vent venligst...",
|
||||
"page_from_to" => "Viser {0} to {1} af {2} rækker",
|
||||
"refresh" => "Opdater",
|
||||
"rows_per_page" => "{0} rækker per side",
|
||||
"toggle" => "Skift",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "Right",
|
||||
"sales_invoice_format" => "Sales Invoice Format",
|
||||
"sales_quote_format" => "Sales Quote Format",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "Configuration save successful.",
|
||||
"saved_unsuccessfully" => "Configuration save failed.",
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "",
|
||||
"logout" => "",
|
||||
"migration_needed" => "",
|
||||
"migration_required" => "",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
"migration_complete" => "",
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "",
|
||||
"required_username" => "",
|
||||
"username" => "",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "",
|
||||
"entry" => "",
|
||||
"error_editing_item" => "",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "",
|
||||
"find_or_scan_item_or_receipt" => "",
|
||||
"giftcard" => "Gavekort",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user