# BLUDIT PRO 3.22.0 — AUTHORITATIVE DEVELOPER REFERENCE  
**Patched codebase** (incorporates all fixes from the 3.22.0 patch manifest)  
Build `20260510`, codename "BrownBear"

---

## 1. IDENTIFICATION

| Property | Value |
|----------|-------|
| `BLUDIT_VERSION` | `'3.22.0'` |
| `BLUDIT_CODENAME` | `'BrownBear'` |
| `BLUDIT_BUILD` | `'20260510'` |
| `BLUDIT_RELEASE_DATE` | `'2026-05-10'` |
| `BLUDIT_PRO` | `true` (when pro file is loaded) |
| `BLUDIT_PRO_HASH` | `substr(md5(BLUDIT_BUILD), 0, 8)` — e.g. `'5d2fcbb4'` |

**Pro file loading** (from `init.php`):
```php
define('BLUDIT_PRO_HASH', substr(md5(BLUDIT_BUILD), 0, 8));
$_bluditProFile = PATH_KERNEL . 'bludit.pro.' . BLUDIT_PRO_HASH . '.php';
if (file_exists($_bluditProFile)) {
    include($_bluditProFile);
}
```
The pro file is named `bludit.pro.5d2fcbb4.php` (hash varies by build).

---

## 2. COMPLETE BOOT SEQUENCE (site side)

```
index.php
  └─ init.php
       ├─ Constants: BLUDIT_VERSION, codename, build, debug flags
       ├─ PHP ini settings (display_errors=0, html_errors=0, log_errors=1)
       ├─ PATH constants (LANGUAGES, THEMES, PLUGINS, KERNEL, CONTENT, etc.)
       ├─ User environment variables (boot/variables.php)
       ├─ mb_internal_encoding / mb_http_output set to CHARSET
       ├─ Abstract classes loaded (dbjson, dblist, plugin)
       ├─ Core classes loaded (pages, users, tags, language, site,
       │   categories, syslog, pagex, category, tag, user, url, login,
       │   parsedown, security)
       ├─ functions.php loaded
       ├─ Helper classes loaded (text, log, date, theme, session, redirect,
       │   sanitize, valid, email, filesystem, alert, paginator, image, tcp,
       │   dom, cookie)
       ├─ BLUDIT_PRO_HASH computed, pro file included if exists
       ├─ Objects created: $pages, $users, $tags, $categories, $site, $url,
       │   $security, $syslog
       ├─ HTML_PATH_ROOT and all DOMAIN_* / HTML_PATH_* constants defined
       ├─ $language = new Language($site->language())
       ├─ $url->checkFilters($site->uriFilters())
       ├─ TAG_URI_FILTER, CATEGORY_URI_FILTER, PAGE_URI_FILTER defined
       ├─ ORDER_BY, EXTREME_FRIENDLY_URL, AUTOSAVE_INTERVAL, IMAGE_RESTRICT,
       │   IMAGE_RELATIVE_TO_ABSOLUTE, MARKDOWN_PARSER defined
       ├─ THEME_DIR_* and DOMAIN_* constants finalized
       ├─ $ID_EXECUTION = uniqid()
       ├─ $WHERE_AM_I = $url->whereAmI()
       └─ $L = $language (shortcut)

  └─ site.php
       ├─ 60.plugins.php   → buildPlugins() populates $plugins and $pluginsInstalled
       ├─ Theme::plugins('beforeAll')
       ├─ 60.router.php    → trailing-slash redirects
       ├─ 69.pages.php     → builds $content, $page, $staticContent
       │    ├─ Scheduler auto-publishes scheduled pages and fires `afterPageCreate` hooks for each key (patched to pass the correct key array)
       │    ├─ Preview tokens now use the site’s random `previewSalt` instead of a predictable file path (patched)
       │    ├─ buildStaticPages() → $staticContent
       │    ├─ If homepage set + whereAmI='home' → buildThePage() w/ homepage slug
       │    ├─ Else if whereAmI='page' → buildThePage()
       │    ├─ Else if whereAmI='tag' → buildPagesByTag()
       │    ├─ Else if whereAmI='category' → buildPagesByCategory()
       │    ├─ Else if whereAmI='home' or 'blog' → buildPagesForHome()
       │    ├─ $page = $content[0] (if set)
       │    └─ If $url->notFound() → $page = buildErrorPage()
       ├─ 99.header.php    → HTTP status header + X-Powered-By
       ├─ 99.paginator.php → Populates Paginator::$pager
       │    └─ Theme::plugins('paginator')
       ├─ 99.themes.php    → Loads theme language, $themePlugin = getPlugin($site->theme())
       ├─ Theme::plugins('beforeSiteLoad')
       ├─ Theme init.php (if exists)
       ├─ Theme index.php   ← THEME RENDERS HERE
       ├─ Theme::plugins('afterSiteLoad')
       └─ Theme::plugins('afterAll')
```

**Admin boot** (from `boot/admin.php`):
```
admin.php
  ├─ Session::start()
  ├─ $login = new Login()
  ├─ $layout array built from URL slug
  ├─ 60.plugins.php → $pluginsInstalled populated
  ├─ Plugin admin controller detection (case-insensitive)
  ├─ If slug === 'ajax' → 99.security.php → load AJAX file
  └─ Else (admin area):
       ├─ 69.pages.php
       ├─ 99.header.php
       ├─ 99.paginator.php
       ├─ 99.themes.php
       ├─ 99.security.php
       ├─ If notFound or not logged → login
       ├─ Theme::plugins('beforeAdminLoad')
       ├─ Admin theme init.php (if exists)
       ├─ Controller loaded
       ├─ Admin theme index.php or login.php
       └─ Theme::plugins('afterAdminLoad')
```

---

## 3. COMPLETE HOOKS REGISTRY

### Site hooks
| Hook | Fires in | Purpose |
|------|----------|---------|
| `beforeAll` | site.php, before router | Earliest hook; routing state is set |
| `afterAll` | site.php, after theme | Final hook |
| `beforeSiteLoad` | site.php, before theme init | Content built, paginator ready |
| `afterSiteLoad` | site.php, after theme render | |
| `siteHead` | Theme `<head>` | Must be called by theme |
| `siteBodyBegin` | Theme `<body>` start | |
| `siteBodyEnd` | Theme `</body>` | |
| `siteSidebar` | Theme sidebar | Sorted by plugin position |
| `pageBegin` | Before page content | |
| `pageEnd` | After page content | |
| `paginator` | 99.paginator.php | After Paginator::$pager populated |

### Admin hooks
| Hook | Fires in | Purpose |
|------|----------|---------|
| `beforeAdminLoad` | admin.php, before theme | Admin boot |
| `afterAdminLoad` | admin.php, after theme | |
| `adminHead` | Admin `<head>` | |
| `adminBodyBegin` | Admin `<body>` start | |
| `adminBodyEnd` | Admin `</body>` | |
| `adminSidebar` | Admin sidebar | Plugin sidebar links |
| `adminContentSidebar` | Admin content sidebar | |
| `dashboard` | Admin dashboard area | |
| `editorToolbar` | Content editor toolbar | |

### Content lifecycle hooks
| Hook | When | Args |
|------|------|------|
| `afterPageCreate` | Page created (manual or scheduler) | `array($key)` |
| `afterPageModify` | Page edited | `array($key)` |
| `afterPageDelete` | Page deleted | `array($key)` |

### Login hooks
| Hook | Fires in | Purpose |
|------|----------|---------|
| `loginHead` | Login page `<head>` | |
| `loginBodyBegin` | Login page body start | |
| `loginBodyEnd` | Login page body end | |

---

## 4. PLUGIN SYSTEM INTERNALS

### 4.1 Plugin Base Class (`Plugin` — `/bl-kernel/abstract/plugin.class.php`)

**Constructor lifecycle**:
1. `$this->dbFields = array()`
2. `$this->customHooks = array()`
3. Directory name auto-detected via ReflectionClass
4. Class name auto-detected
5. `$this->formButtons = true`
6. `$this->init()` — YOUR CODE: define dbFields, customHooks
7. `$this->db = $this->dbFields` (default values)
8. `filenameDb` and `filenameMetadata` set
9. If plugin is installed:
   - Database loaded from file into `$this->db`
   - `$this->prepare()` called

### 4.2 Plugin Properties

| Property | Type | Set in |
|----------|------|--------|
| `$this->dbFields` | array | `init()` |
| `$this->db` | array | Constructor |
| `$this->formButtons` | bool | `init()` |
| `$this->customHooks` | array | `init()` |
| `$this->directoryName` | string | Auto |
| `$this->className` | string | Auto |
| `$this->metadata` | array | Auto |

### 4.3 Plugin Methods

| Method | Notes |
|--------|-------|
| `init()` | Define `$this->dbFields` |
| `prepare()` | After database loaded |
| `form()` | Return settings HTML |
| `post()` | Handle settings form POST |
| `save()` | Persist database |
| `install($position)` | Create workspace + database |
| `uninstall()` | Delete workspace + database |
| `installed()` | Check if database file exists |
| `getValue($field, $html=true)` | Returns sanitized value; if field is not defined, returns `null` (patched to avoid undefined index warning) |
| `setField($field, $value)` | Set + persist |
| `setPosition($position)` | Sidebar position |
| `getMetadata($key)` | Read metadata |
| `setMetadata($key, $value)` | Set metadata (runtime) |
| `includeCSS($filename)` | `<link>` tag |
| `includeJS($filename)` | `<script>` tag |
| `domainPath()` | `DOMAIN_PLUGINS` + dirName + `/` |
| `htmlPath()` | `HTML_PATH_PLUGINS` + dirName + `/` |
| `phpPath()` | Filesystem path to plugin dir |
| `phpPathDB()` | `PATH_PLUGINS_DATABASES` + dirName + `DS` (database storage for multi‑file plugins) |
| `workspace()` | `PATH_WORKSPACES` + dirName + `DS` |
| `webhook($URI, $afterUri, $fixed)` | Match URL against webhook |
| `isCompatible()` | Major.minor version match |
| `name()` | From metadata (set from language file) |
| `description()` | From metadata |
| `label()` | From metadata |
| `author()` | |
| `email()` | |
| `website()` | |
| `position()` | |
| `version()` | |
| `releaseDate()` | |
| `className()` | |
| `formButtons()` | |
| `directoryName()` | |
| `type()` | From metadata |

### 4.4 Plugin Build Process (`60.plugins.php`)

`buildPlugins()`:
1. Scans `PATH_PLUGINS` for directories
2. Includes `plugin.php` from each
3. Instantiates each plugin class
4. Loads language file, sets name/description
5. Merges additional translation keys into `$L`
6. Populates `$plugins['all'][$pluginClass]`
7. If installed (`$Plugin->installed()`):
   - Registers custom hooks
   - Pushes into `$pluginsInstalled[$pluginClass]`
   - Binds to all matching `$pluginsEvents`
8. Sorts `$plugins['siteSidebar']` by position

---

## 5. THEME SYSTEM INTERNALS

### 5.1 Theme File Structure

```
bl-themes/your-theme/
  ├─ index.php           ← required
  ├─ metadata.json       ← required
  ├─ languages/
  │    └─ en.json        ← required (theme-data.name, theme-data.description)
  ├─ init.php            ← optional, runs before index.php
  ├─ install.php         ← optional, runs on theme activation
  ├─ css/
  ├─ js/
  └─ img/
```

### 5.2 `99.themes.php` — Theme Loading

Sets `$themePlugin = getPlugin($site->theme())` — the theme’s companion plugin (if any). This provides theme authors a way to ship configuration settings with their theme. The Alternative theme uses this via its `alternative` plugin.

### 5.3 Theme Class — Complete Static Helper Methods

| Method | Returns | Purpose |
|--------|---------|---------|
| `Theme::title()` | string | `$site->title()` |
| `Theme::description()` | string | `$site->description()` |
| `Theme::slogan()` | string | `$site->slogan()` |
| `Theme::footer()` | string | `$site->footer()` |
| `Theme::lang()` | string | Short language code |
| `Theme::siteUrl()` | string | `DOMAIN_BASE` |
| `Theme::adminUrl()` | string | `DOMAIN_ADMIN` |
| `Theme::rssUrl()` | string/false | RSS feed URL |
| `Theme::sitemapUrl()` | string/false | Sitemap URL |
| `Theme::metaTags('title')` | string | `<title>` tag |
| `Theme::metaTags('description')` | string | `<meta description>` |
| `Theme::metaTagTitle()` | string | Same as metaTags('title'); if the current tag/category is not found, falls back to the homepage title format (patched to avoid undefined variable) |
| `Theme::metaTagDescription()` | string | Same as metaTags('description') |
| `Theme::charset($c)` | string | `<meta charset>` |
| `Theme::viewport($c)` | string | `<meta viewport>` |
| `Theme::keywords($k)` | string | `<meta keywords>` |
| `Theme::favicon($f, $t)` | string | Favicon link |
| `Theme::css($files, $base)` | string | CSS `<link>` tags |
| `Theme::js($files, $base, $attrs)` | string | JS `<script>` tags |
| `Theme::javascript(...)` | string | Alias for `js()` |
| `Theme::src($file, $base)` | string | Asset URL helper |
| `Theme::jquery()` | string | jQuery `<script>` |
| `Theme::jsBootstrap($attrs)` | string | Bootstrap JS bundle |
| `Theme::cssBootstrap()` | string | Bootstrap CSS |
| `Theme::cssBootstrapIcons()` | string | Bootstrap Icons CSS |
| `Theme::cssLineAwesome()` | string | Line Awesome icon font |
| `Theme::jsSortable($attrs)` | string | jQuery sortable |
| `Theme::socialNetworks()` | array | Configured social networks |
| `Theme::plugins($hook, $args)` | void (echo) | Dispatches hook |
| `Theme::headTitle()` | string | **DEPRECATED** → metaTagTitle() |
| `Theme::headDescription()` | string | **DEPRECATED** → metaTagDescription() |

---

## 6. ROUTING AND REQUEST LIFECYCLE

### 6.1 URI Filters (from `site.class.php::uriFilters()`)
```php
$filters['admin'] = '/' . ADMIN_URI_FILTER . '/';  // '/admin/'
$filters['page'] = $this->getField('uriPage');       // '/'
$filters['tag'] = $this->getField('uriTag');         // '/tag/'
$filters['category'] = $this->getField('uriCategory'); // '/category/'
if ($this->getField('uriBlog')) {
    $filters['blog'] = $this->getField('uriBlog');   // '/blog/' (only if homepage set)
}
```

### 6.2 URL Matching (`Url::checkFilters()`)
1. Admin filter first
2. Remaining filters sorted by URI length (longest first)
3. For each filter, URI matched against `HTML_PATH_ROOT + filter`
4. Slug extracted, whereAmI set
5. Special cases: empty slug + `/` → 'home', admin → slug trimmed
6. No match → setNotFound()

### 6.3 Router Rules (`60.router.php`)
1. `/admin` → redirect to `/admin/`
2. `/blog` → redirect to `/blog/` (only if homepage set)
3. `/my-page/` → redirect to `/my-page` (pages only)

### 6.4 `$WHERE_AM_I` Values

| Value | Meaning |
|-------|---------|
| `'home'` | Root URL with no slug |
| `'page'` | Single page |
| `'tag'` | Tag listing |
| `'category'` | Category listing |
| `'blog'` | Blog listing (when homepage is set) |
| `'admin'` | Admin panel |
| `'search'` | Search results page |

---

## 7. CONTENT OBJECT MODEL

### 7.1 Page Object (`class Page` in `pagex.class.php`) — 50 Methods

| Method | Returns | Notes |
|--------|---------|-------|
| `$page->title()` | string | |
| `$page->description()` | string | |
| `$page->content($sanitize=false)` | string | Markdown-parsed, cached |
| `$page->contentRaw($sanitize=false)` | string | Raw file content |
| `$page->contentBreak($sanitize=false)` | string | Before `<!-- pagebreak -->` |
| `$page->readMore()` | bool | Has pagebreak? |
| `$page->readingTime()` | string | Estimated reading time in minutes, computed from plain‑text content (`content(false)`) for accurate word count (patched) |
| `$page->permalink($absolute=true)` | string | Full URL |
| `$page->key()` | string | Database key |
| `$page->slug()` | string | Last segment of key |
| `$page->date($format=false)` | string | Formatted date |
| `$page->dateRaw()` | string | `Y-m-d H:i:s` |
| `$page->dateModified($format=false)` | string | |
| `$page->relativeTime($complete=false)` | string | "5 minutes ago" |
| `$page->username()` | string | Author username |
| `$page->user($method=false)` | User/mixed | e.g. `$page->user('nickname')` |
| `$page->type()` | string | `published`, `static`, `draft`, `sticky`, `scheduled`, `autosave` |
| `$page->published()` | bool | |
| `$page->sticky()` | bool | |
| `$page->isStatic()` | bool | |
| `$page->draft()` | bool | |
| `$page->scheduled()` | bool | |
| `$page->autosave()` | bool | |
| `$page->tags($array=false)` | string/array | Comma-separated or `[tagKey => tagName]` |
| `$page->category()` | string | Category name |
| `$page->categoryKey()` | string | |
| `$page->categoryDescription()` | string | |
| `$page->categoryTemplate()` | string | |
| `$page->categoryPermalink()` | string | |
| `$page->categoryMap($field)` | mixed | `name`, `template`, `description`, `key`, `list` |
| `$page->template()` | string | Custom template filename |
| `$page->coverImage($absolute=true)` | string/false | |
| `$page->thumbCoverImage()` | string/false | |
| `$page->uuid()` | string | |
| `$page->allowComments()` | bool | |
| `$page->position()` | int | |
| `$page->noindex()` | bool | |
| `$page->nofollow()` | bool | |
| `$page->noarchive()` | bool | |
| `$page->isParent()` | bool | |
| `$page->isChild()` | bool | |
| `$page->parent()` | string/false | |
| `$page->parentKey()` | string/false | |
| `$page->parentMethod($method)` | mixed | |
| `$page->hasChildren()` | bool | |
| `$page->childrenKeys()` | array | |
| `$page->children()` | Page[] | |
| `$page->previousKey()` | string/false | |
| `$page->nextKey()` | string/false | |
| `$page->related()` | array | Keys of related pages |
| `$page->custom($field, $options=false)` | mixed | Custom field value |
| `$page->json($array=false)` | string/array | |
| `$page->getValue($field)` | mixed | Raw field access |
| `$page->getDB()` | array | All internal vars |

### 7.2 Category Object

| Method | Returns |
|--------|---------|
| `$category->key()` | string |
| `$category->name()` | string |
| `$category->permalink()` | string |
| `$category->template()` | string |
| `$category->description()` | string |
| `$category->pages()` | array (Page keys) |
| `$category->json($returnsArray=false)` | string/array |

### 7.3 Tag Object

| Method | Returns |
|--------|---------|
| `$tag->key()` | string |
| `$tag->name()` | string |
| `$tag->permalink()` | string |
| `$tag->pages()` | array (Page keys) |
| `$tag->json($returnsArray=false)` | string/array |

---

## 8. SITE OBJECT — COMPLETE REFERENCE

| Method | Returns |
|--------|---------|
| `$site->title()` | string |
| `$site->slogan()` | string |
| `$site->description()` | string |
| `$site->footer()` | string |
| `$site->theme()` | string |
| `$site->adminTheme()` | string |
| `$site->homepage()` | string/false |
| `$site->pageNotFound()` | string |
| `$site->language()` | string |
| `$site->languageShortVersion()` | string |
| `$site->locale()` | string |
| `$site->timezone()` | string |
| `$site->dateFormat()` | string |
| `$site->timeFormat()` | string |
| `$site->itemsPerPage()` | int |
| `$site->orderBy()` | string |
| `$site->url()` | string |
| `$site->domain()` | string |
| `$site->urlPath()` | string |
| `$site->isHTTPS()` | bool |
| `$site->logo($absolute=true)` | string/false |
| `$site->emailFrom()` | string |
| `$site->extremeFriendly()` | bool |
| `$site->autosaveInterval()` | int |
| `$site->markdownParser()` | bool |
| `$site->imageRestrict()` | bool |
| `$site->imageRelativeToAbsolute()` | bool |
| `$site->thumbnailEnable()` | bool |
| `$site->thumbnailWidth()` | int |
| `$site->thumbnailHeight()` | int |
| `$site->thumbnailQuality()` | int |
| `$site->titleFormatPages()` | string |
| `$site->titleFormatHomepage()` | string |
| `$site->titleFormatCategory()` | string |
| `$site->titleFormatTag()` | string |
| `$site->uriFilters($filter='')` | array/string |
| `$site->customFields()` | array |
| `$site->defaultContentStatus()` | string |
| `$site->get()` | array |
| `$site->twitter()` through `$site->bluesky()` | string |
| `$site->xing()` | string |
| `$site->previewSalt()` | string — per‑installation random secret used for preview token HMAC (patched; auto‑generated if empty) |
| `$site->rss()` | string — DEPRECATED |
| `$site->sitemap()` | string — DEPRECATED |

---

## 9. API ENDPOINTS

### 9.1 API Plugin Webhooks

The API plugin listens on `api/` and routes to:

| Method | Endpoint | Auth Required | Returns |
|--------|----------|:---:|---------|
| GET | `/api/pages` | No (token only) | List of pages |
| GET | `/api/pages/{key}` | No | Single page |
| POST | `/api/pages` | Yes (auth token) | Created page |
| PUT | `/api/pages/{key}` | Yes | Updated page |
| DELETE | `/api/pages/{key}` | Yes | Success message |
| GET | `/api/settings` | Yes | Site settings |
| PUT | `/api/settings` | Yes | Success message |
| POST | `/api/images` | Yes | Uploaded image URL; MIME type verified via `Filesystem::mimeType()` before moving (patched) |
| GET | `/api/tags` | No | List of tags |
| GET | `/api/tags/{key}` | No | Tag with pages |
| GET | `/api/categories` | No | List of categories |
| GET | `/api/categories/{key}` | No | Category with pages |
| GET | `/api/users` | No | List of users |
| GET | `/api/users/{username}` | No | User profile |
| GET | `/api/files/{pageKey}` | No | List of files |
| POST | `/api/files/{pageKey}` | Yes | Uploaded file |

### 9.2 AJAX Endpoints (all in `/bl-kernel/ajax/`)

All endpoints require admin login + CSRF token validation.

| File | Purpose |
|------|---------|
| `change-type.php` | Change page type via AJAX |
| `clippy.php` | Quick search with role‑based filtering — author users see only their own pages (patched) |
| `content-get-list.php` | Search content by title |
| `delete-image.php` | Delete image + thumbnail; legacy thumbnail recovery |
| `generate-slug.php` | Generate URL slug |
| `get-published.php` | Search published pages (select2) |
| `list-images.php` | Media manager pagination; scans originals, pairs thumbnails |
| `logo-remove.php` | Remove site logo |
| `logo-upload.php` | Upload site logo |
| `profile-picture-upload.php` | Upload profile picture |
| `save-as-draft.php` | Autosave/preview; async fetch |
| `upload-images.php` | Upload images (media manager); filename sanitization + MIME validation |

---

## 10. EVENT/HOOK SYSTEM

### 10.1 Hook Registration (`60.plugins.php`)

The `$plugins` and `$pluginsEvents` arrays register the hooks as listed in Section 3.

### 10.2 Hook Dispatch (`Theme::plugins()`)
```php
public static function plugins($type, $args = array())
{
    global $plugins;
    foreach ($plugins[$type] as $plugin) {
        echo call_user_func_array(array($plugin, $type), $args);
    }
}
```
Output is echoed directly — no output buffering.

---

## 11. CONFIGURATION SYSTEM

### 11.1 Site Configuration Database (`site.class.php`)

`$dbFields` array:
- `'xing' => ''` — social network field
- `'previewSalt' => ''` — per‑installation preview security token; auto‑generated on first use (patched)
- All other standard fields (title, description, etc.)

### 11.2 User Database (`users.class.php`)

`$dbFields` includes:
- `'xing' => ''` — social network field
- All other standard fields

---

## 12. AUTHENTICATION AND SESSION HANDLING

### 12.1 Session Class (`session.class.php`)
- Uses `session_set_cookie_params()` with array syntax, including `httponly: true` and `samesite: 'Lax'`
- Prepends `__Secure-` prefix to session name when HTTPS is active
- Session name stored in `Session::$sessionName = 'BLUDIT-KEY'`

### 12.2 Cookie Class (`cookie.class.php`)
A helper class for cookie management:

| Method | Purpose |
|--------|---------|
| `Cookie::get($key)` | Read cookie value |
| `Cookie::set($key, $value, $days, $options)` | Set cookie with SameSite=Lax, HttpOnly |
| `Cookie::remove($key)` | Delete cookie |
| `Cookie::isEmpty($key)` | Check if empty |

### 12.3 Login Class (`login.class.php`)
- `setLogin()` calls `session_regenerate_id(true)` to prevent session fixation
- Session validation checks if the user still exists and is not disabled (password `'!'`)

### 12.4 Security Class (`security.class.php`)
- **New method**: `getUserIp()` — single source of truth for client IP
  - Reads only `REMOTE_ADDR` (rejects proxy headers as forgeable)
  - Returns validated IP or empty string
  - Documentation: "Deployments behind a reverse proxy should rewrite REMOTE_ADDR at the web server level"

---

## 13. GLOBAL VARIABLES

| Variable | Type |
|----------|------|
| `$content` | Page[] |
| `$page` | Page/false |
| `$staticContent` | Page[] |
| `$pages` | Pages |
| `$site` | Site |
| `$url` | Url |
| `$tags` | Tags |
| `$categories` | Categories |
| `$L` | Language |
| `$WHERE_AM_I` | string |
| `$plugins` | array |
| `$syslog` | Syslog |
| `$login` | Login |
| `$security` | Security |
| `$themePlugin` | Plugin/false — the companion plugin for the active theme (if any) |

---

## 14. GLOBAL FUNCTIONS (`functions.php`)

### Content & site functions
| Function | Purpose |
|----------|---------|
| `reindexCategories()` | Rebuild category index |
| `reindexTags()` | Rebuild tag index |
| `buildErrorPage()` | Build 404 page |
| `buildThePage()` | Build a single page (uses `$site->previewSalt()` for preview HMAC — patched) |
| `buildPagesForHome()` | Build home page listing |
| `buildPagesByCategory()` | Build category listing |
| `buildPagesByTag()` | Build tag listing |
| `buildPagesFor()` | Build list for a given set of pages |
| `buildStaticPages()` | Build static page array |
| `buildPage()` | Build a page from a key |
| `buildParentPages()` | Build parent page array |
| `createPage()` | Create a new page |
| `editPage()` | Edit a page |
| `deletePage()` | Delete a page |
| `editUser()` | Edit a user |
| `disableUser()` | Disable a user |
| `deleteUser()` | Delete a user |
| `createUser()` | Create a user |
| `editSettings()` | Save site settings |
| `getCategories()` | Get all categories |
| `getCategory()` | Get a category |
| `getTags()` | Get all tags |
| `getTag()` | Get a tag |
| `ajaxResponse()` | Send JSON AJAX response |

### New functions in 3.22.0

| Function | Purpose |
|----------|---------|
| `activatePlugin($className)` | Install and activate a plugin |
| `deactivatePlugin($className)` | Uninstall a plugin |
| `deactivateAllPlugin()` | Uninstall all plugins |
| `changePluginsPosition($classList)` | Reorder plugins |
| `createCategory($args)` | Create a new category |
| `editCategory($args)` | Edit a category |
| `deleteCategory($args)` | Delete a category |
| `changeUserPassword($args)` | Change a user's password |
| `checkRole($allowRoles, $redirect)` | Check if current user has allowed role |
| `activateTheme($themeDir)` | Activate a theme (deactivates old, runs install.php, activates theme plugin) |
| `transformImage($file, $imageDir, $thumbnailDir)` | Process uploaded image, generate thumbnail |
| `mediaManagerListImages($imagePath, $thumbnailPath, $chunk)` | Build file list for Media Manager |
| `downloadRestrictedFile($file)` | Send file for download with proper headers |

---

## 15. PAGES CLASS — NEW METHOD

| Method | Returns | Purpose |
|--------|---------|---------|
| `$pages->countByType($published, $static, $sticky, $draft, $scheduled)` | int | Count pages matching type filters |

---

## 16. IMAGE CLASS — WEBP SUPPORT

The `Image` class (`/bl-kernel/helpers/image.class.php`):
- `openImage()` checks `imagetypes() & IMG_WEBP` and calls `imagecreatefromwebp()`
- `saveImage()` handles `webp` extension via `imagewebp()`
- All decoders guard with capability checks to avoid fatal errors on stripped GD builds

---

## 17. FILESYSTEM CLASS — IMPROVEMENTS

| Method | Purpose |
|--------|---------|
| `mv($old, $new)` | Tries `rename()` first; falls back to `copy()` + `unlink()` for cross‑partition moves |
| `mimeType($file)` | Uses `finfo_file` fallback when `mime_content_type` is unavailable |
| `symlink($from, $to)` | Creates symlink or falls back to `copy()` |
| `zip()` | Creates ZIP archives |
| `unzip()` | Extracts ZIP archives |

---

## 18. USER CLASS — NEW FIELD

`User::$xing()` — returns the Xing social network profile.

### User profilePicture() method
Sanitizes the username for filename:
```php
$sanitizedUsername = Text::removeSpecialCharacters($username, '-');
$sanitizedUsername = Text::removeQuotes($sanitizedUsername);
$sanitizedUsername = Text::removeSpaces($sanitizedUsername, '-');
```

---

## 19. URL CLASS — COMPLETE REFERENCE

| Method | Returns | Notes |
|--------|---------|-------|
| `$url->uri()` | string | Raw request URI (no query string) |
| `$url->slug()` | string | Slug after filter match |
| `$url->whereAmI()` | string | `home`, `page`, `tag`, `category`, `blog`, `admin`, `search` |
| `$url->notFound()` | bool | |
| `$url->pageNumber()` | int | From `$_GET['page']`, defaults to 1 |
| `$url->activeFilter()` | string | |
| `$url->filters($type, $trim=true)` | string | |
| `$url->parameter($field)` | mixed | |
| `$url->explodeSlug($delimiter)` | array | |
| `$url->httpCode()` | int | |
| `$url->httpMessage()` | string | |
| `$url->setWhereAmI($where)` | void | Also sets `$GLOBALS['WHERE_AM_I']` |
| `$url->setSlug($slug)` | void | |
| `$url->setHttpCode($code)` | void | |
| `$url->setHttpMessage($msg)` | void | |
| `$url->setNotFound()` | void | Sets page, notFound, 404 |
| `$url->checkFilters($filters)` | bool | Internal — init.php |

---

## 20. PAGINATOR CLASS — COMPLETE REFERENCE

| Static Method | Returns |
|---------------|---------|
| `Paginator::numberOfPages()` | int |
| `Paginator::currentPage()` | int (1‑based) |
| `Paginator::firstPage()` | int (always 1) |
| `Paginator::nextPage()` | int |
| `Paginator::prevPage()` | int |
| `Paginator::showNext()` | bool |
| `Paginator::showPrev()` | bool |
| `Paginator::firstPageUrl()` | string |
| `Paginator::lastPageUrl()` | string |
| `Paginator::nextPageUrl()` | string |
| `Paginator::previousPageUrl()` | string |
| `Paginator::numberUrl($n)` | string — uses `?page=N` |
| `Paginator::html($prev, $next, $showNum)` | string |
| `Paginator::bootstrap_html($prev, $next, $showNum)` | string |
| `Paginator::get($key)` | mixed |
| `Paginator::set($key, $value)` | void |

---

## 21. KEY CONSTANTS

- `BLUDIT_PRO_HASH` — 8‑character hex hash for pro file naming
- `PROFILE_IMG_WIDTH` = 400, `PROFILE_IMG_HEIGHT` = 400, `PROFILE_IMG_QUALITY` = 100
- `MEDIA_MANAGER_NUMBER_OF_FILES` = 5
- `MEDIA_MANAGER_SORT_BY_DATE` = true
- `NOTIFICATIONS_AMOUNT` = 10
- `$GLOBALS['ALLOWED_IMG_EXTENSION']` = `['gif', 'png', 'jpg', 'jpeg', 'svg', 'webp']`
- `$GLOBALS['ALLOWED_IMG_MIMETYPES']` = `['image/gif', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/webp']`
- `$GLOBALS['ALLOWED_FILE_EXTENSIONS']` = `['gif', 'png', 'jpg', 'jpeg', 'webp', 'pdf', 'txt', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'tar', 'gz', 'mp3', 'mp4', 'wav', 'ogg', 'json', 'md']`

---

## 22. BUILT‑IN PLUGINS SHIPPED WITH 3.22.0

| Plugin | Class | Version | Type | Purpose |
|--------|-------|---------|------|---------|
| API | `pluginAPI` | 3.22.0 | plugin | REST API (image upload now validates MIME type) |
| Alternative | `alternative` | 3.22.0 | theme | Alternative theme companion |
| Backup Manager | `pluginBackupManager` | 3.22.0 | plugin (Pro) | Encrypted backups; restore aborts if emergency backup fails (patched) |
| Canonical | `pluginCanonical` | 3.22.0 | plugin | Canonical URL tags |
| Categories | `pluginCategories` | 3.22.0 | plugin | Sidebar category list |
| Comments | `pluginComments` | 1.0.0 | plugin | Full commenting system |
| Custom Fields Parser | `pluginCustomFieldsParser` | 3.22.0 | plugin | Parse custom field placeholders |
| Domain Migrator | (Pro) | 3.22.0 | plugin (Pro) | Migrate domains |
| EasyMDE | `plugineasyMDE` | 2.18.0 | plugin | Markdown editor |
| Gallery | `pluginGallery` | 3.22.0 | plugin (Pro) | Image galleries |
| Hit Counter | `pluginHitCounter` | 3.22.0 | plugin | Visit counter sidebar |
| HTML Code | `pluginHTMLCode` | 3.22.0 | plugin | Custom HTML injection |
| Links | `pluginLinks` | 3.22.0 | plugin | Sidebar links |
| Maintenance Mode | `pluginMaintenanceMode` | 3.22.0 | plugin | Maintenance page |
| Navigation | `pluginNavigation` | 3.22.0 | plugin | Sidebar navigation |
| Popeye | `popeye` | 3.22.0 | theme | Popeye theme companion |
| Remote Content | `pluginRemoteContent` | 3.22.0 | plugin | Remote ZIP content import |
| Robots | `pluginRobots` | 3.22.0 | plugin | robots.txt + meta robots |
| RSS | `pluginRSS` | 3.22.0 | plugin | RSS feed |
| Search | `pluginSearch` | 3.22.0 | plugin | Full‑text search |
| Sitemap | `pluginSitemap` | 3.22.0 | plugin | XML sitemap |
| Static Pages | `pluginStaticPages` | 3.22.0 | plugin | Sidebar static pages |
| Tags | `pluginTags` | 3.22.0 | plugin | Sidebar tag list |
| TimeMachine X | `pluginTimeMachineX` | 3.22.0 | plugin (Pro) | Content versioning |
| TinyMCE | `pluginTinymce` | 8.3.2 | plugin | WYSIWYG editor |
| Version | `pluginVersion` | 3.22.0 | plugin | Version info sidebar |
| Visits Stats | `pluginVisitsStats` | 3.22.0 | plugin | Visitor analytics |

---

## 23. REMAINING KNOWN BUGS AND PITFALLS

*Note: The following items are present in the patched codebase and are not addressed by the patch manifest.*

1. **Sticky pages only on page 1** — Sticky pages are merged into the content array only when `pageNumber == 1`.
2. **`$staticContent` built even on 404** — The static pages list is constructed unconditionally in `69.pages.php`, even when the request will result in a 404.
3. **Silent reindex loop** — If category or tag data is stale, the index is rebuilt on every hit without any warning.
4. **Hook output via echo** — `Theme::plugins()` echoes output directly; plugins cannot return values for further processing.

Previously reported security and bug issues that have been **patched** in this release:
- Database read race condition
- Predictable preview token
- Admin quick‑search draft/unpublished title leak
- `readingTime()` inaccurate word count
- `metaTagTitle()` undefined variable
- Scheduler missing hook arguments
- `Plugin::getValue()` undefined index warning
- Backup Manager emergency backup failure
- API image upload MIME type verification

---

## 24. COMPLETE FILE PATH REFERENCE (3.22.0, patched)

```
bludit/
├─ index.php
├─ bl-kernel/
│  ├─ boot/
│  │  ├─ init.php        ← BLUDIT_VERSION='3.22.0', build constants
│  │  ├─ admin.php       ← admin boot
│  │  ├─ site.php        ← site boot
│  │  ├─ variables.php   ← user configuration constants
│  │  └─ rules/
│  │     ├─ 60.plugins.php   ← plugin loading
│  │     ├─ 60.router.php    ← URL redirects
│  │     ├─ 69.pages.php     ← content building (scheduler + preview token patched)
│  │     ├─ 99.header.php    ← HTTP headers
│  │     ├─ 99.paginator.php ← pagination setup
│  │     ├─ 99.security.php  ← CSRF validation
│  │     └─ 99.themes.php    ← theme loading
│  ├─ abstract/
│  │  ├─ dbjson.class.php    ← JSON database (flock LOCK_SH added — patched)
│  │  ├─ dblist.class.php    ← List database
│  │  └─ plugin.class.php    ← Plugin base class (getValue() patched)
│  ├─ helpers/
│  │  ├─ alert.class.php
│  │  ├─ cookie.class.php
│  │  ├─ date.class.php
│  │  ├─ dom.class.php
│  │  ├─ email.class.php
│  │  ├─ filesystem.class.php  ← Improved mv(), symlink(), mimeType()
│  │  ├─ image.class.php       ← WebP support
│  │  ├─ log.class.php
│  │  ├─ paginator.class.php
│  │  ├─ redirect.class.php
│  │  ├─ sanitize.class.php
│  │  ├─ session.class.php     ← Array cookie params, __Secure-
│  │  ├─ tcp.class.php
│  │  ├─ text.class.php
│  │  ├─ theme.class.php       ← metaTagTitle() fallback patched
│  │  └─ valid.class.php
│  ├─ ajax/
│  │  ├─ clippy.php            ← Role‑based filtering (patched)
│  │  ├─ … (other AJAX endpoints)
│  ├─ admin/
│  ├─ css/
│  ├─ js/
│  ├─ functions.php            ← buildThePage() uses previewSalt (patched)
│  ├─ pages.class.php          ← countByType(); scheduler return value patched
│  ├─ users.class.php          ← xing
│  ├─ site.class.php           ← xing, previewSalt (patched)
│  ├─ security.class.php       ← getUserIp()
│  ├─ login.class.php
│  ├─ pagex.class.php          ← readingTime() patched
│  ├─ url.class.php
│  ├─ user.class.php           ← xing, improved profilePicture()
│  └─ bludit.pro.*.php        ← Dynamic hash‑based filename
├─ bl-plugins/
│  ├─ api/plugin.php           ← Image upload MIME check patched
│  ├─ backup-manager/plugin.php ← Emergency backup failure fix patched
│  └─ … (other plugins)
├─ bl-themes/alternative/     ← Updated theme
└─ bl-content/                ← Runtime data
```