Официальный сайт SLAED CMS
Журнал изменений
Trims the function comments added by the performance remediation work to the 180-character line limit while keeping their meaning; no code or behavior changes.
Core changes:
- Comment wording (core/system.php, core/classes/cache.php):
updateStatsCookie, updateStatsTrack, marker contract, and page-cache contract comments rewritten concisely within one line each
- Test probe (tests/Support/contract_probe.php):
- header comment shortened to the same limit
Technical notes:
- legacy over-length lines elsewhere are untouched, out of scope
Closes the correctness and availability gaps from the 2026 performance remediation plan: the public page cache can no longer be amplified by arbitrary query input or foreign hosts, and the exact visitor statistics survive IO failures without losing or double-counting entities.
Core changes:
- Bounded page-cache identity (core/system.php, core/classes/cache.php):
Cache::getQueryVars() validates the request query against a per-route key/value regex contract, dropping tracking keys centrally
- unknown, duplicate, or malformed keys render live and create no entry
- replaces the now-dead Cache::filterCacheUrl()
getCacheRouteVars() adds the canonical homeurl host gate and feeds getPageHash(), which builds the pc2-versioned identity from validated values so num=1, encodings, and foreign Host values cannot fork entries
- Dynamic-region marker contract (core/system.php):
checkDynamicMark() allowlists token scopes ajax/account/scheduler, captcha action login, and positive voting IDs
- invalid emitters poison the build, log, and fall back to live rendering
- the contract is revalidated at serve time so forged markers stay inert
- Fail-safe exact statistics (core/system.php):
v2 stats cookie carries only session metrics and country cache; no client state can suppress an exact count
ips.log/user.log sets are the source of truth with hosts/users counters derived from set size, so any single write failure self-heals
statistic.log is replaced atomically via temp file and rename under a stable statistic.lock; partial writes can never trigger a day reset
rollover verifies full day lines, rolls back short appends, aborts on any archive/rename/unlink failure before destroying state, and names archives from the data date
- checkUniqueIp()/check_user() consolidated into updateStatsTrack()
- Regression coverage (tests/):
PageCacheContractTest exercises the production functions directly and through contract_probe.php, which boots the real core per scenario with LOGS_DIR redirected to scratch so failure paths stay assertable
Benefits:
- guest requests cannot grow the cache key space or host namespaces
- exact unique hosts/users stay correct across injected IO failures
- rotation failures preserve data and retry instead of deleting sources
Technical notes:
pc2 identity version makes pre-contract cache files unreachable; normal GC removes them without a purge
- v1 stats cookies are discarded by design (disposable analytics state)
OPcache/cron deployment (Batch 4) remains a manual environment step; docs/PERFORMANCE-REMEDIATION-2026.md stays OPEN with recorded results
Implements the full 2026-07 performance plan: exact locked statistics, deferred post-response tracking, derived config, streaming GeoIP, shared category map, parser cache, cache GC, and a guest page cache that keeps every visitor-bound token isolated through signed dynamic-region markers.
Core changes:
- Statistics and tracking (core/system.php):
Rewrite updateStatsTrack around one c+ handle with a single LOCK_EX
- read, day/month rotation, truncate and write share one lock
- first hit of a new day now records its own IP (hosts=1) and user
- three duplicated counter-field blocks collapsed into one
Replace sessions.log with a signed base64url stats cookie
- fields v1|sid|fst|lst|hits|country|cts|uniq-day|ip-hash, HMAC via
getSecret('stats'), verified with hash_equals, tamper resets session
- country carries a 24h TTL and is bound to the IP hash
- updateSessionState removed, sessions.log and its global lock retired
Split tracking into pre-output cookies and post-response writes
- addDeferredTask/setDeferredTasks queue with shutdown backstop
- session_write_close before deferred writes, drains on all exits
- news article counter update moved into the deferred queue
Literal stripos matching for bots, fbots, and auto_links patterns
- removes regex injection from config and DB values, caches results
- Conditional lastvis update (60s window) and _session upsert
- Session schema (setup/sql/table.sql, table_update6_3.sql):
- UNIQUE KEY on _session.uname for new installs
mksessuniq migration: dedup by max(time, id), drop old index, add unique, idempotent on re-run; code uses INSERT ... ON DUPLICATE KEY
- Derived config cache v2 (core/system.php):
getConfig stores derived data in config/local.php (version 2)
- per-theme asset manifests with stat fingerprints, parsed SEO
graph/schema templates, logo dimensions per theme
doCss/doScript drop per-request glob/stat sweeps, bundle hash only computed when bundling is enabled
- Streaming GeoIP (core/classes/geoip.php):
MMDB reader works through fseek/fread ranges instead of loading the whole database file; metadata resolved from the last 128 KiB
- byte-exact with the old reader, peak memory 25 MB -> 2 MB
- fread(handle, 0) guarded (old substr was silently empty)
Category map and parser cache (core/system.php, core/helpers.php, modules/news/index.php):
getCategoryMap consolidates two duplicated category queries behind an epoch-keyed data cache with raw titles
news list caches filterContent output keyed by content hash, parser config, theme, locale, and parser version; [block]/[hide]/[usephp]/ [attach] and local <img> content bypasses the cache
Guest page cache with dynamic regions (core/system.php, core/classes/cache.php, core/security.php, blocks/*):
Signed markers [[sldyn:type:par:hmac]] via getSecret('dynreg') keep user content from forging substitutable markers
Regions: CSRF tokens (getPageToken), captcha (getPageCaptcha), and the whole voting widget; cache files contain zero live tokens
Poison guard: a live getSiteToken/getCaptcha call during a cacheable build prevents storing the page entirely
- Default-deny route allowlist inside checkPageCache (news list only)
Fail-closed sidecar with body hash and dyn flag; dynamic pages are no-store and never answer 304; substitution runs on hit and miss
- cache = 1 enabled in config/global.php
- Cache GC (core/classes/cache.php, core/system.php):
deleteStaleTree recursively sweeps storage/cache/templates and the data cache joins the cachegc scheduler job
Benefits:
- Guest cache hit generation time 0.307s -> ~0.055s (5.3x)
- Uncached pages roughly halved (search 0.17s -> 0.09s)
- Day counters survive concurrent requests and day rollover
- CSRF/captcha isolation between visitors verified with two cookie jars
Technical notes:
- config/local.php cache version bumped to 2, rebuilt automatically
theme asset changes now require a config rebuild (admin save or deleting config/local.php)
_session migration applied to the dev DB; verify on MySQL before production rollout (developed against MariaDB 11.7)
- ip/agent freshness for logged-in users delayed up to 60 seconds
docs/PERFORMANCE.md updated; docs/PERFORMANCE-REMEDIATION-2026.md is the completed plan, ready for deletion after review
Strengthen OAuth account creation, linking, unlinking, and login finalization with fail-closed transactions, atomic temporary-state consumption, safer provider validation, and consolidated session handling. Add focused regression coverage for JWT validation, transaction failures, redirect safety, and unlink invariants.
Core changes:
- OAuth security and lifecycle (oauth.php, modules/account/index.php):
Harden redirect, provider response, JWKS, claim, and one-time state handling
- Bound provider response bodies and validate JSON content types
- Consume callback state atomically and preserve retryable pending records
Make account creation and unlink operations transaction-safe
- Roll back failed user/link creation as one unit
- Serialize unlink operations and protect the final login method
- Consolidate normal and OAuth login session finalization
- Database transaction support (pdo.php):
- Add begin, commit, and rollback primitives for shared PDO connections
- Normalize transaction exceptions to the existing boolean error contract
- Admin output and regression tests (modules/account/admin/index.php, tests/Unit):
- Render provider identity data through escaped text template fields
- Cover redirects, claims, JWT signatures, transaction failures, and unlink outcomes
Benefits:
- Prevents orphan accounts, replayed callbacks, unsafe redirects, and last-method unlink races
- Reduces duplicate login state handling and unnecessary unlink queries
- Improves maintainability with isolated OAuth and database regression tests
Technical notes:
- Uses the existing OAuth tables and configuration without schema changes
- Preserves current routes and backward-compatible account behavior
Implements a dependency-free OAuth2 Authorization Code Flow with PKCE as the new external sign-in, removes the legacy uLogin integration completely and consolidates theme templates while keeping both themes self-contained.
Core changes:
- OAuth client (core/classes/oauth.php, config/oauth.php):
Static Oauth class: provider config, PKCE auth URL, token exchange, hardened curl layer, JWKS cache with rotation, full RS256 id_token validation (exp/nbf/iat with skew, iss list, aud/azp, nonce)
- Microsoft email trusted only with xms_edov or consumer tenant
- state bound to the browser via __Host-oauth-st cookie (anti link-CSRF)
One-time flow storage in {prefix}_oauth_temp (state/pending kinds), permanent links in {prefix}_user_oauth, audit log in log_oauth.log
- Account module (modules/account/index.php):
New ops: oauth_init, oauth (callback), oauth_finish, oauth_unlink
- first sign-in offers linking to an existing account or creating a new
one; OAuth-created accounts store an unguessable '!'-prefixed marker
- unlink refuses to remove the last login method
uLogin op network and all its guards removed; login/passmail/savepass work by password state instead of the dropped users.network column
users_temp now stores bcrypt hashes; activate() keeps a transitional fallback for old plaintext rows
- Admin (modules/account/admin/index.php):
- Provider settings (activation, Client ID/Secret) in account preferences
oauthlist view with forced unlink via POST buttons, link cleanup on user delete, pending-users list shows the activation code instead of the stored password
- Schema and migration (setup/sql/table.sql, table_update6_3.sql):
- New tables user_oauth and oauth_temp with engine/charset placeholders
Idempotent migration archives legacy identities as provider 'ulogin' rows and drops the users.network column
- Themes (templates/admin, templates/lite):
- OAuth buttons, finish form and profile links tab; unified post-button
Both login blocks render one block-login-form fragment via include, fixing the broken sidebar login (wrong field name, missing CSRF token)
block-search-form merged into search-form; nine twin fragments synced between themes; speed dial fan opens only from its toggle
Session write lock released before scheduler jobs (index.php) so long pseudo-cron runs no longer block the visitor session
Benefits:
- Passwordless sign-in without composer dependencies on shared hosting
- No plaintext passwords at rest anywhere in the system
- Fewer duplicated templates with identical twin fragments across themes
Technical notes:
- Redirect URI: index.php?name=account&op=oauth (HTTPS required)
- Providers ship disabled; admin help documents the full setup procedure
- Verified by 29 JWT unit tests, mock-OIDC E2E flows and phpunit (194 OK)
Port the account cabinet (index.php?name=account) to the Command Deck layout from the 2026 demos: identity rail, action tile shelf, live scroll panels and a full-width lower section, all built on existing profile and category mechanics instead of new one-off styles.
Core changes:
- Cabinet page and data (modules/account/index.php, core/user.php):
profil() renders the new account-home partial
- identity rail: profile avatar ring, online status, rating chip and
the community progress ring (points, reached group, level percent)
- panels: last incoming private messages, favorites, last activity
feed and the RSS reader with the legacy preset select restored
getUserNavItems() builds icon/tone tiles per active module with the new-PM badge and favorites counter; getUserNav() renders the compact strip for inner cabinet pages via the account-nav fragment
getUserLevelData() extracts the ring/level computation shared with the public profile view; rssfeed() serves the feed over htmx POST
- getFavoriteList() initializes its arrays (warnings on empty lists)
Lite templates and styles (partials/account-home.html, fragments/account-nav.html, assets/css/theme.css):
panels reuse sl-profile-panel, sl-profile-scroll and the single sl-profile-feed-live spinner, now title-line aligned in profile and cabinet; panel headings are flex with the spinner in the right corner
action tiles reuse the files-category icon mechanics (sl-cat-ico with sl-cat-tone-N cycling); favorites rows carry configured module icons
sl-cab-* additions cover only the deck grid, tiles, rows, badge, progress ring (sized and shadowed like the avatar) and the RSS forms
- legacy PNG navigation removed (images/account/*.png, catflex flags)
- RSS reader (core/system.php):
rss_read() decodes the entity-encoded config template, so stored markup renders instead of printing as text
Benefits:
- cabinet matches the profile design language with no duplicated styles
- module tiles reflow automatically when modules are disabled
- RSS reading works inline over htmx without page reloads
Technical notes:
- RSS requests must POST the url (security url_get blocks URLs in GET)
- no schema changes; legacy account nav markup and icons are gone
Post-release polish of the redesigned profile plus design demos for the account home page.
Core changes:
- Profile status and identity (modules/account/index.php, account-profile.html):
Online/offline chip with bi-wifi / bi-wifi-off on success/danger tokens; new global _OFFLINE constant and capitalized _ONLINE in all six locales
Avatar ring colored by group like the user block: special group color first, otherwise the highest reached points group, validated hex passed as --sl-profile-ring
Signature block, site and hub queries reuse the existing selects, no extra queries added
- Activity feed (core/user.php, account-profile-feed*.html, theme.css):
Entry dates render through the standard date-badge fragment at system chip sizes; custom 10px sizing removed
Spinning bi-arrow-repeat indicator on the tabs row in primary tone; overlay positioning keeps tab panels at full feed width
- Indicator pauses together with the hovered feed scroll via :has()
- Avatar geometry (theme.css):
box-sizing fixes for the profile avatar and the user block ring and avatar, so images no longer overlap the conic ring and sit centered
- Dead code (theme.css, modules/account/index.php):
unused sl-user-group-special styles and the unused g.color column in the profile select removed
- Cabinet design demos (demo/):
account-home-2026.html with six directions and account-home-final-2026.html with three finals, a module toggle simulator, auto-fit action shelf, files-category tone tiles and auto-scrolling panels; deferred work listed in docs/PROFILE-BACKLOG.md
Benefits:
- Status and group identity readable at a glance on any profile
- Feed visuals match the sitewide chip and date language
- Ready-to-transfer blueprint for the account home redesign
Replace the legacy table profile (account&op=view) with the approved Dense Split composition: identity rail, trust strip, symmetric fact panels, module contribution hub and a tabbed last-activity feed, all rendered from live data in four SQL queries.
Core changes:
- Profile view (modules/account/index.php):
Rebuild view() around template data arrays: role-aware fact panels, trust strip, level progress and group chips
- online status via a subquery in the main user select
- one groups select drives chips, rank fallback and level thresholds
- module hub counts, weighted ratings and favorites in one UNION ALL
- hidden fields keep the localized placeholder for panel symmetry
- profil() reuses the shared feed; the legacy last() helper is removed
- Shared profile helpers (core/user.php):
getProfileModules() maps module tables, icons, rating column pairs and favorites keys in one place
getProfileLastView() renders the per-module activity feed from a single UNION ALL round-trip and respects per-module rating settings
- computed union columns are aliased because a literal 0 column would
otherwise shadow numeric row index 0 under FETCH_BOTH
- Theme (templates/lite):
account-profile.html rewritten for the split card; new feed partials account-profile-feed.html and account-profile-feed-list.html
sl-profile-* styles on base.css tokens; chip labels truncate with an ellipsis; dead sl-table-acc-info and sl-user-group-special styles removed
- signature renders as the system blockquote and is visible to everyone
- Feed auto-scroll (plugins/system/slaed.js):
setProfileScrolls clones visible feed entries and animates them with reduced-motion and hover-pause support, re-measuring on tab switches
- Language constants (lang/, modules/account/lang/):
nine ACCOUNT* module constants and the global _LASTACTIVITY in all six locales
Benefits:
- Profile page cost drops from ~17 module queries to 4
- One module map feeds the hub, the feed and future consolidation
- Deferred follow-ups documented in docs/PROFILE-BACKLOG.md
Align every editor surface with the active theme's visual language and add a size-limited base64 embed option to the image dialog. Harden the integration with the vendor engine after three virtual-DOM regressions.
Core changes:
- Theme token contract (templates/*/assets/css/base.css):
field group (--sl-field-*), --sl-color-text-heading, --sl-color-on-dark, --sl-color-primary-hover, --sl-radius-card, --sl-shadow-input canonized; admin defines the field group, lite gains primary-hover
- --sl-field-focus-ring is a color composed as 0 0 0 3px var() in place
- Editor skin vendor alignment (templates/*/assets/editors/toastui/skin.css):
chrome, work area, toolbar states, mode switch, dropdown, popups, buttons, inputs, tabs, table selector, context menu, focus rings, selection, placeholders, md syntax accents and WYSIWYG content colors mapped to tokens
one control standard across all windows: theme font chain (vendor font stack removed), fields 32px/12px/field tokens, buttons 32px/12px, hints 11px; emoji tabs restyled as the popup button pair with primary active
toolbar header painted by the wrapper (rounded corners fixed), md tab bottom border restored, popup pinned to the right edge by CSS instead of the racy JS left assignment
- Image dialog (plugins/editors/toastui/, templates/*/partials/):
three insert modes as theme-owned radios: upload to server (default), attachment thumbnail, and NEW base64 embed into the text with a 32 KB client limit (_EMODE* constants in all six locales)
- file selection uploads automatically (no OK click), drag and drop included
system sl-alert everywhere: upload limits, messages and errors render in an extras strip; file field and hints unified with the file manager
- Vendor virtual-DOM integration hard rules (editor-upload.js):
no foreign DOM inside vendor popups: SLAED nodes live in the extras strip and absolutely positioned blocks raised together with the window
- vendor node text patched via nodeValue, never textContent
popup closing detected by a MutationObserver on the popup style attribute, fixing stuck header strips for guests and async close paths
- Fullscreen (editor-tags.js):
height driven by the engine setHeight('100%') with save/restore, bottom tabs and mode switch stay visible; the 100vh CSS override removed
Benefits:
- Switching a theme now changes the complete editor look including geometry
- Anonymous and privileged flows verified end to end without JS errors
- docs/DATA-URI-SECURITY-PLAN.md records the follow-up server-side hardening
Remove the last visual-contract leak in the editor plugin JS: setMsg() no longer assembles sl-toastui-message-* class names itself and instead clones the theme-owned msg-info/msg-warn template blocks.
Core changes:
- Upload messages (plugins/editors/toastui/assets/editor-upload.js):
setMsg() clones the msg-info/msg-warn <template> from the theme and only fills textContent; the message container keeps no plugin-chosen classes
getWarn() and the inline empty-list branch consolidated into one getMsg() helper used by all message paths
Benefits:
- Theme designers restyle upload messages purely in the theme partial
- One message-building path instead of three