Markdown Support: Technical Details
Technical details of Markdown Support: plugin metadata, core patch and backup, signal handlers, email pre-processing, preview endpoint, security measures and file structure.
Markdown Support: Technical Details
Plugin metadata
| Property | Value |
|---|---|
| Plugin ID | com.osticket:markdown-support |
| Name | Markdown Support |
| Plugin class | MarkdownPlugin in class.MarkdownPlugin.php |
| Configuration class | MarkdownConfig in config.php |
| Instances | Singleton: exactly one instance, enable() creates it if needed |
| PHP | 8.0 or newer: composer.json declares >=7.4, but ConfigCache uses the PHP 8.0 type mixed. On PHP 7.4 get() and set() raise a TypeError |
| Dependency | erusev/parsedown ^1.7 (locked to 1.7.4) |
Startup flow
graph TD
A["bootstrap()"] --> B["Load configuration into ConfigCache"]
B --> C["Check version and copy preview endpoint"]
C --> D{"Enable Markdown Support on?"}
D -->|"No"| E["End"]
D -->|"Yes"| F["Extend ThreadEntryBody::types with markdown"]
F --> G["Load MarkdownThreadEntryBody, patch class.thread.php"]
G --> H["Pre-process POST for email"]
H --> I["Connect signals object.view and threadentry.created"]
I --> J["Output buffer for editor pages"]
Core intervention
The plugin needs two interventions in osTicket:
- Via reflection: It adds the value
markdownto the static propertyThreadEntryBody::$types. No file change is needed for this. - Via file patch: In
include/class.thread.phpit inserts the branchcase 'markdown': return new MarkdownThreadEntryBody(...)after thecase 'html':branch ofThreadEntryBody::fromFormattedText(). If it cannot find the pattern (a different osTicket version), it logsUnable to apply patch - pattern not foundto the PHP error log and does not patch.
If the file already contains a case 'markdown':, the plugin does nothing. Before patching, it copies the file to include/class.thread.php.markdown-backup if no backup exists yet. On deactivation it restores the file from this backup and deletes it. If the backup is missing, it restores nothing. An existing backup is not overwritten (see Installation, section Updating osTicket).
Rendering
MarkdownThreadEntryBody extends ThreadEntryBody with the type markdown and renders in display():
ParsedownwithsetSafeMode(true)converts Markdown to HTML.- osTicket's
Format::sanitize()cleans the result. - In
pdfmode the plugin replacesimgtags with anhttp(s)source with the text[Image]. Inemailmode the HTML stays unchanged.
Search indexing (getSearchable()) converts the rendered HTML to text with Format::html2text().
Email handling
osTicket uses the raw POST text for emails. The Markdown editor sends plain text, so emails would otherwise show Markdown syntax. The plugin proceeds as follows:
- On a POST with
format=markdownit saves the fieldsresponse,noteandmessage, renders them to HTML withMarkdownThreadEntryBodyand setsformattohtmlfor osTicket. - osTicket creates the entry as HTML and sends the email with the rendered text.
- The handler for
threadentry.createdremembers the ID of the entry. - A shutdown function writes the original Markdown and
format = 'markdown'back toTHREAD_ENTRY_TABLEvia SQL at the end of the request.
In addition, the handler corrects the format via threadentry.created when the request carries a valid format (see API integration).
Editor assets
- Signal
object.view: For aTicketthe plugin outputs the configuration blockwindow.osTicketMarkdownConfig(defaultFormat,allowFormatSwitch,showToolbar,previewApiUrl), the stylesheet and the scriptjs/markdown-editor.js. The files get?v=<installed version>as a cache parameter. - Output buffer: On non-AJAX pages with the script name
tickets.phporticket.php(andajax.phpURLs containingticket) the plugin also starts an output buffer that inserts the same assets before</head>. A check forwindow.osTicketMarkdownConfigprevents double insertion.
Preview endpoint
markdown-preview.php is copied to api/markdown-preview.php on activation (and on every start if the file differs), because include/ is blocked by .htaccess. The endpoint:
- accepts only
POSTwith JSON{"markdown": "..."}and answers with{"success": true, "html": "..."}, - renders with Parsedown in SafeMode and then filters
javascript:anddata:values inhref/src,on*attributes,stylewithexpression()andscripttags, - does not load osTicket and therefore checks neither login nor CSRF token,
- does not limit the size of the
markdownfield, - looks for Parsedown at
vendor/autoload.phpnext to the file or at../include/plugins/markdown-support/vendor/autoload.php.
The endpoint exists at two paths: as
api/markdown-preview.php(copied there by the plugin) and as the original in the plugin folderinclude/plugins/markdown-support/markdown-preview.php, which the plugin's.htaccessopens. Both paths are reachable without login, check no CSRF token, do not limit the size of the input and do not load osTicket. The editor's live preview uses only the copy inapi/.
The preview does not pass through
Format::sanitize(), only through its own filter. It therefore does not always show exactly the HTML that later appears in the ticket view.
Security
| Measure | Where |
|---|---|
| Parsedown SafeMode (HTML in the text is escaped, unsafe URL schemes are neutralized) | Rendering and preview |
Format::sanitize() (osTicket) |
Rendering of thread entries |
Regex filter for javascript:/data: URLs, on*, expression(), script |
Preview endpoint |
Image upload: MIME whitelist (JPEG, PNG, GIF, WebP, BMP), URL check, alt text without [](/), abort without CSRF token |
Editor in the browser |
Own .htaccess in the plugin folder: allows markdown-preview.php, blocks *.log |
Plugin folder |
SQL values via db_input() |
Format and body updates |
The class MarkdownSanitizer is included with tests but is not called in the plugin's runtime flow.
Changes to include/.htaccess
On activation the plugin inserts two blocks: a FilesMatch block that allows js, css, map, json, images and fonts, and a block for PHP files under plugins/. On deactivation it removes both blocks again. There is no automatic adjustment for NGINX.
Apache matches the rule for PHP files (<FilesMatch "^plugins/.+\.php$">) only against file names without a path, so it probably never matches. markdown-preview.php is opened by the Files directive in the plugin folder's .htaccess.
File structure
markdown-support/
βββ plugin.php # plugin metadata
βββ class.MarkdownPlugin.php # plugin class, signals, enable/disable
βββ config.php # MarkdownConfig with the options
βββ markdown-body.php # MarkdownThreadEntryBody
βββ markdown-preview.php # preview endpoint (a copy goes to api/)
βββ MarkdownDetector.php # Markdown syntax detection
βββ .htaccess # allows the endpoint, blocks logs
βββ src/
β βββ Config/ConfigCache.php
β βββ Core/CorePatcher.php
β βββ Asset/AssetInjector.php
β βββ Asset/AssetDeployer.php
β βββ Http/MarkdownPostProcessor.php
β βββ Signal/ThreadEntryHandler.php
β βββ Format/ # sanitizer and interfaces
βββ css/markdown-editor.css
βββ js/markdown-editor.js # bundled from js/src/
βββ i18n/de_DE/ # German translation of the configuration
βββ vendor/ # Parsedown (Composer)
The repository additionally contains tests (PHPUnit), build sources for the JavaScript and documentation. CI runs a syntax check (php -l) with PHP 7.4 to 8.3.