Open source

Tools I wrote, given away

Written for my own projects, then made general enough to hand over. Every one of them runs on a real site before it is published, and the documentation lives here rather than in the README.

GPLv3
fork it, ship it to clients
Running in production
on this site, first
5 repositories
over 19 years of work

WP Bootstrapper

Head cleanup, hardening and uploads protection.

WordPress prints a dozen lines into the document head that almost nobody needs: the generator with its version number, RSD, wlwmanifest, the shortlink, oEmbed discovery, the emoji styles. Gutenberg adds its stylesheet even to pages holding no blocks. All of it comes off with filters — the trouble is remembering which ones, every time.

This is 48 switches across seven tabs, each one a single core hook or constant. The labels say so outright — wp_generator, DISABLE_WP_CRON, should_load_separate_core_block_assets, big_image_size_threshold — so what is being turned off is visible, and checkable against the WordPress documentation.

  1. The document head

    The version generator, RSD, wlwmanifest, the shortlink together with its HTTP header, feed links, the recent-comments widget styles, the emoji script and stylesheet, the dns-prefetch to s.w.org.

  2. Gutenberg

    Fifteen switches of its own: wp-block-library and its theme stylesheet, global styles, classic-theme-styles, core block patterns, the duotone SVG filters in the head, the footer and the admin, layout support, the group inner container.

  3. Uploads protection

    A managed block is written into wp-content/uploads/.htaccess refusing to execute php, php3 through php8, phtml and phar, and a web.config is built alongside it for IIS — through DOMDocument rather than string concatenation, so rules already in the file survive the write. The write is atomic; a daily cron job and a self-check put the protection back if the file is replaced.

  4. SVG uploads

    SVG and SVGZ are allowed only to a user holding manage_options, and are checked before they reach uploads: first for script and foreignobject elements, on* handlers and javascript: URLs, then by parsing through DOMDocument with the network switched off. A separate plugin for this is not needed.

  5. Author enumeration

    Requests for ?author=1 and author_name return a 404 to guests, and the canonical redirect is suppressed so WordPress does not helpfully supply the right address itself.

  6. What the plugin itself costs

    Every setting lives in a single option and is read once per request. There are no dependencies at all: composer install is not part of installing it, and the repository runs as it stands.

  7. For plugin authors

    Inside is an attribute of its own, #[Hook]: methods are marked with it in place, and a loader collects the hook map once through reflection and compiles it to a static file — after which no reflection runs and a class is instantiated only when its hook fires. Beside it sits a fluent settings API built on DTOs, which is what describes all seven tabs. If you write your own plugins, this is the part worth forking.

PHP 8.4+ · WordPress 6.7+ · GPLv3 — kostyasorokin/wp-bootstrapper on GitHub

WC Regional Catalog

Prices and catalogue by city, with a URL per region.

One store sells in several cities, each with its own prices and sometimes its own range. The usual answers are duplicating the products per city or swapping the price with a script at one address. The first breaks the books; the second breaks the cache and the search results, because Kyiv and Lviv share a URL and there is nothing left to rank.

What this does instead is a hierarchical region taxonomy, real addresses of the shape /shop/kyiv/, and a separate pair of prices — regular and sale — per region, held in the product's own meta. The product stays a single product.

  1. An address per city

    Every region gets its own URL through rewrite rules of the plugin's own. That is a landing page for a regional search, it lives happily behind a full-page cache, and it can be promoted on its own.

  2. Prices per region

    The regular and the sale price are set per city on the product itself. There are no duplicate products — so stock, SKUs and reports do not drift apart from each other.

  3. Range per region

    A product appears where it is attached to a region. The filtering runs through the taxonomy rather than a heavy query across meta fields — on a large catalogue that is a difference in speed, not in style.

  4. A Google Merchant feed per region

    A separate XML feed per region, so campaigns can run per city without anybody assembling a feed by hand. A product with no price set for the region asked for falls back to its ordinary one — worth knowing before a budget is pointed at the feed.

  5. A switcher with no JavaScript

    Choosing a city is an ordinary form posted to the server. Not one line of front-end script, so there is nothing to collide with the theme and nothing to wait for before the page draws.

PHP 8.3+ · WooCommerce · GPLv3 — kostyasorokin/wc-regional-catalog on GitHub

Telegram Integration

Site events into Telegram, and sign-in back out of it.

Email is a poor channel for a notification nobody replies to. It is slow, it lands in spam, and on a site whose server is not allowed to send mail it does not arrive at all. Telegram is instant, already open on the phone, and answers the question a notification exists to answer.

The half nobody builds properly is the way back: signing in. Most plugins ship the copy-paste login widget. This one implements the authorization code flow with PKCE and verifies the identity token cryptographically before a WordPress session exists.

  1. A session that cannot be stolen mid-flight

    The flow remembers which browser started it. A callback link copied out of somebody else's address bar cannot be replayed to hand over their account — the state cookie is per flow, and a mismatched one ends the attempt rather than logging anyone in.

  2. The two gaps Telegram leaves

    It returns no email address and no dependable avatar. A new account is asked for an email once, and a profile photo is fetched only over HTTPS and only from Telegram's own hosts, so importing a picture cannot be turned into a way of making the site fetch arbitrary URLs.

  3. Notifications, by kind rather than by list

    Content, comments, users, media, security, updates and Site Health each have their own switch, and WooCommerce adds orders, stock and cart events. The first line of every message is assembled from a header the owner configures, not from a fixed template.

  4. It says when it is broken

    A failed delivery keeps Telegram's own words, the chat it was for and the size of the message — never the body, because a notification can carry a form submission. Delivery state is reported through Site Health, so a channel that has gone quiet is visible rather than eventually noticed.

  5. A spam gate that is watched

    An unbroken run of Contact Form 7 submissions rejected as spam is treated as evidence about the check rather than about the senders. Real spam arrives in bursts; a gate that refuses everything does not. That distinction was paid for on this site.

  6. For plugin authors

    Integrations with other plugins are found by scanning a directory and cached, not held in a list somebody has to remember to edit. The login-button helper returns an empty string when the plugin has not booted, when login is off and when the visitor is already signed in — so a template can echo it with no guard around it.

GPLv3 — kostyasorokin/wp-telegram-integration on GitHub

Disable All Email

One file that makes outgoing mail impossible.

A site that cannot deliver mail is not merely quiet. Bots fill its forms, it emails a confirmation to whatever address the bot supplied, and those addresses belong to real people who press "spam". Bounces pile up behind them and the server's address ends up on a blocklist — usually noticed when the host writes to ask what is going on.

Stopping the mail daemon does not prevent it. The sendmail binary is still there, PHP hands messages straight to it, and it opens its own connection outward. The daemon is off and the mail leaves anyway.

  1. Two layers, one of which runs

    As a must-use plugin the file loads before WordPress declares its own wp_mail, so it takes the name first and nothing downstream can send. If another must-use plugin got there first, a filter stops the core sender instead.

  2. What callers are told

    A constant decides. Reporting success keeps forms and checkouts finishing normally; reporting failure is the honest answer when there is no other delivery channel, at the cost of showing every visitor an error.

  3. Getting back in

    WordPress emails a recovery link when a plugin fails fatally and stores only a hash of it, so the usable link exists nowhere but in that message. It is rebuilt and written where it can still be read, and handed to an action so it can be sent somewhere else instead.

  4. What it does not claim

    Nothing in PHP can intercept code that calls the mail function itself, opens its own SMTP socket, or posts to a mail API with raw cURL. Those close at the server. This closes the WordPress half and says plainly where the other half lives.

GPLv3 — kostyasorokin/wp-disable-all-email on GitHub

KS Projects

A portfolio archive that survives the theme it was built in.

A portfolio normally lives in the theme: the post type, the tags and every case study are declared there. That works until the theme is replaced, and then the whole archive goes with it — which is the one thing a portfolio must not do.

This is that archive as its own content type, with the line drawn in a particular place: the plugin owns the data and the editing screens, the theme owns how any of it looks.

  1. Defaults a theme beats by name

    Three plain templates ship with the plugin so a fresh site renders as a portfolio rather than a blog roll. A theme takes any of them over simply by having a file of the same name — no setting to switch, nothing to deregister. The defaults carry no framework classes on purpose: a default that arrived with a design would look wrong on every theme except the one it came from.

  2. What a case study actually needs

    Two metaboxes. The card holds the short name, a subtitle and the address the work went live at, because a portfolio title tends to be a sentence and a grid needs a name. The credits hold what the client asked for, who worked on it and what shipped — as fields rather than hand-written HTML, so a redesign does not mean editing every project by hand.

  3. A template per project

    One case study wants a full-bleed gallery, another a long read. A project may name a PHP file in the theme, and it renders in place of the post content and nothing else — the heading, the tags and the credits stay with the theme. Turning it off brings the editor's version straight back, and a file that goes missing falls back to it rather than leaving an empty page.

  4. The setting that is deliberately absent

    URL slugs are editable. The post type and taxonomy names are not: those are the strings every row in the database is filed against, and offering them in a settings screen offers a one-click way to lose an archive. They can still be set from wp-config.php, before the first project exists and never after.

  5. How it is put together

    PSR-4 classes, and a hook declared on the method that answers it rather than in a registration list somewhere else — no compiled cache and no directory scan behind that, so one file says what the plugin consists of. The API a theme calls is one class with one import. No runtime dependencies at all: the repository runs the moment it is cloned.

  6. What runs before a release

    Static analysis at PHPStan's strictest level, coding standards with every departure recorded next to its reason, and unit tests over the parts that can be reasoned about on their own. Two checks exist because their failure is silent otherwise: one refuses a shipped autoloader that disagrees with the source, one refuses a translation edited but never compiled.

PHP 8.5+ · WordPress 6.7+ · GPLv3 — kostyasorokin/wp-ks-projects on GitHub

Take them and use them

All of it is free and open — put it on your own projects and on your clients'. Bugs and suggestions go to the issues on GitHub. Adapting one of these to a particular site, or building something new for a system that is not here yet, is work of its own and worth writing about.