update php
Tests / PHPUnit (push) Failing after 35s

This commit is contained in:
root
2026-07-29 19:03:57 -04:00
parent 8509497326
commit f8f00c70c8
15 changed files with 1876 additions and 418 deletions
+1
View File
@@ -47,6 +47,7 @@ Thumbs.db
/coverage/
/build/
/build.tar.gz
/builds
/phpunit.xml.cache
/.phpunit.result.cache
+2 -2
View File
@@ -62,7 +62,7 @@ Problems with it can be raised on our forum, or as issues in the main repository
## Server Requirements
PHP version 8.1 or higher is required, with the following extensions installed:
PHP version 8.5 or higher is required, with the following extensions installed:
- [intl](http://php.net/manual/en/intl.requirements.php)
- [mbstring](http://php.net/manual/en/mbstring.installation.php)
@@ -71,7 +71,7 @@ PHP version 8.1 or higher is required, with the following extensions installed:
> - The end of life date for PHP 7.4 was November 28, 2022.
> - The end of life date for PHP 8.0 was November 26, 2023.
> - If you are still using PHP 7.4 or 8.0, you should upgrade immediately.
> - The end of life date for PHP 8.1 will be December 31, 2025.
> - The end of life date for PHP 8.1 was December 31, 2025.
Additionally, make sure that the following extensions are enabled in your PHP:
-32
View File
@@ -1,32 +0,0 @@
Al Rahma Sunday School — API Docs Pack
=================================================
Generated: 2025-11-07T21:01:43.815073
WHAT'S INCLUDED
---------------
- app/Controllers/Api/*.php (API controllers)
- app/Controllers/{DocsController, ApiDocsController}
- app/Filters/ApiDocsAuthFilter.php
- app/Views/docs/{index.php, swagger_ui.php, swagger_ui_public.php}
- public/docs/openapi/*.yaml (separate specs + master.yaml)
- ROUTES_SNIPPET.php (add to app/Config/Routes.php)
- FILTERS_SNIPPET.php (add alias to app/Config/Filters.php)
INSTALL
-------
1) Unzip into your project root.
2) Merge routes:
- Open app/Config/Routes.php and paste the contents of ROUTES_SNIPPET.php
3) Add filter alias:
- Open app/Config/Filters.php and add the 'apiDocsAuth' alias from FILTERS_SNIPPET.php
4) Ensure your Models exist and match the controller expectations.
5) Set JWT secret in .env:
JWT_SECRET=your_super_secret_key
6) Visit:
- /docs (landing)
- /docs/api (secured, admin only)
- /docs/api/public (public read-only)
7) API base path:
- /api/v1/... (see YAMLs for full paths)
Enjoy!
+46 -5
View File
@@ -47,28 +47,63 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var list<string>|string|null
*/
public $defaultSrc;
public $defaultSrc = 'self';
/**
* Lists allowed scripts' URLs.
*
* @var list<string>|string
*/
public $scriptSrc = 'self';
public $scriptSrc = [
'self',
'unsafe-inline',
'https://cdn.jsdelivr.net',
'https://code.jquery.com',
'https://cdnjs.cloudflare.com',
];
/**
* Lists allowed stylesheets' URLs.
*
* @var list<string>|string
*/
public $styleSrc = 'self';
public $styleSrc = [
'self',
'unsafe-inline',
'https://cdn.jsdelivr.net',
'https://fonts.googleapis.com',
'https://cdnjs.cloudflare.com',
];
/**
* Lists allowed stylesheet element URLs.
*
* @var list<string>|string
*/
public $styleSrcElem = [
'self',
'unsafe-inline',
'https://cdn.jsdelivr.net',
'https://fonts.googleapis.com',
'https://cdnjs.cloudflare.com',
];
/**
* Allows inline style attributes used by legacy views.
*
* @var list<string>|string
*/
public $styleSrcAttr = [
'self',
'unsafe-inline',
];
/**
* Defines the origins from which images can be loaded.
*
* @var list<string>|string
*/
public $imageSrc = 'self';
public $imageSrc = ['self', 'data:', 'https:'];
/**
* Restricts the URLs that can appear in a page's `<base>` element.
@@ -99,7 +134,13 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var list<string>|string
*/
public $fontSrc;
public $fontSrc = [
'self',
'data:',
'https://fonts.gstatic.com',
'https://cdn.jsdelivr.net',
'https://cdnjs.cloudflare.com',
];
/**
* Lists valid endpoints for submission from `<form>` tags.
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' https://cdnjs.cloudflare.com; connect-src 'self' http://localhost:8080 <?= base_url() ?> https://cdnjs.cloudflare.com; script-src 'self' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data:; font-src https://cdnjs.cloudflare.com;" />
content="default-src 'self'; connect-src 'self' http://localhost:8080 <?= base_url() ?> https://cdnjs.cloudflare.com; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com;" />
<title>Al Rahma Sunday School API Docs</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.5/swagger-ui.min.css" />
<style>
+1
View File
@@ -9,6 +9,7 @@
<meta content="" name="description">
<!-- Bootstrap CSS -->
<link href="<?= base_url('assets/boot_css/bootstrap.min.css') ?>" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Google Web Fonts -->
+709
View File
@@ -0,0 +1,709 @@
#!/usr/bin/env bash
#
# build_production.sh
#
# Build a deployable CodeIgniter 4 production directory from a complete
# project checkout. The script deliberately refuses incomplete projects,
# development Composer dependencies, and non-production environment files.
#
# Usage:
# ./build_production.sh
#
# Optional:
# --skip-composer Use the existing vendor directory. The script rejects
# known development packages such as PHPUnit.
# --skip-lint Skip PHP syntax checks.
# --no-archive Do not create alrahma.zip.
# --no-force Refuse to replace an existing output directory.
# --help Show help.
#
# The web-server document root must be <production-folder>/public.
#
set -Eeuo pipefail
IFS=$'\n\t'
umask 027
SCRIPT_NAME="$(basename "$0")"
SOURCE_DIR="$(pwd)"
OUTPUT_DIR="$(pwd)/build"
ENV_FILE="$(pwd)/env.production"
SKIP_COMPOSER=0
SKIP_LINT=0
MAKE_ARCHIVE=1
FORCE=1
STAGE_ROOT=""
log() {
printf '[%s] %s\n' "$SCRIPT_NAME" "$*"
}
warn() {
printf '[%s] WARNING: %s\n' "$SCRIPT_NAME" "$*" >&2
}
die() {
printf '[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2
exit 1
}
usage() {
cat <<'USAGE'
Build a CodeIgniter 4 production directory.
Default:
./build_production.sh builds the current project into ./build, copies
./env.production as .env, replaces any existing ./build directory, and
creates ./alrahma.zip.
Options:
--source DIR Complete project root. Default: current directory.
--output DIR Destination. Default: ./build.
--env FILE Production environment file. Default: ./env.production.
--skip-composer Keep the existing vendor directory instead of running
Composer. Known development dependencies are rejected.
--skip-lint Skip PHP syntax checks.
--archive Create ./alrahma.zip after building. Enabled by
default. The archive contains .env and must be
protected.
--no-archive Do not create ./alrahma.zip.
--force Replace an existing output directory. Enabled by
default.
--no-force Refuse to replace an existing output directory.
-h, --help Show this help.
Example:
./build_production.sh
USAGE
}
cleanup() {
if [[ -n "${STAGE_ROOT:-}" && -d "$STAGE_ROOT" ]]; then
rm -rf -- "$STAGE_ROOT"
fi
}
trap cleanup EXIT
trap 'die "Build failed near line $LINENO."' ERR
while [[ $# -gt 0 ]]; do
case "$1" in
--source)
[[ $# -ge 2 ]] || die "--source requires a directory."
SOURCE_DIR="$2"
shift 2
;;
--output)
[[ $# -ge 2 ]] || die "--output requires a directory."
OUTPUT_DIR="$2"
shift 2
;;
--env)
[[ $# -ge 2 ]] || die "--env requires a file."
ENV_FILE="$2"
shift 2
;;
--skip-composer)
SKIP_COMPOSER=1
shift
;;
--skip-lint)
SKIP_LINT=1
shift
;;
--archive)
MAKE_ARCHIVE=1
shift
;;
--no-archive)
MAKE_ARCHIVE=0
shift
;;
--force)
FORCE=1
shift
;;
--no-force)
FORCE=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
for command_name in php rsync find awk sed grep sort xargs sha256sum zip; do
command -v "$command_name" >/dev/null 2>&1 \
|| die "Required command is missing: $command_name"
done
[[ -d "$SOURCE_DIR" ]] || die "Source directory does not exist: $SOURCE_DIR"
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd -P)"
OUTPUT_PARENT="$(dirname "$OUTPUT_DIR")"
mkdir -p -- "$OUTPUT_PARENT"
OUTPUT_PARENT="$(cd "$OUTPUT_PARENT" && pwd -P)"
OUTPUT_DIR="$OUTPUT_PARENT/$(basename "$OUTPUT_DIR")"
[[ "$OUTPUT_DIR" != "/" ]] || die "Refusing to use / as the output directory."
[[ "$OUTPUT_DIR" != "$SOURCE_DIR" ]] \
|| die "Output directory must not be the project root."
[[ -n "$ENV_FILE" ]] \
|| die "Pass a production environment file with --env."
[[ -f "$ENV_FILE" ]] \
|| die "Environment file does not exist: $ENV_FILE"
ENV_FILE="$(cd "$(dirname "$ENV_FILE")" && pwd -P)/$(basename "$ENV_FILE")"
required_paths=(
"app"
"app/Config/Paths.php"
"public"
"public/index.php"
"writable"
"spark"
"composer.json"
"composer.lock"
)
missing_paths=()
for required_path in "${required_paths[@]}"; do
if [[ ! -e "$SOURCE_DIR/$required_path" ]]; then
missing_paths+=("$required_path")
fi
done
if (( ${#missing_paths[@]} > 0 )); then
printf '[%s] Missing required project items:\n' "$SCRIPT_NAME" >&2
printf ' - %s\n' "${missing_paths[@]}" >&2
die "The source is not a complete deployable CodeIgniter project."
fi
[[ -f "$SOURCE_DIR/vendor/autoload.php" || "$SKIP_COMPOSER" -eq 0 ]] \
|| die "--skip-composer requires an existing vendor/autoload.php."
if [[ -e "$OUTPUT_DIR" ]]; then
(( FORCE == 1 )) \
|| die "Output already exists. Use --force to replace it: $OUTPUT_DIR"
log "Removing existing output: $OUTPUT_DIR"
rm -rf -- "$OUTPUT_DIR"
fi
env_value() {
local key="$1"
local file="$2"
awk -F '=' -v wanted="$key" '
/^[[:space:]]*[#;]/ { next }
NF < 2 { next }
{
key = $1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
if (key != wanted) {
next
}
value = substr($0, index($0, "=") + 1)
sub(/[[:space:]]+[;#].*$/, "", value)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
gsub(/^["'\'']|["'\'']$/, "", value)
print value
exit
}
' "$file"
}
CI_ENVIRONMENT_VALUE="$(env_value "CI_ENVIRONMENT" "$ENV_FILE" || true)"
CI_ENVIRONMENT_VALUE="$(printf '%s' "$CI_ENVIRONMENT_VALUE" | tr '[:upper:]' '[:lower:]')"
[[ "$CI_ENVIRONMENT_VALUE" == "production" ]] \
|| die "The supplied environment file must set CI_ENVIRONMENT = production."
for recommended_key in \
"app.baseURL" \
"database.default.hostname" \
"database.default.database" \
"database.default.username"
do
if [[ -z "$(env_value "$recommended_key" "$ENV_FILE" || true)" ]]; then
warn "Production environment does not define '$recommended_key'. Verify that it is injected another way."
fi
done
STAGE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/ci4-production-build.XXXXXXXX")"
STAGE_DIR="$STAGE_ROOT/release"
mkdir -p -- "$STAGE_DIR"
log "Copying application files into a clean staging directory."
copy_production_path() {
local relative_path="$1"
local source_path="$SOURCE_DIR/$relative_path"
local destination_path="$STAGE_DIR/$relative_path"
[[ -e "$source_path" ]] || return 0
mkdir -p -- "$(dirname "$destination_path")"
if [[ -d "$source_path" ]]; then
mkdir -p -- "$destination_path"
rsync -a \
--exclude='.DS_Store' \
--exclude='__MACOSX/' \
"$source_path/" "$destination_path/"
else
install -m 640 "$source_path" "$destination_path"
fi
}
copy_public_files() {
mkdir -p -- "$STAGE_DIR/public"
rsync -a \
--exclude='.DS_Store' \
--exclude='__MACOSX/' \
--exclude='/docs/***' \
--exclude='/gitlab/***' \
--exclude='/jenkins/***' \
--exclude='/uploads/***' \
"$SOURCE_DIR/public/" "$STAGE_DIR/public/"
}
production_paths=(
"app"
"bootstrap"
"routes"
"composer.json"
"composer.lock"
"spark"
"preload.php"
"LICENSE"
)
for production_path in "${production_paths[@]}"; do
copy_production_path "$production_path"
done
copy_public_files
mkdir -p -- "$STAGE_DIR/writable"
copy_production_path "writable/.htaccess"
copy_production_path "writable/index.html"
copy_production_path "writable/.gitkeep"
if (( SKIP_COMPOSER == 1 )); then
copy_production_path "vendor"
fi
install -m 600 "$ENV_FILE" "$STAGE_DIR/.env"
# Remove platform debris even when it was nested in an unexpected directory.
find "$STAGE_DIR" -name '.DS_Store' -type f -delete
find "$STAGE_DIR" -name '__MACOSX' -type d -prune -exec rm -rf {} +
runtime_directories=(
"writable/cache"
"writable/debugbar"
"writable/logs"
"writable/session"
"writable/tmp"
"writable/uploads"
"writable/uploads/class_material"
"writable/uploads/early_dismissal_signatures"
"writable/uploads/exams"
"writable/uploads/print_requests"
"writable/uploads/receipts"
"writable/uploads/reimbursements"
"writable/reports"
"writable/report_assets"
"public/uploads"
"public/uploads/event_flyers"
"public/uploads/print_requests"
)
for runtime_directory in "${runtime_directories[@]}"; do
mkdir -p -- "$STAGE_DIR/$runtime_directory"
: > "$STAGE_DIR/$runtime_directory/.gitkeep"
done
chmod 750 "$STAGE_DIR/spark"
chmod 600 "$STAGE_DIR/.env"
if (( SKIP_COMPOSER == 0 )); then
command -v composer >/dev/null 2>&1 \
|| die "Composer is required. Install Composer or use --skip-composer with an already production-only vendor directory."
log "Installing production Composer dependencies."
(
cd "$STAGE_DIR"
COMPOSER_ALLOW_SUPERUSER=1 composer install \
--no-dev \
--prefer-dist \
--optimize-autoloader \
--classmap-authoritative \
--no-interaction \
--no-progress
COMPOSER_ALLOW_SUPERUSER=1 composer check-platform-reqs --no-dev
)
else
log "Using the existing vendor directory."
[[ -f "$STAGE_DIR/vendor/autoload.php" ]] \
|| die "Existing vendor/autoload.php is missing."
forbidden_vendor_paths=(
"vendor/phpunit"
"vendor/fakerphp"
"vendor/mockery"
"vendor/myclabs/deep-copy"
"vendor/phar-io"
"vendor/sebastian"
"vendor/theseer"
)
found_dev_dependencies=()
for vendor_path in "${forbidden_vendor_paths[@]}"; do
if [[ -e "$STAGE_DIR/$vendor_path" ]]; then
found_dev_dependencies+=("$vendor_path")
fi
done
if (( ${#found_dev_dependencies[@]} > 0 )); then
printf '[%s] Development packages remain in vendor:\n' "$SCRIPT_NAME" >&2
printf ' - %s\n' "${found_dev_dependencies[@]}" >&2
die "Run Composer with --no-dev instead of using --skip-composer."
fi
fi
[[ -f "$STAGE_DIR/vendor/autoload.php" ]] \
|| die "Production vendor/autoload.php was not created."
php -r '
require $argv[1];
fwrite(STDOUT, "Composer autoload check passed.\n");
' "$STAGE_DIR/vendor/autoload.php"
if (( SKIP_LINT == 0 )); then
log "Linting application PHP files."
lint_failure_file="$STAGE_ROOT/php-lint-failures.txt"
: > "$lint_failure_file"
while IFS= read -r -d '' php_file; do
if ! php -l "$php_file" >/dev/null 2>&1; then
printf '%s\n' "$php_file" >> "$lint_failure_file"
fi
done < <(
find \
"$STAGE_DIR/app" \
"$STAGE_DIR/public" \
-type f -name '*.php' -print0
)
if [[ -s "$lint_failure_file" ]]; then
cat "$lint_failure_file" >&2
die "PHP lint failed."
fi
fi
# Apply conservative baseline permissions. The deployment user or PHP-FPM
# group can be supplied later using chown/chgrp on the server.
find "$STAGE_DIR" -type d -exec chmod 750 {} +
find "$STAGE_DIR" -type f -exec chmod 640 {} +
chmod 750 "$STAGE_DIR/spark"
chmod 600 "$STAGE_DIR/.env"
for runtime_directory in "${runtime_directories[@]}"; do
chmod 770 "$STAGE_DIR/$runtime_directory"
done
mkdir -p "$STAGE_DIR/.deployment"
cat > "$STAGE_DIR/.deployment/release-check.sh" <<'RELEASE_CHECK'
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
errors=0
fail() {
printf 'FAIL: %s\n' "$*" >&2
errors=$((errors + 1))
}
pass() {
printf 'PASS: %s\n' "$*"
}
[[ -f "$ROOT/.env" ]] \
&& pass ".env exists" \
|| fail ".env is missing"
if grep -Eq '^[[:space:]]*CI_ENVIRONMENT[[:space:]]*=[[:space:]]*["'\'']?production["'\'']?[[:space:]]*$' "$ROOT/.env"; then
pass "CI_ENVIRONMENT is production"
else
fail "CI_ENVIRONMENT is not production"
fi
for required in \
"app/Config/Paths.php" \
"public/index.php" \
"vendor/autoload.php" \
"spark"
do
[[ -e "$ROOT/$required" ]] \
&& pass "$required exists" \
|| fail "$required is missing"
done
for writable_path in \
"writable/cache" \
"writable/logs" \
"writable/session" \
"writable/tmp" \
"writable/uploads"
do
if [[ -d "$ROOT/$writable_path" && -w "$ROOT/$writable_path" ]]; then
pass "$writable_path is writable"
else
fail "$writable_path is not writable"
fi
done
for forbidden in \
".git" \
"tests" \
"node_modules" \
"vendor/phpunit" \
"phpunit.xml"
do
[[ ! -e "$ROOT/$forbidden" ]] \
&& pass "$forbidden is absent" \
|| fail "$forbidden must not be deployed"
done
php -r 'require $argv[1];' "$ROOT/vendor/autoload.php" \
&& pass "Composer autoload works" \
|| fail "Composer autoload failed"
printf '\nRequired web document root:\n %s/public\n' "$ROOT"
printf '\nBefore traffic is enabled, run database migration status and the application financial smoke tests.\n'
if (( errors > 0 )); then
printf '\nRelease check failed with %d error(s).\n' "$errors" >&2
exit 1
fi
printf '\nRelease check passed.\n'
RELEASE_CHECK
chmod 750 "$STAGE_DIR/.deployment/release-check.sh"
cat > "$STAGE_DIR/.deployment/nginx.conf.example" <<'NGINX'
server {
listen 80;
server_name __DOMAIN__;
# Point the web root only at public. Exposing the project root is unsafe.
root __APP_ROOT__/public;
index index.php;
client_max_body_size 25m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_pass __PHP_FPM_SOCKET__;
}
location ~ /\.(?!well-known).* {
deny all;
}
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff|woff2|ttf)$ {
expires 7d;
access_log off;
try_files $uri =404;
}
}
NGINX
cat > "$STAGE_DIR/.deployment/apache-vhost.conf.example" <<'APACHE'
<VirtualHost *:80>
ServerName __DOMAIN__
DocumentRoot "__APP_ROOT__/public"
<Directory "__APP_ROOT__/public">
AllowOverride All
Options FollowSymLinks
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/school-app-error.log
CustomLog ${APACHE_LOG_DIR}/school-app-access.log combined
</VirtualHost>
APACHE
cat > "$STAGE_DIR/.deployment/DEPLOYMENT.md" <<'DEPLOYMENT'
# Production Deployment Notes
## Required document root
Configure the web server document root as:
```text
<release-directory>/public
```
Never expose the project root, `app`, `vendor`, `writable`, or `.env`.
## Server preparation
1. Install the PHP version and extensions accepted by:
```bash
composer check-platform-reqs --no-dev
```
2. Set the release owner and PHP-FPM group:
```bash
sudo chown -R DEPLOY_USER:PHP_FPM_GROUP /path/to/release
```
3. Keep application code read-only for PHP-FPM. Grant write access only to runtime locations:
```bash
sudo find /path/to/release -type d -exec chmod 750 {} \;
sudo find /path/to/release -type f -exec chmod 640 {} \;
sudo chmod 600 /path/to/release/.env
sudo chmod 750 /path/to/release/spark
sudo chmod -R 770 /path/to/release/writable
sudo chmod -R 770 /path/to/release/public/uploads
```
4. Run the packaged release check:
```bash
/path/to/release/.deployment/release-check.sh
```
## Database deployment
Back up the production database first.
Review migration status before executing migrations:
```bash
cd /path/to/release
php spark migrate:status
php spark migrate --all
```
Financial hardening migrations must fail if required constraints cannot be installed. Do not accept warning-only deployment of uniqueness or check constraints.
## Persistent data
Do not overwrite persistent production data during release replacement:
- `.env`
- `writable/uploads`
- `writable/reports`
- `writable/report_assets`
- `public/uploads`
Mount or copy these from controlled persistent storage.
## Traffic activation
Before enabling traffic:
1. Run migrations.
2. Run release checks.
3. Run financial smoke tests for invoice, payment, refund, discount, expense, reimbursement, and purchase-order workflows.
4. Confirm scheduled jobs and workers point at the new release.
5. Confirm writable ownership.
6. Confirm the web server points to `public`.
7. Confirm `CI_ENVIRONMENT = production`.
8. Confirm debug output is disabled.
9. Confirm database backups and rollback instructions exist.
Use an atomic `current` symlink switch where possible rather than copying files over a live release.
DEPLOYMENT
SOURCE_COMMIT="not-a-git-checkout"
if command -v git >/dev/null 2>&1 && git -C "$SOURCE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
SOURCE_COMMIT="$(git -C "$SOURCE_DIR" rev-parse HEAD)"
fi
cat > "$STAGE_DIR/.deployment/BUILD_INFO" <<BUILD_INFO
built_at_utc=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
source_directory=$SOURCE_DIR
source_commit=$SOURCE_COMMIT
php_version=$(php -r 'echo PHP_VERSION;')
composer_mode=$([[ "$SKIP_COMPOSER" -eq 1 ]] && printf 'existing-vendor' || printf 'composer-no-dev')
BUILD_INFO
(
cd "$STAGE_DIR"
find . -type f \
! -path './.env' \
! -path './.deployment/MANIFEST.sha256' \
-print0 \
| sort -z \
| xargs -0 sha256sum \
> .deployment/MANIFEST.sha256
)
log "Running packaged release verification."
"$STAGE_DIR/.deployment/release-check.sh"
log "Publishing production directory: $OUTPUT_DIR"
mkdir -p -- "$(dirname "$OUTPUT_DIR")"
mv -- "$STAGE_DIR" "$OUTPUT_DIR"
if (( MAKE_ARCHIVE == 1 )); then
ARCHIVE_PATH="$(dirname "$OUTPUT_DIR")/alrahma.zip"
rm -f -- "$ARCHIVE_PATH"
warn "The archive contains the production .env. Store and transfer it as a secret."
(
cd "$(dirname "$OUTPUT_DIR")"
zip -qr "$ARCHIVE_PATH" "$(basename "$OUTPUT_DIR")"
)
chmod 600 "$ARCHIVE_PATH"
log "Created protected archive: $ARCHIVE_PATH"
fi
cat <<SUMMARY
Production build completed.
Directory:
$OUTPUT_DIR
Web document root:
$OUTPUT_DIR/public
Verification:
$OUTPUT_DIR/.deployment/release-check.sh
Deployment notes:
$OUTPUT_DIR/.deployment/DEPLOYMENT.md
The build excludes runtime logs, sessions, caches, existing uploads, tests,
development metadata, and local environment files. The supplied production
environment file was copied as .env with mode 600.
SUMMARY
+1 -1
View File
@@ -10,7 +10,7 @@
"slack": "https://codeigniterchat.slack.com"
},
"require": {
"php": "^8.1",
"php": "^8.5",
"codeigniter4/framework": "^4.6",
"doctrine/migrations": "^3.8",
"dompdf/dompdf": "^3.0",
Generated
+640 -308
View File
File diff suppressed because it is too large Load Diff
+230
View File
@@ -0,0 +1,230 @@
#!/bin/bash
# ===============================
# Al Rahma Sunday School Deployment Script
# ===============================
# ----- Domain and App Info -----
DM_NAME="home.alrahmaisgl.org"
domain_app="alrahma"
# ----- Database credentials -----
DB_HOST="localhost"
DB_NAME="u280815660_school"
DB_USER="u280815660_melabidi"
DB_PASS=">tNxlRzP/W8"
# ----- Directories -----
ZIP_FILE="$1"
BASE_DIR="/home/u280815660/domains"
DEPLOY_DIR="$BASE_DIR/$domain_app"
APP_DIR="$BASE_DIR/$DM_NAME/$domain_app"
PUBLIC_DIR="$BASE_DIR/$DM_NAME/public_html"
BACKUP_DIR="$BASE_DIR/archive"
SECRETS_DIR="$BASE_DIR/deploy_secrets"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# ----- Runtime binaries -----
# Hostinger/CloudLinux exposes PHP 8.5 here. The default CLI `php` may still be
# PHP 8.2, which cannot install this project's PHP 8.5 lock file.
PHP_BIN="${PHP_BIN:-/opt/alt/php85/usr/bin/php}"
COMPOSER_BIN="${COMPOSER_BIN:-$(command -v composer2 || command -v composer || true)}"
# ===============================
# 1. VALIDATE INPUT
# ===============================
if [ -z "$ZIP_FILE" ] || [ ! -f "$ZIP_FILE" ]; then
echo "? Please provide the ZIP file: ./deploy_home.sh alrahma_deploy.zip"
exit 1
fi
echo "?? Starting deployment of $ZIP_FILE..."
if [ ! -x "$PHP_BIN" ]; then
echo "? PHP 8.5 binary was not found or is not executable: $PHP_BIN"
echo " Set PHP_BIN=/path/to/php85 and rerun the deployment."
exit 1
fi
if [ -z "$COMPOSER_BIN" ] || [ ! -e "$COMPOSER_BIN" ]; then
echo "? composer2/composer was not found in PATH."
echo " Set COMPOSER_BIN=/path/to/composer2 and rerun the deployment."
exit 1
fi
echo "?? Using PHP: $("$PHP_BIN" -v | head -n 1)"
echo "?? Using Composer: $COMPOSER_BIN"
# ===============================
# 2. BACKUP EXISTING SITE
# ===============================
echo "??? Backing up current site..."
mkdir -p "$BACKUP_DIR"
cp -r "$PUBLIC_DIR" "$BACKUP_DIR/public_html_$TIMESTAMP"
cp -r "$APP_DIR" "$BACKUP_DIR/$domain_app_$TIMESTAMP"
# ===============================
# 3. BACKUP PERSISTENT FILES
# ===============================
echo "?? Backing up persistent files..."
mkdir -p "$SECRETS_DIR"
PERSIST_BACKUP="$BASE_DIR/persist_backup_$TIMESTAMP"
mkdir -p "$PERSIST_BACKUP"
# writable/
if [ -d "$APP_DIR/writable" ]; then
echo " ? Backing up writable/"
cp -r "$APP_DIR/writable" "$PERSIST_BACKUP/writable"
fi
# .env
if [ -f "$APP_DIR/.env" ]; then
echo " ? Backing up .env"
cp "$APP_DIR/.env" "$PERSIST_BACKUP/.env"
fi
# public/.htaccess backup is kept for rollback only. The new release file is
# deployed below so stale CSP/rewrite rules do not survive deployments.
if [ -f "$PUBLIC_DIR/.htaccess" ]; then
echo " ? Backing up public .htaccess"
cp "$PUBLIC_DIR/.htaccess" "$PERSIST_BACKUP/.htaccess"
fi
# public/index.php backup is kept for rollback only. The new release file is
# deployed below and then patched to point at the sibling app directory.
if [ -f "$PUBLIC_DIR/index.php" ]; then
echo " ? Backing up public index.php"
cp "$PUBLIC_DIR/index.php" "$PERSIST_BACKUP/index.php"
fi
# ===============================
# 4. CLEAN & EXTRACT NEW DEPLOY
# ===============================
echo "?? Cleaning old deployment..."
rm -rf "$DEPLOY_DIR"
unzip "$ZIP_FILE" -d "$BASE_DIR"
# ===============================
# 5. DEPLOY APP FILES
# ===============================
echo "?? Deploying new app files..."
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR"
cp -r "$DEPLOY_DIR"/. "$APP_DIR"
# ===============================
# 6. RESTORE PERSISTENT FILES
# ===============================
echo "?? Restoring persistent files..."
# writable/
if [ -d "$PERSIST_BACKUP/writable" ]; then
echo " ? Restoring writable/"
rm -rf "$APP_DIR/writable"
cp -r "$PERSIST_BACKUP/writable" "$APP_DIR/writable"
fi
# .env
if [ -f "$PERSIST_BACKUP/.env" ]; then
echo " ? Restoring .env"
cp "$PERSIST_BACKUP/.env" "$APP_DIR/.env"
fi
# ===============================
# 7. DEPLOY PUBLIC FILES
# ===============================
echo "?? Deploying public files..."
mkdir -p "$PUBLIC_DIR"
find "$PUBLIC_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -a "$APP_DIR/public/." "$PUBLIC_DIR/"
# ===============================
# 8. FIX index.php PATH
# ===============================
echo "?? Fixing index.php path..."
sed -i "s|require FCPATH . '../app/Config/Paths.php';|require FCPATH . '../alrahma/app/Config/Paths.php';|" "$PUBLIC_DIR/index.php"
# ===============================
# 9. UPDATE BASE URL & DB CONFIG
# ===============================
APP_CONFIG="$APP_DIR/app/Config/App.php"
DB_CONFIG="$APP_DIR/app/Config/Database.php"
echo "?? Updating configuration files..."
# Base URL
sed -i "s|public string \$baseURL = .*|public string \$baseURL = 'https://$DM_NAME/';|" "$APP_CONFIG"
# Database.php
sed -i "s|'hostname' => .*|'hostname' => '$DB_HOST',|" "$DB_CONFIG"
sed -i "s|'username' => .*|'username' => '$DB_USER',|" "$DB_CONFIG"
sed -i "s|'password' => .*|'password' => '$DB_PASS',|" "$DB_CONFIG"
sed -i "s|'database' => .*|'database' => '$DB_NAME',|" "$DB_CONFIG"
# ===============================
# 10. UPDATE db_connection.php
# ===============================
DB_CONN_FILE="$APP_DIR/app/db_connection.php"
if [ -f "$DB_CONN_FILE" ]; then
echo "??? Updating db_connection.php..."
sed -i "s|\$host = '.*';|\$host = '$DB_HOST';|" "$DB_CONN_FILE"
sed -i "s|\$dbname = '.*';|\$dbname = '$DB_NAME';|" "$DB_CONN_FILE"
sed -i "s|\$username = '.*';|\$username = '$DB_USER';|" "$DB_CONN_FILE"
sed -i "s|\$password = '.*';|\$password = '$DB_PASS';|" "$DB_CONN_FILE"
fi
# ===============================
# 11. FIX FPDF PATHS
# ===============================
echo "?? Fixing FPDF paths..."
grep -rl "ThirdParty\\\\fpdf\\\\fpdf.php" "$APP_DIR/app" | while read -r file; do
sed -i "s|ThirdParty\\\\fpdf\\\\fpdf.php|ThirdParty/fpdf/fpdf.php|g" "$file"
echo "? Fixed path in: $file"
done
# ===============================
# 12. COMPOSER INSTALL
# ===============================
echo "?? Installing production Composer dependencies with PHP 8.5..."
cd "$APP_DIR" || exit
export PATH="$(dirname "$PHP_BIN"):$PATH"
export COMPOSER_ALLOW_SUPERUSER=1
if "$PHP_BIN" "$COMPOSER_BIN" --version >/dev/null 2>&1; then
"$PHP_BIN" "$COMPOSER_BIN" install \
--no-dev \
--prefer-dist \
--optimize-autoloader \
--classmap-authoritative \
--no-interaction \
--no-progress
else
"$COMPOSER_BIN" install \
--no-dev \
--prefer-dist \
--optimize-autoloader \
--classmap-authoritative \
--no-interaction \
--no-progress
fi
"$PHP_BIN" spark --version
# ===============================
# 13. RESTORE PERMISSIONS
# ===============================
echo "?? Setting permissions..."
chmod 644 "$PUBLIC_DIR/.htaccess"
chmod 644 "$PUBLIC_DIR/index.php"
chmod -R 755 "$APP_DIR"
chmod 644 "$APP_DIR/app/db_connection.php"
chmod -R 777 "$APP_DIR/writable"
# ===============================
# ? DONE
# ===============================
echo "? Deployment completed successfully at $TIMESTAMP"
echo "?? Backup stored in: $BACKUP_DIR"
echo "?? Persistent files restored from: $PERSIST_BACKUP"
echo "?? Writable and .env restored; public .htaccess and index.php deployed from the new release."
+242
View File
@@ -0,0 +1,242 @@
#--------------------------------------------------------------------
# Example Environment Configuration file
#
# This file can be used as a starting point for your own
# custom .env files, and contains most of the possible settings
# available in a default install.
#
# By default, all of the settings are commented out. If you want
# to override the setting, you must un-comment it by removing the '#'
# at the beginning of the line.
#--------------------------------------------------------------------
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT = production
# Which profile to use if none is supplied to sendEmail()
# Options: default, communication, payment, ...
MAIL_PROFILE_DEFAULT=default
# ===== DEFAULT (system) =====
MAIL_DEFAULT_HOST=smtp.gmail.com
MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com
MAIL_DEFAULT_PASS="psnp emdq dykw ypul"
MAIL_DEFAULT_PORT=587
MAIL_DEFAULT_ENCRYPTION=tls
MAIL_DEFAULT_FROM_EMAIL=alrahma.sunday.school@gmail.com
MAIL_DEFAULT_FROM_NAME="Al Rahma Sunday School"
# Optional Reply-To (uncomment if you want a replyable inbox)
# MAIL_DEFAULT_REPLY_TO=communications@alrahmaisgl.org
MAIL_DEFAULT_REPLY_TO_NAME="No-Reply"
# Optional bounce address (envelope sender)
# MAIL_DEFAULT_RETURN_PATH=bounces@alrahmaisgl.org
# Optional DKIM
# MAIL_DEFAULT_DKIM_DOMAIN=alrahmaisgl.org
# MAIL_DEFAULT_DKIM_SELECTOR=default
# MAIL_DEFAULT_DKIM_PRIVATE=/path/to/private.key
# ===== COMMUNICATION =====
MAIL_COMMUNICATION_HOST=smtp.gmail.com
MAIL_COMMUNICATION_USER=alrahma.sunday.school@gmail.com
MAIL_COMMUNICATION_PASS="psnp emdq dykw ypul"
MAIL_COMMUNICATION_PORT=587
MAIL_COMMUNICATION_ENCRYPTION=tls
MAIL_COMMUNICATION_FROM_EMAIL=alrahma.sunday.school@gmail.com
MAIL_COMMUNICATION_FROM_NAME="School Communications"
MAIL_COMMUNICATION_REPLY_TO=alrahma.sunday.school@gmail.com
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
# MAIL_COMMUNICATION_RETURN_PATH=bounces@alrahmaisgl.org
# ===== PAYMENT =====
MAIL_PAYMENT_HOST=smtp.gmail.com
MAIL_PAYMENT_USER=alrahma.sunday.school@gmail.com
MAIL_PAYMENT_PASS="psnp emdq dykw ypul"
MAIL_PAYMENT_PORT=587
MAIL_PAYMENT_ENCRYPTION=tls
MAIL_PAYMENT_FROM_EMAIL=alrahma.sunday.school@gmail.com
MAIL_PAYMENT_FROM_NAME="School Payments"
# MAIL_PAYMENT_RETURN_PATH=bounces@alrahmaisgl.org
# ---- Legacy fallbacks (kept for compatibility) ----
SMTP_HOST = smtp.gmail.com
SMTP_USER = alrahma.sunday.school@gmail.com
SMTP_PASS = "psnp emdq dykw ypul"
SMTP_PORT = 587
SMTP_ENCRYPTION = tls # was SSL; set to ssl to match 587
# Reply-To for all mail profiles
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
# Optional sender directory you already use elsewhere
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
\"registration\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Register Office\"},\
\"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
# Principal notification recipient for TimeOff requests (comma-separated allowed)
PRINCIPAL_EMAIL="principal@alrahmaisgl.org"
MAIL_PARENT_REPORT_TO="alrahma.isgl@gmail.com"
MAIL_FROM_ADDRESS = "alrahma.sunday.school@gmail.com"
session.expiration = 43200
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
# If you have trouble with `.`, you could also use `_`.
app.baseURL = 'https://test.alrahmaisgl.org/'
# app.forceGlobalSecureRequests = true
# app.CSPEnabled = false
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
database.default.hostname = localhost
database.default.database = u280815660_school
database.default.username = u280815660_melabidi
database.default.password = >tNxlRzP/W8
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306
#--------------------------------------------------------------------
# CONTENT SECURITY POLICY
#--------------------------------------------------------------------
# contentsecuritypolicy.reportOnly = false
# contentsecuritypolicy.defaultSrc = 'none'
# contentsecuritypolicy.scriptSrc = 'self'
# contentsecuritypolicy.styleSrc = 'self'
# contentsecuritypolicy.imageSrc = 'self'
# contentsecuritypolicy.baseURI = null
# contentsecuritypolicy.childSrc = null
# contentsecuritypolicy.connectSrc = 'self'
# contentsecuritypolicy.fontSrc = null
# contentsecuritypolicy.formAction = null
# contentsecuritypolicy.frameAncestors = null
# contentsecuritypolicy.frameSrc = null
# contentsecuritypolicy.mediaSrc = null
# contentsecuritypolicy.objectSrc = null
# contentsecuritypolicy.pluginTypes = null
# contentsecuritypolicy.reportURI = null
# contentsecuritypolicy.sandbox = false
# contentsecuritypolicy.upgradeInsecureRequests = false
# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
# contentsecuritypolicy.autoNonce = true
#--------------------------------------------------------------------
# COOKIE
#--------------------------------------------------------------------
# cookie.prefix = ''
# cookie.expires = 0
# cookie.path = '/'
# cookie.domain = ''
# cookie.secure = false
# cookie.httponly = false
# cookie.samesite = 'Lax'
# cookie.raw = false
#--------------------------------------------------------------------
# ENCRYPTION
#--------------------------------------------------------------------
# encryption.key =
# encryption.driver = OpenSSL
# encryption.blockSize = 16
# encryption.digest = SHA512
#--------------------------------------------------------------------
# HONEYPOT
#--------------------------------------------------------------------
# honeypot.hidden = 'true'
# honeypot.label = 'Fill This Field'
# honeypot.name = 'honeypot'
# honeypot.template = '<label>{label}</label><input type="text" name="{name}" value=""/>'
# honeypot.container = '<div style="display:none">{template}</div>'
#--------------------------------------------------------------------
# SECURITY
#--------------------------------------------------------------------
# security.csrfProtection = 'cookie'
# security.tokenRandomize = false
# security.tokenName = 'csrf_token_name'
# security.headerName = 'X-CSRF-TOKEN'
# security.cookieName = 'csrf_cookie_name'
# security.expires = 7200
# security.regenerate = true
# security.redirect = false
# security.samesite = 'Lax'
#--------------------------------------------------------------------
# SESSION
#--------------------------------------------------------------------
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
# session.cookieName = 'ci_session'
# session.expiration = 7200
# session.savePath = null
# session.matchIP = false
# session.timeToUpdate = 300
# session.regenerateDestroy = false
#--------------------------------------------------------------------
# LOGGER
#--------------------------------------------------------------------
# logger.threshold = 4
#--------------------------------------------------------------------
# CURLRequest
#--------------------------------------------------------------------
# curlrequest.shareOptions = false
mail.protocol = smtp
mail.SMTPHost = ${SMTP_HOST}
mail.SMTPUser = ${SMTP_USER}
mail.SMTPPass = ${SMTP_PASS}
mail.SMTPPort = ${SMTP_PORT}
mail.SMTPCrypto = tls
mail.fromEmail = ${SMTP_USER}
mail.fromName = "Al Rahma Sunday School"
mail.mailType = text
mail.charset = UTF-8
# ---- Printer Mode (network, usb, windows, or disabled)
PRINTER_MODE=disabled
# ---- Network printer settings
PRINTER_HOST=192.168.1.100
PRINTER_PORT=9100
# ---- USB printer settings (used when PRINTER_MODE=usb)
PRINTER_USB_VID=0x0dd4
PRINTER_USB_PID=0x0285
PRINTER_WINDOWS_NAME="CUSTOM P3L"
# ---- Formatting
# 80mm paper ~ 48-56 characters per line
PRINTER_CHARS_PER_LINE=48
PRINTER_FEED_LINES=3
+1 -1
View File
@@ -24,7 +24,7 @@ RedirectMatch 404 /\.git
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=()"
Header always set Content-Security-Policy "default-src 'self';"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://code.jquery.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com; style-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com; style-src-attr 'self' 'unsafe-inline'; font-src 'self' data: https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self';"
</IfModule>
# Disable Apache signature
+1 -1
View File
@@ -6,7 +6,7 @@
*---------------------------------------------------------------
*/
$minPhpVersion = '8.1'; // If you update this, don't forget to update `spark`.
$minPhpVersion = '8.5'; // If you update this, don't forget to update `spark`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
$message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
+1 -1
View File
@@ -35,7 +35,7 @@ if (str_starts_with(PHP_SAPI, 'cgi')) {
*---------------------------------------------------------------
*/
$minPhpVersion = '8.1'; // If you update this, don't forget to update `public/index.php`.
$minPhpVersion = '8.5'; // If you update this, don't forget to update `public/index.php`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
$message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
-66
View File
@@ -1,66 +0,0 @@
Cron ideas:
Run every night (safe):
0 2 * * * php /path/to/spark attendance:auto-publish >> /var/log/attendance-autopublish.log 2>&1
Or run every Sunday at 23:59 school time.
//attendance tracking for non reported only,
ignore all breaks and school cancelation from weeks calculation analysis
1 not reported absence => send email automated without involving team intervention
2 absences in row or in 3 consecutive weeks, notify the team to call parent and send email, yellow color code
3 absences in row or in 4 consecutive weeks, notify the team to call parent and send email, orange color code
4 absences in row or in 5 consecutive weeks, notify the team to notify parent about student expeled send email, red color code.
2 lates in row or in 3 consecutive weeks, send email automated to parent, color code light Blue
3 lates in row or in 4 consecutive weeks or 2 Lates and 1 Absence, notify the team to call parent and send email, color code dark Blue.
4 lates in row or in all passed weeks whole school-year, notify the team to call parent and send email, color code Red.
//attendance emails:
Assalamu Alaikum parents Br. Talha Kamran and Sr. Zaheen Asia,
Insha Allah this email will find you well. ahmed ahmed to you and your family. Thank you very much for taking the time to speak with us. This is just a reminder that ahmed ahmed was absent 2 times in a row without notice. Any student that has to be absent due to illness or family commitment, that's OK as long as the school is given prior notice. Please let us know ahead of time. You can always email us/call us/text us at 978-364-0219 to let us know in advance if your child will be absent/late.
Kind Regards,
-Al Rahma Admin
///////////////////////////////////
Assalamu Alaikum parents Br. Elias Fahfouhi and Sr. Angelica Fahfouhi,
Insha Allah this email will find you well. Thank you very much for taking the time to speak with us. This is just a reminder that ahmed ahmed were absent 2 times in the last 3 weeks without notice. If your child has to be absent due to other commitments or illness, that's OK as long as the school is given prior notice. Please let us know ahead of time. You can always email us or call us/text us at 978-364-0219 to let us know in advance if your child will be absent/late.
Kind Regards,
-Al Rahma Admin
////////////////////////
Assalamu Alaikum parents Br. Talha Kamran and Sr. Asia Zaheen,
Insha Allah this email will find you well. We tried getting in touch with you but unfortunately we couldn't reach you, and left a voice message at 978-893-9703. This is just a reminder that ahmed ahmed was absent once and late twice over the last three school days without notice. Students have to be in class by 10 AM. If your child has to be absent or late due to illness or family commitment, that's OK as long as the school is given prior notice. Please let us know ahead of time. You can always email us/call us/text us at 978-364-0219 to let us know in advance if your child will be absent/late.
Kind Regards,
-Al Rahma Admin
////////////////////////
Assalamu Alaikum parents Br. Mubiru Sheba and Sr. Sylbert Jackson,
Insha Allah this email will find you well. Thank you very much for taking the time to speak with us. This is just a reminder that ahmed ahmed was absent 2 times in a row without notice. If your child has to be absent due to illness or family commitment, that's OK as long as the school is given prior notice. Please let us know ahead of time. You can always email us/call us/text us at 978-364-0219 to let us know in advance if your child will be absent/late.
Kind Regards,
-Al Rahma Admin
Christy Wyatt
259
STU2500259
Zaineb
Elmerse
christy.lee.wyatt@gmail.com