Custom certificate
Versions
Current version
5.2.3 (2026042004)
5.2.3
Plugin type: Activities
Frankenstyle component name: mod_customcert
Version build number: 2026042004
Version release name: 5.2.3
Maturity: Stable
Supported Moodle versions: 5.2
Repository URL (Git): https://github.com/mdjnelson/moodle-mod_customcert
Repository branch:
Repository tag: v5.2.3
Issue/bug tracker URL: https://github.com/mdjnelson/moodle-mod_customcert/issues
Changelog
All notable changes to this project will be documented in this file.
Note - All hash comments refer to the issue number. Eg. #169 refers to https://github.com/mdjnelson/moodle-mod_customcert/issues/169.
[5.2.3] - 2026-07-06
Added
- Added new Group name element that displays the name of the group(s) a student belongs to in the course (#155).
Fixed
- Fixed
dml_missing_record_exceptionwhen previewing a site-level template that has no associated certificate activity (#840).
[5.2.2] - 2026-06-10
Security
- Hardened element and page ownership validation across edit, move, AJAX, external API, and fragment handlers to prevent cross-template access using mismatched request IDs (#818).
[5.2.1] - 2026-06-08
Fixed
- Fixed
TypeErrorcaused by$instanceidnot being cast tointin thecoursefieldelement (#804). - Fixed
TypeErrorin thedateandgradeelements whereget_mod_grade_info,get_grade_item_info, andget_course_grade_inforequire integer arguments. - Fixed
TypeErrorin thedateandexpiryelements caused by legacy integerdateformatvalues not being cast tostring.
[5.2.0] - 2026-05-24
Requirements
- Minimum supported version is Moodle 5.2 (PHP 8.3+).
Added
Major element API changes
Custom Certificate elements now separate element payload, layout, and rendering concerns.
Element data that was previously stored in dedicated columns (font, fontsize, colour, width) is now stored inside the element JSON payload in customcert_elements.data. Layout values (posx, posy, refpoint, alignment) are managed by the element repository/layout service rather than by individual element implementations.
Third-party elements should migrate to the new element interfaces under mod_customcert\element.
The legacy mod_customcert\element base class remains available for compatibility during this major release, but legacy hooks with replacement interfaces are deprecated and will be removed in a future major version.
Important: Third-party element plugins that extend
mod_customcert\elementand overriderender()orrender_html()must update their method signatures to match the new typed signatures before upgrading to 5.2. Plugins with old untyped signatures for these two methods will fail at class-load time. The deprecated legacy hooks (render_form_elements,definition_after_data,validate_form_elements,save_form_elements,save_unique_data,after_restore,copy_element) retain untyped base signatures and are intended to remain compatible with old-style untyped overrides.can_add()retains its typedboolreturn and is not deprecated; third-party overrides must declarepublic static function can_add(): bool. See the "Element rendering signatures" section below for the requiredrender/render_htmlsignatures.
New elements should use:
element_interface— for the core element contract (identity, payload, type)form_element_interface— required for registered certificate elements (along withrenderable_element_interface); provides edit-form lifecycle hooks includingbuild_form(). Note:form_buildable_interfacewas never released and has been fully replaced byform_element_interface.stylable_element_interface— only when the element supports standard visual styling (font,fontsize,colour,width)layout_element_interface— exposes repository-managed layout values (posx,posy,refpoint,alignment) for common PDF/HTML rendering helperspersistable_element_interface— normalises submitted form data into the JSON payloadvalidatable_element_interface— validates submitted form datapreparable_form_interface— populates edit-form fields from stored payloadrestorable_element_interface— remaps internal references after backup restorecopyable_element_interface— implementcopy_from(\stdClass $source): boolwhen an element needs custom logic during copy operations (e.g. copying associated files to a new context); the repository/service layer calls this instead of the deprecatedcopy_element()hook- Repository/service APIs for persistence and layout changes
Elements using the common renderer helpers (element_helper::render_content() / render_html_content()) must also implement both stylable_element_interface and layout_element_interface. Elements with fully custom rendering may implement renderable_element_interface directly without these.
An unknown_element fallback is used when a requested element type is not registered; it renders gracefully rather than throwing.
No deprecated bundled element implementations are shipped in this release. Deprecated element hooks remain only on the legacy mod_customcert\element base class to support third-party plugin migration.
Legacy internal state removed: Third-party elements that accessed protected internal state directly will need to update. In particular, direct access to
$this->element,$this->font,$this->fontsize,$this->colour,$this->width, or magic property fallback (__get()) must be replaced with the new getters, payload helpers, and repository-managed layout APIs. Visual values now live incustomcert_elements.data; layout values are managed separately by the repository/layout services.
Template export and import
- Templates can now be exported as a ZIP archive and imported on any Moodle site running this plugin.
- Export: from the Manage templates page, click the download icon next to any template to download a
.zipfile containing the template structure (pages and elements) and any associated files (e.g. images used by elements). - Import: from the Manage templates page, click Import template to upload a previously exported
.zipand recreate the template in the current context. - New export/import service layer under
mod_customcert\export:
template_file_manager— orchestrates ZIP creation and extraction, delegates totemplatefor DB writes, and rolls back copied files on failure.template/page/element— transactional DB import of template structure; each level delegates to the next.template_appendix_manager— handles copying element-associated files in and out of the ZIP (manifest-driven, with path-traversal and size-limit guards).template_import_logger_interface/template_logger— collects warnings produced during import (e.g. unknown element types) and surfaces them as Moodle notifications.
Service layer
- New service-layer APIs for template/page/element CRUD:
mod_customcert\service\template_service— orchestrates template/page/element create, update, delete, and move operations (plus DTOs such aspage_update)mod_customcert\service\template_repository— low-level DB access forcustomcert_templatesmod_customcert\service\page_repository— low-level DB access forcustomcert_pagesmod_customcert\service\element_repository— low-level DB access forcustomcert_elementsmod_customcert\service\template_duplication_service— transactional copy of a template with all its pages and elementsmod_customcert\service\template_load_service— replaces an existing template's pages/elements with content from another templatemod_customcert\service\item_move_service— handles moving pages and elements by swapping sequencesmod_customcert\service\pdf_generation_service— PDF generation, preview, and filename computation
- New certificate issuance and delivery services:
mod_customcert\service\certificate_issue_service— issues certificates and generates unique codesmod_customcert\service\certificate_download_service— handles bulk certificate download for instances and site-widemod_customcert\service\certificate_time_service— computes course time for certificate conditionsmod_customcert\service\certificate_email_service— sends certificate emails to students and teachersmod_customcert\service\certificate_issuer_service— orchestrates the scheduled issuance and email pipeline
- New repository classes:
mod_customcert\service\issue_repository— queriescustomcert_issuesmod_customcert\service\certificate_repository— queries certificates per usermod_customcert\service\issue_email_repository— tracks email send state per issue
- New rendering infrastructure:
mod_customcert\service\element_renderer— interface for the v2 element rendering pipeline (PDF and HTML surfaces)mod_customcert\service\pdf_renderer—element_rendererimplementation for PDF outputmod_customcert\service\html_renderer—element_rendererimplementation for the designer preview
Other additions
mod_customcert\template::from_record(\stdClass $record)is now the preferred entry point for instantiating a template from a database record;mod_customcert\template::create(string $name, int $contextid)is the preferred entry point for creating a new template. The constructor signature has changed to acceptint $id, string $name, int $contextidindividually (previously it accepted a raw\stdClass $record) — new code should usefrom_record()rather than callingnew template(...)directly, though the constructor remains functional.mod_customcert\element\legacy_element_adapter— internal compatibility adapter used by the factory to expose legacy elements extendingmod_customcert\elementthrough the new element interfaces. Plugin authors should migrate elements to the new interfaces directly rather than depending on the adapter.- Static utility methods on
certificatehave been moved to dedicated service/repository classes:
certificate::get_issues()/certificate::get_number_of_issues()/certificate::get_conditional_issues_sql()'issue_repositorycertificate::get_number_of_certificates_for_user()/certificate::get_certificates_for_user()'certificate_repositorycertificate::get_fonts()/certificate::get_font_sizes()'element_helpercertificate::set_protection()'form_servicecertificate::upload_files()'form_service
- Expanded automated tests covering element API behaviour and upgrade/restore data migration paths.
Changed
Element storage and data format
customcert_elementsno longer hasfont,fontsize,colour, orwidthcolumns; these values are now stored inside the JSON payload incustomcert_elements.data.customcert_elements.datais now treated as a JSON payload:
- Legacy scalar values are migrated into a JSON envelope (e.g. legacy
"Hello"'{ "value": "Hello" }). - Upgrade and restore routines automatically migrate existing data.
- Legacy scalar values are migrated into a JSON envelope (e.g. legacy
Migration impact for element plugins:
- Do NOT read/write
$element->font,$element->fontsize,$element->colour,$element->widthfrom DB fields (they no longer exist). - Use element getters (
get_font(),get_fontsize(),get_colour(),get_width()) which now read from the JSON payload. - Element plugins should return all relevant keys from
persistable_element_interface::normalise_data(), including the standard visual keys (font,fontsize,colour,width) where applicable; they are no longer merged centrally.
Reserved JSON keys (visuals):
- The following keys are used by core as standard visual fields in
customcert_elements.data:
width,font,fontsize,colour
- Element plugins that expose these visual attributes should return them from
normalise_data()explicitly. Plugins should not use these keys for unrelated element-specific payload data.
Editing + persistence flow
- Element edit handlers normalise element-specific data into a JSON payload via
normalise_data(); visual fields (font,fontsize,colour,width) must be returned explicitly by the element's ownnormalise_data()implementation so the stored payload is self-contained. - Saving elements is hardened to avoid corrupting stored JSON payloads when a caller provides a JSON list/array.
Element construction + adapters
- Registered element classes are constructed by the element factory using
new Class(stdClass $record). The removed experimentalconstructable_element_interface::from_record()path was never released and is no longer part of the API. - Factory returns
mod_customcert\element\element_interface; legacy elements may be wrapped bymod_customcert\element\legacy_element_adapter.
- Avoid
instanceofchecks against legacy concrete classes; prefer$element->get_type()and/or$element->get_inner().
- Avoid
Element rendering signatures
Breaking change for third-party element plugins: The
render()andrender_html()abstract methods onmod_customcert\elementnow have strict typed signatures and return types. Third-party element plugins that override these methods with the old untyped signatures will fail at class-load time with a PHP fatal error, even before the adapter layer can help. Plugins must update their method signatures before upgrading to 5.2. See the before/after signatures below.
- Element render methods are now typed and accept an optional renderer.
Before (legacy):
public function render($pdf, $preview, $user) { /* ... */ }
public function render_html() { /* ... */ }
After (5.2+):
use mod_customcert\service\element_renderer;
public function render(
pdf $pdf,
bool $preview,
\stdClass $user,
?element_renderer $renderer = null
): void {}
public function render_html(?element_renderer $renderer = null): string {}
element_helper::render_content()remains unchanged for backward compatibility, but element plugins must update their method signatures as shown above.
Restore / backup compatibility
- Restoring backups created on older versions is supported: legacy
font/fontsize/colour/widthvalues are merged into the JSON payload during restore. - Backups created on 5.2+ store visual fields inside JSON and are not guaranteed to restore correctly on older plugin versions (no backwards compatibility guarantee).
Fixed
- Improved robustness of upgrade/restore migrations:
- Better handling of missing/partial legacy fields.
- Safer normalisation of element payloads during restore.
- Legacy border elements that stored thickness as a scalar
datavalue are now migrated so width is preserved correctly.
certificate_issue_service::generate_code()now throws amoodle_exceptionafter 10 failed attempts to generate a unique code, preventing an infinite loop in systems with a very large number of issued certificates.- Web service hardening:
external::save_element()ignores JSON list payloads fordatato prevent numeric-key pollution of stored element JSON.
- Fixed issuing certificates to suspended users (#781).
Security
- Fixed authorization bypass (CVE-2026-30884, CWE-639) where a user with mod/customcert:manage in any
single course could read and overwrite certificate elements belonging to other courses by supplying
an arbitrary elementid. Element ownership is now validated against the authorised context/template
before any read or write operation is performed.
Deprecated
Template methods
The following template methods are now shims that emit developer debugging and delegate to service classes. Third-party developers should swap legacy calls for the service methods; the shims will be removed in a future release.
template::save()'template_service::update()template::add_page()/template::save_page()'template_service::add_page()/template_service::save_pages()template::delete()/template::delete_page()/template::delete_element()'template_service::delete()/template_service::delete_page()/template_service::delete_element()template::copy_to_template()'template_service::copy_to_template()template::move_item()'template_service::move_item()(service constants are available; raw strings remain supported)template::generate_pdf()'pdf_generation_service::generate_pdf()