Writing a plugin¶
A plugin is a small PHP package that Cloudlog loads from application/plugins. An administrator installs the zip from Plugin Manager, then enables it. Until it is enabled, Cloudlog leaves it alone.
The package is scanned when it is uploaded. It runs inside the same PHP process as Cloudlog, with the same database. Ship one to people who trust you.
The zip¶
manifest.json sits at the top of the zip, or inside a single folder at the top. Paths in the zip stay inside that package. A path that climbs out with .., or starts with / or a drive letter, is rejected.
example-award/
manifest.json
Plugin.php
Cloudlog copies the files to application/plugins/{slug}/ and stores a row in the plugins table. A new plugin is installed disabled. Uploading the same slug again replaces the files. If that plugin was already enabled, it stays enabled.
The web server has to be able to write to application/plugins. The install notes list that folder with the other writable ones.
manifest.json¶
{
"name": "Example Award",
"slug": "example-award",
"version": "1.0.0",
"description": "A small award page and a note in the log when a contact is saved.",
"entry": "Plugin.php",
"class": "Example_award",
"award_menu": {
"title": "Example Award",
"icon": "fas fa-award",
"order": 100,
"method": "renderAwardPage"
},
"hooks": {
"qso.filter.before_save": "beforeSave",
"qso.action.after_save": "afterSave",
"cron.action.run": "onCron"
}
}
slug is the folder name. Lower case letters, numbers, _ and -. name, version, and description are what Plugin Manager shows.
entry is the PHP file, relative to the plugin folder. It defaults to Plugin.php. class is the class in that file. It defaults to Plugin. Give the class its own name. PHP can only load one class of a given name, so two plugins both called Plugin will trip over each other.
entry and class are read from the manifest Cloudlog stored at upload. Change them by uploading the zip again.
The class¶
Cloudlog constructs the class with the CodeIgniter instance:
<?php
class Example_award {
private $ci;
public function __construct($ci)
{
$this->ci = $ci;
}
public function renderAwardPage($context)
{
$user_id = (int)$context['user_id'];
return array(
'page_title' => 'Example Award',
'content' => '<p>Signed in as user ' . $user_id . '.</p>',
);
}
public function beforeSave($qso, $context)
{
if (!isset($qso['COL_COMMENT'])) {
$qso['COL_COMMENT'] = '';
}
return $qso;
}
public function afterSave($payload, $context)
{
$qso_id = $payload['qso_id'];
log_message('info', 'Example award saw QSO ' . $qso_id);
}
public function onCron($payload, $context)
{
log_message('info', 'Example award cron at ' . $payload['started_at']);
}
}
$this->ci is the application. $this->ci->db is the database, and $this->ci->logbook_model can be loaded the usual CodeIgniter way. The log table name is $this->ci->config->item('table_name').
An award page¶
award_menu adds a line to the Awards menu while the plugin is enabled. Award Settings lists that line already ticked. Unticking it hides the line for that operator.
| Field | Meaning |
|---|---|
title |
The menu label. Required. |
icon |
A Font Awesome class. Defaults to fas fa-award. |
order |
Lower numbers sit higher in the plugin block. Defaults to 100. The same number is sorted by title. |
method |
The method that draws the page. Defaults to renderAwardPage. |
route |
Defaults to plugin_awards/view/{slug}. Letters, numbers, _, -, and /. |
The default route calls your method with:
array(
'slug' => 'example-award',
'manifest' => array(),
'user_id' => 1,
)
Return a string of HTML, or an array with content and an optional page_title. An empty content is treated as a failure. The HTML is placed in the page as you returned it, so escape callsigns, names, and comments you read out of the log.
The method has to exist in the entry file. If the name is wrong, the upload still succeeds and Plugin Manager adds a warning to the result.
Opening the page needs a logged-in operator. A missing method, a thrown error, or a policy failure disables the plugin and writes the reason to the CodeIgniter log.
Hooks¶
hooks maps a hook name to a method on your class. Only enabled plugins are called. A missing method or a thrown error disables that plugin.
A filter is apply_filters. Your method is method($payload, $context). Return the value the next plugin should see. Return null to leave the value unchanged.
An action is do_action. Your method is method($payload, $context). The return value is ignored.
These are the hooks Cloudlog calls today.
qso.filter.before_save¶
Runs from create_qso, the save on the logging page, after Cloudlog has built the row and before it is inserted.
$qso is that row. Keys are the log columns, such as COL_CALL, COL_BAND, COL_MODE, COL_TIME_ON. $context is source (manual) and station_id.
Return the row. Cloudlog uses the return value when it is an array.
qso.action.after_save¶
Runs from add_qso, after the insert. That covers the logging page and an ADIF import.
array(
'qso_id' => 123,
'qso' => array(),
)
$context is source and station_id. source is import when the save was told to skip the usual exports, and manual otherwise.
qso.action.after_edit¶
Runs after an existing contact is updated.
array(
'qso_id' => 123,
'qso' => array(),
)
$context is station_id.
qso.action.before_delete¶
Runs after Cloudlog has confirmed the contact can be deleted, and before the row is removed. $payload['qso'] is a short row: station_id, COL_CALL, COL_TIME_ON, COL_BAND, COL_MODE, COL_SUBMODE. $context is station_id.
cron.action.run¶
index.php/plugins/cron runs every enabled plugin that listens for this hook. There is no login on that URL. The home page lists it with the other cron lines, hourly:
0 * * * * curl --silent https://your-cloudlog.example/index.php/plugins/cron
$payload is started_at, a Y-m-d H:i:s timestamp. $context is source set to cron. The response body is Plugin cron hooks run.
What the upload will refuse¶
Every .php file in the package is scanned for a function call. A match rejects the zip. The same scan runs again when an award page is opened, and a match then disables the plugin.
The scan is a text search for these names followed by (.
Writing or moving files: file_put_contents, fopen, fwrite, fputs, mkdir, rmdir, rename, unlink, copy, touch, symlink, link, move_uploaded_file, chmod, chown, chgrp, ftruncate, tempnam, tmpfile.
Running a program: system, exec, shell_exec, passthru, proc_open, popen, pcntl_exec.
eval and assert.
Read a file with file_get_contents. The scan treats fopen as a write, so leave it out of the package. Read the log through $this->ci->db.