# Kleeja — File Upload Service for your website ::u-page-hero #title [Run a File Upload Service on your website]{.text-primary} #description Kleeja is the powerful and easiest way to run a file upload service on your website. Trusted by thousands of webmasters since 2007. It's written in PHP, which is available in almost all hosting solutions. #links :::u-button --- size: xl to: https://kleeja.net/getting-started/installation trailing-icon: i-lucide-arrow-right --- Get started ::: :::u-button --- color: neutral icon: i-lucide-download size: xl target: _blank to: https://github.com/kleeja/kleeja/releases variant: outline --- Download latest ::: :::u-button --- color: neutral icon: i-simple-icons-github size: xl target: _blank to: https://github.com/kleeja/kleeja variant: ghost --- Star on GitHub ::: :::u-button --- color: neutral icon: i-simple-icons-discord size: xl target: _blank to: https://discord.gg/Mp3XVKP variant: ghost --- Join our Discord ::: :: ::u-page-section #title How to #description Everything you need to get Kleeja installed, updated and extended. #body :::card-group ::::card --- icon: i-lucide-package title: How to install Kleeja to: https://kleeja.net/getting-started/installation --- Upload the files, run the installer and get your upload center online in a few minutes. :::: ::::card --- icon: i-lucide-arrow-up-circle title: How to update or upgrade to: https://kleeja.net/getting-started/update-upgrade --- Move an existing installation to a newer release without losing your files or settings. :::: ::::card --- icon: i-lucide-palette title: How to install styles to: https://kleeja.net/guides/install-styles --- Change the look of your upload center by installing a style from the Admin Panel. :::: ::::card --- icon: i-lucide-plug title: How to install plugins to: https://kleeja.net/guides/install-plugins --- Extend Kleeja with plugins for SEO, comments, payments, forum integration and more. :::: ::::card --- icon: i-lucide-file-cog title: The config.php file to: https://kleeja.net/guides/config-file --- Learn what every option in Kleeja's main configuration file does and when to change it. :::: ::::card --- icon: i-lucide-blocks title: Plugin development to: https://kleeja.net/development/plugin-development --- Build your own plugins: the folder layout, `init.php` , and the hooks core fires while a page runs. :::: ::::card --- icon: i-lucide-paintbrush title: Styles development to: https://kleeja.net/development/styles-development --- Build your own styles: the templates, assets and syntax behind Kleeja's front end. :::: ::::card --- icon: i-lucide-book-open title: Browse the documentation to: https://kleeja.net/getting-started/introduction --- The documentation collects guides, answers and reference material for every part of Kleeja. :::: ::::card --- icon: i-lucide-users title: Meet the contributors to: https://kleeja.net/contributors --- Kleeja is open source. See everyone who has written code that ships in the release you download. :::: ::: :: ::u-page-section #title Key features & highlights #description Everything an upload center needs, controlled from a single Admin Panel. #body :::card-group ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-languages title: Multi-lingual --- The script allows you to change the language from the Admin Panel easily. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-upload title: User-friendly interface --- The admin can choose the number of file fields shown to the user, in a very elegant way. The script interface is also very user-friendly. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-shield-check title: Security --- The script has a user sessions system, and the admin can sign out from the Admin Panel and act as a normal user. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-file-check title: Limit file types and sizes --- An advanced file types management system. The admin can control the file size for every single file type, for users and guests separately. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-hard-drive title: Limited upload space --- Limit your upload space, and the upload center closes automatically with a simple message when it reaches the limit. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-chart-line title: Google Analytics --- Add your Google Analytics code in the script settings without having to edit any of the script files. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-users title: Forum user integration --- Integrate your current phpBB or vBulletin forum users with your upload center easily. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-image title: Watermark & thumbnails --- Enable the watermark option so your site logo appears on uploaded images, and give users thumbnail links for them. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-flag title: Report abuse system --- Users can report abusive files from a simple page without entering the file URL. Reports land in the Admin Panel, where the admin can manage and reply to them. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-mail title: Advanced contact system --- Users can contact the admin, and messages are sent to the Admin Panel with the ability to reply. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-zap title: Cache system --- Kleeja uses cache (APC or flat files) so that pages load faster. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-user-plus title: Registration control --- The admin can open or close new user registration at any time with ease. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-power title: Closing the upload center --- The admin can open or close the upload center at will, with a custom message. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-type title: UTF-8 --- The script uses UTF-8 encoding throughout. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-eye title: Who's online --- Enable the "Who's online" option, with a clean separation between registered users and guests. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-timer title: Adjustable download counter --- Adjust the waiting period before users can download — great for showing ads, so users spend more time on your pages. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-ban title: Single or mass banning --- The admin can ban users through a simplified banning system. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-user-cog title: Multiple admins --- Add more than one admin user from the Admin Panel. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-wrench title: Total repair system --- Deletes cache, re-checks counters and repairs tables. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-file-key title: Changing file names --- Rename uploaded files automatically using the time function or MD5 encryption — or keep the original name. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-layout-dashboard title: Advanced admin dashboard --- The admin home page is the starting point, listing statistics and usage details at a glance. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-scroll-text title: Terms of service --- Users can't upload before agreeing to the terms, which you can easily edit. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-trash-2 title: File deletion URL --- Because we care about our users' privacy, a URL is given after a successful upload that lets the user delete the file. :::: ::::card --- class: hover:border-primary hover:bg-primary/10 icon: i-lucide-shield-alert title: Spam & flood protection --- Kleeja has spam and flood protection, and comes with a captcha system. :::: ::: :: # Introduction Welcome to the **Kleeja** documentation, the online manual for Kleeja and a living repository for Kleeja information and documentation. Kleeja is a file upload script written in PHP. You install it on your own hosting, and it turns any domain — or any folder on it — into a complete upload center with its own users, styles, plugins and Admin Panel. ## How-to ::card-group :::card --- icon: i-lucide-package title: How to install Kleeja to: https://kleeja.net/getting-started/installation --- Upload the files, run the installation wizard and get your upload center online. ::: :::card --- icon: i-lucide-arrow-up-circle title: How to update or upgrade Kleeja to: https://kleeja.net/getting-started/update-upgrade --- Move an existing installation to a newer release without losing your files. ::: :::card --- icon: i-lucide-plug title: How to install plugins on Kleeja to: https://kleeja.net/guides/install-plugins --- Extend Kleeja with plugins from the Admin Panel. ::: :::card --- icon: i-lucide-palette title: How to install styles on Kleeja to: https://kleeja.net/guides/install-styles --- Change the look of your upload center by installing a style. ::: :::card --- icon: i-lucide-file-cog title: Edit the config.php file to: https://kleeja.net/guides/config-file --- Database settings and the advanced options you can add by hand. ::: :::card --- icon: i-lucide-blocks title: Kleeja plugin development to: https://kleeja.net/development/plugin-development --- Build your own plugins: the folder layout, `init.php` , and the hooks core fires. ::: :::card --- icon: i-lucide-paintbrush title: Kleeja styles development to: https://kleeja.net/development/styles-development --- Build your own styles: the templates, assets and syntax behind the front end. ::: :: ## Key features & highlights - Multi-lingual. - Plugins system (download, install and update from the Admin Panel). - Auto update system (from the Admin Panel). - User-friendly interface with multiple file fields. - Advanced file types management system. - Limited upload space. - Google Analytics support. - User integration (vBulletin, phpBB, etc.) through plugins. - Watermark on images. - Generate image thumbnails. - Report-abuse system. - Advanced contact system. - Cache system. - User registration control (on/off). - Maintenance mode with a custom message. - UTF-8 encoding. - Adjustable waiting period for downloading. - Mass banning system. - Multiple admins (Admins or Founders). - Total repair system (caching, counters re-calibrating). - Filenames manipulation (by "time", "MD5 encryption" or a custom prefix). - Advanced admin dashboard. - Custom terms of service. - File deletion URL. - Spam & flood protection. ## Other ::card-group :::card --- icon: i-lucide-scroll-text target: _blank title: CHANGELOG to: https://github.com/kleeja/kleeja/blob/master/CHANGELOG.md --- What changed in every Kleeja release. ::: :::card --- icon: i-simple-icons-github target: _blank title: Kleeja on GitHub to: https://github.com/kleeja/kleeja --- Source code, releases and issue tracker. ::: :: # Installation ## Requirements We recommend running Kleeja on PHP 8.2. ## Download Kleeja Download the latest version of Kleeja from the releases page: ::card --- icon: i-lucide-download target: _blank title: Kleeja releases to: https://github.com/kleeja/kleeja/releases --- github.com/kleeja/kleeja/releases :: ## Upload Kleeja files to your website After downloading Kleeja you will have a zip archive. Extract it and you will get a folder titled `kleeja`. ![The extracted kleeja folder](https://kleeja.net/images/docs/kleeja-folder.png) - The `kleeja` folder is what you have to upload to your website using any FTP application. - You can rename the `kleeja` folder to anything you like, such as `up` — short for upload. - If you don't want Kleeja to live in a sub-folder, upload only the **contents** of the `kleeja` folder to your website. You will then get Kleeja on `example.com` instead of `example.com/kleeja`. ## Request the Kleeja installation wizard ::note This step comes after uploading the Kleeja contents to your website. :: Using any browser, request this link: ```bash [Browser] example.com/kleeja/install ``` Where: - `example.com` is your domain. - `kleeja` is the folder where you uploaded the Kleeja contents. ## Installation wizard steps ::steps{level="3"} ### Choose your language The first step gives you a list of languages — choose yours. ![Language selection step](https://kleeja.net/images/docs/install-language.png) ### Read the information about Kleeja Then you will see information about Kleeja. Click **next**. ![Information about Kleeja](https://kleeja.net/images/docs/install-info.png) ### Start the installation A step with an install icon appears. Click **install**. ![Install step](https://kleeja.net/images/docs/install-start.png) ### Accept the license The license page is shown. Click **next** to accept it. ![License page](https://kleeja.net/images/docs/install-license.png) ### Check the server functions Kleeja checks the server functions. If nothing is wrong, a **next** button appears — click it. Otherwise, make sure you fix the errors shown before moving on. ![Server functions check](https://kleeja.net/images/docs/install-server-check.png) ### Fill in the database information The next step requires your database information. Fill it in and click **next**. :::tip You can create the database from cPanel before starting this step. ::: ![Database information step](https://kleeja.net/images/docs/install-database.png) ### Create the configuration file Kleeja tries to create the configuration file in this step. If Kleeja cannot create it, the file is given to you so you can upload it manually before continuing. ![Configuration file step](https://kleeja.net/images/docs/install-config-file.png) :::note{to="https://kleeja.net/guides/config-file"} Learn what every option in `config.php` does. ::: ### Check the connection Kleeja checks the connection and the MySQL version. Click **next** if everything is OK. ![Connection and MySQL version check](https://kleeja.net/images/docs/install-db-check.png) ### Fill in your site information Kleeja asks you to fill in some information about your site, such as the website name and your admin account. ![Site information step](https://kleeja.net/images/docs/install-site-info.png) ### Create the database tables Kleeja then creates the database tables. Click **next**. ![Creating database tables](https://kleeja.net/images/docs/install-tables.png) ### Delete the install folder Congratulations, Kleeja is installed. All you have to do now is delete the `install` folder. ![Installation finished](https://kleeja.net/images/docs/install-done.png) :: ::warning Leaving the `install` folder on your server after the installation is a security risk. Delete it before using your upload center. :: # Update & Upgrade ::warning Always take a backup of your files and your database before updating. :: ## Auto-update (Kleeja 3.1.0 and later) From version 3.1 you can update Kleeja from the Admin Panel, on the **check for updates** page. Follow the steps from there. ## Upload Kleeja files to your website (manually) Download the latest version of Kleeja from the releases page: ::card --- icon: i-lucide-download target: _blank title: Kleeja releases to: https://github.com/kleeja/kleeja/releases --- github.com/kleeja/kleeja/releases :: After downloading Kleeja you will have a zip archive. Extract it and you will get a folder titled `kleeja`. ![The extracted kleeja folder](https://kleeja.net/images/docs/kleeja-folder.png) Upload all files from the `kleeja` folder to your website using any FTP application, replacing the old files — **except the `uploads` folder**. ## Request the Kleeja upgrade wizard ::note This step comes after uploading the Kleeja contents to your website. :: Using any browser, request this link: ```bash [Browser] example.com/kleeja/install ``` Where: - `example.com` is your domain. - `kleeja` is the folder where you uploaded the Kleeja contents. Go through all the steps until you get the **update** button, then click it. If there is an update to the database, Kleeja displays it to you — choose it and continue. Congratulations, Kleeja is updated now. # Install Styles Installing a style on Kleeja is easy and effortless. This article works for both old and recent versions of Kleeja. ## Install from the Kleeja Store Kleeja ships with a built-in store, so you rarely need to hunt for a style yourself. From the Kleeja Admin Panel, open the **Styles** page and switch to the **Kleeja Store** tab. The store lists the official styles with a preview image and the latest version. Click **Install** next to the one you want, and Kleeja downloads the archive and unpacks it into your styles folder. Set it as the default style afterwards. ::note Only the founder account can install from the store, and Kleeja refuses any style that does not support your version of Kleeja. :: ::tip The store lists only the styles you have not installed yet. To upgrade the ones you already have, open **Check for updates** on the same page. :: The store offers the same styles this website lists on the [Plugins & Styles](https://kleeja.net/store) page. ## Find a style for Kleeja Install a style from a file when it is not in the store, or when your server cannot reach the store. After finding a style that you like, download it to your computer. ::note Style file format: a style comes as a `zip` archive file. :: ![A style archive file](https://kleeja.net/images/docs/style-archive.png) ## Upload it to Kleeja From the Kleeja Admin Panel, go to the **Styles** page, then click on the **upload from your computer** button. ::tip If you cannot upload the style from within the Admin Panel, upload it using FTP. :: ![Uploading a style from the Admin Panel](https://kleeja.net/images/docs/style-upload.png) ## Set it as default After uploading the style, you can set it as the default style from the Styles page in the Kleeja Admin Panel. If you did the steps correctly, you will find the style listed there with a **set as default** option. ![Setting a style as default](https://kleeja.net/images/docs/style-default.png) # Install Plugins ## Install from the Kleeja Store Kleeja ships with a built-in store, so most plugins need no downloading at all. Open the **Plugins** page of the Kleeja Admin Panel and switch to the **Kleeja Store** tab. The store lists the official plugins with their descriptions and latest versions. Click **Install** next to the one you want, and Kleeja fetches the archive and unpacks it for you. The plugin then appears in the available plugins section, ready to enable. ::note Only the founder account can install from the store, and Kleeja refuses any package that does not support your version of Kleeja. :: ::tip The store lists only the plugins you have not installed yet. To upgrade the ones you already run, open **Check for updates** on the same page, then update them one by one or with **Update All** . :: The store offers the same plugins this website lists on the [Plugins & Styles](https://kleeja.net/store) page. ## Find a plugin to install Install a plugin from a file when it is not in the store, or when your server cannot reach the store. Download a plugin that you like from the internet. ::warning Make sure you trust the developer — plugins can break your website and spy on you. :: ::note Plugin file format: a plugin comes as a `zip` archive file. :: ![A plugin archive file](https://kleeja.net/images/docs/plugin-archive.png) ## Upload it to Kleeja From the **Plugins** page of the Kleeja Admin Panel, click on the **Upload from your computer** button and choose the plugin archive file. ![Uploading a plugin from the Admin Panel](https://kleeja.net/images/docs/plugin-upload.png) ## Enable the plugin After uploading the plugin file, the plugin appears in the available plugins section. Click on the **enable** button in front of its name. ![Enabling a plugin](https://kleeja.net/images/docs/plugin-enable.png) Congratulations, you installed and enabled a plugin. ::note During the plugin installation, a page with after-installation notes appears. Make sure you read it and follow any instructions shown there. :: ::note If the plugin has extra options, they appear on the settings page or as a separate side menu item. :: # The config.php File One of the most important files in your Kleeja installation is the `config.php` file. This file is located in the root of your Kleeja directory and contains your website's base configuration details, such as the database connection information. ## Where is config.php When you first download Kleeja, the `config.php` file isn't included. The [Kleeja installation process](https://kleeja.net/getting-started/installation) creates a `config.php` file for you based on the information you provide. ## Creating config.php manually You can manually create a `config.php` file by copying the simple code below: ```php [config.php] callable */ ]; ``` ::warning Define `information` , `install` , `update` , `uninstall` and `functions` even when a callback is empty. Only `first_run` is optional. The loader and the admin manager read the other keys directly, and a missing key raises a PHP warning. A plugin without an `uninstall` callback cannot be uninstalled from the admin panel at all, because the manager redirects back to the plugin list. :: ### Plugin information The `information` array describes the plugin to the admin panel and drives compatibility checks and load order. | Key | Type | Purpose | | --------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `plugin_title` | `array` | Display name per language code, for example `['en' => 'My Plugin', 'ar' => 'إضافتي']`. Falls back to `en`, then to the first entry. | | `plugin_developer` | `string` | Author name, stored in `plg_author`. | | `plugin_version` | `string` | Plugin version. Drives the update callback and the store's update check, so bump it on every release. | | `plugin_description` | `array` | Short description per language code. Shown in the plugin list, truncated to 100 characters. | | `plugin_kleeja_version_min` | `string` | Lowest supported Kleeja version. Installation fails below it. Leave it empty or `0` for no limit. | | `plugin_kleeja_version_max` | `string` | Highest supported Kleeja version. Installation fails above it. Leave it empty or `0` for no limit. | | `plugin_priority` | `int` | Load order. Higher numbers run first; `0` is normal. | | `settings_page` | `string` | Optional. Query string of the plugin's settings page, appended to `admin/?`. Adds a settings button next to the plugin and a link on the Help page. | ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['information'] = [ 'plugin_title' => [ 'en' => 'My Plugin', 'ar' => 'إضافتي', ], 'plugin_developer' => 'Your Name', 'plugin_version' => '1.0', 'plugin_description' => [ 'en' => 'Does something useful', 'ar' => 'تقوم بشيء مفيد', ], 'plugin_kleeja_version_min' => '4.0.0', 'plugin_kleeja_version_max' => '4.9', 'plugin_priority' => 0, 'settings_page' => 'cp=options&smt=my_plugin', ]; ``` ::tip Version limits are checked once, at install time, with `version_compare()` against `KLEEJA_VERSION` . Set `plugin_kleeja_version_max` generously, because a tight maximum blocks installation on newer Kleeja releases even when the plugin still works. A Kleeja 3 plugin with a maximum of `3.9` cannot be installed on Kleeja 4.0.0, and a maximum of `4.9` stops at 4.9.1. :: Write `settings_page` with a plain `&`, as above. The Help page encodes the link itself, so `&` there turns into a broken link. A full `http://` or `https://` URL is ignored. ### First run message The `first_run` key holds an HTML message shown right after a successful install. Kleeja picks the entry matching the site language and falls back to `en`. ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['first_run']['en'] = ' Thank you for installing this plugin. Report bugs to:
info@example.com '; ``` When `first_run` is missing, the admin panel redirects back to the plugin list after two seconds instead of waiting for a click. ### Install callback The `install` callback runs once, after the plugin row is inserted into the `plugins` table. It receives the new `plg_id`, which you pass to `add_config_r()` and `add_olang()` so Kleeja can tie your settings and language strings to the plugin. ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['install'] = function ($plg_id) { add_config_r([ 'my_plugin_enabled' => [ 'value' => '1', 'html' => configField('my_plugin_enabled', 'yesno'), 'plg_id' => $plg_id, 'type' => 'my_plugin', 'order' => '1', ], 'my_plugin_api_key' => [ 'value' => '', 'html' => configField('my_plugin_api_key'), 'plg_id' => $plg_id, 'type' => 'my_plugin', 'order' => '2', ], ]); add_olang([ 'CONFIG_KLJ_MENUS_MY_PLUGIN' => 'My Plugin settings', 'MY_PLUGIN_ENABLED' => 'Enable the plugin', 'MY_PLUGIN_API_KEY' => 'API key', ], 'en', $plg_id); }; ``` ::caution Kleeja suppresses SQL errors during installation, so a failing query fails silently, and the plugin row already exists by then. Check the return value of `$SQL->query()` for the queries that matter, and test the install callback on both MySQL and SQLite before you publish. :: ### Update callback The loader compares the installed version stored in `plg_ver` with `plugin_version` from `init.php` on every request. When the file declares a newer version, Kleeja calls `update`, then writes the new version to the database. The callback runs while the plugins are still loading, before Kleeja has loaded the current user and the groups. Use `$SQL->build()` directly there: - `add_config()`, `update_config()`, `delete_config()` and `get_config()` read the groups of the current user, and they stop the request with a fatal error. - `delete_cache()`, `kleeja_unlink()` and the other helpers that fire a hook start a second plugin loader. Delete cache files with `unlink()` instead. ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['update'] = function ($old_version, $new_version) { global $SQL, $dbprefix; $result = $SQL->build([ 'SELECT' => 'plg_id', 'FROM' => "{$dbprefix}plugins", 'WHERE' => 'plg_name = :name', 'BIND' => ['name' => 'my_plugin'], ]); $plg_id = (int) ($SQL->fetch($result)['plg_id'] ?? 0); $SQL->freeresult($result); $result = $SQL->build([ 'SELECT' => 'name', 'FROM' => "{$dbprefix}config", 'WHERE' => 'name = :name', 'BIND' => ['name' => 'my_plugin_timeout'], ]); $has_timeout = $SQL->fetch($result) !== false; $SQL->freeresult($result); if (version_compare($old_version, '1.1', '<') && ! $has_timeout) { $SQL->build([ 'INSERT' => '`name`, `value`, `option`, `display_order`, `type`, `plg_id`', 'INTO' => "{$dbprefix}config", 'VALUES' => ':name, :value, :option, :order, :type, :plg_id', 'BIND' => [ 'name' => 'my_plugin_timeout', 'value' => '30', 'option' => configField('my_plugin_timeout'), 'order' => 3, 'type' => 'my_plugin', 'plg_id' => $plg_id, ], ]); } if (version_compare($old_version, '2.0', '<')) { $SQL->build([ 'UPDATE' => "{$dbprefix}config", 'SET' => 'value = :value', 'WHERE' => 'name = :name', 'BIND' => ['value' => '', 'name' => 'my_plugin_api_key'], ]); } // the settings are cached, and delete_cache() can't run here $cache_file = PATH . 'cache/data_config.php'; if (file_exists($cache_file)) { if (function_exists('opcache_invalidate')) { @opcache_invalidate($cache_file, true); } @unlink($cache_file); } }; ``` Guard each migration with `version_compare()` on `$old_version`. Users may upgrade across several releases at once, so the callback must handle every path from any older version to the current one. Make every step safe to run twice. SQL errors are not hidden during an update, so a failing query shows an error page, and Kleeja saves the new version only after the whole callback has run. A step that fails, such as inserting a setting that exists already, therefore stops every page of the site until the plugin is fixed. ::note `$kleeja_plugin` is a local variable of the loader, not a global. A callback that needs the plugin's own `information` takes it with `function (...) use ($kleeja_plugin)` . An update does not clear the compiled templates either, so a plugin that changes markup through `style_parse_func` deletes its `cache/tpl_*.php` files in the update callback. :: ### Uninstall callback The `uninstall` callback runs before the plugin row is deleted. Remove everything the install callback created: settings, language strings and any custom tables. ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['uninstall'] = function ($plg_id) { delete_config([ 'my_plugin_enabled', 'my_plugin_api_key', ]); // a plg_id of 0 would delete the words of every plugin if ($plg_id > 0) { delete_olang(null, null, $plg_id); } }; ``` ::warning Passing `null` as the first argument to `delete_olang()` deletes every string of the given language, and a `null` language means all of them. With a `$plg_id` , only the plugin's strings go. The admin manager passes `0` when the plugin has no row, and `delete_olang(null, 'en', 0)` deletes the English strings of every installed plugin, so always check `$plg_id > 0` first. :: ## Hooks Hooks are the plugin system's only entry point into core logic. Kleeja ships more than 200 of them, scattered through `index.php`, `go.php`, `do.php`, `ucp.php`, `admin/index.php` and the files in `includes/`. ### How core fires a hook Every core call site looks like this. The hook receives every variable in scope at that point, and `extract()` writes the returned values back over them. ```php [do.php] extract(runHook('begin_download_page', get_defined_vars())); ``` `runHook()` is a shorthand for `Plugins::getInstance()->run()`. Kleeja 3 core also used a longer `is_array($plugin_run_result = ...)` form, which the Kleeja 4 core no longer uses. ### Writing a hook function Register hook functions in the `functions` array, keyed by hook name. Each function takes a single `$args` array and returns an array of variables to write back, or nothing when it only observes. ```php [plugins/my_plugin/init.php] $kleeja_plugin['my_plugin']['functions'] = [ // change a variable in the caller's scope 'Saaheader_links_func' => function ($args) { $extra = $args['extra'] . ''; return compact('extra'); }, // observe only — no return value 'ok_added_users_register' => function ($args) { my_plugin_log('new user: ' . kleeja_html_decode($args['name'])); }, ]; ``` ::important Hook functions are closures, so they do not inherit the caller's scope. Read core state from `$args` , and pull globals in explicitly with `global $config;` when you need them. :: The return value matters: | Return | Effect | | --------------------------------------- | ------------------------------------------- | | `compact('var')` or `['var' => $value]` | Overwrites `$var` in the calling scope. | | `[]`, `null`, or no `return` | Leaves the caller untouched. | | A new key not present in the caller | Creates that variable in the calling scope. | ### Priority and chaining `plugin_priority` controls order. The loader sorts hooks from high priority to low, so a plugin with priority `10` runs before one with priority `0`. Hooks chain: `run()` merges each function's return into `$args` before calling the next one. A later plugin therefore sees the values an earlier plugin produced, not the original ones. Core calls `extract()` only after the last function of the hook has run. A later plugin that reads a value with `global $x` still sees the old value, so a function that changes a global for the others sets the global and also returns it. ::warning Give plugins distinct priorities where order matters. The loader keys its internal plugin list by priority, so two plugins sharing a value overwrite each other in that list. Their hooks still run, but the bookkeeping is ambiguous. :: ### Dynamic hook names Some hooks are built from a request variable, which lets a plugin claim a page name that does not exist in core. The admin panel builds three of them from the requested `cp` value: ```php [admin/index.php] extract(runHook("require_admin_page_begin_{$go_to}", get_defined_vars())); extract(runHook("require_admin_page_end_{$go_to}", get_defined_vars())); extract(runHook("not_exists_{$go_to}", get_defined_vars())); ``` ### Hook reference A selection of the most useful hooks. Search the codebase with `grep -rn "runHook(" --include="*.php" .` for the full list, and check every hook of an old plugin against it: a hook that core no longer fires fails silently. | Hook | Location | Fires when | | ----------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `boot_common` | `includes/common.php` | Config, database, template engine and current user are ready. The language is not loaded yet. | | `end_common` | `includes/common.php` | Bootstrap has finished, before the page script runs. | | `Saaheader_links_func` | `includes/functions_display.php` | Header menus are built. Add meta tags via `$extra`, and menu items via `$top_menu` or `$side_menu`. | | `Saaheader_func` | `includes/functions_display.php` | Header HTML is rendered, before it is echoed. | | `Saafooter_func` | `includes/functions_display.php` | Footer variables are assigned, before rendering. | | `print_Saafooter_func` | `includes/functions_display.php` | Footer HTML is rendered, before it is echoed. | | `begin_index_page` | `index.php` | The upload page starts. Replace the uploader by changing `$uploadingMethodClass`. | | `end_index_page` | `index.php` | The upload page finishes. | | `begin_go_page` | `go.php` | A `go.php` request starts. | | `default_go_page` | `go.php` | The requested `go` value matches no core page. | | `begin_download_page` | `do.php` | A download request starts. | | `down_go_page` | `do.php` | The download page is about to be displayed. | | `begin_usrcp_page` | `ucp.php` | A user control panel request starts. | | `login_after_submit` | `ucp.php` | A login form has been submitted. | | `ok_added_users_register` | `ucp.php` | A new user row has been inserted. | | `begin_admin_page` | `admin/index.php` | The admin panel starts, after extensions are discovered. Add pages to `$adm_extensions` and icons to `$ext_icons`. | | `end_admin_page` | `admin/index.php` | Everything is ready to render. Set `$extra_admin_header_code` or `$extra_admin_footer_code`. | | `admin_help_guides` | `includes/adm/s_help.php` | The Help page builds its guides. Add yours to `$help_guides`. | | `kleeja_begin_send_mail_func` | `includes/functions.php` | An email is ready. Send it yourself and set `$sending_mail_handled` and `$mail_sent`. | | `kleeja_send_mail` | `includes/functions.php` | Kleeja is about to send an email with `mail()`. | | `kleeja_mail_template_func` | `includes/functions.php` | The HTML of an email is rendered. Change `$template_name` and `$template_path` to use your own template. | | `kleeja_fetch_file_start` | `includes/FetchFile.php` | A remote fetch begins. | | `style_parse_func` | `includes/style.php` | A template is compiled. Rewrite `$html` to inject markup. | | `style_parse_func_step_2` | `includes/style.php` | A template has compiled to PHP. Add `regex => replacement` pairs to `$rep` for your own tags. | | `delete_cache_func` | `includes/functions.php` | A cache entry is being cleared. | ## The database Kleeja 4 talks to MySQL, MariaDB or SQLite through PDO. The global `$SQL` is a `KleejaDatabase` from `includes/pdo.php`. Write a query as an array and pass it to `$SQL->build()`, with every value in `BIND` and a `:name` placeholder in its place. ```php [plugins/my_plugin/init.php] global $SQL, $dbprefix; $result = $SQL->build([ 'SELECT' => 'f.id, f.name, f.size', 'FROM' => "{$dbprefix}files f", 'WHERE' => 'f.user = :user AND f.type IN (:types)', 'ORDER BY' => 'f.id DESC', 'LIMIT' => '10', 'BIND' => ['user' => $user_id, 'types' => ['jpg', 'png']], ]); while ($row = $SQL->fetch($result)) { // ... } $SQL->freeresult($result); ``` The keys are the SQL words themselves: `SELECT`, `FROM`, `JOINS`, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`, `INSERT` with `INTO` and `VALUES`, `UPDATE` with `SET`, and `DELETE`. An array value in `BIND` expands to a list for `IN`, and an empty array matches no rows. A placeholder can appear more than once, and a value whose placeholder is missing from the query is dropped. | Method | Purpose | | ---------------------------------------- | ----------------------------------------------------------------------------- | | `build(array $query)` | Run a query array. Returns a `PDOStatement`, or `false` on error. | | `query(string $sql, array $params = [])` | Run raw SQL with the same placeholders, for example a `CREATE TABLE`. | | `fetch($result)` | The next row as an associative array, or `false`. | | `freeresult($result)` | Free a result. Call it before another query changes the same table on SQLite. | | `num_rows($result)` | The number of rows of a `SELECT`. | | `insert_id()` | The id of the last inserted row. | | `affected()` | The number of rows the last query changed. | | `get_error()` | The code and message of the last error. | ::warning `$SQL->escape()` and `$SQL->real_escape()` still exist for old plugins, but they are deprecated. Bind every value instead of building SQL strings, and encode text for HTML with `kleeja_html_encode()` . :: Kleeja stores text HTML-encoded and templates print it as it is. Encode a value once on the way into the database with `kleeja_html_encode()`, and print stored text with `kleeja_html_display()`. That function decodes every level of encoding, then encodes once, so text that was encoded twice when it was saved still shows correctly. For plain text, like an email subject, use `kleeja_html_decode()`. ### Custom tables Write `CREATE TABLE` queries in the MySQL dialect and prefix the table name with `{$dbprefix}`. On SQLite, Kleeja converts the query with the regular expressions in `KleejaDatabase::SQLITE_TYPES`. ::caution The conversion replaces every `int` in the query with `INTEGER` , even inside a name, so a column called `point` becomes `poINTEGER` . It also breaks `UNIQUE KEY name (column)` lines. Keep `int` out of your table and column names, put `UNIQUE` on the column itself, and create the table on both databases before you publish. :: ## Adding settings to the admin panel Kleeja stores settings in the `config` table. A setting with an `option` value appears in **Admin → Settings**, grouped into a tab by its `type`. ### Registering settings Use `add_config_r()` in the install callback. Set `type` to your plugin's slug so the settings land in their own tab, and pass `plg_id` so they disappear from the tab list when the plugin is disabled. ```php [plugins/my_plugin/init.php] add_config_r([ 'my_plugin_mode' => [ 'value' => 'fast', 'html' => configField('my_plugin_mode', 'select', [ 'Fast' => 'fast', 'Accurate' => 'accurate', ]), 'plg_id' => $plg_id, 'type' => 'my_plugin', 'order' => '1', ], ]); ``` `configField()` from `includes/functions_display.php` generates the input HTML in Kleeja's template syntax: | Type | Renders | | -------- | ---------------------------------------------------------------------------------- | | `text` | A text input bound to `{con.name}`. This is the default. | | `yesno` | A pair of radio buttons using the `YES` and `NO` language strings. | | `select` | A dropdown built from the `$select_options` array, in `['Label' => 'value']` form. | You can also write the `html` yourself. The settings page adds `class="form-control"` to every `` and ` ``` | Block | Shown for | | --------------- | ------------------------------------------- | | `image_thumb` | An uploaded image with a thumbnail. | | `image` | An uploaded image, direct and BBCode links. | | `file` | A non-image upload. | | `del_file_code` | The deletion code for the upload. | Available placeholders are `{b_title}`, `{b_bbc_title}`, `{b_url_link}`, `{b_img_link}`, `{b_code_link}`, plus `{siteurl}` and `{sitename}`. All four blocks must exist, because the uploader requests them by name. A child style without its own `up_boxes.html` uses the parent's. ::caution Kleeja removes every line break and the indentation after it before it uses the blocks. Keep each tag on one line, or attributes on separate lines run together. :: ## Style inheritance Set `depend_on` in `info.txt` to build on an existing style. Activating the child writes both `style` and `style_depend_on` to the settings, and the engine then resolves every missing template against the parent. ```ini [styles/bootstrap_black/info.txt] name = Bootstrap Black version = 2.0 kleeja_version = 4.0.0 depend_on = bootstrap ``` Template resolution runs in this order: ::steps ### The active style `styles/