Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[5.2.1] - 2026-09-03
[5.2.1]: https://github.com/codesaur-php/Raptor/compare/v5.2.0...v5.2.1
Added
- Signing in returns the admin to the page they originally requested. Opening a dashboard URL while signed out (a bookmarked
/dashboard/news, a link from a Discord notification, an expired session) redirected to/dashboard/login, and after signing in the admin always landed on the home page and had to navigate back by hand.JWTAuthMiddlewarenow sends the original path and query string along as/dashboard/login?redirect=/dashboard/news/view/12?tab=comments,LoginControllerreads it into the login form'sdata-redirectattribute (renderLogin()), andlogin.htmlnavigates there after a successful sign-in - falling back to thehomeroute when absent. An already-authenticated visit to/dashboard/login?redirect=...(LoginController::index()) also honors the target instead of always going home. The value is sanitized byLoginController::sanitizeRedirectTarget()(open-redirect guard, covered bytests/Unit/Authentication/LoginRedirectTargetTest.php): it must be a same-origin relative path under the dashboard mount (script path +/dashboard), so absolute URLs, protocol-relative//hostforms, backslashes, CR/LF (header injection) and paths outside the dashboard are dropped, and login pages themselves are refused to avoid a redirect loop. The middleware only records a target for browser page loads (GET/HEAD withAccept: text/html) - afetch()-ed JSON or fragment route that hits an expired session must not become the post-login landing page - and skips the dashboard root / home since they are the default destination anyway. The login URL no longer carries over the original request's unrelated query string (previously/dashboard/news?page=2redirected to/dashboard/login?page=2); it now carries onlyredirect.
Fixed
- CI workflow failed on the PHP syntax check and merge-marker steps because they scanned a
publicfolder that does not exist..github/workflows/ci.ymlstill referenced the document root by its formerpublicname; both steps now scanapplicationandpublic_html, sofindno longer exits non-zero and the conflict-marker grep covers the actual entry point and assets.
[5.2.0] - 2026-08-10
[5.2.0]: https://github.com/codesaur-php/Raptor/compare/v5.1.1...v5.2.0
Added
- Dashboard sidebar shows the project version and its last-updated date to signed-in admins. Collaborators who cannot reach the live server can verify a deploy landed by checking the version in the sidebar - previously there was no way to tell from the UI. The version's single source of truth is the new
extra.versionvalue incomposer.json(set to5.2.0), bumped manually by the developer;composer.jsonreaches the server on every deploy path, so no extra deploy machinery is involved. It lives underextrarather than the rootversionfield on purpose: a rootversionon a Packagist-published package must match every release git tag (Packagist silently skips mismatching tags) and failscomposer validate --strict, whileextracarries no such constraints.DashboardTrait::dashboardTemplate()reads the value intoraptor_version- along withraptor_name(the basename ofcomposer.json'sname, vendor prefix stripped, so every downstream project shows its own name) andextra.modifiedintoraptor_modified(an explicit "last modified" date maintained beside the version and bumped together with it) - and the sidebar bottom shows a single dimmed right-aligned{name} {version} | {date}line (.sidebar-versionindashboard.html, styled indashboard.css?v=5; the sidebar menu now stretches to full viewport height on desktop so the line sticks to the actual bottom). The block renders nothing whenextra.versionis absent, and the display can be turned off by commenting out the$dashboard->set(...)lines. CLAUDE.md documents the bump rule for AI coding sessions: the framework repo bumps per release (kept equal to the release tag), while a downstream project repo patch-bumpsextra.versionin the same commit as any change - without being asked - so non-technical admins can verify from the sidebar that a delegated change reached the live server. Following the version-disclosure best practice (WordPress/Grafana/GitLab), the version renders only inside the authenticated dashboard layout - nothing is exposed on the login page or the public web.
Changed
.envis now auto-created oncomposer install/composer update, not only oncomposer create-project. The.envbootstrap moved frompost-root-package-installinto a sharedsetup-envComposer script (bin/setup-env.php) wired topost-root-package-install,post-install-cmdandpost-update-cmd, so agit clone+composer installyields a working.envwith a fresh secret just likecreate-projectdoes. The logic lives in a script file rather than inline@php -rcode on purpose: Composer runs scripts through the OS shell, and on Unix the shell expands$variablesinside the double-quoted-rargument to empty strings, breaking the PHP code with a parse error (the Windows shell leaves$untouched, which is why the same inline code worked locally but failed on Linux CI/deploy). The cPanel deploy scaffold examples (docs/conf.example/.cpanel.yml.example,auto-deploy.sh.example) copy an enumerated folder list, so both now clean-syncbin/to the server as well - without it, the server-sidecomposer installwould fail onpost-install-cmdbecausebin/setup-env.phpis missing at the deploy path. The script copiesdocs/conf.example/.env.exampleto.envonly when.envis missing, and skips the copy entirely when the example file itself is absent (downstream projects do not always ship it).RAPTOR_JWT_SECRETis generated only when the value is missing, empty, or still the.env.exampleplaceholder - an existing real secret is never rotated, since rotating it on everycomposer updatewould invalidate all issued JWTs and log every user out. The generator also no longer echoes the secret value to the console (it previously printed the full secret, which would leak into CI/deploy logs) - it prints a plain notice instead.
[5.1.1] - 2026-08-05
[5.1.1]: https://github.com/codesaur-php/Raptor/compare/v5.1.0...v5.1.1
Fixed
- Password reset tokens were generated with
uniqid()- predictable and guessable within the reset window.LoginController::forgot()built theforgot_passwordtoken as\uniqid('forgot')- 13 hex characters derived solely from the server clock (Unix seconds + microseconds), which PHP explicitly documents as not suitable for security purposes. The reset link endpoint has no per-guess throttle (the cooldown only limits requesting new tokens), so an attacker who can roughly time a victim's reset request could enumerate the small microsecond search space within the token's validity window (RAPTOR_PASSWORD_RESET_MINUTES, default 10 minutes) and reset the victim's password. The token is now\bin2hex(\random_bytes(32))- the same cryptographically secure 64-hex-character format already used for the CSRF token and the signup email-verification token. No schema change needed (forgot_passwordisvarchar(255)), and outstanding old-format tokens simply keep working until they expire.
[5.1.0] - 2026-07-23
[5.1.0]: https://github.com/codesaur-php/Raptor/compare/v5.0.0...v5.1.0
Changed
- Admin signup requests modal now lists every signup request, including email-unverified ones.
UsersController::requestsModal('signup')previously filteredverified_at IS NOT NULL, so requests whose confirmation email was never clicked were invisible to admins - they sat in thesignuptable (blocking their UNIQUE username/email) with no way to review or clean them up. The modal now loads all rows andsignup-index-modal.htmlshows a fourth status badge:unverified(pending +verified_atempty) alongsidepending/approved/rejected. Unverified requests get no Accept button (double opt-in stays mandatory;signupApprove()keeps its server-side 403 guard for unverified email) but can be rejected - and a rejected row can then be permanently deleted to Trash, freeing the name/email for a new request. The users manual (MN/EN) is updated to document the four states, replacing badge names (waiting/deactivated) that had drifted from the implementation.
Fixed
- Error toasts rendered as blue info instead of red -
'error'is not a validNotify()type.Notify()indashboard.jsaccepts onlysuccess/danger/warning/primary; any other value silently falls back to the cyan info style. Six template call sites (news-view.htmlx2,comments-view.html,devrequest-create.html,devrequest-view.html,trash-index.html) passed'error'directly, andRBACController::setRolePermission()'s catch block responded with'type' => 'error'whichrbac-alias.htmlfeeds straight intoNotify(response.type ?? 'warning', ...)- all seven now use'danger'. The valid-type rule (including for server'type' =>values consumed byNotify(response.type ...)) is documented in theNotify()docblock and CLAUDE.md UI Conventions. - RSS feed generation spammed
code.logwith PHP 8.1+ deprecations and emitted 1970 pubDates whenpublished_atwas NULL. Rows published before thepublished_atcolumn existed (legacy data) carry NULL there;SeoController::rss()passed that straight intostrtotime()in both the merge-sort comparator and the<pubDate>builder - every feed request logged a "Passing null to parameter" deprecation per NULL row, andgmdate()over the resultingfalseproducedThu, 01 Jan 1970publication dates in the feed. Both call sites now guard NULL (and unparseable values): the sort treats missing dates as 0 so those items order last, and<pubDate>falls back to the current time instead of 1970. The sitemap'slastmodlines were already NULL-safe via?? 'now'and are unchanged. - Web visit statistics cache died with 'Data too long' (1406) on real traffic - MySQL
web_log_cachecolumns upgraded TEXT -> MEDIUMTEXT. A single day's aggregated JSON (news_dataetc.) exceeds 400KB on a busy site, overflowing the 64KB TEXT limit, sorefreshCache()failed on insert and the stats cache stopped advancing (on hosts without strict mode the data was silently truncated to corrupt JSON instead). The MySQLCREATE TABLEinWebLogStats::ensureCacheTable()now uses MEDIUMTEXT (16MB) for all seven*_datacolumns, so fresh installs are correct from the start. Already-deployed MySQL/MariaDB databases need a one-time migration:ALTER TABLE web_log_cache MODIFY actions_data MEDIUMTEXT DEFAULT NULL, MODIFY pages_data MEDIUMTEXT DEFAULT NULL, MODIFY news_data MEDIUMTEXT DEFAULT NULL, MODIFY products_data MEDIUMTEXT DEFAULT NULL, MODIFY orders_data MEDIUMTEXT DEFAULT NULL, MODIFY ips_data MEDIUMTEXT DEFAULT NULL, MODIFY ua_data MEDIUMTEXT DEFAULT NULL;. PostgreSQL is unchanged - its TEXT type is unlimited, so no migration is needed there. The MySQLCREATE TABLEalso now takes its charset/collation fromRAPTOR_DB_CHARSET/RAPTOR_DB_COLLATION(sameutf8mb4/utf8mb4_unicode_cifallbacks asDatabaseConnection) instead of a hardcodedCHARSET=utf8mb4with no COLLATE - previously the table silently took the server's default collation for that charset, which could differ from every other table in the database. - motable showed "no data in the table" while an AJAX table was still loading. The constructor unconditionally ran
setBody()->setReady(), which overwrote the initial "Loading table ..." label with the empty-table label whenever the tbody had zero rows - exactly the state every AJAX-loaded index table (news, users, products, orders, ...) is in between construction and the fetch response, misleading admins into thinking the table was empty. The constructor now computes the state immediately only when the table already has a<tbody>element; a table without<tbody>keeps the loading label untilsetBody()/setReady()/error()is called. This convention (AJAX-loaded tables omit<tbody>from markup, server-rendered tables always write the tag - even when the row loop produces nothing) already held across all shipped templates and is now documented in CLAUDE.md UI Conventions plus a bilingual coupling comment inmotable.js. Asset reference bumped tomotable.js?v=1indashboard.html. - Signup form never sent its Turnstile token - every signup failed with 400 when
RAPTOR_TURNSTILE_SECRET_KEYwas configured. The Turnstile widget lives inside the#registerform, but the signup submit JS builds its JSON payload by hand (not viaFormData), so the hiddencf-turnstile-responseinput never reached the server andLoginController::spamCheck()always verified an empty token.login.htmlnow includes'cf-turnstile-response'in the signup payload, andLoginController::signup()unsets the field beforeSignupModel->insert()(thesignuptable has no such column). - Turnstile widgets were not reset after AJAX submits - retrying after a server error (or reusing the form after success) always failed siteverify. Turnstile tokens are single-use and
form.reset()does not reset the widget. All fetch-based forms carrying acf-turnstilewidget (contact form incontact.html, comment form innews.html, review form inproduct.html, signup form inlogin.html) now callwindow.turnstile.reset()in their.finally()blocks.order.htmlis a plain browser POST (full page reload re-renders the widget), so it needs no reset. - Every forgot password request displayed as "used" in the admin requests modal. The status badge in
forgot-index-modal.htmlchecksrow['is_active'] == 0first, butForgotModelhad nois_activecolumn - so the expression wasnull == 0(true) for every row, including brand-new requests. Theis_activecolumn (tinyint, default 1) is restored onForgotModel, and the reset flow inLoginController::setPassword()now consumes the token withdeactivateById()instead ofdeleteById()- so all three states (used / expired / ready) display correctly. Token lookups (forgotPassword(),setPassword()) filteris_active=1so a used token stays invalid, and the resend cooldown counts only active requests (used tokens no longer block a new request, matching the old delete behavior). Already-deployed databases need a migration:ALTER TABLE forgot ADD COLUMN is_active TINYINT NOT NULL DEFAULT 1;(MySQL/MariaDB) /ALTER TABLE forgot ADD COLUMN IF NOT EXISTS is_active SMALLINT NOT NULL DEFAULT 1;(PostgreSQL); fresh installs create the column automatically.
[5.0.0] - 2026-07-08
[5.0.0]: https://github.com/codesaur-php/Raptor/compare/v4.5.2...v5.0.0
Changed
- BREAKING:
application/raptor/merged intoapplication/dashboard/-application/now holds exactly two apps,dashboard/andweb/. The separate "framework core" layer is gone: all former raptor modules (authentication, content, exception, localization, log, mail, migration, notification, organization, rbac, template, trash, user, plus the root Controller/middleware/DatabaseConnection files) now live inapplication/dashboard/, and theRaptor` PHP namespace is renamed toDashboard` across the whole tree (application code, tests,public_html/index.php, composer PSR-4 maps).Raptor\ApplicationandDashboard\Applicationare merged into a single concreteDashboard\Applicationthat registers the full middleware pipeline and all sixteen routers. Nothing changes at runtime - routes, permissions, DB tables, env variables (RAPTOR_*) and thecodesaur/raptorpackage name stay as they are - but any downstream project importingRaptor` classes must rename its imports. Rationale: aftercomposer create-project` the whole tree is the developer's project code, and the core-vs-app split kept suggesting otherwise - customization guidance across code comments and docs now says to edit files directly instead of subclassing controllers or overriding routes. - Critical warning comments are now bilingual (Mongolian + English). Comments guarding critical invariants - hidden coupling between distant files (TemplateService cache key vs ReferencesController invalidation, LogsController OFFSET vs dashboard.js infinite-scroll stop, MenuSeed vs Permissions::RESERVED), ordering rules (middleware registration order, handle()-outside-try), and security rules (JWT secret with no default, log masking, username enumeration, /protected/* anonymous fall-through, root-user RBAC guards, badge mount-naive map keys) - now carry an English version below the Mongolian text so non-Mongolian-reading contributors cannot miss them. Ordinary comments stay single-language; the rule is documented in CLAUDE.md Documentation Style.
- Bootstrap CDN upgraded 5.3.6 -> 5.3.8 (latest 5.3.x patch; SRI integrity hashes updated to the official values) across
dashboard.html,login.html,login-reset-password.html, webindex.htmlandpage-404.html. Bootstrap Icons stays at 1.13.1 (still latest). RAPTOR_SIGNUP_VERIFY_HOURSdefault raised from 48 to 72 hours (3 days). The signup email verification link now stays valid for 3 days when the env variable is not set;.env.exampleupdated to match. Explicitly configured environments are unaffected.- Exception handlers no longer log 404s in production.
ErrorHandler,JsonExceptionHandlerand the webExceptionHandlernow skiperror_log()for 404s unlessCODESAUR_DEVELOPMENTis on - unknown-route hits are overwhelmingly bot scans and were bloatinglogs/code.logwith noise. Bot traffic remains fully visible in the web server's access log (Apache/nginxaccess.log, hosting panel Raw Access Logs) with IP and User-Agent. - File management consolidated into one module:
application/dashboard/file/(Dashboard\File). Thecontent/filemodule (FileController,FilesController,FilesModel+ the file modal templates) moved up out ofcontent/and merged with theprotected/module (ProtectedFilesController,ProtectedRouter- the latter renamed toFileRouteras the file module now owns it) - general upload/attachment handling and protected file serving are one concern. NamespacesDashboard\Content\File*andDashboard\Protected*becameDashboard\File*; routes, route names, permissions and the{table}_filestables are unchanged. The /files management routes moved fromContentsRouterintoFileRouter(paths and route names unchanged), so the module owns all its routes.FilesModel's docs were generalized: it is a companion table for attaching files to any model's records ({table}_filesviasetTable()), not just the shipped content tables. - Badge org-scoping: record-level filtering and system-wide viewers. For modules listed in
BadgeController::orgScopedModules()the count query now filters byCOALESCE(record_organization_id, auth_user.organization_id)in the log context - a tenant-scoped module's controller can log the record's owning organization asrecord_organization_id(same convention asrecord_id), so the badge reaches the record's organization even when the change was made by an admin logged into another organization (previously only the actor's org was consulted, so a system-org admin editing org B's record never badged org B's users). Entries with neither key (old logs, web frontend) still count for all admins. NewisSystemWideViewer()hook (default: current organization is the system primary org,id=1) joinssystem_coderin bypassing scoping entirely - the same user sees all organizations' activity while switched into the system organization and only their own org's while switched into a common one. auto-deploy.shlogs connectivity outages as one line down, one line up. When GitHub was unreachable, every cron run appended a fresh fetch error and the log grew without bound. The scaffold now keeps an offline marker file (/tmp/PROJECT-auto-deploy.offline): the first failed fetch writes a singlegit fetch амжилтгүй...line, subsequent failures stay silent, and the first successful fetch writes a singlegit fetch сэргэв...line and clears the marker - the quiet gap between the two lines is the outage duration.
Removed
Application::overrideDashboardLayout(), thedashboard_layoutsrequest attribute andDashboardTrait::layout(). The dashboard layout templates (dashboard.html,alert-no-permission.html,modal-no-permission.htmlinapplication/dashboard/template/) are project code - edit them in place; the indirection layer (and itsDashboardLayoutOverrideTest) is gone.DashboardTraitnow resolves the three layout files directly via__DIR__.- Subclass/route-override customization guidance removed from code comments and docs. BadgeRouter/BadgeController, FileRouter/ProtectedFilesController, the exception handlers,
Web\Applicationand the module guides (CLAUDE.md, READMEs, api.md) no longer present "subclass the controller and override the route" as a customization path - the documented path is editing the module in place.
[4.5.2] - 2026-07-07
[4.5.2]: https://github.com/codesaur-php/Raptor/compare/v4.5.1...v4.5.2
Fixed
- SSH deploy silently ignored its entire
exclude:list - every deploy would have wiped server-side runtime data. Theburnett01/rsync-deploymentsaction has noexcludeinput, so the list the workflow passed was discarded (GitHub Actions only emits an "unexpected input" warning) and the job ran plainrsync -avz --delete. That both uploaded repo-only files (.git,docs/,tests/,phpunit.xml) to the server and, far worse, deleted every server-generated file the repo does not track: uploaded images underpublic_html/public/, protected files,logs/,cache/and the per-environment migration audit trail underdatabase/migrations/. All filters now live inswitchesas native rsync rules, ordered include-before-exclude (rsync is first-match-wins): guard-file--includes first, then anchored content excludes in the--exclude=/folder/*form - which transfers the folder itself (so it is created on a fresh server) while leaving its contents out of both the transfer and--delete(rsync never deletes excluded files without--delete-excluded). - Name-based deploy excludes also excluded the
application/dashboard/protected/module. FTP's**/protected/**glob and robocopy's bare-name/XD protectedmatch a directory calledprotectedat any depth, soProtectedFilesController.php/ProtectedRouter.phpnever reached the server - fatal on a fresh deploy, sinceDashboard\ApplicationregistersProtectedRouterunconditionally. The FTP job no longer excludes runtime folders at all (see Changed), and the robocopy/XDentries for runtime folders are now full source+destination path pairs ("%GITHUB_WORKSPACE%\protected" "%DEPLOY_PATH%\protected") - the source side keeps the folder out of the transfer, the destination side keeps/MIRfrom deleting the server's copy, and nested module folders are untouched. Verified with a full robocopy simulation (runtime files survive/MIR, the module deploys, stale files still get mirrored away). logs/was never created by the FTP/SSH/Windows deploy paths, silently losing the PHP error log. The workflow header claimed "logs/ is auto-created on the server" but nothing creates it -public_html/index.phppointserror_logatlogs/code.logand PHP does not create missing directories, so on a deployed server without the folder every logged error vanished without trace. All three paths now deploy the runtime folders themselves (with their.htaccessguard files) while leaving their contents alone; the false header note is replaced with the real semantics.cache/was wiped on every SSH/Windows deploy. It appeared in no exclude list, sorsync --delete/robocopy /MIRmirrored it back to the two guard files the repo tracks on every run - a needless cache cold start (and a behavior the FTP path did not share). It is now protected like the other runtime folders on all three paths.
Changed
- Runtime folders unified: one gitignore pattern, one deploy semantic.
cache/,logs/andprotected/now each carry the same self-contained.gitignore(*,*/,!.gitignore,!.htaccess) plus adeny from all.htaccess-protected/gets its own.gitignore(its block moves out of the root.gitignore),logs/gains the.htaccessit was missing. The deploy semantic across all three workflow paths (pluspublic_html/public/anddatabase/migrations/, which fall in the same class): the folder and its guard files deploy and are created on the server; the runtime contents (cache entries, logs, uploads, migration SQL) are never uploaded, overwritten or deleted. Each path implements this with its own mechanism, so the three filter lists intentionally differ: FTP relies on the action's state-file (it never deletes files it did not upload, so runtime folders need no exclude at all), rsync uses include-before-exclude/folder/*rules insideswitches, robocopy uses/XDsource+destination path pairs plus small non-/MIRguard-file copies. The workflow header documents this so the lists do not get "harmonized" back into a bug.
[4.5.1] - 2026-07-07
[4.5.1]: https://github.com/codesaur-php/Raptor/compare/v4.5.0...v4.5.1
Changed
- Directory trees in the docs de-duplicated to a single app-level diagram. The root
README.md"Directory Structure" tree is now the only tree, trimmed to the app level (no per-module enumeration);docs/mn/README.mdanddocs/en/README.mdreplace their deep trees with a short pointer to it (module locations stay documented in each module's own section 6 subsection), andCLAUDE.mddrops its module lists too. The router enumeration in the root README "Quick Architecture" block is generalized the same way (Routers (one per feature module, registered in Application.php)instead of naming each router), and the docs READMEs' table-of-contents entry for section 6 drops its module-name list and hardcoded subsection ranges (6.1-6.13 Core | 6.14-6.29 Shop, ...) - the numbering shifted on every module addition, making it the most drift-prone line of all. Deep trees duplicated in four files rotted silently on every module move (the 4.5.0 development-module relocation left the root README tree showingdevelopment/underraptor/) - one shallow tree has nothing left to drift.
[4.5.0] - 2026-07-07
[4.5.0]: https://github.com/codesaur-php/Raptor/compare/v4.4.1...v4.5.0
Added
- Topbar organization switcher. The old standalone
/organization/user/listpage (OrganizationUserController+organization-user.html) is replaced by a dropdown on the topbar brand area, shown when the user has more than one organization.DashboardTrait::getUserOrganizations()builds the list (all active organizations forsystem_coder, membership-only for everyone else; the currently selected organization is always included; organizationid=1always sorts first when present, the rest by name). The dropdown gets a search filter, result counter and scrollable list when there are more than 10 organizations (initOrgSwitcher()indashboard.js; the filter hides items with inlinedisplay:none !importantbecause Bootstrap'sd-flexon the items would otherwise win). Assets bumped todashboard.css?v=4/dashboard.js?v=6(one?v=increment from v4.4.1 for the whole release). User::hasRoleAlias(string $alias). Returns true when the user holds any role under the given alias (role keys are{alias}_{name}). Lets multi-tenant apps gate cross-organization visibility by role alias.- Hardcoded sidebar home link.
dashboard.htmlsidebar now starts with a permission/alias-independent "Dashboard" link ('home'|link,bi-speedometer2). - cPanel Git deploy scaffold. Templates (
docs/conf.example/.cpanel.yml.example,docs/conf.example/auto-deploy.sh.example) and developer guide (docs/mn/CPANEL.md) for cPanel Git + cron auto-deploy - a fallback for SSH-less shared hosting the.github/workflows/deploy.ymljobs cannot reach:.cpanel.ymltask list, self-update-safeauto-deploy.sh(flock, CLI-SAPI php lookup,composer installon lock change,composer dump-autoload -oon autoload-onlycomposer.jsonchange), placeholder table and the two-phase sequencing rule for changing the deploy script itself. The READMEs present this scaffold as deploy path D - an explicit last-resort fallback listed after the A) FTP / B) SSH / C) Windows-runner workflow jobs, not under the server config examples table, so being hosted on cPanel is not read as "must use the CPANEL.md scaffold" (a real-world D case: the National Data Center of Mongolia shared hosting for government portals, where no workflow job can reach the server). - Global search now covers organizations, dev-requests, messages, comments and reviews in addition to news, pages, products, orders and users. Every new source keeps the permission invariant (gated exactly like its module's index page): organizations ->
system_organization_index, messages/comments ->system_content_index, reviews ->system_product_index, dev-requests -> visible to any authorized user but limited to own/assigned rows withoutsystem_development(mirrorsDevRequestController::list()). Click targets follow each module's own view style: dev-requests open their view page, comments jump to the news page#commentsanchor (vianews_id), organizations and messages load their view modal inside#static-modal(newmodal: trueflag inSOURCE_META), reviews link to the reviews index (no per-record view). - Topbar redesigned: quick icons (search | language | theme), a flat profile link and a logout button with confirmation replace the expanding search input, the "Language & Options" modal and the user dropdown. Search opens a centered modal (
#global-search, also via Ctrl+K / Cmd+K) with the global record search - sameSearchControllerendpoint, debounced, grouped with source badges, arrows + Enter keyboard navigation, Esc closes (initGlobalSearch()replacesinitTopbarSearch()indashboard.js). If an app has not registered the search route (|linkresolves to'#'),initGlobalSearch()removes the search icon and exits; individual results whose view-route pattern resolves to'#'(module removed while search kept) are hidden instead of linking nowhere. Language is a dropdown listing the active languages with flag icons via flagcdn.com (enmaps to theusflag, same convention the old modal used; current one checked; selection is session-persisted via the existinglanguageroute, then reload) - hidden when only one language is active. Theme is a light/dark dropdown applied instantly vialocalStorage+data-bs-theme, no reload (initTopbarQuick()indashboard.js). The avatar + name link straight to the admin's own profile page (user-updateroute; tooltip carries the newmy-profilekeyword) - no dropdown in between. Logout is an icon-only button after a second.topbar-sepseparator; because it is a plain GET link one accidental click away, it always asks for confirmation (initLogoutConfirm()): a Bootstrap modal (#logout-confirm-modalindashboard.html) when Bootstrap JS is available, falling back to nativeconfirm()when the CDN failed to load - logout stays functional even fully offline, and custom dashboard layouts that keep the shipped logout button must also keep the modal markup (or accept the fallback). Spacing in the actions area is flex-gap based with optical compensation on the separators (icon buttons carry ~.5rem of invisible hit-area), so both separators sit visually centered. The organization name truncates with an ellipsis (.topbar-brand-name, 45vw / 30vw below 576px) so long names never squeeze the quick icons or wrap the topbar on small screens; below 576px spacing tightens. All topbar{{ ... }}interpolations of admin-editable text (organization name, user name, menu titles, language title) are|e-escaped, and the theme toggle + search modal carryaria-label/role. New translation keywordstheme-dark/theme-light/theme/my-profile/confirm-logout(deployed systems need a migration insert). Application::overrideDashboardLayout()- swap the dashboard layout from your own Application. The three layout templatesDashboardTraitrenders internally (dashboard.html,alert-no-permission.html,modal-no-permission.html) can now be replaced from the developer's own Application constructor:$this->overrideDashboardLayout('dashboard.html', __DIR__ . '/template/dashboard.html'). Same explicit-override philosophy as routeroverride()- the override is visible in the bootstrap. The map is injected as thedashboard_layoutsrequest attribute inRaptor\Application::handle()and resolved by the newDashboardTrait::layout()helper; registration fail-fasts withInvalidArgumentExceptionwhen the custom file does not exist. Pages with their own route (login etc.) are intentionally out of scope - override their route instead. Covered bytests/Unit/Template/DashboardLayoutOverrideTest.php.
Changed
- Sidebar badge system moved into the dashboard app layer, and made multi-tenant aware.
BadgeController,BadgeRouterandAdminBadgeSeenModelmoved fromapplication/raptor/template/(Raptor\Template) toapplication/dashboard/badge/(Dashboard\Badge; new PSR-4 map;BadgeRouterregistration moved fromRaptor\ApplicationtoDashboard\Application). Route names (dashboard-badges) and the JS/CSS are unchanged. This putsBADGE_MAP/PERMISSION_MAP/ badge behavior next to the rest of the code a project is most likely to customize. New:BadgeController::orgScopedModules()(default[]) - modules it lists have their badges scoped to the viewing admin's current organization (so org A's activity no longer badges an org B admin), via a newcontext.auth_user.organization_idrecorded in every log entry byController::log(). NULL-org log rows (old data / web-frontend) still count for all admins (backward-compatible), andsystem_coderbypasses scoping as a cross-tenant superuser. The shipped content modules are global (noorganization_id) so the default list is empty; multi-tenant apps list theirs inorgScopedModules()(edit directly, or override in a subclass). - Framework file cache moved from
protected/cache/to a dedicated top-levelcache/directory (outside the document root, sibling oflogs/, kept in git via its own.gitignorewith contents ignored). Runtime-generated cache is framework state, not user content, so it no longer shares the/protectedfolder thatProtectedFilesControllerserves.ContainerMiddlewarenow points thecacheservice atdirname(SCRIPT_FILENAME, 2) . '/cache'. As a consequenceProtectedFilesControllerlost its cache-specificread()/setFolder()guards (the cache is no longer under/protected, and hardcoding a blockedcachesubfolder in a generic protected-files controller was a latent footgun for any project whose protected files legitimately include a folder namedcache). Existing deployments regenerate cache automatically (12h TTL + explicit invalidation); the oldprotected/cache/can be deleted. system_codercross-tenant access is now derived from the role, not materialized as data.JWTAuthMiddlewareloads RBAC before the organization check and branches: coders only need the target organization to exist and be active; regular users still need anorganizations_usersmembership row.LoginController::selectOrganization()no longer auto-inserts a membership row when a coder switches into a foreign organization - previously those rows accumulated as fake memberships and, worse, outlived thesystem_coderrole itself (removing the role no longer revoked the access it had silently granted). Existing deployments should clean up previously auto-inserted rows with a one-off migration (DELETE FROM organizations_users WHERE organization_id <> 1 AND user_id IN (SELECT ur.user_id FROM rbac_user_role ur INNER JOIN rbac_roles r ON ur.role_id = r.id WHERE r.alias = 'system' AND r.name = 'coder')).ProtectedFilesControllermoved toapplication/dashboard/protected/(Dashboard\Protected) with an overridableauthorizeRead()hook. The class serves files from the document-root-external/protectedfolder viaGET /dashboard/protected/file(ProtectedRouter, registered inDashboard\Application). No shipped module uses protected storage (every module writes to/publicviaFileController::setFolder()), so it was relocated fromapplication/raptor/content/file/toapplication/dashboard/protected/(newDashboard\Protected` PSR-4 map; route moved out ofContentsRouter).read()now callsprotected function authorizeRead(string $relativePath): boolbefore serving; the default is permissive (any authenticated user may read;system_coderalways). Projects serving sensitive files tightenauthorizeRead()with their module's permission or tenant-ownership rule (e.g. the file's owning organization id - see the commented example in the method) - either by editing it in place or by subclassing and$this->override()`-ing the route to their controller.- Development module moved to the dashboard app layer.
DevelopmentRouter,DevRequestController,DevRequestModelandDevResponseModelmoved fromapplication/raptor/development/(Raptor\Development) toapplication/dashboard/development/(Dashboard\Development; new PSR-4 map; router registration moved fromRaptor\ApplicationtoDashboard\Application). Routes, route names, permissions, thedev_requestslog channel and DB tables are unchanged - deployed systems need no migration. Same rationale as the badge and protected-files relocations: the dev-request workflow is dashboard-app code a project is likely to customize, not shared framework infrastructure. Two accuracy fixes rode along:getDevRecipients()now qualifies its permission lookup withp.alias = 'system'(matching bynamealone can hit a same-named permission under another alias), and the docs' misleading "protected bydevelopment:developmentRBAC permission" line was replaced with the real access rules (every route requires login; users withoutsystem_developmentsee only their own/assigned requests;system_developmentmanages all). - Dashboard home route split: named
'home'moved to/home,/stays as an unnamed alias. Sidebar active-detection is prefix-based (href.startsWith(link)), so a home link pointing at the dashboard root was active on every page./remains registered (unnamed) because the public web layout links to{{ index }}/dashboarddirectly. Because the bare root does not prefix-match the/homelink,dashboard.html's inline script activates the home link when the current path equals the dashboard root (template-level check against{{ index }}/dashboard) - so the Dashboard item is highlighted right after login too. - Coder-only sidebar items moved out of System into their own "Coder" section. Database Migrations, Trash and Manage Menu (all gated by
system_coder) now live under a fourth top-level sidebar section (mn "Кодер" / en "Coder", position 990) instead of being appended to System. The section's parent row itself carriespermission => 'system_coder'andalias => 'system', so non-coders (and coders browsing a non-system organization) never see an empty section header. Deployed systems migrate with:INSERT INTO raptor_menu (parent_id, position, alias, permission, is_visible, created_at) VALUES (0, 990, 'system', 'system_coder', 1, NOW());then insert the mn/en titles intoraptor_menu_contentfor the new id andUPDATE raptor_menu SET parent_id = <new_id> WHERE permission = 'system_coder' AND href IS NOT NULL;(menu cache invalidates on next menu CRUD or 12h TTL - or clearcache/). - moedit AI models are now .env-configurable and defaults moved off the retiring GPT-4o family.
AIHelperhadgpt-4o-mini(HTML mode) andgpt-4o(vision/OCR) hardcoded; OpenAI retired the GPT-4o family from ChatGPT on 2026-02-13 and an API sunset is expected to follow. The model ids now come fromRAPTOR_OPENAI_MODEL/RAPTOR_OPENAI_VISION_MODEL(.env, optional) with safe defaults inAIHelper::DEFAULT_MODEL(gpt-5-mini) andDEFAULT_VISION_MODEL(gpt-5.1), so future model deprecations are a one-line config change instead of a code edit. Existing API keys keep working - keys are not tied to model generations. Permissions::RESERVEDconstant centralizes the reserved-permission guard. The inlinesystem_coderchecks that 4.4.1 shipped inPermissions::insert()/updateById()moved intoassertValidIdentity(), driven by a new publicPermissions::RESERVED = ['system_coder']list - one place to extend when another role-name key must never become a permission. The error message now reads"..." is a reserved permission (it is an RBAC role name).
Removed
OrganizationUserController,organization-user.htmland theorganization-userroute - superseded by the topbar organization switcher. The usermenu dropdown entry pointing at the page is gone as well.- "Language & Options" modal (
user-optionroute,TemplateController::userOption(),user-option-modal.html) - superseded by the topbar quick icons (search / language / theme). The usermenu dropdown entry is gone as well. - Unused translation keywords
average-ratingandconfirm-deactivatedropped fromTextInitial.php- neither is referenced anywhere inapplication/,public_html/ortests/(verified by scanning all 288 seeded keywords against every php/html/js file; these two never appear even as a bare substring, so dynamic construction is ruled out). Deployed systems can clean their live database withDELETE FROM localization_text_content WHERE parent_id IN (SELECT id FROM localization_text WHERE keyword IN ('average-rating', 'confirm-deactivate')); DELETE FROM localization_text WHERE keyword IN ('average-rating', 'confirm-deactivate');.
Security
- Session hardening: session ID is regenerated on privilege change, and the session cookie now sets HttpOnly / Secure / SameSite explicitly.
LoginController::entry()(login) andselectOrganization()(organization switch) now callsession_regenerate_id(true)before writing the new JWT/CSRF token, so a session ID an attacker may have fixed before authentication is invalidated at login (the session cookie is long-lived, which made fixation more valuable).SessionMiddlewarereplaces the single-argumentsession_set_cookie_params(2592000)with the array form, addinghttponly => true(blocksdocument.cookietheft via XSS),secure => <https-detected>(auto-detected fromHTTPS, port 443, orX-Forwarded-Protoso it is on over HTTPS and off over plain-HTTP dev), andsamesite => 'Lax'(CSRF mitigation). The 30-day lifetime is unchanged; removing the call still hands session config entirely to php.ini. - SQL injection in
FilesController::list()on PostgreSQL. TheGET /files/list/{table}route segment is untyped, so the router matches it withDEFAULT_REGEX(which allows single quotes) andrawurldecodes the value.list()interpolated it straight into the table-existence query (... AND tablename = '{$table}_files') without going throughModel::setTable()sanitization like the siblingpost()/modal()/update()/delete()actions do, so an authenticatedsystem_content_indexuser could run a UNION injection through the{table}parameter on the PostgreSQL branch (the MySQL branch was safe viaquote()).list()now sanitizes{table}with the samepreg_replace('/[^A-Za-z0-9_-]/', '', ...)whitelistindex()uses before any interpolation. - Stored XSS via public-visitor comments, reviews and contact messages. The template engine has no autoescape and the
nl2brfilter does not escape, so anonymously-submitted content (news comments, product reviews, contact messages) was rendered raw with{{ ...|nl2br }}- a visitor posting<img src=x onerror=...>executed JavaScript in every reader's browser, and (via the dashboard views) in authenticated admin sessions. The six sinks now escape before line-breaking with{{ ...|e|nl2br }}:web/content/news.html,web/shop/product.html,dashboard/shop/products-view.html,raptor/content/messages/messages-view-modal.html,raptor/content/news/comments-view.html,raptor/content/news/news-view.html. (The email notifiers already didhtmlspecialchars()beforenl2brserver-side; this brings the on-page views in line.) - Reflected XSS in the development-mode exception stack trace.
ErrorHandlerand the webExceptionHandlerrenderjson_encode($throwable->getTrace(), JSON_PRETTY_PRINT)inside a<pre>whenCODESAUR_DEVELOPMENTis on;json_encodeescapes/but leaves</>intact, so an<img src=x onerror=...>payload appearing in a trace argument executed in the dev error page. Both now addJSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT. Dev-mode only, but it is the same reflected-XSS class the upstreamcodesaur/http-application7.0.1ExceptionHandlerjust hardened. - Dashboard global search leaked shop data to non-shop users. The PRODUCTS and ORDERS blocks in
SearchController::search()were gated withsystem_content_index, while the Products/Orders index pages themselves requiresystem_product_index. A user holding only the content permission could not open the shop pages but could still read product rows and - worse - order rows with customer PII (name, email, phone) through topbar search. Both blocks now requiresystem_product_index, restoring the invariant that search results are a subset of what the user could see by browsing. - moedit AI endpoint had no permission gate and no cost limit.
AIHelper::moeditAI()(POST /dashboard/content/moedit/ai) only checkedisUserAuthorized(), so any logged-in dashboard user - including a zero-permissionviewer- could drive the server's OpenAI key: run the prompt as a free general-purpose LLM proxy (thepromptfield is fully caller-controlled), and in vision mode make onegpt-4ocall per image through an unboundedforeachloop with no throttle, letting a single request run up arbitrary API spend. Three fixes: (1) acanUseAI()gate requiringsystem_content_insert/updateorsystem_product_insert/update(the permissions for the news/pages/products modules that actually embed moedit;coderbypasses as usual) - a viewer now gets 403; (2) vision mode caps images atMAX_IMAGES_PER_REQUEST(8) per request; (3) a per-user fixed-window rate limit (RATE_LIMIT_MAX30 calls /RATE_LIMIT_WINDOW60s, vision counting one call per image) stored in the file cache - session can't be used because dashboardSessionMiddlewarecloses it early on non-login routes; the limiter is fail-open if the cache service is unavailable, since the permission gate is the primary control. Also hardened thecatchblock to coerce a non-integer exception code (OpenAI returns string codes likeinvalid_api_key) to a valid HTTP status instead of risking aTypeError.
Fixed
- Hard-deleting a user or organization with a photo/logo reported failure and orphaned the file.
UsersControllerandOrganizationControllerhard-delete called$this->getPublicPath(), a method that exists nowhere on the controller chain. The DB row was already deleted and copied to trash, then the undefined-methodErrorfired and was swallowed by the surroundingcatch (\Throwable), so the admin got an error JSON response for an operation that actually succeeded, the attempt was logged as a failure, and the physical photo/logo was never unlinked (orphaned on disk). Replaced with a newFileController::getStoredFilePhysicalPath()helper that correctly maps a stored public URL ({scriptPath}/public/...,rawurlencoded filename) back to its physical path (strips the script-path prefix,rawurldecodes, prepends the document root). - Sidebar badge JS now degrades cleanly when the badge route is not registered. The
|linktemplate filter returns'#'for an unknown route name (fail-safe), but'#'passed the truthy guard ininitSidebarBadges(): the loader thenfetch('#')-ed the current dashboard page itself on every load - doubling the server render work per page view, with the JSON parse failing silently.initSidebarBadges()now treats'#'as "route not registered in this app" and skips entirely, so an app that dropsBadgeRoutergets a clean page with no wasted requests. (The newinitGlobalSearch()ships with the same'#'guard - see the topbar entry under Added.) - Dashboard global search and order-list errors returned HTTP 200 instead of an error code.
SearchController::search()and twoOrdersControllerhandlers passed$err->getCode()(0 for a plain exception) torespondJSON(), which leaves a code-0 response at HTTP 200 - the global search modal then rendered a genuine query failure as an empty "no results". These three now pass$err->getCode() ?: 500, matching the convention already used in the badge/reviews/web controllers. - Global search silently swallowed real query errors. Every per-module block in
SearchController::search()used an emptycatch (\Throwable) {}so that a not-yet-created module table (partial install) is tolerated - but this also hid genuine query regressions (bad column, driver mismatch), making a module vanish from search with no trace. The catches now log the failure viaerror_log()whenCODESAUR_DEVELOPMENTis on, keeping production quiet while surfacing regressions in development. - Missing translation keywords
draftandnot-publishedrendered as raw slugs.comments-view.html(published badge) andproducts-view.html/news-view.html/page-view.html(draft tooltip) used'not-published'|text/'draft'|text, but onlypublishedexisted inTextInitial.php- the|textfilter falls back to the literal slug, so admins saw "not-published" / "draft" instead of translated text. Both keywords added (deployed systems need a migration insert). - RBAC permission cache was dead code - every authenticated request reloaded all roles/permissions from the DB.
JWTAuthMiddlewareread thecacheservice via$request->getAttribute('container')?->get('cache'), butContainerMiddleware(which registers the container) runs AFTERJWTAuthMiddlewarein the pipeline, so thecontainerattribute did not exist yet - the?->short-circuited tonull, therbac.{userId}cache never populated, and a fullnew RBAC(...)->jsonSerialize()DB load ran on every request (withRBACController's cache-clear having nothing to clear).JWTAuthMiddlewarenow builds the cache directly via the newCacheService::fromDefaultPath()factory (shared withContainerMiddleware, samecache/directory, soRBACControllerinvalidation still works). Regression test added inCacheTest.
Dependencies
- Upgraded the codesaur packages.
containerv3.1.3 -> v3.2.0,dataobjectv10.1.0 -> v10.1.1,http-applicationv7.0.0 -> v7.0.1,http-clientv2.1.1 -> v2.1.2,http-messagev3.0.3 -> v3.0.4,routerv6.0.0 -> v6.0.1,templatev4.1.0 -> v4.1.1. No application code changes were required; Raptor (mysql/pgsql only) benefits automatically from several upstream fixes:http-messagenow strips a duplicatedhost:portfrom generated absolute URLs (previouslybase_url, sitemap, RSS and redirect URLs came out ashttp://host:8080:8080on non-standard ports) and validates header names/values against CRLF injection;routerescapes literal dots in static route segments (/sitemap.xmlno longer also matches/sitemapXxml) and fixes partial-parametergenerate();templatefixes the ternary/??/and/orparser leaving tokens unconsumed inside parentheses and function arguments; andhttp-application'sExceptionHandlernow escapes the error title/message/host. The full test suite (836 tests) passes on the new versions.
[4.4.1] - 2026-06-27
[4.4.1]: https://github.com/codesaur-php/Raptor/compare/v4.4.0...v4.4.1
Fixed
- RBAC permission uniqueness contradicted the
{alias}_{name}key design. The runtime permission key is built as{alias}_{name}(seeRole::fetchPermissions()andRBAC), so the samenameis meant to be reusable under different aliases - e.g.system_request_updateandcommon_request_update. ButPermissionsdeclared thenamecolumn->unique()on its own, so only one row pernamecould ever exist; the secondINSERTfailed with a rawDuplicate entry '...' for key 'name'and the two keys could never coexist. This was dormant in the shipped framework only because every seeded permission carries a globally-unique, module-prefixedname(user_insert,content_index, ...) all underalias = 'system'. The constraint is now compositeUNIQUE (alias, name), matching the key semantics.
Changed
rbac_permissionsunique constraint:(name)->(alias, name).Permissionsno longer marksnameas->unique(); instead__initial()addsADD CONSTRAINT rbac_permissions_uq_alias_name UNIQUE (alias, name)(rawALTER, works on both MySQL and PostgreSQL).RolePermissionSeedpermission lookups narrowed toalias = 'system'. Both theadminbulk assignment andassignPermissions()matched permissions bynamealone (p.name IN (...)). With the composite constraint allowing same-named permissions under different aliases, those queries could match and assign more than the intended system permissions, soAND p.alias = 'system'is now part of both - this is exactly the seed's intent (default roles get the default system permissions).
Added
Permissions::assertValidIdentity()insert/update validation. Surfaces clear errors before the database constraint is hit, following the existingsystem_coderguard style (English\RuntimeException, surfaced to the UI via the controller'sgetMessage()): (1) rejects analiascontaining_, because the separator-less{alias}_{name}concatenation would otherwise let(system_request, update)and(system, request_update)collide into the same key - aliases are clean grouping values (system,common, ...) tied to the sidebar menu and never need an underscore; (2) pre-checks(alias, name)for duplicates and throwsPermission "..." already exists.instead of leaking the raw SQLDuplicate entryexception.
[4.4.0] - 2026-06-24
[4.4.0]: https://github.com/codesaur-php/Raptor/compare/v4.3.1...v4.4.0
Added
- moedit: Video Poster feature. After a video is inserted (uploaded file or YouTube embed), moedit now offers to capture the video's first frame and use it as the content's header image. For uploaded videos the frame is drawn on a canvas and auto-uploaded via the existing
opts.uploadendpoint; for YouTube, the thumbnail is fetched (maxresdefault.jpg, falling back tohqdefault.jpg). The offer appears as a modal dialog; accepting triggers upload and sets the header image inline without a page reload. New methods:_confirmDialog(generic confirm dialog helper),_captureVideoPosterBlob(canvas frame capture with 15 s timeout and cross-origin guard),_youtubePosterBlob(thumbnail fetch with quality fallback),_applyHeaderImageFromBlob(upload + preview + callback chain),_offerVideoPoster(orchestrates the prompt after insert). - moedit:
_extractYouTubeIdcentralized. YouTube ID extraction is now a single prototype method coveringyoutube.com/watch?v=,youtube.com/embed/,youtube.com/v/,youtube.com/shorts/,youtu.be/, and bare 11-character IDs. Previously duplicated between the YouTube dialog and embed path. - moedit:
_insertYouTubeEmbed,_doInsertImage,_doInsertMedia,_insertImageByUrl. Insertion helpers refactored or extracted from inline code to reusable prototype methods, shared between the direct-insert and video-poster code paths. tests/Unit/Content/ProtectedFilesTraversalTest.php. Unit tests for theread()directory-traversal fix, in two layers. (1) Source-code guards: assertsread()canonicalizes both the protected dir and the requested file withrealpath(), anchors the prefix check withDIRECTORY_SEPARATOR, treats an unresolvable path as forbidden, and throws403 Forbidden. (2) Behavioral: replicatesread()'s containment algorithm against a real temp filesystem - legit root/nested files are served, while../traversal payloads are blocked (parent escape/../secret.txt, nested-then-up/sub/../../secret.txt, the/../../../somesecretfolder/dangerinfo.txtexample, and the/../protected-evil/x.txtprefix-collision sibling), plus empty and nonexistent names. Null-byte and absolute-path payloads are not covered.
Fixed
ProtectedFilesController::read(): directory traversal (security).read()serves protected files from the?name=query parameter. It built the path withgetDocumentPath('/../protected' . $name)and then only checked whether the result fell inside the cache directory; theprotected/root itself was never enforced. BecausegetDocumentPath()just concatenates the string (it does not resolve../), a craftednamesuch as/../../../etc/passwdcould escape the protected folder entirely and read arbitrary server files.read()now resolves the canonical path withrealpath(), returns404unless that path is an existing file (is_file(), which - unlikefile_exists()- also rejects directories), and returns403if the canonical path does not begin with the protected root plusDIRECTORY_SEPARATOR(the trailing separator also prevents prefix-collision bypasses such asprotected-evil/). All downstream operations - extension/basename blocklist, MIME sniff, headers,readfile- run on the canonical$realFile, not the raw concatenated path. The cache-directory check is layered on top of the containment check.- moedit
destroy()header-image button listener leak. The click listeners added to the Change / Remove buttons in_initHeaderImage()were anonymous closures not stored anywhere, sodestroy()never removed them. Post-destroy clicks calledthis._selectHeaderImage()/this._removeHeaderImage()against a destroyed instance (this.opts === null), crashing with TypeError. Handlers are now stored as_boundHandlers.headerImageChange/_boundHandlers.headerImageRemoveand explicitly removed indestroy()before refs are nulled. - moedit
_captureVideoPosterBlob:seekedlistener lingered after capture. Theseekedevent listener forcaptureFramewas registered without{ once: true }, so it remained active after the frame was drawn. Added{ once: true }. - moedit
_cleanToVanillaHTML: XSS filter incomplete. Thehref/srcattribute sanitizer blockedjavascript:,vbscript:, anddata:text/htmlbut allowed other scriptabledata:MIME types:data:text/xml,data:text/javascript,data:application/xhtml+xml,data:application/xml,data:application/javascript, anddata:application/ecmascript. All are now blocked by the same regex. - moedit table / accordion dialog: zero silently became three.
parseInt(rowsInput.value) || 3(and the equivalent for cols and accordion count) treated a user-entered0as "use the default 3" with no feedback - the dialog closed and inserted a 3x3 table. Replaced withparseInt(value, 10)and a!(n >= 1)guard: on invalid input an error notification is shown and the dialog stays open. - moedit dialogs: debounce timer not cleared on close. The URL-preview
setTimeoutinside the input handler of the Image-URL, YouTube, Facebook, Twitter, and Map dialogs was not cancelled when the dialog closed. If the user closed the dialog before the debounce delay elapsed, the timer fired and accessed detached DOM elements.clearTimeout(debounceTimer)is now called in everycloseDialog(). - moedit
_selectHeaderImage:prevSrcabsolutized URL. The upload-failure rollback readthis.headerImagePreview.src(the DOMsrcproperty, which the browser absolutizes to a full URL) as the previous image to restore. On hosts with a path like/uploads/img.jpgthis producedhttp://host/uploads/img.jpg, which could behave differently from the original relative path. Now readsthis._headerImagePath(the internal server-relative path), matching the same fix already applied to_applyHeaderImageFromBlob. - moedit
_selectHeaderImage: file input orphaned on cancel in old browsers. The hidden<input type="file">was removed from the DOM oncancel(modern browsers) and onchange, but browsers withoutcancelsupport (Firefox < 91, Safari < 16.4) left it dangling indefinitely. Awindow.focusfallback (500 ms delay,fileInputDoneflag) now removes the input when the window regains focus and no file was selected.
Changed
- moedit assets cache-busted to
?v=4.moedit.jsandmoedit.ui.jsversion query strings updated fromv=3tov=4in all six dashboard editor templates: news-insert, news-update, page-insert, page-update, products-insert, products-update. - moedit manuals updated.
moedit-manual-en.htmlandmoedit-manual-mn.htmldocument the new video/YouTube header-image capture prompt.
[4.3.1] - 2026-06-22
[4.3.1]: https://github.com/codesaur-php/Raptor/compare/v4.3.0...v4.3.1
Reverted
- The server-side session management added in 4.3.0 is reverted - server-side session lifetime now follows host defaults. Raptor no longer forces
session.save_path,session.gc_maxlifetime, orsession.gc_probabilityfrom code. Removed: theprotected/sessionsstorage block and gc tweaks inindex.php, theprotected/sessions/block inProtectedFilesController, and theRAPTOR_SESSION_SAVE_PATH/RAPTOR_SESSION_LIFETIMEenv keys. Reason: forcing a 30-daygc_maxlifetimefrom code was unreliable on shared hosting (runtimeini_setis ignored by the Debiansessioncleancron; shared/tmpand host crons purge files independently) and added a session-file accumulation / cleanup burden, while not being the actual fix for the reported 403s. Raptor does NOT configure the server-side session lifetime - that is left to the host'sphp.ini/.user.ini, and developers may optionally configure it - seedocs/en/SESSION-LIFETIME.mdanddocs/mn/SESSION-LIFETIME.md.
Added
docs/en/SESSION-LIFETIME.mdanddocs/mn/SESSION-LIFETIME.md- a per-environment guide (Ubuntu/Debian VPS, cPanel shared hosting, Windows XAMPP/WAMP, macOS Homebrew/MAMP, Nginx-FPM/Docker/mod_php) for configuringsession.gc_maxlifetimecorrectly. Linked from each language's README andapi.md.
Kept
SessionMiddlewarestill sets the session cookie lifetime to 30 days viasession_set_cookie_params(...)(client-side cookie only - this is what keeps an admin logged in across browser restarts).- The WAF-compatibility feature from 4.3.0 (
MethodOverrideMiddleware,BodyEncodingMiddleware,csrfFetchverb tunneling + body encoding,RAPTOR_WAF_BODY_ENCODING) is unchanged.
[4.3.0] - 2026-06-19
[4.3.0]: https://github.com/codesaur-php/Raptor/compare/v4.2.3...v4.3.0
Added
- Shared hosting / WAF compatibility layer. Many cPanel/LiteSpeed hosts with mod_security broke dashboard saves in three ways; all are now handled framework-wide with no per-module code. (1)
MethodOverrideMiddleware(new) tunnels PUT/PATCH/DELETE through POST via theX-HTTP-Method-Overrideheader for hosts that block those verbs before PHP - POST-only, never overrides to GET, so CsrfMiddleware still applies. (2)BodyEncodingMiddleware(new) base64-decodes form field values when the client sendsX-Body-Encoding: base64, defeating WAF XSS rules that flag HTML/JS-like rich-text content in the POST body (strict base64, recursive, header-gated, files/field-names untouched; works because every controller readsgetParsedBody(), none read$_POST). Both are registered inRaptor\ApplicationandWeb\Application. The client side lives incsrfFetch()(dashboard.js): it rewrites verbs and base64-encodesFormDatastring values (UTF-8-safe, files skipped), gated by the<meta name="waf-body-encoding">flag thatController::template()emits. Single encode / single decode, so already-base64 content (inlinedata:image URIs) round-trips byte-for-byte. New env vars (see below);dashboard.jsbumped to?v=5. Tests:MethodOverrideMiddlewareTest,BodyEncodingMiddlewareTest.
Fixed
- Intermittent CSRF 403 on shared hosting from lost sessions. Sessions stored in the shared
/tmpwere purged by system cron / a shortsession.gc_maxlifetime, emptying$_SESSION['CSRF_TOKEN']so a later mutating request failedCsrfMiddleware.public_html/index.phpnow stores sessions inprotected/sessions(outside/tmp, already web-blocked by.htaccess) and raisesgc_maxlifetime.SessionMiddlewarecookie params are corrected:session_set_cookie_params()was passing an absolute timestamp where a duration (seconds) is expected; it now passes a real lifetime plushttponly/samesite=Laxandsecureonly on HTTPS (honouringX-Forwarded-Protobehind a proxy).ProtectedFilesControllernow blocksprotected/sessions/from the file-read API the same wayprotected/cache/is blocked.
Configuration
- New
.envkeys under "Shared Hosting / WAF Compatibility":RAPTOR_SESSION_SAVE_PATH(empty = autoprotected/sessions),RAPTOR_SESSION_LIFETIME(seconds, default2592000= 30 days; drives both the session cookie and server-side gc), andRAPTOR_WAF_BODY_ENCODING(defaulttrue; setfalseon hosts without a body-inspecting WAF to skip client-side base64 encoding).
Changed
- Started using
codesaur/template^4.1.0, which adds the Twig-compatiblein/not in,ends with,matches, andis even/is oddoperators. The shop product template now uses{% if file['type'] in ['image', 'video', 'audio'] %}instead of the previousor-chain.
[4.2.3] - 2026-06-17
[4.2.3]: https://github.com/codesaur-php/Raptor/compare/v4.2.1...v4.2.3
Fixed
- A stray
Wbefore the opening<?phptag inapplication/raptor/migration/MigrationRouter.phpis removed. The file's first line wasW<?php- a leftover keystroke. Because the character sits outside the PHP tag, PHP treats it as raw output and echoes it the moment the file is autoloaded, printing a strayWat the top of the response body and triggering "headers already sent" warnings on any request that loaded the migration router. The line is now a clean<?php.
Changed
- Code-style cleanup only (no behavior change): removed a redundant blank line in
public_html/assets/moedit/moedit.ui.js(before_insertImage), continuing the formatting pass started in 4.2.1.
Notes
- Release-process correction for Packagist version immutability.
v4.2.1was first tagged at commit1fc8c5dand published to Packagist, then the tag was deleted, re-created on a later commit, and force-pushed. Packagist rejected the update because a published stable version's source/dist reference is immutable - once a tag is published it must keep pointing at the same commit forever. To realign,v4.2.1was restored to its originally published commit (1fc8c5d) and the two follow-up fixes above were shipped as this fresh version instead of by re-tagging. Going forward the rule is one version number = exactly one commit: published tags are never moved or recreated; any further change ships under a new, higher version (which is why this release is4.2.3).
[4.2.1] - 2026-06-17
[4.2.1]: https://github.com/codesaur-php/Raptor/compare/v4.2.0...v4.2.1
Fixed
- Editing a menu item with no
href/iconno longer shows the literal string"null"in the form, nor emits PHP warnings. This was one bug with two surfaces, both in the dashboard menu manager (application/raptor/template/). On the client (manage-menu.html), the edit/view modal populated the inputs withrecord.icon/record.hrefdirectly; for a section-header menu (Contents, System, etc.) those columns areNULL, and assigningnullto an<input>'svalueIDL attribute coerces to the string"null"(thevaluesetter has no[LegacyNullToEmptyString]), so the field showednullinstead of being empty. Now guarded withrecord.icon ?? ''/record.href ?? ''. On the server (TemplateController::manageMenuUpdate()), the change-detection loop compared$record[$index]/$record['localized'][$key][$index]against the submitted value; for a header menu with nohref/iconcolumn those keys are undefined, raisingUndefined array key "href"/"icon"warnings inerror.logon every menu update. Both comparisons now use?? null(kept asnull, not'', so the!=change check still distinguishes an unset column from a submitted empty value).
Changed
- Applying a migration now auto-clears the cache. A migration can write to any table - including the ones whose rows are cached (
rbac_*permissions,raptor_menu,localization_*translations, settings). Previously those caches kept serving stale data after a successful apply until their 12-hour TTL expired or an unrelated admin CRUD action happened to invalidate them.MigrationController::apply()now callscache->clear()(guarded byhasService('cache'), fail-safe) right after a successful apply, matching the full-clear pattern already used for RBAC changes. Only runs on$result['ok']; a failed apply leaves the cache untouched. Documented indatabase/migrations/README.md(workflow step 4) and the controller PHPDoc. codesaur/dataobjectupgraded from^10.0.1to^10.1.0(composer.json+composer.lock). The 10.1.0 release broadens cross-driver column type conversion inTableTrait::getSyntax(), so a column type declared for one database resolves to a valid native type on the others instead of reaching the engine verbatim and raising a runtime SQL error - e.g.double/floatand the*blob/binary/varbinaryfamily now map correctly to PostgreSQL (double precision/real/bytea), whilejsonb/uuid/inet/cidr/bytea/double precisionmap back on MySQL and SQLite. It also stops emitting an invalid(length)suffix on length-less types (no more malformed DDL likebytea(16)). This directly strengthens the framework's "raw SQL/models must work on both MySQL and PostgreSQL" invariant; no application code changes were required (the public API is unchanged from 10.0.x).- Code-style cleanup only (no behavior change): removed redundant blank lines (block-start/block-end and double blanks), fixed indentation, switched single-quoted template expressions containing inner quotes to backticks, and converted Unicode box-drawing tree diagrams to ASCII across PHP, HTML template, and Markdown files.
[4.2.0] - 2026-06-16
[4.2.0]: https://github.com/codesaur-php/Raptor/compare/v4.0.0...v4.2.0
Removed
- PHPStan static analysis removed entirely. The
phpstan/phpstandev dependency (added in 4.1.1) was dropped fromrequire-dev, thephpstanandphpstan:baselineComposer scripts were removed, and the three config files (phpstan.neon.dist,phpstan-bootstrap.php,phpstan-baseline.neon) were deleted.composer.lockandvendor/were synced (composer update phpstan/phpstan), so the package is gone from the dependency tree. No runtime/request-path impact - PHPStan was dev-only and already excluded from production viacomposer install --no-dev.
[4.1.1] - 2026-06-16
Security
- Log secret masking no longer silently misses
*_tokenkeys.Logger::encodeContext()maskedPASSWORDvia substring match but checkedPIN,JWT,TOKENwith an exactin_array(), so realistic keys like_token,csrf_token,access_tokenwere written to*_log.contextin plaintext - even thoughController::log()auto-injects the full parsed request body intoserver_request.body. The login form's spam_tokenfield was leaking this way today.TOKENis now matched as a substring (alongsidePASSWORD) via a single precompiled regex, whilePINandJWTstay on exactin_array()matching on purpose: as short substrings they would over-mask legitimate keys (PINis a substring ofmapping,opinion,shipping; masking the wrong data is the same class of silent failure in reverse).
Fixed
- JSON error endpoints no longer return HTTP 200 on an exception with code 0.
BadgeController::badges()/seen()andLogsController::retrieve()passed$err->getCode()straight torespondJSON(); a plain exception thrown without an explicit HTTP code carries0, which falls outside the 100-599 range check and leaves the status at the default200- so a real server-side failure reached the client as200 OKand thefetch().okcheck passed, swallowing the error. These now use$err->getCode() ?: 500, matching the fallback already used byLogsController::errorLogRead().
Added
- PHPStan static analysis (dev-only).
phpstan/phpstanadded torequire-dev(excluded from production viacomposer install --no-dev; zero runtime/request-path impact). Config inphpstan.neon.distruns at a conservativelevel: 1overapplication/, withphpstan-bootstrap.phpdefining the runtime-onlyCODESAUR_DEVELOPMENTconstant for analysis. The 301 pre-existing findings are frozen inphpstan-baseline.neonso the check is green and only NEW code is analyzed; the level can be raised over time. Run withcomposer phpstan; regenerate the baseline withcomposer phpstan:baseline. This catches the class of typo that caused the 4.0.0ReasonPrhasebug (undefined-class reference) automatically. PHPStan reads code structure/types only - Mongolian text in strings/comments/docblock descriptions is never flagged, and code formatting/naming is out of scope (that is PHP_CodeSniffer/CS-Fixer territory).
Changed
- HTTP status code validation switched from a
ReasonPhrase::STATUS_*constant lookup to a numeric 100-599 range check (RFC 9110).Controller::headerResponseCode(),Controller::respondJSON(),Raptor\Exception\JsonExceptionHandler,Raptor\Exception\ErrorHandler, andWeb\Template\ExceptionHandlerpreviously validated a code with\defined(ReasonPhrase::class . "::STATUS_$code"). That pattern is silently fragile: a wrong class reference (theReasonPrhasetypo fixed in 4.0.0) resolves to a literal string and\defined()just returnsfalse, so validation cannot distinguish "unknown code" from "wrong class name". Validation is now\is_numeric($code) && $code >= 100 && $code <= 599, which has no class coupling and cannot be disabled by a symbol typo.respondJSON()now delegates status handling toheaderResponseCode(), removing a duplicated check and an inconsistency (the oldrespondJSONused\is_int(), so it rejected numeric-string codes like'403'thatheaderResponseCodeaccepted). The now-unuseduse codesaur\Http\Message\ReasonPhrase;import was removed from all four files; theReasonPhraseclass itself is unchanged and still used for PSR-7 reason-phrase text. Docs (docs/en/api.md,docs/mn/api.md) updated to describe the 100-599 range. delete-languageroute renamed tolanguage-delete(LocalizationRouter) to match the{entity}-deletenaming convention used by every other delete route (text-delete,news-delete,user-delete, etc.). The{{ 'delete-language'|link }}reference inlocalization-index.htmlwas updated; the unrelated.delete-languageCSS class on the button is unchanged. No DB impact (route names are code-only).- Reference-table cache coupling documented. Added comments in
TemplateService::loadAllForCode()(the literalreference.templates.$coderead/write key) and the threeReferencesControllerinvalidateCache("reference.$table.{code}")calls, recording the invariant that only thetemplatesreference table is cached, so invalidating any other table is a harmless no-op.
Removed
templates_indexpermission removed fromPermissionsSeed.phpand themanager/editorassignments inRolePermissionSeed.php. It was redundant: it only gated the "Reference Tables" sidebar menu entry whileReferencesControlleractually authorizes onsystem_content_index. TheMenuSeedentry for/referencesnow usessystem_content_index, aligning menu visibility with the controller's real access check. Also dropped from the RBAC manual tables (rbac-manual-{mn,en}.html). Deploy note: for already-deployed databases, write a migration to delete thetemplates_indexrows fromrbac_permissions/rbac_role_permissionand to update theraptor_menurow's permission tosystem_content_index(seed files only run on fresh installs).
Security
- Removed the hardcoded fallback secret for the login anti-spam HMAC token.
LoginControllergenerated and verified the login form's spam token with$_ENV['RAPTOR_JWT_SECRET'] ?? 'raptor-form-secret'. The fallback is a public, source-visible string, so a deployment that forgot to setRAPTOR_JWT_SECRETwould silently sign tokens with a known key, making the anti-spam HMAC forgeable while still appearing to work. Spam-token handling is now centralized inSpamProtectionTrait: a newgetSpamSecret()(fail-loud - throwsRuntimeExceptionifRAPTOR_JWT_SECRETis unset, no default) andgenerateSpamToken($formName, $ts).LoginController(generate + verify) and the webContactController,NewsController,ShopController(which each previously duplicated a privategetJwtSecret()and an inlinehash_hmac()generation call) now route both generation and validation throughgenerateSpamToken(), so the two sides provably share one formula and one fail-loud secret source. The four duplicategetJwtSecret()methods were deleted.
Fixed
ProtectedFilesController::read()now returns a real401for unauthenticated requests instead of being short-circuited by the login redirect. The route (/dashboard/protected/file) is in the dashboard app, whereJWTAuthMiddlewareredirected every unauthenticated non-login route to/dashboard/loginbefore the controller ran - so the controller's ownif (!isUserAuthorized()) throw 401never executed, and a protected-file URL embedded in an<img>/download for a logged-out user received302+ login HTML instead of a clean401.JWTAuthMiddlewarenow exempts the/dashboard/protected/*path segment from the login redirect (alongside the existingloginexemption): on auth failure it falls through anonymously soread()runs and returns its own401/403/404. Established as a convention: every route under/dashboard/protected/*(current and future) falls through anonymously and must enforce its own auth (return401/403) instead of relying on the redirect - so adding a new protected route needs no middleware change. (Pairs with the earlierheaderResponseCoderange-check fix, which is what now actually emits the401.) The route stays at/dashboard/protected/fileso the mount-awaregenerateRouteLink('protected-file-read')links generated across the dashboard controllers keep resolving.- Protected file serving now sends
Content-DispositionandContent-Length.ProtectedFilesController::read()previously emitted onlyContent-Type+readfile(). Since the route URL carries no file extension (/dashboard/protected/file), a saved file defaulted to the namefilewith no extension.read()now addsContent-Disposition: inline; filename="<basename>"(a save/download gets the real name and extension; browsers that can render the type may show it inline) andContent-Length(enables download progress). Whether a given type renders inline or downloads remains the browser's/OS's decision - the server only supplies the correct headers.
[4.0.0] - 2026-06-13
[4.0.0]: https://github.com/codesaur-php/Raptor/compare/v3.1.0...v4.0.0
This cycle consolidates database access behind a single DatabaseConnection factory, adds a fully file-based per-user SQL migration system, adopts the codesaur/http-application v7 application-wide mount feature (prefix-naive routers mounted at /dashboard), moves CSRF protection from an app-wide middleware to explicit per-route middleware, and renames the authenticated file-serving controller and its storage folder from Private*/private/ to Protected*/protected/. A round of correctness fixes (middleware control flow, return types, cross-driver SQL handling, defensive I/O checks) is also included.
Added
Raptor\DatabaseConnection(application/raptor/DatabaseConnection.php) - single PDO factory used bypublic_html/index.phpand tests.connect()readsRAPTOR_DB_DRIVERfrom.env(mysql|pgsql) and returns a ready PDO. Web and Dashboard share the same connection via$request->withAttribute('pdo', $pdo)set in the entry point.- Fully file-based, per-user migration system under
database/migrations/{userId}-{username}/[ran/]- no tracking table; state is derived from file location (*.sql= pending,ran/*.sql= applied). The folder is git-ignored so each environment uploads its own SQL via/dashboard/migrations(requiressystem_coder). SQL files are plain statement lists; an optional first-line--comment becomes the UI summary. MigrationSecurityScanner(application/raptor/migration/MigrationSecurityScanner.php) - static SQL scanner with case-insensitive, comment- and string-aware pattern matching against sensitive tables (users,rbac_*,organizations*,localization_language,raptor_menu) and DCL (GRANT/REVOKE/CREATE-DROP-ALTER USER); apply requires a typedCONFIRMwhen any pattern matches. Also flagsCREATE TABLEto nudge coders toward defining a Model withsetTable()(auto-creates the table on first use) instead of duplicating that in a migration.- Upload UI on
/dashboard/migrationswith per-folder grouping, per-row view/apply/delete actions, and a CONFIRM modal for sensitive applies. - SHA-256 hash + summary + statement count logged to
dashboard_logfor every upload, apply, and delete (action: migration-upload | migration-apply | migration-delete). - Manual pages:
application/dashboard/manual/migrations-manual-{mn,en}.html. - Tests:
MigrationSecurityScannerTest(pattern matches, case-insensitivity, comment/string false-positive avoidance) andMigrationRunnerIntegrationTest(apply/move-to-ran, failure-stays-pending, path-traversal rejection, cross-OS folder name sanitization). - Delete action on the reviews moderation list (
application/dashboard/shop/reviews-index.html), gated bysystem_product_deleteand reusing the existingproducts-reviews-deleteroute - previously reviews could only be deleted from the product view page.
Removed
application/raptor/MySQLConnectMiddleware.phpandapplication/raptor/PostgresConnectMiddleware.php- replaced by the singleDatabaseConnectionfactory wired inindex.php.
Changed
codesaur/http-applicationupgraded to v7.0.0 (multi-router + application-wide mount feature). Dashboard routers are now prefix-naive and the Dashboard app is mounted at/dashboardvia->mount('/dashboard')inindex.php.- CSRF protection moved from app-wide to per-route.
Applicationno longer registersCsrfMiddlewareglobally; instead every mutating route (POST/PUT/PATCH/DELETE andGET_POST/GET_PUTcompounds) attaches->middleware([CsrfMiddleware::class])(69 routes across 12 routers; login routes are exempt). The middleware now only validates; token provisioning moved to login andController::template()(reads from session, regenerates as a fallback for old sessions). This makes each route's CSRF requirement explicit at the router. PrivateFilesControllerrenamed toProtectedFilesController, and the storage folderprivate/toprotected/(cache moved toprotected/cache/). Access is gated on authentication only (not per-owner), so "protected" is more accurate than "private". Route is/dashboard/protected/file(nameprotected-file-read);.gitignoreupdated. Deploy note: the folder is git-ignored, so each environment must renameprivate/toprotected/manually.database/migrations/*is git-ignored (only.gitkeepandREADME.mdtracked).BadgeControllerfile-count badge for/dashboard/migrationsglobs*/*.sqlto count pending files across user folders (excludingran/).getUserFolderPath()sanitizes username for cross-OS safety: whitelist[A-Za-z0-9._-], strip leading/trailing. - _(Windows NTFS trailing-dot stripping), fallback touserfor empty /./.., cap at 50 chars.- Private property names no longer use a leading underscore (PSR-12 compliance).
User:$_rbac->$rbac.FileController:$_overwrite->$overwrite,$_size_limit->$size_limit,$_allowed_exts->$allowed_exts,$_upload_error->$upload_error. Internal only - all properties areprivate, so the public API is unchanged.
Fixed
ReasonPhraseclass typo - the class name was misspelledReasonPrhaseacrossController,ErrorHandler,JsonExceptionHandler, and webExceptionHandler. Since::classresolves to a literal string without requiring the class to exist,\defined(ReasonPrhase::class . "::STATUS_$code")and\class_exists(ReasonPrhase::class)always returned false. As a resultController::headerResponseCode()returned early for every code and never calledhttp_response_code()- so all error responses (401/403/404/500, e.g.ProtectedFilesController::read()rejecting an unauthenticated request or aprotected/cache/access) silently fell back to200 OKwith an empty body. Corrected toReasonPhraseeverywhere; HTTP error codes are now emitted as intended.- JWTAuthMiddleware -
$handler->handle()is now called exactly once outside try/catch. Previously it was called insidetry, so a downstream controller exception was caught here and masked as a login redirect (or double-handled on login pages); exceptions now propagate to the ErrorHandler as intended. - OrganizationUserModel::retrieve() - return type
: array->: array|false.return falseon a no-row result threw a TypeError, which broke thesystem_coderorganization-switch flow. - WebLogStats - dashboard home order stats queried a non-existent
orderstable (always 0); now queriesproducts_orders. - NewsController::update() - now captures the
updateById()result ($updated); the success log stores the actual record instead ofnull. - PagesController::view() - avoids a malformed
id=SQL condition for a top-level page (NULLparent_id). - SettingsController - reads
*_removedform fields with?? 0to avoid undefined-array-key warnings. - SearchController (dashboard) - NEWS/PAGES/USERS branches are wrapped in inner try/catch, matching PRODUCTS/ORDERS (degrades gracefully on partial installs).
- MigrationRunner::splitStatements() - SQL splitting is now driver-aware: PostgreSQL dollar-quoting (
$$...$$) is parsed only on pgsql, and MySQL\'backslash escapes only on mysql. Prevents stray$...$pairs (MySQL/SQLite) or a backslash-before-quote (PostgreSQL) from merging statements across;. - MigrationSecurityScanner - DML patterns use
[^;]*(single-statement scope) instead of.*with/s, so a sensitive table name in a different statement no longer produces a false warning.stripCommentsAndStrings()also keeps'-- UPDATE users'string literals and-- UPDATE userscomments from triggering false warnings. - MigrationController - rejects uploads when
getSize()returns null (PSR-7) instead of silently bypassing the size check. - MigrationRunner::apply() - returns an error when
file_get_contents()fails, instead of treating an unreadable file as an empty (successful) migration and moving it toran/. - MigrationController / MigrationRunner -
mkdir()failures are now checked (upload folder andran/folder), surfacing a clear error instead of a confusing downstream failure. - DiscordNotifier - skips the POST when
json_encode()returns false (e.g. invalid UTF-8) instead of sending an empty body. - Deploy (Windows job) -
deploy.ymlrobocopy/XDexcluded a nonexistentprivatedirectory and did not excludeprotected/; the/MIRmirror would delete the server's liveprotected/(uploads + cache). Now excludesprotected/, matching the FTP and SSH jobs. - TrashController::restore() - the primary insert, localized
_contentinsert, and trash-row removal are now wrapped in a single transaction; a failure in any step rolls back, preventing an orphan primary row on partial restore.insertPrimary()now pre-checks whether the original ID is free instead of catch-and-retry on a failed INSERT (PostgreSQL aborts the whole transaction on a failed statement, which would have broken the auto-increment fallback inside the transaction). - products-view.html - replaced the unsupported
inmembership operator (codesaur/templatehas noin) with an explicit==/orchain, so the attachment preview-vs-download logic evaluates correctly. - ProductsController::update() - clears
published_at/published_bywhen a product is unpublished (1 -> 0); previously stale publish metadata persisted after un-publishing. - MigrationRunner::splitStatements() - MySQL quote-escape detection now counts consecutive preceding backslashes (even count = literal
\, not an escape) instead of a single-character lookback, so a string literal ending in\no longer swallows the following;and merges statements. - contact.html - the public contact page title (admin-entered content) is now HTML-escaped (
|e). Controller::respondJSON()PHPdoc clarified: a valid HTTP integer code is returned as the response code, while a string or unrecognized code is intentionally ignored (stays 200) with the error conveyed through the JSONstatus: 'error'envelope. Behavior unchanged - documentation only, for future maintainers and AI agents.- Documentation accuracy - corrected the web middleware pipeline diagram (removed the deleted
MySQLconnection middleware) inREADMEandapi.md(mn/en); event classes (EventDispatcher,ListenerProvider,ContentEvent,UserEvent,OrderEvent,DevRequestEvent) now documented underapplication/raptor/notification/(not a nonexistentevent/);ContentEvent/OrderEvent/DevRequestEventproperty tables matched to their actual constructors;ReviewsControllerdelete route corrected to/dashboard/products/reviews/delete;FTP_SERVER_DIRexample aligned across docs; migration README example usesIF NOT EXISTS.
[3.1.0] - 2026-05-06
[3.1.0]: https://github.com/codesaur-php/Raptor/compare/v3.0.0...v3.1.0
codesaur/dataobject upgraded to v10.0.0 (which dropped setForeignKeyChecks()); 24 redundant FK-toggle wrappers around ALTER TABLE ADD CONSTRAINT FOREIGN KEY removed framework-wide; PostgreSQL deployments no longer require the application DB user to hold SUPERUSER privilege. Plus PostgreSQL compatibility fixes in news archive and language copy-content flows, and safer file lifecycle around hard-delete and profile/logo update.
Breaking Changes
codesaur/dataobjectupgraded to v10.0.0 which dropssetForeignKeyChecks(bool $enable)fromPDOTrait. Anyone with custom Models underapplication/that wrappedALTER TABLE ADD CONSTRAINT FOREIGN KEYin$this->setForeignKeyChecks(false)...(true)brackets must remove those calls (they were redundant on freshly-created empty tables anyway). If real toggling is genuinely needed, write the raw SQL directly:- MySQL:
$this->exec('SET foreign_key_checks=0')/=1 - PostgreSQL:
$this->exec("SET session_replication_role='replica'")/'origin'(still requires SUPERUSER) - SQLite:
$this->exec('PRAGMA foreign_keys=OFF')/=ON
- MySQL:
Removed
- 24 redundant
setForeignKeyChecks()calls stripped from framework Models and Controllers:- 22 model
__initial()hooks - ProductsModel, ReviewsModel, ProductOrdersModel, DevRequestModel, DevResponseModel, LanguageModel, TextModel, SignupModel, ForgotModel, SettingsModel, OrganizationModel, OrganizationUserModel, Roles, Permissions, RolePermission, UserRole, FilesModel, NewsModel, CommentsModel, PagesModel, ReferenceModel, MenuModel - 2 controller hard-delete blocks - UsersController, OrganizationController
- All wrapped only
ALTER TABLE ADD CONSTRAINT FOREIGN KEYon JUST-CREATED (empty) tables, where FK validation cannot fail. The toggling was over-defensive copy-paste from earlier MySQL-only days. - Side effect: PostgreSQL deployments no longer need
SUPERUSER. After upgrade runALTER USER <app_db_user> NOSUPERUSER;to revoke the previously-required privilege.
- 22 model
Changed
- Hard-delete file cleanup ordering -
UsersController::delete()andOrganizationController::delete()nowunlink()the profile photo / logo file after$model->deleteById($id)succeeds. Previously the file was deleted first; if the DB delete failed (e.g. FK violation, permission denied) the record stayed pointing to a missing file - "хагас орхигдсон линк". - Profile/Org update file lifecycle -
UsersController::update()andOrganizationController::update()defer old-fileunlink()until afterupdateById()returns successfully, and thecatchblock rolls back any newly uploaded file if the UPDATE failed. Previously the old file was unlinked before UPDATE, and a failed UPDATE left a freshly uploaded file orphaned on disk while the record still referenced the deleted old path. - Stale docblock fragments referencing the removed FK-toggle pattern cleaned up in SignupModel, OrganizationUserModel, OrganizationModel, SettingsModel. Stale "// FK constraint түр унтрааж устгана" inline comments removed from UsersController and OrganizationController delete blocks.
Fixed
- PostgreSQL: news archive crashed on first request with
SQLSTATE[42883]: function year(timestamp without time zone) does not exist.Web\Content\NewsController::archive()now branches ongetDriverName():EXTRACT(YEAR FROM published_at)::int/EXTRACT(MONTH FROM published_at)::inton PostgreSQL,YEAR()/MONTH()on MySQL. - PostgreSQL: language copy-content broke on second metadata lookup - the first column-metadata query in
Raptor\Localization\LanguageControllerwas already driver-aware (information_schema.columnsvsSHOW COLUMNS) but a sibling lookup a few lines below ran an unconditionalSHOW COLUMNS FROM $tableand accessed$column['Field']. Both branches now match:information_schema.columns+column_nameon PostgreSQL,SHOW COLUMNS+Fieldon MySQL.
[3.0.0] - 2026-04-28
[3.0.0]: https://github.com/codesaur-php/Raptor/compare/v2.2.0...v3.0.0
Trash system, PSR-14 event dispatcher, soft-delete removal across 15 models, hard-delete for users/organizations, file-based DB cache, admin notification emails, HTML validation, HTTP PATCH for partial updates, query optimizations, Mailer rewrite (PHPMailer/Brevo SDK/Guzzle removed), codesaur dependency upgrades (template v4.0.1, dataobject v9.1.0, http-client v2.1.0), SSH deploy, comprehensive documentation expansion.
Breaking Changes
- Soft delete (deactivateById) removed from most models - only UsersModel, OrganizationModel, SignupModel retain
is_activecolumn is_activecolumn removed from 15 models: NewsModel, PagesModel, ProductsModel, ProductOrdersModel, ReviewsModel, CommentsModel, FilesModel, MenuModel, TextModel, ReferenceModel, SettingsModel, LanguageModel, ForgotModel, DevRequestModel, MessagesModel- All content controllers
deactivate()method renamed todelete()with harddeleteById() - Route paths changed from
/deactivateto/deleteacross all content routers getRowWhere(['id' => $id, 'is_active' => 1])patterns replaced withgetById($id)
Added
- Trash system (
Raptor\Trash) - Deleted records are stored intrashtable before deletionTrashModelcolumns:id,table_name,log_table,original_id,record_data(JSON mediumtext),deleted_by,deleted_at. Thelog_tablecolumn holds the log channel name thatrestore()writes the "restored" row to (e.g.'products','news','content'); controllers pass it directly toTrashModel::store()TrashController- index, list (filter bytable_name), view (JSON detail), restore (recover record), delete (permanent), empty (clear all)TrashRouter- 6 routes under/dashboard/trash/(trash,trash-list,trash-view,trash-restore,trash-delete,trash-empty)- Dashboard UI with table-name filter, detail modal, restore button, SweetAlert confirmations
- system_coder only access
- Menu entry added to MenuSeed (position 365, before Manage Menu)
- Restore feature - schema-aware UNIQUE pre-flight check (information_schema for MySQL, pg_index for PostgreSQL); attempts original ID first to preserve FK references, falls back to auto-increment on PRIMARY KEY conflict (SQLSTATE 23000); restores LocalizedModel
_contentrows with new parent_id; aborts with admin-friendly message on UNIQUE collision (slug, keyword, code, sku, etc.) - Dual restore audit logging - every restore writes to
trash_log(full audit) AND the restored record'slog_tablechannel (so the entry appears in Logger Protocol on the record's view/update page). Thelog_tablevalue is set at delete time by the controller callingTrashModel::store()with its log channel name (e.g. ReviewsController passes'products', ReferencesController passes'content', TemplateController menu delete passes'dashboard')
record_idlog context convention - Standardized record-identifier key across all CRUD log calls. Previously some modules used'id' => $id, others'record_id' => $id, breaking Logger Protocol filters. Refactored 10 controllers (Messages, Language, Text, Organization, RBAC, Users, References, web News/Page/Shop) and updatedWebLogStatsJSON_EXTRACT path from$.idto$.record_id. Reference view/update templates updated to filter byrecord_id. New rule: any log entry tied to a record must use'record_id' => $idin context (auth_user.idretains its own semantic for Badge system)- PSR-14 Event Dispatcher (
Raptor\Notification) - Decoupled notification systemEventDispatcherimplementingPsr\EventDispatcher\EventDispatcherInterfaceListenerProviderimplementingPsr\EventDispatcher\ListenerProviderInterfaceEventbase class implementingStoppableEventInterfaceContentEvent- content CRUD events (news, pages, text, references, files, etc.)UserEvent- user signup/approve eventsOrderEvent- order new/status_changed eventsDevRequestEvent- dev request new/updated eventsDiscordListener- subscribes to all events, delegates toDiscordNotifier- Registered as
eventsservice in Container
- Hard delete for Users/Organizations -
delete()method withsetForeignKeyChecks(false)for permanently deleting deactivated records- Fire icon button in UI for is_active=0 records
- Trash icon button retained for is_active=1 records (deactivate)
- Routes:
user-delete,user-signup-delete,organization-delete
- Controller helpers added to base
Raptor\Controller:dispatch(object $event)- PSR-14 event dispatchgetAdminName()- current admin full namegetDashboardUrl()- dashboard base URL
- CacheService - Custom file-based DB cache (PSR-16 SimpleCache). Гадаад dependency-гүй. Stored in
private/cache/. Registered as container service. Caches: languages, translations, settings, dashboard menu, RBAC permissions, pages navigation, featured pages, recent news, reference data. Auto-invalidated on CRUD operations viaController::invalidateCache()helper. 12-hour TTL safety net. Fail-safe: system works without cache if unavailable - TemplateService caching - Email/notification templates now load through 2-mode strategy: cache enabled = full table cached as
reference.templates.{code}map; cache disabled = per-keyword DB query (single keyword) or batch IN(...) query (multiple keywords) - Admin email notifications - Configurable email notifications for 4 channels: contact messages (
RAPTOR_CONTACT_EMAIL_NOTIFY), orders (RAPTOR_ORDER_EMAIL_NOTIFY), comments (RAPTOR_COMMENT_EMAIL_NOTIFY), product reviews (RAPTOR_REVIEW_EMAIL_NOTIFY). Each channel has independent toggle and recipient email in.env. Contact and order enabled by default, comment and review disabled by default - Email notification UI - Toggle switch and recipient email settings at top of messages, orders, comments, reviews index pages. Visible to
system_coderrole only. Directly modifies.envfile viaMessagesControllertoggle/update methods - Email templates -
contact-message-notify,order-status-update,comment-notify,review-notifytemplates inreference_templatesfor admin notification emails - HtmlValidationTrait - Server-side HTML content validation for Pages, News, Products. Detects unclosed HTML comments and broken tags causing >20% content loss. Rejects save with error message
- NewsModel::getRecentPublished() - Reusable method for recent published news with cache-friendly field selection (excludes dynamic
read_count) - moedit built-in notify -
_notify(type, msg)method with built-in popup notification (no external dependency). Displays centered toast with icon, colored background, auto-dismiss after 2.5s. Supportssuccess,warning,danger,infotypes - moedit HTML validation - Client-side validation when switching Source to Visual mode. Detects unclosed comments and broken tags. Stays in Source mode on failure with warning notification
- SSH Deploy - New deploy job using
rsync+ssh-actionfor Linux servers with SSH access (VPS, cloud VM, dedicated). Post-deploycomposer dump-autoloadvia SSH
Changed
- All direct
$this->getService('discord')?->calls (35 occurrences across 16 controllers) replaced with$this->dispatch(new Event(...))pattern - Removed duplicated
$adminName/$appUrlvariable assignments from controllers (now helper methods) codesaur/dataobjectupgraded to v9.1.0 - significant ORM upgrade with breaking error-handling changes and new helper methods that Raptor adopts framework-wide:- New
Constantsclass centralizes all magic values previously scattered through the ORM. Raptor now imports it everywhere a literal would have been used:Constants::DRIVER_PGSQL/DRIVER_MYSQL/DRIVER_SQLITEreplaces hardcoded'pgsql'string comparisons ingetDriverName()checks (25 occurrences across 14 files: WebLogStats, TrashController, Application init, multiple migration helpers, etc.)Constants::DEFAULT_CODE_LENGTHreplaces hardcoded2innew Column('code', 'varchar', 2)(9 models with localization code columns)Constants::CONTENT_TABLE_SUFFIX('_content') used byTrashController::insertLocalizedContent()to derive the LocalizedModel content-table name without hardcoding the suffix- Other constants available for future use:
COL_ID,COL_IS_ACTIVE,COL_PARENT_ID,COL_CODE,LOCALIZED_KEY,CONTENT_KEY_COLUMNS,PRIMARY_ALIAS_PREFIX,CONTENT_ALIAS_PREFIX,TABLE_NAME_PATTERN, error codes
- Breaking:
insert()andupdateById()now throw on failure instead of returningfalse. Return type narrowed fromarray|falsetoarray. All Raptor callers updated; pattern=== falsechecks removed in favour of try/catch around the ORM call - Breaking:
deactivateById()throws when row is already inactive (previously returnedfalse). Also no longer mutates UNIQUE column values on deactivation (Raptor'sUsersModel/SignupModel/OrganizationModelno longer rely on the old "negate numeric / prefix[uniqid]" hack - the username/email column lengths could be reduced from 143 to 128 chars accordingly) - New shortcut methods leveraged across Raptor:
getById(int $id)adopted across 18 controllers (replacing the oldergetRowWhere(['id' => $id])pattern);countRows(array $condition)used byTrashController::empty()for the pre-clear summary count;existsById(int $id)available for lightweight presence checks (SELECT 1 ... LIMIT 1) - PDO error extraction centralized via
throwPdoError()helper inside the package (no Raptor change needed - exceptions now carry richer context automatically)
- New
codesaur/templateupdated to v4.0.1 - adds{% for %}{% else %}{% endfor %}empty-iterable branch and object method calls in expressions ({{ user.can('edit') }},{% if auth.is('admin') %}). Both were silently failing before; permission-gated UI is now visible to authorized users, and templates with{% else %}no longer truncate everything after{% endfor %}.composer.json:psr/simple-cachemoved from dev to stable version,minimum-stability: devremoved,psr/event-dispatcheradded- Example
deactivateById()calls wrapped in try/catch (adapts to v9.1.0's throw-on-already-inactive behaviour) - Shop module router merge -
OrdersRouter,ProductsRouter,ReviewsRouterconsolidated into singleShopRouter. Reviews routes renamed:reviews->products-reviews(GET=HTML, POST=JSON),reviews-delete->products-reviews-delete;reviews-listandreviews-viewremoved (handler logic merged into index/products view) twigDashboard()renamed todashboardTemplate()- Method inDashboardTraitrenamed across 23 controllers and 4 docs/CLAUDE files. Old name was a leftover from when codesaur/template embedded Twig- CacheService cross-platform robustness - All filesystem ops (
unlink,file_put_contents,file_get_contents,mkdir,rmdir) wrapped with@to suppress warnings on Windows file locks/race conditions.clear()now returnsfalseon partial failure (PSR-16 compliance).set()now properly handles\DateIntervalTTL.has()correctly returns true for explicitly-stored null values - Mailer rewritten with native PHP / codesaur primitives - All third-party email dependencies (PHPMailer, Brevo SDK, Guzzle) removed:
- Brevo transport (
sendBrevoTransactional) - rewritten usingcodesaur/http-client'sJSONClient(no more Brevo SDK + Guzzle dependency tree) - SMTP transport (
sendSMTP) - rewritten using nativestream_socket_client(no more PHPMailer). Visibility changed frompublictoprotected. New helpers:smtpRead,smtpCommand,smtpExpect,buildMimeMessage,formatAddress - Mail transport (
sendMail) - new method using PHPmail()for cPanel/VPS sendmail/postfix - Transport selection via new
RAPTOR_MAIL_TRANSPORTenv var:brevo(default),smtp,mail.send()dispatches to the appropriate handler based on this setting .envkeys added:RAPTOR_MAIL_TRANSPORT,RAPTOR_SMTP_HOST,RAPTOR_SMTP_PORT,RAPTOR_SMTP_USERNAME,RAPTOR_SMTP_PASSWORD,RAPTOR_SMTP_SECURE- Attachments - URL/base64 (Brevo), local file/base64 (SMTP), all types via parent class (Mail)
- Brevo transport (
- WebLogStats query optimization - Consolidated 14 separate
GROUP BY+JSON_EXTRACTqueries into 2 single-scan queries (1 for today's live data, 1 per cached date). Eliminates MySQL temporary table creation that caused "Disk full" errors on shared hosting.refreshCache()batch limited to 5 dates per request to prevent overloading/tmppartition - moedit.js - Removed
opts.notifyoption._notifydefinition moved entirely tomoedit.ui.jsas built-in. Version bumped to v3 - moedit.ui.js -
_notifyrewritten as built-in 2-parameter method(type, msg). All 3-parameter calls consolidated to 2 parameters. RemovedNotifyglobal fallback andalert()fallback. Version bumped to v3 - dashboard.js -
copyContent()moved to Files moduleindex.html(only used by file tag/location modals).initLoggerProtocol()LIMIT capped at 100 entries with idempotent guard against duplicate fetches.initInvalidTabFocus()MutationObserver auto-switches Bootstrap tab when invalid input detected on hidden tab. Version bumped to v4 - News list query - Correlated subquery for comments count replaced with single LEFT JOIN. 4000 news = 4000 subqueries reduced to 1 grouped JOIN
- Database indexes - Added
idx_news_content_active_created,idx_news_content_code,idx_news_comments_newsid_activefor news list performance - DashboardMenus renamed to MenuSeed - Class and file renamed for consistency with other seed classes (PermissionsSeed, RolePermissionSeed). CLAUDE.md and CHANGELOG.md updated
- PagesModel::getFeaturedLeafPages() - SELECT fields expanded for developer flexibility (added description, photo, code, type, category, position, published_at, created_at)
- PrivateFilesController -
read()andsetFolder()now block access toprivate/cache/directory - Cache invalidation - Moved from
finallyblocks totryblocks (after successful DB write only). Exception-safe with error logging in development mode - Partial update routes: PUT -> PATCH - 4 routes that perform partial resource updates now use HTTP PATCH instead of PUT, following RESTful conventions:
PATCH /dashboard/orders/{id}/status- Order status update (single field)PATCH /dashboard/files/{table}/{id}- File metadata update (changed fields only)PATCH /dashboard/settings/env- Single .env key-value toggle/updatePATCH /dashboard/messages/replied/{id}- Mark message as replied (is_read + replied_note)
- Frontend templates updated - All
csrfFetch()calls for the 4 PATCH routes changed frommethod: 'PUT'tomethod: 'PATCH'across 6 HTML files (orders-view, orders-index, reviews-index, messages-index, comments-index, files-update-modal) - CsrfMiddleware - PHPDoc updated to document PATCH as a protected method (POST/PUT/PATCH/DELETE). No code change needed - middleware already validates all non-safe methods
- LogsController::retrieve() - Now sanitizes and applies
OFFSETparameter (was silently ignored, causing infinite-scroll endless loop onindex-list-logs.html).LIMITcapped at 200 max with default 100 - Empty
WHERESQL guard - 3 list controllers (NewsController, PagesController, DevRequestController) now build optionalWHEREclause to avoidWHERE ORDER BYsyntax error when no filter params are provided - codesaur/http-client upgraded to v2.1.0 - New features:
Responseobject (statusCode,headers,body,isOk(),isError(),json()),CurlClient::send()returning Response,CurlClient::sendWithRetry()with exponential backoff,CurlClient::upload(),JSONClientbase URL support, auto SSL verify based onCODESAUR_APP_ENV - DiscordNotifier -
CurlClient::request()replaced withsendWithRetry()for automatic retry on transient failures (2 retries with exponential backoff). Response status logged in development mode. AddednewReview()method for product reviews showing star rating + comment - BadgeController -
trashlog table now bypassesauth_user.id != adminIdself-filter (admin's own trash entries should appear in their own badge).storeaction color changed from green to red (semantically a destructive operation) - deploy.yml - Renamed from cPanel-specific to generic naming: "cPanel FTP Deploy" -> "FTP Deploy", job
cpanel->ftp. Pre-check jobcheck-ftp->check-targetsdetects both FTP and SSH. "XAMPP htdocs" -> "Server project directory". All 3 jobs (FTP, SSH, Windows) run in parallel when configured - SpamProtectionTrait - Turnstile verification uses
CurlClient::send()withResponseobject. HTTP status check (isError()) added before JSON parsing.json_decode()replaced with$response->json()
Removed
is_activecolumn and all related SQL queries, indexes, filter logic from 15+ models and their controllers- Direct Discord notification calls from all controllers (replaced by PSR-14 events)
- Codecov integration from CI workflow (coverage reports remain local)
OrdersRouter,ProductsRouter,ReviewsRouterfiles (merged intoShopRouter)ReviewsController::list()andReviewsController::view()methods (functionality merged intoindex()with method-aware response)is_activeorphan check inreferences-index.html(Reference module never hadis_activecolumn)phpmailer/phpmailer ^7.0.1dependency (replaced by nativestream_socket_clientSMTP)getbrevo/brevo-php ^2.0.14dependency (replaced bycodesaur/http-client'sJSONClient)guzzlehttp/guzzletransitive dependency (was pulled in by Brevo SDK; no longer needed)
Tests
- PatchRoutesTest - 28 tests (40 assertions) covering PATCH route matching, PUT rejection, parameter extraction, URL generation, CsrfMiddleware PATCH validation, source code verification
- CacheTest expanded - 46 tests covering PSR-16 compliance: TTL expiry,
\DateIntervalTTL,has()semantics for null/false/zero,getMultiple/setMultiple/deleteMultiple, edge cases (long keys, unicode, special chars, corrupted files, nonexistent dir, invalid path)
Documentation
- moedit manual (MN/EN) - Added HTML validation section covering editor-side and server-side validation
- messages manual (MN/EN) - Added admin email notification settings section
- comments manual (MN/EN) - Added admin email notification settings section
- orders manual (MN/EN) - Added admin email notification section, separated customer and admin email sections
- reviews manual (MN/EN) - Added admin email notification settings section
- pages manual (MN/EN) - Added HTML content validation warning in create/edit sections
- news manual (MN/EN) - Added HTML content validation warning in create/edit sections
- products manual (MN/EN) - Added HTML content validation warning in create/edit sections
- docs/en/api.md, docs/mn/api.md - Added 10 new sections: HtmlValidationTrait, DashboardTrait, FileController, AIHelper, Badge System, MenuModel, Dashboard Home, Dashboard Manual, ExceptionHandler, Seed/Initial Data. Route tables updated for PATCH. ShopRouter unified route table added (replaces ProductsRouter/OrdersRouter/ReviewsRouter). CsrfMiddleware added to pipeline. Web controller methods expanded (favicon, commentSubmit, reviewSubmit). Return types updated from
TwigTemplate(removed) toFileTemplate - docs/en/README.md, docs/mn/README.md - Added 5 new module sections (6.24-6.28): Badge System, Dashboard Home, Dashboard Manual, AI Helper, Seed/Initial Data. Template module expanded with DashboardTrait, MenuModel, FileController. CSRF section added (6.20). Middleware pipeline table updated. Architecture flow updated. Features list expanded. Router example code expanded with PATCH. Template engine section rewritten - codesaur/template (NOT Twig) limitations table added (range operator,
inmembership,ends with, etc.) - CLAUDE.md - Template engine clarification (codesaur/template, not Twig). Twig features NOT supported listed with replacements.
dashboardTemplate()method (renamed fromtwigDashboard()). CsrfMiddleware updated to PATCH-aware - README.md - Added CSRF protection and sidebar badge system to features list (MN/EN). Architecture diagram updated with CSRF middleware
- PHPDoc - Updated in OrdersController, ContentsRouter, MessagesController, SettingsController, FilesController, CsrfMiddleware, ShopRouter, Application.php (web), CacheService
[2.2.0] - 2026-03-19
[2.2.0]: https://github.com/codesaur-php/Raptor/compare/v2.1.0...v2.2.0
Security hardening (CSRF, login rate limiting, SQL injection protection), Discord notifications expansion, test coverage 3x increase, web content metadata and social sharing, news category listing, page sidebar, files module consolidation, and database compatibility improvements.
Added
- CsrfMiddleware - Per-session CSRF token validation for all dashboard POST/PUT/PATCH/DELETE requests. Token generated at login and auto-generated for existing sessions. Delivered to frontend via
<meta name="csrf-token">tag - csrfFetch() / getCsrfToken() - JS wrapper in
dashboard.jsthat auto-attachesX-CSRF-TOKENheader to fetch requests - Login rate limiting -
checkLoginAttempts()queriesdashboard_logfor failed attempts by IP or username. 10+ failures within 15 minutes triggers 429 lockout - Forgot password cooldown -
checkForgotCooldown()queriesforgottable to prevent repeat requests withinRAPTOR_PASSWORD_RESET_MINUTES - initLoggerProtocol() - Centralized log display function in
dashboard.js. Templates only needdata-retrieve,data-view,data-contextattributes on<ul>element - initInvalidTabFocus() - Auto-focuses invalid form inputs in inactive Bootstrap tabs via MutationObserver
- SQL injection protection -
LogsController::retrieve()sanitizes CONTEXT field names (allowlist regex), ORDER BY (column + direction only), LIMIT (integer only). Client-supplied WHERE/HAVING/JOIN keys are stripped - Discord notifications expanded -
contentAction()added to: ReferencesController (insert/update/delete), TextController (insert/update/delete), LanguageController (insert/update/delete), MessagesController (reply/delete), CommentsController (delete), ReviewsController (delete) - News type listing page -
NewsController::newsType()supportstype=all(all news) and specific type filtering. Two-column layout with news cards (left) and category sidebar (right). Route:GET /news/type/{type} - PagesSamples news link - "Мэдээлэл" / "News" navigation page entry with
link=/news/type/alladded for MN and EN - NewsSamples categories - Sample news now have distinct types:
technology,announcement,guidefor category sidebar demo - Web content metadata - News, Page, Product detail pages now display: published_at, creator, publisher, read_count, word_count, read_time, is_featured, category/type. Controllers JOIN users table for creator/publisher names
- Social share + PDF - Facebook, Twitter/X share buttons and Print/PDF button added to news, page, and product detail pages
- OG meta improvements -
og:urladded,og:imageuses absolute URL viabase_url.TemplateControllernow passesbase_urlandcurrent_urlto all web layouts - Page sidebar - Two-column layout with siblings navigation (parent title as header), metadata card, and share/PDF card
- Translations -
too-many-login-attempts,password-reset-cooldown,optimize-images,all,share,words,minkeywords added to TextInitial - Unit tests - 372 new tests (189 -> 561 total, 1419 assertions) covering: CsrfMiddleware, Logger masking/interpolation, BadgeController structure, Controller permissions, LocalizationMiddleware, SettingsMiddleware, SessionMiddleware CSRF, ErrorHandler, LoginController helpers (normalizeEmail, isGibberishUsername), FileController validation, FilesController access control, published/draft access pattern, DiscordNotifier, SpamProtection edge cases, LogsRetrieve SQL injection sanitization
Changed
- dashboard.js -
eval()replaced withcreateElement('script')for AJAX modal scripts. Badge seen POST usescsrfFetch(). Version bumped to v3 - moedit.ui.js - Upload uses
csrfFetchwhen available, falls back tofetch. Version bumped to v2 - All dashboard/raptor templates (53 files) -
fetch()replaced withcsrfFetch()for state-changing requests - login.html - Uses plain
fetch()(login is CSRF-exempt,dashboard.jsnot loaded) - web-log-stats.html - Uses plain
fetch()(GET-only, runs beforedashboard.jsdefer) - Files module consolidated -
index-files.htmlmerged intoindex.html. Upload/delete/update only available onfilestable; attachments tables are list-only - FilesController -
post()rejects non-files table withrecord_id=0(403).deactivate()rejects non-files table (403). Default table selection fixed tofilesinstead of first alphabetical key - Language insert modal - Script wrapped in IIFE to prevent
constredeclaration on reopen. Copy language flag display removed to avoid UX confusion - Localization log actions - Renamed from
text-*/language-*tolocalization-text-*/localization-language-*prefix - BadgeController -
BADGE_MAPlocalization actions updated with new prefix. JSON query now supports both MySQL (JSON_EXTRACT) and PostgreSQL (::jsonb) with driver detection - Auto-increment reset -
clearSamples()in ProductsController, NewsController, PagesController now usessetval()for PostgreSQL,AUTO_INCREMENTfor MySQL - Dev requests naming - Tables renamed:
development->dev_requests,dev_request_responses->dev_requests_responses. Log table, files tables, and BadgeController updated accordingly - Dev requests attachments - Index list now shows combined attachment count (request + response files) via subquery
- SessionMiddleware - Dashboard closure extended: session stays writable when
CSRF_TOKENis empty (for first-time token generation) - Controller.php - CSRF token passed to Twig via request attribute (
csrf_token). Removed unusedrequestTwig variable (was path-only, never used in templates) - Application.php - Middleware pipeline renumbered 1-9, CsrfMiddleware registered as step 6 after JWTAuth
- Files manual (MN/EN) - Added upload, edit/delete sections with files vs attachments distinction and permission table
- SECURITY.md - Added CSRF, login rate limiting, password reset cooldown, SQL injection documentation
- CLAUDE.md - Added CsrfMiddleware, Logger Protocol sections. Updated SessionMiddleware and dashboard.js documentation
- Language delete - Changed from soft delete (deactivate) to hard delete. Unique constraints on
code,locale,titlecolumns prevent soft delete. Route renamed:/dashboard/language/deactivate->/dashboard/language/delete, methoddeactivate()->delete(), log actionlocalization-language-delete - codesaur/http-message upgraded to v3.0.2 - Fixed
initFromGlobal()to populate$this->headersfromgetallheaders(), enablinggetHeaderLine()for all HTTP headers
Removed
- index-files.html - Merged into index.html (renamed to .bak for verification)
- Logger protocol duplication - ~50-80 lines of duplicated JS log retrieval code removed from 13 template files
Fixed
- Language insert modal -
Identifier already declarederror when reopening modal (IIFE fix) - Language insert modal -
aria-hiddenwarning on focused element - Files index default table - URL without
?table=parameter now correctly defaults tofilesinstead of first alphabetical table - Log protocol undefined -
log.contextnull check added to preventCannot read properties of undefinederrors
Security
- CSRF protection - All dashboard state-changing requests now require valid CSRF token via
X-CSRF-TOKENheader - Login brute force - Rate limited via
dashboard_loganalysis (10 attempts / 15 min per IP or username) - Forgot password abuse - Cooldown enforced per email via
forgottable timestamp check - Log retrieve injection - Context field names sanitized with
/^[a-zA-Z0-9_.]+$/, ORDER BY validated ascolumn ASC|DESC, LIMIT validated as integer, WHERE/HAVING/JOIN stripped from client input - File upload restriction - Attachment tables reject direct upload/delete from files index page (backend enforcement)
[2.1.0] - 2026-03-18
[2.1.0]: https://github.com/codesaur-php/Raptor/compare/v2.0.1...v2.1.0
Product review/rating system, comments consolidation, and dashboard UX improvements.
Added
- Product Reviews - Star rating (1-5) with written review on product detail pages. New model (
ReviewsModel, table:products_reviews), controller (ReviewsController), and router (ReviewsRouter) - Web review form - Spam-protected review submission via
POST /session/product/{id}/reviewwith honeypot, HMAC, rate limiting, Turnstile support - Web product media gallery - Thumbnail strip + large preview replacing the old attachment table. Images open in Fancybox, video/audio play inline, documents download or open in new tab (PDF)
- Web products list ratings - Average star rating and review count displayed on product cards
- Dashboard review management - Reviews list accessible from products-index header, product reviews shown in products-view with delete (SweetAlert2 confirmation)
- Badge info color - New
info(bg-info, cyan) badge color for comment/review activity, distinct fromblue(bg-primary) used for updates - Translations -
average-rating,can-review,rating,reviews,write-reviewkeywords added to TextInitial - Manual -
reviews-manual-mn.html,reviews-manual-en.htmlfor the reviews module
Changed
- ProductsModel -
commentcolumn renamed toreview(tinyint toggle for enabling reviews) - ProductsController -
list()includesreview_countandavg_ratingvia subquery;view()passes full reviews list to template - OrdersController - Log table changed from
'product'to'products_orders'for dedicated order logging - ShopController (web) -
orderSubmit()logs to'products_orders';product()fetches reviews + spam tokens;products()LEFT JOINs review stats - Comments consolidated into news-view -
CommentsController::view()now redirects to/dashboard/news/view/{id}#commentsinstead of rendering separatecomments-view.html. Comments sidebar menu removed, link moved to news-index header - Reviews consolidated into products-view - No separate reviews-view page. Reviews sidebar menu removed, link moved to products-index header
- BadgeController BADGE_MAP - Comment actions (
comment-insert,comment-reply) now badge/dashboard/newswithinfocolor. Review actions (review-insert) badge/dashboard/productswithinfocolor. Order actions moved toproducts_orderslog table key - BadgeController PERMISSION_MAP - Removed
/dashboard/commentsand/dashboard/reviewsentries (no longer sidebar modules) - DashboardMenus - Removed Comments and Reviews sidebar menu entries
- dashboard.js -
COLOR_MAPand badge render order updated:green -> info -> blue -> red - products-insert.html / products-update.html -
commentfield renamed toreview, label changed toreviewswithcan-reviewtext, icon changed tobi-star-half - products-view.html - Comment badge replaced with review badge, full reviews section with scrollable list and delete buttons
Removed
- reviews-view.html - Consolidated into products-view.html
- TextInitial unused keywords - Removed 14 keywords not used via
|textor$this->text():allow-write,clear,download,empty-directory,empty-result,items,lines,log-file-empty,network-error,newer,older,refresh,rows-found,running
[2.0.1] - 2026-03-18
[2.0.1]: https://github.com/codesaur-php/Raptor/compare/v2.0.0...v2.0.1
GitHub Actions Node.js 24 compatibility update.
Changed
- ci.yml -
actions/checkoutv4 -> v6 (Node.js 24 support) - cpanel.deploy.yml (example) -
actions/checkoutv4 -> v6,SamKirkland/FTP-Deploy-Actionv4.3.5 -> v4.3.6
[2.0.0] - 2026-03-18
[2.0.0]: https://github.com/codesaur-php/Raptor/compare/v1.9.1...v2.0.0
Dashboard sidebar badge system - log-based activity notifications for admin users.
Added
- Badge system - Dashboard sidebar menu items display colored badge pills showing unseen activity counts. Green = create/insert, blue = update, red = delete/deactivate. Multiple badges per module shown in green-blue-red order
- BadgeController (
raptor/template/) - BADGE_MAP constant maps log table + action pairs to sidebar modules with colors. PERMISSION_MAP controls per-module RBAC access. Queries*_logtables via JSON_EXTRACT to count actions since admin's last visit - AdminBadgeSeenModel (
raptor/template/) - Trackschecked_atdatetime per admin per module.last_seen_countfor file-count badges (manual, migrations) - BadgeRouter (
raptor/template/) -GET /dashboard/badgesreturns all badge counts as JSON,POST /dashboard/badges/seenmarks a module as seen - initSidebarBadges() (
dashboard.js) - AJAX badge loader. Fetches badge data on page load, renders colored pills on matching sidebar links, POSTs seen on click - Sidebar badge CSS (
dashboard.css) - flex layout for nav-link with auto-margin badge positioning
Changed
- ContactController -
contactSend()log context now includesauth_userwithoutidfield so badge system distinguishes web visitors from admin users - Web NewsController -
commentSubmit()log context now includesauth_userwithoutidfield for the same reason - Dashboard Application - Registered BadgeRouter
- dashboard.html - Added
initSidebarBadges()call on DOMContentLoaded, bumped CSS/JS versions to v=2 - CLAUDE.md - Added "Dashboard Sidebar Badge System" section documenting architecture, BADGE_MAP, PERMISSION_MAP, and integration guide
[1.9.1] - 2026-03-18
[1.9.1]: https://github.com/codesaur-php/Raptor/compare/v1.9.0...v1.9.1
Contact page layout redesign, sample data improvements.
Changed
- Contact page layout - Photo and content moved to left column, contact info card and message form to right column. Photo renders with Fancybox lightbox like other page templates
- Contact info card - Removed "Холбогдох мэдээлэл" heading. Card hidden entirely when no contact info items exist
- Settings seed data - Added sample phone, email, address, open-hours, and social media links so contact info card is visible on fresh install
- PagesSamples contact content - Replaced email-centric text with generic message encouraging form use. Added Google Maps embed (Ulaanbaatar) as reference for developers. Added office-view.jpg as header image
[1.9.0] - 2026-03-17
[1.9.0]: https://github.com/codesaur-php/Raptor/compare/v1.8.2...v1.9.0
Messages, Comments, Spam Protection, Discord notifications, template refactoring.
Added
- Messages module - Contact form submissions saved to
messagesDB table with dashboard management (index, view, delete). Admin can mark messages as New/Read/Replied with note (phone/email contacted). Dashboard menu under Contents - Comments module - News article comments with 1-level reply thread.
news_commentstable withparent_id(self FK) andcreated_by(admin/guest). Web visitors can post comments whencomment=1on news. Admin can reply from dashboard - CommentsController (Dashboard) - View all comments across news, reply to comments, delete with cascade. Accessible from Contents -> Comments menu and news-index publish column
- Comments view news metadata - Comments view page shows news article photo (if exists), publisher name, read count, published status, and full content so admin can read the article before replying to comments
- ContactController - Extracted from PageController to
Web\Service\ContactControlleras standalone controller with own template - SpamProtectionTrait - Unified spam protection: honeypot, HMAC token + timestamp, session rate limit, Cloudflare Turnstile (optional), link spam filter. Used by ContactController, NewsController, ShopController, LoginController
- Cloudflare Turnstile - Optional CAPTCHA via
RAPTOR_TURNSTILE_SITE_KEY/RAPTOR_TURNSTILE_SECRET_KEYenv vars. When keys are empty, Turnstile is skipped. Applied to contact form, comment form, order form, signup form - Link spam filter -
checkLinkSpam()blocks messages/comments with 3+ URLs - Comment reply validation - Backend enforces 1-level reply limit: reply-to-reply requests rejected with 400 error. Both web (
NewsController::comment) and dashboard (NewsController::commentReply) validate thatparent_idpoints to a root comment - CI workflow -
.github/workflows/ci.ymldefault workflow runs on every push and PR: composer validate, PHP syntax check, merge conflict markers, debug statement detection, autoload verification - cPanel deploy workflow -
docs/conf.example/cpanel.deploy.ymlrestructured to useworkflow_runtrigger - waits for CI to succeed before deploying. CI failure skips deploy automatically - Discord notifications -
settingsUpdated()for settings changes (texts/files/options),contentAction()now shows changed fields on update. App URL shown in footer of all notifications - DiscordNotifier app URL - All notification methods accept
$appUrlparameter, displayed as embed footer - SettingsModel default config - Initial settings include JSON structure for social media links and open hours
- TextInitial keywords -
messages,read,replied,reply,reply-method,contacted-by-phone,contacted-by-email,comments,write-comment,contact-info,send-message,working-hours,social-media,thank-you,server-error,confirm-delete,delete-with-replies,view,no-email - Manual pages - Comments manual (EN/MN) and Messages manual (EN/MN) added to
application/dashboard/manual/. Covers overview, list view, actions (reply, delete), and required permissions for each module - Unit tests - 14 SpamProtectionTrait tests (honeypot, HMAC, timestamp, rate limit, Turnstile skip/verify, link spam)
Changed
template()renamed totwigWebLayout()- Web TemplateController method renamed to matchtwigDashboard()pattern. Auto-mapstitle,code,description,photofrom$varsto index layout SEO meta. All web controllers updated- Session routes use
/session/prefix - All web routes that write to$_SESSIONnow use/session/prefix (/session/language/{code},/session/contact-send,/session/order,/session/news/{id}/comment). SessionMiddleware simplified tostr_starts_with($path, '/session/') - Discord
contentAction()title simplified - Removed redundant(Updated)/(Deleted)label from title since description already states the action. ID moved from separate field to description as#ID - Contact form localization - All hardcoded
lang == 'mn' ? ... : ...text replaced with|textTwig filter using TextInitial keywords. JS text passed via Twig{{ 'keyword'|text }}directly - Contact form error display -
alert()replaced with Bootstrap modal for both success and error messages - Order form submit protection - Submit button disabled with spinner while processing to prevent duplicate submissions
- Comment form UX - Send button disabled until both name and comment fields are filled. Placeholder shows
*for required fields. Prevents confusing silent failures - Messages reply dialog - "Contacted by email" checkbox disabled with "(No email)" label when message has no email address. Prevents admin from selecting an unavailable reply method
- Dev request view - Response thread order changed to
flex-column-reverse(oldest first at bottom, newest at top)
Removed
- Pages
commentfield - Removed from PagesModel, PagesController permission checks, page-insert/update/view templates. Comments are a News-only feature per best practice
[1.8.2] - 2026-03-16
[1.8.2]: https://github.com/codesaur-php/Raptor/compare/v1.8.1...v1.8.2
Web layer action logging for public-facing controllers.
Added
- ShopController::order() - Log when order form is opened, includes product_id and title when a product is pre-selected
- SearchController::search() - Log search queries with result count
- SeoController::sitemap() - Log HTML sitemap page views
- NewsController::archive() - Log news archive page views with selected year
[1.8.1] - 2026-03-16
[1.8.1]: https://github.com/codesaur-php/Raptor/compare/v1.8.0...v1.8.1
Web layer naming cleanup and Mongolian text corrections.
Changed
Web\Seorenamed toWeb\Service-application/web/seo/folder renamed toapplication/web/service/. Search, Sitemap, RSS are site-wide services, not SEO-specificSiteRouterrenamed toWebRouter- Class and file renamed fromSiteRouter.phptoWebRouter.phpfor consistency withWeb\Applicationnaming- Mongolian text fix -
'products'keyword translation changed from "Бүтээгдэхүүнүүд" to "Бүтээгдэхүүн" inTextInitial,DashboardMenus, andPagesSamplesseed data
Documentation
- All
*.mdfiles updated to reflect renamed paths and namespaces (CLAUDE.md,README.md,docs/mn/,docs/en/)
[1.8.0] - 2026-03-15
[1.8.0]: https://github.com/codesaur-php/Raptor/compare/v1.7.1...v1.8.0
Major code review, refactoring, shared middleware consolidation, seed data extraction, RBAC UI redesign, security hardening, and CLAUDE.md rewrite.
Added
- SearchController (
Web\Seo) - Dedicated web search controller with full-text search across Pages (title, slug, description, content, source, link), News (title, slug, description, content, source), Products (title, slug, description, content, link). Strips<img>tags from content before matching to avoid false positives from image URLs - PrivateFilesController security - Blocks dangerous file extensions (php, phtml, phar, sh, bat, cmd, exe, ini, log, sql) and sensitive filenames (.env*, .htaccess, .htpasswd, .gitignore, composer.json, composer.lock) with 403 Forbidden
- Owner access pattern - Users without
_update/_deletepermission can edit/delete their own unpublished (published=0) records in News, Pages, Products controllers - Published view access -
published=1records viewable by all authenticated admins regardless of_indexpermission - Seed data classes - Extracted seed data from Model files into dedicated classes to reduce runtime file size:
NewsSamples,PagesSamples,ProductsSamples- Content sample dataDashboardMenus- Dashboard sidebar menu structurePermissionsSeed- RBAC permissions (parameterized queries)RolePermissionSeed- Role-permission assignments + role creation (admin, manager, editor, viewer)
- PrivateFilesBlockedTest - 21 tests for blocked file extension/name security
- RAPTOR_PASSWORD_RESET_MINUTES - Renamed from
CODESAUR_PASSWORD_RESET_MINUTES, added to.envand.env.example
Changed
- SessionMiddleware consolidated -
Web\SessionMiddlewareandRaptor\Authentication\SessionMiddlewaremerged into singleRaptor\SessionMiddlewarewithneedsWriteclosure constructor - LocalizationMiddleware consolidated -
Web\LocalizationMiddlewaremerged intoRaptor\Localization\LocalizationMiddlewarewithsessionKeyconstructor parameter. Controllers read session key fromlocalizationattribute instead of hardcoding - Web namespace refactored -
Web\Home` renamed toWeb\Site`,HomeRouterrenamed toSiteRouter - RBAC UI redesigned -
rbac-alias.htmlchanged from wide table matrix to responsive card-based layout (2 cards per row) with per-role permission checkboxes, select all/deselect all buttons, and badge counters. Roles and permissions display asalias_nameformat - ReferenceInitial refactored - Single-line insert statements broken into readable multi-line format with numbered sections
- error.html optimized - particles.js moved from inline (~15KB) to CDN, CSS reformatted to multi-line
- reset() permission lowered - News, Pages, Products reset changed from
_deleteto_indexpermission so editors can clear sample data - reset() enhanced - All reset methods now also delete
is_active=0records alongside sample data - FilesController owner access -
update()allows own file edit,deactivate()allows own unattached (record_id=0) file delete without permission - UsersController email normalization - Gmail
normalizeEmail()added toinsert()andupdate()methods - DevRequestController auth fix - Replaced
!$this->getUserId()with!$this->isUserAuthorized()in all 7 methods to prevent id=0 bypass - PagesModel::buildTree() - Changed from
publictoprivate(only used internally) - Twig comments removed - All
{# #}comments in 31 HTML files converted to vanilla<!-- -->for template engine independence - SessionMiddlewareTest updated - Tests both Web and Dashboard closure logic (21 tests)
- Language dropdown - Hidden when only one language is active (web navbar, dashboard login)
Removed
- products-read.html - Unused template, route, and
ProductsController::read()method removed - Web\SessionMiddleware - Replaced by shared
Raptor\SessionMiddleware - Web\LocalizationMiddleware - Replaced by shared
Raptor\Localization\LocalizationMiddleware
Documentation
- CLAUDE.md rewritten - Restructured as forward-looking AI guide with "Adding a New Module" 11-step checklist, shared middleware configuration, owner access pattern, migration rules, and general conventions
- CONTRIBUTING.md - Removed
## Project Structuresection (duplicated in CLAUDE.md) - docs/en/api.md, docs/mn/api.md - Updated SessionMiddleware and LocalizationMiddleware docs with shared middleware details
- docs/en/README.md, docs/mn/README.md - Updated directory tree (
web/site/,SessionMiddleware.phplocation), removed deleted files
[1.7.1] - 2026-03-13
[1.7.1]: https://github.com/codesaur-php/Raptor/compare/v1.7.0...v1.7.1
Shop router registration moved from shared base application to Dashboard application.
Changed
- Dashboard\Application - Shop module routers (
ProductsRouter,OrdersRouter) now registered here instead of inRaptor\Application, since shop routes are dashboard-specific - Raptor\Application - Removed
Dashboard\Shop\ProductsRouterandDashboard\Shop\OrdersRouterregistration from shared base class
[1.7.0] - 2026-03-13
[1.7.0]: https://github.com/codesaur-php/Raptor/compare/v1.6.0...v1.7.0
Anti-spam hardening: username gibberish detection, Gmail email normalization, scoring-based validation system.
Added
- Username Gibberish Detection - Score-based system to reject bot-generated random usernames (
LoginController::isGibberishUsername())- Shannon entropy check: high randomness in character distribution (+2/+3 points)
- Vowel ratio check: rejects consonant-heavy nonsense (+1/+3 points)
- Consecutive consonant check: flags long unpronounceable clusters (+1/+2 points)
- Case change ratio check: detects random casing patterns like
CdIrBVTolz(+1/+3 points) - Score threshold of 3+ required to reject (no single check causes false rejection)
- Designed for Mongolian transliteration compatibility (
munkhtseteg,tserenboldpass cleanly)
- Username Format Validation - Regex: 3-63 characters, must start with a letter, allows letters/numbers/underscores/dots
- Gmail Email Normalization -
LoginController::normalizeEmail()strips Gmail alias tricks before uniqueness checks- Removes dots from Gmail/Googlemail local parts (
y.i.r@gmail.com->yir@gmail.com) - Strips
+sub-addressing (user+spam@gmail.com->user@gmail.com) - Normalizes
googlemail.com->gmail.com - Normalized email stored in database, preventing future bypass with dot variations
- Removes dots from Gmail/Googlemail local parts (
- Anti-Spam Tests -
LoginSpamProtectionTestwith 43 test cases- 22 valid username tests (Mongolian, Slavic, German names, common patterns)
- 8 gibberish username tests (random chars, keyboard smash, bot patterns)
- 8 Gmail normalization tests (dots, plus, googlemail, uppercase)
- 4 non-Gmail preservation tests (Yahoo, Outlook, custom domains)
- Real spam signup integration test (
CdIrBVTolzIvAxjqdF+yir.o.h.obo.j.u.k.1.0@gmail.com)
[1.6.0] - 2026-03-13
[1.6.0]: https://github.com/codesaur-php/Raptor/compare/v1.5.0...v1.6.0
Database migration system, security hardening (removed dangerous dev tools), error log integration, route optimization, documentation update.
Added
- Database Migration - SQL file-based forward-only migration system (
Raptor\Migration)MigrationRunner- Core engine: parse SQL files, execute pending migrations, track statusMigrationMiddleware- PSR-15 middleware that auto-runs pending migrations on each requestMigrationController- Dashboard UI for viewing migration status and SQL file contents (system_coder only)MigrationRouter- Routes:/dashboard/migrations,/dashboard/migrations/status,/dashboard/migrations/view- Pending migrations in
database/migrations/, ran migrations moved todatabase/migrations/ran/ - Advisory lock (
GET_LOCK) prevents concurrent migration execution - Rename with unlink fallback for servers with permission issues
.htaccessprotection (deny from all) blocks direct browser access to SQL files- Nginx
.sqlfile deny rule added to.nginx.conf.example database/migrations/example_never_runs.sql- Example pending migration for testing
- Error Log Tab - PHP error.log viewer integrated as the last tab in Access Logs page (
/dashboard/logs)- Lazy-loaded on tab activation via AJAX (
error-log-readendpoint) - Pagination support (newest entries first)
- Syntax-highlighted log output
- Visible to all
system_coderusers (no user ID restriction)
- Lazy-loaded on tab activation via AJAX (
- Migration Tests - 35 tests covering the migration system
tests/Unit/Migration/MigrationRunnerTest.php- 22 unit tests (parseFile, splitStatements, hasPending, status)tests/Integration/Migration/MigrationRunnerIntegrationTest.php- 13 integration tests (migrate, partial failure, lock, file naming)
- Middleware Pipeline -
MigrationMiddlewareregistered afterMySQLConnectMiddlewarein both Dashboard and Web applications - Autoload -
Raptor\Migration` PSR-4 namespace added tocomposer.json`
Changed
- Error Log - Moved from standalone Development Tools page to Access Logs tab;
errorLogRead()method moved fromFileManagerControllertoLogsController - Route Name Optimization - Removed
->name()from 11 routes across 7 routers whose names were never referenced via|linkorredirectTo():organization-view(OrganizationRouter)manage-menu(TemplateRouter)user-view(UsersRouter, removed fromupdateroute renamed path)product-read,product-view(ProductsRouter)order-view(OrdersRouter)private-files-read,news-view,page-view(ContentsRouter)products-page,sitemap-xml(HomeRouter)
- Spam Protection - Login rate limit reduced from 3s to 2s; minimum form fill speed reduced from 2s to 1s
- Dashboard Sidebar - Removed entire "Developer Tools" section (file-manager, sql-terminal, error-log links)
- DevelopmentRouter - Only DevRequest routes remain; SQL Terminal, Error Log, File Manager routes removed
- Documentation - All 5 markdown files updated (
README.md,docs/en/README.md,docs/mn/README.md,docs/en/api.md,docs/mn/api.md)
Removed
- SqlTerminalController - Production security risk: allowed arbitrary SQL execution. Completely removed (
SqlTerminalController.php,sql-terminal.html) - FileManagerController - Production security risk: allowed arbitrary file system browsing. Completely removed (
FileManagerController.php,filemanager.html) - Error Log Page - Standalone error log page removed (
error-log.html); functionality preserved in Access Logs tab - Localization - Removed 7 unused text keywords:
developer,error-log,file-manager,file-too-large-preview,sql-query-placeholder,sql-terminal,execute
[1.5.0] - 2026-03-12
[1.5.0]: https://github.com/codesaur-php/Raptor/compare/v1.4.0...v1.5.0
Pages module simplification: flexible type system, removed read mode, improved navigation filtering, getExcerpt fix, Discord notification improvement.
Added
- Pages - User-editable
typeinput with dropdown suggestions (menu,mega-menu,special) on insert and update forms; type field is free-text with max 32 characters - Pages -
has_childrenalert (alert-primary) on update and view forms warning that parent page content/links may not be directly used - Pages -
linkalert (alert-info) on update and view forms explaining that linked pages redirect from menu - Localization - New text keywords:
parent-page-content-warning(mn/en),link-page-content-warning(mn/en)
Changed
- Pages - Type system simplified: replaced rigid
nav/content/linkenum with flexible user-editable field; default changed fromcontenttomenu - Pages - All form fields (description, content, link, featured) always visible regardless of type or children; removed all conditional field hiding
- Pages -
typefield is now editable on update (previously locked after creation) - Pages - Navigation tree (
pages-nav.html): folder icon based onhasChildreninstead oftype=nav; type and category shown as badges - Pages - Seed data updated to use
type='menu'(relies on column default) - PagesModel -
getNavigation()now filters bytype='menu' OR type LIKE '%-menu'; only menu-type pages appear in website navigation - getExcerpt() - Fixed text sticking together when stripping HTML block tags (
<p>text1.</p><p>text2.</p>now producestext1. text2.instead oftext1.text2.); applied toPagesModel,NewsModel,ProductsModel - DiscordNotifier -
contentAction()now includes admin name in description (e.g. "Narankhuu N updated a page.") and removed redundant Type/Action fields that duplicated the title
Removed
- Pages -
read()method andpage-read.htmltemplate removed; blog-style page reading no longer supported - News -
read()method andnews-read.htmltemplate removed; blog-style news reading no longer supported - Routes -
page-readandnews-readroutes removed fromContentsRouter - Pages - Read button removed from
pages-index.htmlaction buttons - News - Read button removed from
news-index.htmlaction buttons - Localization - Removed unused
parent-pageandparent-page-hinttext keywords
[1.4.0] - 2026-03-11
[1.4.0]: https://github.com/codesaur-php/Raptor/compare/v1.3.1...v1.4.0
Log system consolidation, model rename, Discord notification improvement, automated testing, product RBAC permissions. Removed SQLite support from Raptor due to lack of practical use case.
Added
- RBAC - New
system_product_*permissions for Shop module:product_index,product_insert,product_update,product_publish,product_delete; assigned to admin (all), manager (all), editor (index/insert/update/publish), viewer (index) - Automated Testing - PHPUnit 11 test suite with unit and integration tests (47 tests, 83 assertions)
- Unit tests:
User::is(),User::can(),Controller::text(),Controller::getLanguageCode() - Integration tests:
UsersModel,OrganizationModel,SignupModelCRUD, RBAC seed data verification, JWT encode/decode - Transaction-based test isolation (auto-rollback)
.env.testingfor test database configuration
- Unit tests:
- DiscordNotifier -
newOrder()method now includes customer phone number in Discord embed notification - DiscordNotifier -
newDevRequest()method for notifying when a new development request is created (shows author and assigned user) - DiscordNotifier -
devRequestUpdated()method for notifying when a development request receives a response (shows responder and new status) - DevRequestController - Discord notifications on
store()andrespond()actions
Changed
- Shop RBAC - Shop module (
ProductsController,OrdersController, templates) now uses dedicatedsystem_product_*permissions instead of sharedsystem_content_* - OrdersModel -> ProductOrdersModel - Renamed class and file; table name changed from
orderstoproducts_orders - Log consolidation - All shop-related logs (orders CRUD, products CRUD, orderSubmit) now write to a single
'product'log channel instead of dynamic$tablenames - Log consolidation - All development module logs (DevRequest, FileManager, SqlTerminal) now write to a single
'development'log channel instead of separate'dev_requests','file_manager','sql_terminal'channels - Error logging - All non-critical
error_log()calls across the application now only execute whenCODESAUR_DEVELOPMENTistrue; critical exception/error handlers remain unconditional - FileManagerController - All UI-facing text (exception messages, log messages, response strings) changed from Mongolian to English; PHPDoc and code comments remain in Mongolian
- SqlTerminalController - All UI-facing text (exception messages, log messages, response strings) changed from Mongolian to English; PHPDoc and code comments remain in Mongolian
Removed
- SQLite support - Removed
SQLiteConnectMiddlewareand all SQLite-specific code branches (sqlite_master queries, PRAGMA table_info, sqlite_sequence, json_extract without JSON_UNQUOTE). Application now supports MySQL and PostgreSQL only - Controller::errorLog() - Removed
protected final function errorLog(\Throwable $e)method; all call sites replaced with inlineif (CODESAUR_DEVELOPMENT) { \error_log(...); }pattern
[1.3.1] - 2026-03-10
[1.3.1]: https://github.com/codesaur-php/Raptor/compare/v1.3.0...v1.3.1
Honeypot spam field cleanup and numeric payload sanitization for Products.
Fixed
- LoginController - Signup form honeypot fields (
website,_ts,_token) were not removed from payload before DB insert, causing "column not found" error on thesignuptable - ProductsController - Empty string values from frontend for numeric columns (
price,sale_price,stock, etc.) caused database type errors on insert and update; addedsanitizePayload()to convert empty strings tonullfor numeric fields
[1.3.0] - 2026-03-10
[1.3.0]: https://github.com/codesaur-php/Raptor/compare/v1.2.1...v1.3.0
Dev Request user assignment and CI/CD quality checks.
Added
- Dev Request assignment - Assign requests to a specific user when creating
assigned_tocolumn added todev_requeststable (FK tousers.id)- User selector dropdown on create form with two groups: Developers (coder role / development permission) shown first, then all other users
- Only the assigned user receives the new-request email notification (instead of all dev users)
- Assigned user can view, respond to, and deactivate the request
- Assigned user name displayed in request list and detail view
- CI/CD quality checks -
validatejob added tocpanel.deploy.yml(runs before deploy)composer validate --strict- Validates composer.json structure- PHP syntax check (
php -l) on all application and public PHP files - Merge conflict marker detection in PHP, HTML, JS, CSS, JSON, YML files
- Debug statement detection (
var_dump,dd,print_r) as warning - PSR-4 autoload verification (
composer dump-autoload --strict-psr)
- Localization - New dashboard text entries:
assign-to,assigned-to,developers,fill-required-fields,other-users
Changed
- Dev Request notifications -
notifyNewRequest()now sends email only to the assigned user instead of broadcasting to all users with coder role or development permission - Dev Request permissions -
list(),view(),respond(),deactivate()now grant access to the assigned user in addition to the creator and users withsystem_developmentpermission - Dev Request list query - Users without
system_developmentpermission now see both their own requests and requests assigned to them
[1.2.1] - 2026-03-09
[1.2.1]: https://github.com/codesaur-php/Raptor/compare/v1.2.0...v1.2.1
Comprehensive PHPDoc documentation and HTML comment security cleanup.
Added
- PHPDoc - Full Mongolian PHPDoc comments for all Web layer PHP files
HomeRouter,HomeController,PageController,NewsController,ShopController,SeoController- Class-level docs with route listings, method-level docs with
@param,@return,@throws
- PHPDoc - Full Mongolian PHPDoc comments for all Dashboard Shop PHP files
ProductsModel,ProductsController,OrdersModel,OrdersController
- PHPDoc - Expanded PHPDoc for Raptor module files
DiscordNotifier- All 6 notification methods documentedDevResponseModel- All 3 methods documented
- Spam protection docs - Detailed Mongolian comments in
ShopController::order()explaining honeypot field, HMAC token, and timestamp mechanism
Changed
- HTML comment security - Removed all
<!-- -->doc comments from public Web templates to prevent information disclosure- Affected:
index.html,home.html,page.html,news.html,news-type.html,archive.html,search.html,sitemap.html,products.html,product.html,order.html,order-success.html,page-404.html
- Affected:
- Honeypot comment removal - Removed honeypot-identifying comments from HTML templates (
order.html,login.html) to prevent bot detection bypass; moved explanations to PHP controller code - Twig to HTML comments - Converted all remaining Twig
{# #}inline comments to vanilla<!-- -->insitemap.htmlandarchive.htmlsection markers - Dashboard HTML - Removed internal architecture doc comments from Dashboard Shop templates (
products-index,products-insert,products-update,products-view,products-read,orders-index,orders-view)
[1.2.0] - 2026-03-09
[1.2.0]: https://github.com/codesaur-php/Raptor/compare/v1.1.0...v1.2.0
E-commerce shop module, spam protection, Discord notifications, SEO features, development tools, dashboard statistics, and Web layer refactor.
Added
- Shop Module - Full e-commerce with products and orders
ProductsModel- Product catalog with slug generation, excerpt, price/sale_price, SKU, barcode, sizes, colors, stock, photo, category, featuredOrdersModel- Customer orders with product reference, customer info, quantity, status trackingProductsController,ProductsRouter- Dashboard CRUD for productsOrdersController,OrdersRouter- Dashboard CRUD for orders- Sample product data seeded on first run
- Notification Module - Discord webhook integration (
Raptor\Notification\DiscordNotifier)- Notification types: user signup request, user approval, new order, order status change, content actions (insert/update/delete/publish)
- Color-coded embeds (SUCCESS, INFO, WARNING, DANGER, PURPLE)
- Graceful skip when webhook URL is not configured
RAPTOR_DISCORD_WEBHOOK_URLenv variable added to.env.example
- Development Module - Admin development tools (
Raptor\Development\DevelopmentRouter)- Dev Requests - Development request tracking system (
DevRequestController,DevRequestModel,DevResponseModel) - SQL Terminal - Database query interface (
SqlTerminalController) - Error Log viewer
- File Manager (
FileManagerController) - New RBAC permission:
development:development
- Dev Requests - Development request tracking system (
- SEO & Content Discovery (Web layer)
SeoController- Search across pages, news, and products (min 2 chars)- Sitemap page (
/sitemap) - Hierarchical page tree with news/product counts - XML sitemap (
/sitemap.xml) - For search engines with lastmod, changefreq, priority - RSS 2.0 feed (
/rss) - Latest 20 news and 20 products - Search form added to web navbar
- Footer links: Archive, Sitemap, RSS
- RSS
<link>tag in web layout<head>
- News Archive -
/archiveroute with year/month filtering and grouping - News by Type -
/news/type/{type}route for category-based news listing - Spam Protection - Comprehensive anti-spam on login, signup, forgot, and order forms
- Honeypot hidden field (
websiteinput) - HMAC token validation with timestamp
- Rate limiting (login 3s, signup 5s, forgot 10s)
- Form expiration check (1 hour max)
- Minimum fill speed check (2 seconds)
- Honeypot hidden field (
- Web Controllers - Separated
HomeControllerlogic into dedicated controllersPageController- Page and contact display (pageById,page,contact)NewsController- News display (newsById,news,newsType,archive)ShopController- Product listing, product detail, order form/submit, order confirmation emailSeoController- Search, sitemap, XML sitemap, RSS feed
- Web Routes - New public routes
/page/{uint:id}and/news/{uint:id}- Access by ID (redirect to slug)/products,/product/{uint:id},/product/{slug}- Product pages/order(GET+POST) - Order form and submission/search,/sitemap,/sitemap.xml,/rss- SEO routes
- Localization - 100+ new i18n text entries (MN/EN) for shop, archive, search, newsletter, and UI labels
- Database Indexes - Performance optimization
filestable:idx_record_id (record_id, is_active)loggertables:idx_created_at (created_at)productstable:idx_active_published (is_active, published)orderstable:idx_active_status (is_active, status)
- Dashboard Statistics -
HomeController::stats(),logStats(),refreshCache()JSON endpoints for web traffic and log statistics - RBAC - Detailed descriptions added to all system permissions; new permissions for template and development modules
- cPanel Deploy - GitHub Actions workflow example (
docs/conf.example/cpanel.deploy.yml) for automated FTP deployment to cPanel on push tomain
Changed
- Web HomeController - Refactored: page, news, contact methods moved to
PageController,NewsController; simplified to basic news listing - Web HomeRouter - Completely restructured with all new routes for shop, SEO, archive, news type
- Web templates - All hardcoded MN/EN text replaced with i18n
|textfilter ('read-more'|text,'attachments'|text,'file'|text,'size'|text,'type'|text,'source'|text,'deactivated'|text) - Web index.html - Menu titles rendered with
|rawfilter (allows HTML); search form in navbar; RSS feed link in head - Controller - Now logs
HTTP_USER_AGENTalongsideREMOTE_ADDRin action logs - ContainerMiddleware - Registers
DiscordNotifierservice in DI Container - Application - Registers
ProductsRouter,OrdersRouter,DevelopmentRouter - Login templates - Terms & conditions text now uses i18n keys instead of hardcoded bilingual text
- UsersController - Sends Discord notification when admin approves a new user
- composer.json - New autoloader namespaces:
Dashboard\Shop`,Raptor\Notification`, `Raptor\Development`
[1.1.0] - 2026-03-06
[1.1.0]: https://github.com/codesaur-php/Raptor/compare/v1.0.0...v1.1.0
Codebase-wide cleanup enforcing old-school coding style. Codesaur dependency patch versions bumped.
Changed
- Old-school coding style - Removed all emoji and decorative Unicode characters from every project file (MD, PHP, HTML, CSS, JS, conf)
- Unicode arrows (->), box-drawing characters (|, \, -), smart quotes, ellipsis, bullets, NBSP, ZWJ, BOM all replaced with plain ASCII equivalents
- Only Mongolian Cyrillic text and ASCII characters remain throughout the project
- codesaur/http-application ^6.0.0 -> ^6.0.1
- codesaur/dataobject ^9.0.0 -> ^9.0.2
- codesaur/http-client ^2.0.0 -> ^2.0.4
- codesaur/template ^3.0.0 -> ^3.0.1
- codesaur/container ^3.1.0 -> ^3.1.3
Fixed
- moedit - Internal copy/paste not working: pasting content copied from within the editor produced no result. Root cause was the image detection check intercepting paste events when clipboard contained both image and HTML data. Restructured
_handlePasteto check HTML content before image detection. Added_insertHtmlAtCursor()helper with proper_emitChange()sync. - composer.json - Removed stray emoji from post-install script output
[1.0.0] - 2026-02-25
codesaur/raptor v1.0.0 - Stable Release. First stable release of the framework under the codesaur/raptor name.
Changed
- Environment variables standardized to the
RAPTOR_*prefix - Session name set to
raptor - Default database name set to
raptor - Audit logging method exposed as
log()
[0.8.0] - 2026-02-24
Added
- Pages - "Parent page (navigation)" switch in page-insert form to create
type=navpages directly - Pages -
linkfield with frontend (JS) and backend (isValidLink()) validation; supports URL, local path (/path),mailto:,tel: - Pages -
PagesController::read()guards: published check (403), parent check (403), link redirect - Pages - Auto-reset
is_featured=0on parent page when a child is inserted orparent_idchanges - Sample Data Reset - Reset button in pages-index, pages-nav, news-index when sample data exists
PagesController::reset()andNewsController::reset()methods- Routes:
pages-sample-reset,news-sample-reset - Detection:
created_by IS NULL AND created_at = published_at AND category='sample' - Truncates main table and files table, resets auto-increment to ID=1
Changed
- Pages - Removed page type wizard from page-insert; all fields (title, description, content, link) visible in a single form
- Pages - Merged Content and Link types into one form;
linkinput always visible below content editor - Pages - Parent pages (pages with children) hide description, content, link, featured fields in page-update
- Pages - page-view shows description/content/link conditionally (only when non-empty), link with
border-info - Pages / News - Action buttons in index/nav: link button if
linkis set and not parent, read button if no link and not parent, neither if parent or unpublished - Pages / News - Seed data uses
category='sample'instead of'general',created_byandpublished_byareNULL - News - news-index layout: "New" button moved from header to filter row (matches pages-index layout)
Fixed
- Pages -
type="url"input rejecting local paths like/contact; changed totype="text"
[0.7.2] - 2026-02-12
Added
- moedit -
attachFilesoption to enable/disable the Attach File button (trueby default,falsedisables the button)
Fixed
- moedit - Header image toolbar group responsive CSS selectors now require
.is-visibleclass, preventing hidden elements from displaying incorrectly
[0.7.1] - 2026-02-10
Fixed
- SettingsController - Settings config save crash when no record exists (empty table)
LocalizedModel::insert()requires non-empty content, but Config tab only sends non-localized fields- Now populates empty localized content for each language before insert
- SettingsController - Localized field change detection used swapped indices (
[$field][$code]instead of[$code][$field]) - TextController - Same swapped localized indices bug in
update()method
[0.7.0] - 2026-02-09
Added
- Pages Navigation (
/dashboard/pages/nav) - Tree-structured page navigation management- Display cards grouped by language
- Published/unpublished distinction: blue title + green check icon / eye-slash icon
- Display page photo thumbnails
- Removed the "System page" concept - All published pages are now visible in navigation
- Insert form: automatic position calculation (parent position + 10 for first child, max sibling + 10 for subsequent, max top-level + 100 for same-language)
- Page view: position field (language, category, position displayed in a single row)
- OG meta tags for page and news:
og:title,og:description,og:image,og:type,og:site_name, logo fallback <title>tag: "Record Title | Site Title" format for content pages
Changed
getNavigation(): removed(type='nav' OR parent_id>0)filter, addedslugfieldgetFeaturedPages(): addedslugfieldgetInfos(): addedpositionandcodefields- Web page/news links use
sluginstead ofid(/page/{slug},/news/{slug})
[0.6.0] - 2026-02-06
Full CMS framework major release. Multi-DB support, DI Container, OpenAI integration, full documentation.
Added
SQLiteConnectMiddleware- SQLite database support (on top of MySQL, PostgreSQL)ContainerMiddleware- PSR-11 Dependency Injection Container (codesaur/container)- OpenAI integration - moedit editor AI button (
INDO_OPENAI_API_KEY) - Image optimization via GD extension -
INDO_CONTENT_IMG_MAX_WIDTH,INDO_CONTENT_IMG_QUALITY ext-gd,ext-intlPHP extension requirementsis_featuredfield in Pages module (featured pages for footer)- File attachments in Web templates (page.html, news.html)
basenameTwig filter for Web templates- Native JS file upload (replaced Plupload external library)
- Native JS image preview (replaced Fancybox external library)
INDO_JWT_SECRETauto-generation via Composer post-install script- Full MN/EN documentation (
docs/mn/,docs/en/) - API Reference (
docs/mn/api.md,docs/en/api.md) CHANGELOG.mdversion history
Changed
codesaur/http-application^5.7 -> ^6.0.0 (breaking)codesaur/dataobject^7.1 -> ^9.0.0 (breaking,localizedaccess pattern changed)codesaur/template^1.6 -> ^3.0.0 (breaking)firebase/php-jwt>=6.7 -> ^7.0.2 (HS256 key length requirement added)getImportantMenu()->getFeaturedPages()refactorForgotModel,SignupModelremoved - authentication flow simplifiedJsonExceptionHandlerremoved- Full PHPDoc added to all PHP files
Fixed
localizedaccess pattern bugs in dashboard templates (DataObject ^9.0 refactor)- Web news.html file list incorrect column names
- Web page.html stray character
[0.5.0] - 2025-09-22
Content modules reorganized into subdirectories. Web layer template system improved. Multi-DB support started.
Added
- `Web\Template` namespace - Web ExceptionHandler, TemplateController
Web\SessionMiddleware,Web\LocalizationMiddleware(separate from Dashboard)PostgresConnectMiddleware- PostgreSQL supportSignupModel(user signup request model)
Changed
PDOConnectMiddleware->MySQLConnectMiddlewarerenamed- Content modules moved to subdirectories:
file/,news/,page/,reference/,settings/ - Localization module separated:
language/,text/subdirectories UserRequestModel->SignupModelrenamedcodesaur/dataobject^5.2 -> ^7.1
[0.4.0] - 2024-09-28
Full architectural overhaul. Migrated from library to project/application structure. Two-layer (Dashboard + Web) architecture introduced.
Added
application/new directory structure (raptor/,dashboard/,web/)Raptor`,Dashboard`,Web` new namespaces (migrated fromRaptor`)public_html/index.phpentry point - automatic Dashboard/Web routing.envconfiguration (vlucas/phpdotenv) - all settings in environment file- Twig template engine support (
codesaur/template) Raptor\Application- Dashboard middleware pipeline baseWeb\Application- Public website applicationDashboard\Application- Dashboard application- Brevo (SendInBlue) email API (
getbrevo/brevo-php) SettingsMiddleware- system settings middlewareDashboardTrait- Dashboard UI common functionsTemplateRouter,TemplateController- Dashboard template systemErrorHandler- Dashboard template-based error handling- PSR-3
Loggerclass (built-in) Mailerclass (Brevo + PHPMailer)Uservalue object (profile, organization, RBAC permissions)psr/log^3.0 direct dependency
Changed
Raptor` ->Raptor` full namespace changesrc/->application/raptor/directory structure updatedIndoApplication->Raptor\ApplicationIndoController->Raptor\Controllercodesaur/rbac-> `Raptor\RBAC` built-in (separated from external package)codesaur/logger->Raptor\Log\Loggerbuilt-inphpmailer/phpmailerre-added (^6.8)codesaur/dataobject^5.2 re-added (was removed in v5-8)
Removed
InternalRequest,InternalControllerclasses removedJsonResponseMiddlewareremovedRecordController,StatementControllerremovedcodesaur/rbacexternal dependency removed (built-in)codesaur/loggerexternal dependency removed (built-in)
[0.3.0] - 2024-07-30
Simplified to minimal base library. All CMS modules removed, only core classes remain.
Changed
- Framework simplified to minimal base (9 PHP files)
codesaur/rbac^2.3 -> ^2.5codesaur/logger^1.5 -> ^2.0
Removed
- Auth, Localization, Contents, File, Mailer modules fully removed
CountriesController,CountriesModelLanguageController,LanguageModelTextController,TextModel,TextInitialFilesController,FilesModel,FileModel,FilesRouterNewsModel,PagesController,PagesModelReferenceController,ReferenceModel,ReferenceInitialSettingsModelMailerController,MailerModel,MailerRouterLoggerController,LoggerModel,LoggerRouter
[0.2.0] - 2023-07-19
CMS modules added. PHP 8.2.1 requirement. File management, news, pages, references, and settings modules.
Added
Contentsmodule:NewsModel,PagesController,PagesModel,ReferenceController,ReferenceModel,SettingsModelFilemodule:FileModel,FilesController,FilesModel,FilesRouterMailermodule:MailerController,MailerModel,MailerRouterRecordmodule:RecordController,RecordRouterStatementmodule:StatementController,StatementRouterPDOConnectMiddleware- DB connection middlewareJsonResponseMiddleware- JSON response middlewareJsonExceptionHandler- JSON exception handlerInternalRequest- Internal API requestContentsRouter- CMS routescodesaur/http-clientdependency added
Changed
- PHP >=7.2 -> PHP 8.2.1 requirement
Account` ->Auth` namespace changedTranslationController->TextControllerrenamedfirebase/php-jwt>=5.2 -> >=6.7codesaur/http-application>=1.2 -> >=5.5.2codesaur/rbac>=1.4 -> >=2.3.7
Removed
phpmailer/phpmailerdirect dependency removedcodesaur/dataobjectdirect dependency removedcodesaur/localizationdirect dependency removedAccountErrorCodeclass removed
[0.1.0] - 2021-04-18
Initial release. REST API-based server framework.
Features
- PHP >=7.2 support
IndoApplication- PSR-15 Application baseIndoController- Base ControllerIndoExceptionHandler- Exception handler- JWT authentication (
firebase/php-jwt) - Account module:
AuthController,AccountController,AccountRouter OrganizationModel,OrganizationUserModelForgotModel- Password recovery- Localization module:
LanguageController,CountriesController,TranslationController - Logger module:
LoggerController,LoggerRouter - RBAC access control (
codesaur/rbac) - PSR-3 logging system (
codesaur/logger) - Email sending (
phpmailer/phpmailer) codesaur/http-applicationPSR-7/PSR-15 basecodesaur/dataobjectPDO ORM- MIT License
[1.0.0]: https://github.com/codesaur-php/Raptor/releases/tag/v1.0.0