Repository navigation
To upgrade from 0.1.x, do what each entry marked Action. says (most say which sites they apply
to). The steps, the recipes and the rollback are in the upgrade guide,
Upgrading.
Security
- Action. Request and
HttpClientbodies and query strings are traced as fields, so masking by
key reaches them; a body that is neither JSON nor a form stays a raw string. Weft masks nothing
itself: list personal fields (email,phone) in the newapp.trace.sensitive_keys, or set the
newapp.trace.log_bodytofalseto log bodies as their size and content type only. - Action.
Modelrejects a column name that is not a plain identifier with
InvalidArgumentException: array keys went into the SQL as-is, socreate($request->body())was
an SQL injection. Still passcreate()andupdate()only the fields you expect; write queries
with quoted or qualified names (posts.id) withDatabase. - Action. An enabled captcha without both keys no longer passes every submission: outside
DEBUGit rejects them (misconfigured) and sends a critical alert. Set both keys or disable it. - Action.
RateLimitercounts per client IP (IPv6 per/64), atomically, in Redis or in files
under the newapp.rate_limit.path, not in the session, so clients without a session cookie are
limited too. Make that directory writable by PHP; behind a proxy, list it inPROXY_IPS. - Action. Behind a proxy in
PROXY_IPS, the client IP is the rightmostX-Forwarded-Forentry
not inPROXY_IPS, andX-Client-Ipis no longer read: a forged header got around rate limits. A
proxy that passes the address only inX-Client-IporX-Real-IPmust appendX-Forwarded-For. - Action. The example's config template sent contact requests to the Weft author
(hello@golovanov.dev); its addresses are now on example.com. If your config came from it, set
MAIL_TOandMAIL_FROMof your own.
Added
Mail::send(): plain-text UTF-8 email through PHPmail()or SMTP (app.mail.*), with no extra
package; it returnsfalseand writesMail not senttoerrors/instead of throwing.- Alert channels:
app.alerts.channelslists any ofalertloop,telegram,mailandlog,
tried in order;alert.failedtrace events name thechannel. - Yandex SmartCaptcha:
app.captcha.driversmartcaptcha, with the same keys and diagnostics as
reCAPTCHA; the example's pages need three files changed (features guide). vendor/bin/weft-health, run in the site root, reports whether the site it boots is ready for
production and exits with 1 when not (--devon a development machine). Its fields:
Troubleshooting.Health::check($name, $check)adds a check of the site to that report; the example checks
MAIL_TO(checks.contact_form).Request::isHttps()believesX-Forwarded-Protofrom proxies inPROXY_IPS; the example uses it
for theSecureflag of the session cookie. Action. If your site copiedpublic/index.php,
take its session cookie block, which now comes afterrequire bootstrap.php.PROXY_IPSaccepts IPv4 and IPv6 CIDR ranges ('173.245.48.0/20'); host names are ignored.- Catch-all pages:
pages/<dir>/[...path].phpserves every deeper URL with$path = 'a/b/c'; a
template canthrow Abort::notFound()orAbort::redirect($url). Pagination::fromRequest($request, $total, $perPage)for?page=N:?page=1is a 301, a missing
page a 404, and page 2 on gets its own canonical and a title suffix (app.seo.page_suffix).- JSON-LD builders
Schema::organization(),localBusiness(),webSite(),article(),
product(),faq()andbreadcrumbs(), which check the fields search engines require. - robots.txt groups per crawler (
app.seo.robots_groups) with the presetsai-search,ai-user
andai-training; without the key robots.txt is unchanged. /llms.txtfromapp.llms, an array or a callable; without the key it stays a 404.- IndexNow: with
app.indexnow.keyset, the key is served at/<key>.txt, and
vendor/bin/weft-indexnow(for cron) submits the sitemap addresses that are new or changed. vendor/bin/weft-seo-audit <site-url>checks a live site's pages over HTTP and exits with 1 when
one fails, for CI. It needsext-dom, now insuggest; Basic auth comes fromWEFT_AUDIT_AUTH.
Changed
- Action. The health check moved from the example's
scripts/health.phpinto the engine,
vendor/bin/weft-health: new fields,okfalse on more, notraceloomoralertloop_enabled.
Deletescripts/health.phpand call the new command from deploy scripts, CI and cron. - Action. Weft requires
golovanov/traceloom^0.5: update with
composer require golovanov/weft:^0.2 -W, and raise your own traceloom constraint if you have one. - Action. Each page has one address: a GET or HEAD for
/about/,/homeor/ru/gets a 301 to
/about,/or/ru(Url::link('/', 'ru'), hreflang and the sitemap give/rutoo), and/404and
/<dir>/indexare a 404. Point your links at the final address, and run
php scripts/sitemap.phpagain if you keep a static sitemap. - Action. A path Weft used to clean up silently is a 404 (anything but
A-Z a-z 0-9 / _ - .,
an empty,.or..segment): fix links that use anything else. - Canonical,
og:urland hreflang are built from the matched route, never from the request URI; a
404 gets none of them.Url::routePath()is the matched path. - Action. Trace format:
bodyis an object{size, content_type, data}ornull, and the
query string moved fromuriandurltoquery. Update anything that parses traces. - Action. Trace files are deleted after
app.trace.retention_days(30), from the first request
after the upgrade: set it before upgrading to keep them (0keeps them forever). - Action. Console scripts (
cliandphpdbg) trace toLOGS_PATH/cli/, not
LOGS_PATH/requests/: point whatever reads script traces there. - Action. A relative
app.cache.pathis taken fromAPP_ROOT, not from the working directory
(withoutAPP_ROOT, the defaultstorage/cacheis in the working directory, not the system temp
directory): make its new location writable by PHP. - Action.
example/public/index.phpno longer callssession_start(), so readers get no session
cookie. Remove the line from your copy; sendCache-Controlyourself if you relied on the
session's no-cache headers. - Action. Alerts are switched on by
app.alerts.channelsalone, andapp.alertloop.enabledis
gone;Alertreads AlertLoop's settings only fromapp.alertloop.*(it read a top-level
alertloop.*, so alerts configured as documented were off). Add
'alerts' => ['channels' => ['alertloop']]to keep them. Alert::send()returnstrueonly when a channel that reaches a person took the alert.- Action. Weft sets no permissions on the error log, cache entries and rate-limit counters (it
created their directories at most0770): they get the process umask. Set upstorage/with one
group, setgid directories andumask 002, and run Weft scripts as the deploy user (upgrade guide). - Action. The Redis key prefix defaults to the host of
app.url.base(example.com:), not
weft:, andCache::flush()andcache-clear.phpdelete only cache entries. The cache starts
empty; delete the example's'prefix' => 'weft:docs:'line. The old keys: upgrade guide. - Action. The cache restores values without their classes (
__PHP_Incomplete_Class): cache
arrays and scalars. - Action.
HttpClientreturnsok => falsefor a status of 400 or higher (erroris
HTTP <status>): where you read an error response's body, branch onstatus. - Action. An alert with a dedupe key is sent at most once per five minutes, whether the first was
delivered or not: send without a key where every occurrence must arrive. - Action. The example's contact form answers success only when the email to
MAIL_TOwas
accepted, otherwise 503; its alert carries no form data. A 422 listserrors: [{name, text}](was
fields), and a form posted without JavaScript gets a 303. If you copied the form, copy the files
the upgrade guide lists. - Action. The example's constants
RECAPTCHA_SITEKEYandRECAPTCHA_SECRETKEYare now
CAPTCHA_SITE_KEYandCAPTCHA_SECRET_KEY; with the old names the captcha is off. Rename them in
your copy, and in the server's when you deploy 0.2.0 (step 5 of the upgrade guide). New constants:
MAIL_DRIVER,SMTP_*,TELEGRAM_BOT_TOKEN,TELEGRAM_CHAT_ID,ALERT_MAIL_TO,INDEXNOW_KEY. - Action. The sender moved from
app.contact.mail_fromtoapp.mail.from(stillMAIL_FROM),
andMAIL_TOtakes bare addresses separated by commas:Name <owner@site>now fails every contact
email andweft-health. - Action.
Databasesends no alert:Database::setErrorNotifier()andAlert::notifier()are
removed, and an uncaught database error alerts as any unhandled exception. Remove the
setErrorNotifier()call fromapp/Bootstrap.php, or every request stops with a fatal error. - Action.
Database::execute(),Model::update()andModel::delete()return the number of rows
changed, not alwaystrue: compare the count where you compared with=== true. - Action.
Databaseno longer runsSELECT 1before every call: a call on a lost connection
throws and the next one reconnects, so the first call after an idle longer thanwait_timeout
throws; insidetransaction()every call throws until it returns. Retry the whole transaction. - Connecting to MySQL gives up after 5 seconds, not 30; the new optional
DB_TIMEOUTsets it. - Action.
Jwt::decode()rejects a token withoutexp, andJwt::encode()withoutttlissues
one that expires in an hour: tokens issued withoutttlstop working, issue new ones. - Action. HSTS sends
includeSubDomainsonly with the newapp.security.hsts_subdomains, and the
newhsts_max_agesetsmax-age(default31536000). The example ships withhstsoff. - Action. A relative
og:imagegetsBASE_URIahead of it: in a sub-directory, remove it from
the value ('/img/og.png', not'/site/img/og.png').
Fixed
- Action. The example under PHP's built-in server serves its CSS, JS and images. If you copied
public/index.phpand develop withphp -S, copy thecli-serverblock at its top. - 65 smaller fixes: 5 security, 30 reliability, 30 docs.
Removed
- Action. Unused API:
Config::isLoaded(),Locale::fromAcceptLanguage(), theLOCALE
constant,RequestContext::hasRequest(),logPath(),setLogPath(), and the manifest of Vite
before 5. Uselocale()andRequestContext::request() !== null; build with Vite 5 or later. - Action.
RedisStore::client(): open your ownnew \Redis()with theREDIS_*settings.