Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
with:
python-version: '3.12'

- name: Install jsonschema
run: python -m pip install --quiet jsonschema

- name: Rebuild the index
run: python3 scripts/build_index.py --output index.json

Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Check the analyzer against its own corpus.
#
# The source rules in scripts/analyze.py are the gate every submission passes
# through. A rule that quietly stops matching would not fail anything, every
# pull request would simply start passing, so the corpus in tests/ pins what
# each rule has to report and this runs it on every change to the scripts.

name: Self test

on:
push:
branches: [main]
paths:
- 'scripts/**'
- 'tests/**'
- 'rules/**'
- '.github/workflows/selftest.yml'
pull_request:
paths:
- 'scripts/**'
- 'tests/**'
- 'rules/**'
- '.github/workflows/selftest.yml'

permissions:
contents: read

jobs:
selftest:
name: Analyzer corpus
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Show the PHP version used to tokenize the corpus
run: php --version

- name: Run the corpus
run: python3 scripts/selftest.py
48 changes: 35 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,41 @@ fill it in, open a pull request. That is the whole thing.

```json
{
"id": "hello-world",
"name": "Hello World",
"description": "One line describing what the plugin does.",
"description": {
"en": "One line describing what the plugin does.",
"es": "Una linea describiendo lo que hace el plugin."
},
"author": "Your Name",
"website": "https://github.com/your-user/hello-world",
"license": "MIT",
"compatible": "4.0",
"version": "1.0.0",
"releaseDate": "2026-01-31",
"download": "https://github.com/your-user/hello-world/releases/download/v1.0.0/hello-world.zip",
"type": "",
"tags": ["example"]
}
```

`type` and `tags` are optional, everything above them is required. `id`,
`sha256` and `size` are added by the workflow, do not write them yourself.

Three things trip people up:

- **The filename must match the `id`**, and the `id` is the directory Bludit
creates inside `bl-plugins`.
- **The filename is the id.** `plugins/hello-world.json` becomes
`bl-plugins/hello-world`, so use lowercase letters, digits and hyphens.
- **`download` has to be a zip attached to a GitHub release.** Not
`/archive/main.zip` — GitHub regenerates those, so the bytes change and the
checksum recorded for your plugin would stop matching.
- **`version` has to be the same** as the one in your `metadata.json`.
- **`compatible` decides who is offered the plugin.** Bludit only lists a
plugin that names the `major.minor` the site is running, so `4.0` today.

`description` takes one line per language, keyed by a Bludit language code.
English is required and is what a site falls back to.

Nothing is read out of your zip. This file is the listing, so it is worth
getting right. The bot does compare the two and points out any difference for a
maintainer to look at, but it never rewrites what you wrote.

A bot checks the pull request and comments with anything that needs fixing,
pointing at the file and the line. Push a fix and the comment updates itself.
Expand All @@ -51,23 +63,33 @@ and the zip is built and attached to the release.

## Releasing a new version

Open a pull request changing `version`, `releaseDate` and `download`.
Open a pull request changing `version` and `download`.

The checksum recorded for a plugin is what guarantees the bytes people install
are the bytes that were reviewed, so a new version is reviewed too.

## Selling a plugin

Set `price_in_usd` and leave `download` out. Bludit cannot install an asset it
has to pay for, so a priced plugin is a listing only: it is hidden from the
plugin directory in the admin panel, it carries no checksum, and **its source
is never analyzed**. Sell and deliver it from your own website.

## What the bot rejects

**Blocks the merge:** a zip that cannot be downloaded or is not a valid plugin,
entries with `..` or symbolic links, a missing `plugin.php`, `metadata.json` or
`languages/en.json`, a version that disagrees with `metadata.json`, PHP that does
not parse, no class extending `Plugin`, an id or class name already used by
Bludit, and `eval`, shell commands or hidden encoded code.
`languages/en.json`, a `metadata.json` without `version` or `compatible`
(Bludit refuses to install it), PHP that does not parse, no class extending `Plugin`, an id or class name already used by
Bludit, and `eval`, shell commands or hidden encoded code. Also blocked:
`include` or `unserialize` reaching `$_GET`, `$_POST`, `$_REQUEST` or `$_COOKIE`,
and calling a function whose name was assembled at runtime.

**Flagged for a person to read, not blocked:** outbound HTTP requests, writing
files, dynamic calls, printing `$_GET` without `Sanitize::html()`. These are
legitimate for plenty of plugins — mention in the pull request why you need
them and it will go faster.
files, calling a closure held in a variable, including a path built from
constants, `unserialize` on your own data, printing `$_GET` without
`Sanitize::html()`. These are legitimate for plenty of plugins — mention in the
pull request why you need them and it will go faster.

The checks read the parsed PHP, so the word `system` in a comment or a string
is not a problem.
Expand Down
49 changes: 28 additions & 21 deletions rules/plugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/bludit/plugins/main/rules/plugin.schema.json",
"title": "Bludit plugin submission",
"description": "One file per plugin inside plugins/. The filename must be <id>.json. Fields derived by CI (sha256, size) must not be present.",
"description": "One file per plugin inside plugins/. The filename is the id, it is the directory Bludit creates inside bl-plugins. Nothing here is read from the zip, this file is the record. Fields derived by CI (id, sha256, size) must not be present.",
"type": "object",
"additionalProperties": false,
"required": ["id", "name", "description", "author", "website", "license", "compatible", "version", "releaseDate", "download"],
"required": ["name", "description", "author", "website", "license", "compatible", "version"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]{1,48}$",
"description": "Directory name created inside bl-plugins. Must match the filename and the directory inside the zip."
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 60
"maxLength": 60,
"description": "Shown in the plugin directory of the admin panel."
},
"description": {
"type": "string",
"minLength": 10,
"maxLength": 200,
"pattern": "^[^\\n\\r]+$",
"description": "One line, no line breaks."
"type": "object",
"description": "One line per language, the key is a Bludit language code such as en, es or pt_BR. English is required and is the fallback when the admin panel runs in a language the plugin does not provide.",
"required": ["en"],
"additionalProperties": false,
"maxProperties": 40,
"patternProperties": {
"^[a-z]{2,3}(_[A-Z]{2})?$": {
"type": "string",
"minLength": 10,
"maxLength": 300,
"pattern": "^[^\\n\\r]+$"
}
}
},
"author": {
"type": "string",
Expand All @@ -33,7 +37,8 @@
"type": "string",
"format": "uri",
"pattern": "^https://",
"maxLength": 300
"maxLength": 300,
"description": "Where a user reads about the plugin. Linked from the author name in the admin panel."
},
"license": {
"type": "string",
Expand All @@ -44,23 +49,25 @@
"compatible": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+(,[0-9]+\\.[0-9]+)*$",
"description": "Comma separated list of major.minor Bludit versions, for example 4.0 or 4.0,4.1"
"description": "Comma separated list of major.minor Bludit versions, for example 4.0 or 4.0,4.1. Bludit only offers a plugin that names the version the site is running."
},
"version": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Must be identical to the version inside the metadata.json of the plugin."
},
"releaseDate": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
"description": "The version this submission lists. Bump it in a new pull request to publish a new release."
},
"download": {
"type": "string",
"format": "uri",
"pattern": "^https://github\\.com/[^/]+/[^/]+/releases/download/[^/]+/[^/]+\\.zip$",
"description": "GitHub release asset. Archives generated by GitHub (/archive/*.zip) are not accepted, their bytes are not stable and the checksum would not hold."
"description": "GitHub release asset. Archives generated by GitHub (/archive/*.zip) are not accepted, their bytes are not stable and the checksum recorded for the plugin would stop matching. Required for a free plugin, and must be absent when price_in_usd is set."
},
"price_in_usd": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 100000,
"description": "Leave it out for a free plugin. A priced plugin is a listing only: Bludit cannot install an asset it has to pay for, so it is hidden from the plugin directory in the admin panel and its source is never analyzed. Sell it from your website."
},
"type": {
"type": "string",
Expand Down
Loading
Loading