Jump to content

ICT:FinalConfig - Asset v2.1 - private extension

From Costa Sano MediaWiki
Revision as of 20:30, 21 February 2026 by Mngr (talk | contribs)

Final Configuration for the Asset Entity – v2.1 - with private extension

(Automatic Identification, Provenance Capture, File Normalization)

Document revision: 2026-02-21 by Mngr Status: Authoritative

This document defines the intermediate configuration for the Asset entity (version 2.1). It supersedes all previous drafts and experimental implementations.


0. Runtime Baseline (LOCKED)

The following environment is assumed and MUST NOT change during Asset v2 implementation:

  • OS: AlmaLinux 10.1 with * PHP: 8.3
  • MediaWiki: 1.45.x runnin on Almalinix 10.1 Virtual Machine 1.
  • DocumentRoot: /var/ww/mediawiki
  • Page Forms: 6.x
  • MariaDB and Cargo running on AlmaLinux 10.1 Virtual Machine 2
  • Scribunto (Lua 5.1)
  • Virtual Machines running on top of Hyper-V
  • Reverse Proxy use on IIS
  • Redirects defined in Vhost
  • Site URL https://mwiki.costasano.club

Remark Lua 5.4 is explicitly excluded being incompatible with the precious setup.


1. Asset Concept (Normative)

An Asset represents the historical metadata of exactly one digital file.

Rules:

  • One Asset = one File
  • The Asset identifier and the File name are identical (except extension)
  • The Asset/File pair is immutable after creation.
  • Only specific informative fields are editable after first creation.
  • Assets may reference parent Assets (derivatives, OCR, AI output, annexes)

] Deletion of a pair is only permitted by sysop.

The Asset identifier is the archival reference and MUST NEVER change once created.


2. Identifier Scheme (Normative)

Asset identifiers follow this pattern:

<ChapterCode>-<ContextCode>-<SequenceNumber>

Where:

  • ChapterCode comes from Chapter.Code
  • ContextCode comes from either Place.Code OR Organisation.Code
  • SequenceNumber is a 4-digit integer

Example:

CH03-BER-0007

Rules:

  • Sequence numbers are computed automatically
  • Gaps are allowed in the numbering
  • Numbers are never reused
  • Contiguity is NOT guaranteed

3. Draft Asset Strategy (Mandatory)

Asset creation uses a unit draft based on a timestamp.

3.1 Draft Page

The draft page exist after creating and needs to be filled in, migrated (moved) to a final version and should not survice.

  • All new Assets are initially created with a timestamp as initial name
  • This inital draft page itself is never queried or displayed in dashboards.
  • The page is "moved" to its final identifier on save
  • The file uploaded during the draft phase is also "moved" to use exactly te same automatic code as the asset it is linked to. The original name will be stored in the OrignalFilename field for later reference.

3.2 imolementation strategy

It has been decided to realise the above requirement server-side and as such a private extension is to be created. Lua and Javascript are not forbidden if for one reason this might simplify the design or be needed to overcome MediaWiki 1.43 limits. It has been observed that AlmaLinux 10.1 with php 8.3 and MediaWiki 1.43 are all very new and porely documented which makes it difficult to come to a working setup due to lack of documentation. A lot of trial and error is needed to detect by inspection code how this setup behaves. The design should also take care to look for robust solution which need to last for a decade at least and survice software updates of all sorts.


4. Cargo Table and Template

4.1 Template:Asset

<noinclude>
Asset data template (v2)

{{#cargo_declare:
 |_table=Assets
 |Code=Page
 |Label=String
 |Chapter=Page
 |Place=Page
 |Organisation=Page
 |Sequence=Integer
 |File=String
 |OriginalFilename=String
 |Parent=Page
 |AssetType=Page
 |Description=Text
 |Notes=Text
}}
</noinclude>

{{#cargo_store:
 |Code={{FULLPAGENAME}}
 |Label={{{Label|}}}
 |Chapter={{{Chapter|}}}
 |Place={{{Place|}}}
 |Organisation={{{Organisation|}}}
 |Sequence={{{Sequence|}}}
 |File={{{File|}}}
 |OriginalFilename={{{OriginalFilename|}}}
 |Parent={{{Parent|}}}
 |AssetType={{{AssetType|}}}
 |Description={{{Description|}}}
 |Notes={{{Notes|}}}
}}

== {{{Label|}}} ==
{{DISPLAYTITLE:{{{Label}}}}}

{{#if:{{{File|}}}|'''File:''' [[{{{File}}}]]}}

{{#if:{{{OriginalFilename|}}}|'''Original filename:''' {{{OriginalFilename}}}}}

{{#if:{{{Parent|}}}|'''Parent asset:''' [[{{{Parent}}}]]}}

'''Description:'''
{{{Description|}}}

'''Notes:'''
{{{Notes|}}}

4.2 Cargo Setup

After saving the template:

  • Go to Special:CargoTables
  • Manually create the table Assets

5. Page Form

5.1 Form:Asset

<noinclude>
Form for creating and editing Asset pages (v2)
</noinclude>

{{{info
|page name=Asset:<Asset[Label]>
|no summary
|no preview
|no minor edit
|no watch
|no footer
}}}

{{{for template|Asset}}}

{| class="formtable"

! Chapter (*)
| {{{field|Chapter
 |input type=combobox
 |values from namespace=Chapter
 |existing values only
}}}
|-

! Place
| {{{field|Place
 |input type=combobox
 |values from namespace=Place
 |existing values only
 |placeholder=Use Organisation instead
}}}
|-

! Organisation
| {{{field|Organisation
 |input type=combobox
 |values from namespace=Organisation
 |existing values only
 |placeholder=Use Place instead
}}}
|-

! Identifier
| {{{field|Label|readonly}}}
|-

! Sequence
| {{{field|Sequence|readonly}}}
|-

! File
| {{{field|File
 |input type=page
 |namespace=File
 |uploadable=yes
}}}
|-

! Original filename
| {{{field|OriginalFilename|readonly}}}
|-

! Asset type
| {{{field|AssetType
 |input type=combobox
 |values from namespace=AssetType
 |existing values only
}}}
|-

! Parent asset
| {{{field|Parent
 |input type=combobox
 |values from namespace=Asset
 |existing values only
 |placeholder=Top level
}}}
|-

! Description
| {{{field|Description|input type=textarea}}}
|-

! Notes
| {{{field|Notes|input type=textarea}}}

|}

{{{standard input|save}}}

{{{end template}}}


6. Dashboard

6.1 Dashboard:Asset

= 🗂️ Asset Dashboard =

{| class="wikitable sortable"
! Code !! Chapter !! Place !! Organisation !! File !! Type
{{#cargo_query:
 tables=Assets
 |fields=_pageName,_pageTitle,Chapter,Place,Organisation,File,AssetType
 |where=_pageName!='Asset:DRAFT'
 |order by=_pageTitle
 |format=template
 |template=AssetRow
 |named args=yes
 |cache=no
}}
|}
{{#formlink:
 form=Asset

 |target=Asset:{{#time: YmdHis }}
 |link text=➕ New Asset
 |link type=button

 |query string=Asset[Label]=GENERATING...&Asset[File]=
 |returnto=Dashboard:Asset
}}

6.2 Template:AssetRow

<includeonly>
|-
| {{#formlink:
   form=Asset
   |target={{{_pageName}}}
   |link text={{{_pageTitle}}}
   |returnto=Dashboard:Asset
 }}
| {{{Chapter}}}
| {{{Place}}}
| {{{Organisation}}}
| {{{File}}}
| {{{AssetType}}}
</includeonly>


7. Private Extension AssetLifecycle

7.1 LocalSystems.php loading

   wfLoadExtension( 'AssetLifecycle' );

7.2 Extension.json

{
    "manifest_version": 2,
    "name": "AssetLifecycle",
    "version": "1.0",
    "author": "Heritage Research Club",
    "description": "Automated labeling for Heritage Assets",
    "AutoloadClasses": {
        "LegacyHooks": "LegacyHooks.php"
    },
    "Hooks": {
        "PageForms::WritePageData": "LegacyHooks::onPFWritePageData",
        "PageForms::SetTargetName": "LegacyHooks::onPFSetTargetName"
    }
}

7.2 LegacyHooks.php

<?php

use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Context\RequestContext;

class LegacyHooks {

    /**
     * Hook: PageForms::WritePageData
     */
    public static function onPFWritePageData( $formName, $title, &$wikitext ) {
        $logFile = '/var/www/mediawiki/hook_debug.log';
        $time = date('H:i:s');

        if ( !is_string( $wikitext ) ) return true;

        // 1. Extract values using multiline regex with flexible whitespace
        preg_match( '/^\|\s*Chapter\s*=\s*(.*?)\s*$/m', $wikitext, $matchCh );
        preg_match( '/^\|\s*Place\s*=\s*(.*?)\s*$/m', $wikitext, $matchPl );
        preg_match( '/^\|\s*Organisation\s*=\s*(.*?)\s*$/m', $wikitext, $matchOrg );
        preg_match( '/^\|\s*File\s*=\s*(.*?)\s*$/m', $wikitext, $matchFile );
        
        $chapter = isset($matchCh[1]) ? trim($matchCh[1]) : '';
        $place = isset($matchPl[1]) ? trim($matchPl[1]) : '';
        $org = isset($matchOrg[1]) ? trim($matchOrg[1]) : '';
        $oldFile = isset($matchFile[1]) ? trim($matchFile[1]) : '';

        if ( empty( $chapter ) ) return true;

        try {
            // 2. Query External DB (10.10.10.2)
            global $wgCargoDBserver, $wgCargoDBname, $wgCargoDBuser, $wgCargoDBpassword, $wgCargoDBtype;
            $dbFactory = MediaWikiServices::getInstance()->getDatabaseFactory();
            $db = $dbFactory->create( $wgCargoDBtype, [
                'host' => $wgCargoDBserver, 'user' => $wgCargoDBuser, 'password' => $wgCargoDBpassword, 'dbname' => $wgCargoDBname
            ]);

            // Query MAX Sequence
            $row = $db->selectRow('Assets', ['MAX(Sequence) AS m'], ['Chapter' => $chapter], __METHOD__);
            $nextSeq = ( $row && $row->m ) ? (int)$row->m + 1 : 1;
            $db->close();

            // 3. Generate Label
            $cleanCh = preg_replace('/^.*?:/', '', $chapter);
            $subVal = !empty($place) ? $place : $org;
            $cleanSub = preg_replace('/^.*?:/', '', $subVal);
            $newLabel = sprintf("%s-%s-%04d", $cleanCh, $cleanSub, $nextSeq);

            // 4. Update OriginalFilename (Crucial: Replace the whole line including potential whitespace)
            if ( !empty($oldFile) ) {
                $wikitext = preg_replace('/^\|\s*OriginalFilename\s*=.*$/m', "|OriginalFilename=$oldFile", $wikitext);
                
                $ext = pathinfo($oldFile, PATHINFO_EXTENSION);
                $newFileName = "$newLabel.$ext";
                
                // Update File field
                $wikitext = preg_replace('/^\|\s*File\s*=.*$/m', "|File=$newFileName", $wikitext);
                
                // 5. Physically rename the file
                self::moveUploadedFile( "File:$oldFile", "File:$newFileName" );
            }

            // 6. Update Label and Sequence
            $wikitext = preg_replace('/^\|\s*Label\s*=.*$/m', "|Label=$newLabel", $wikitext);
            $wikitext = preg_replace('/^\|\s*Sequence\s*=.*$/m', "|Sequence=$nextSeq", $wikitext);

            file_put_contents($logFile, "[$time] SUCCESS: Updated $newLabel. Original: $oldFile\n", FILE_APPEND);

        } catch ( \Exception $e ) {
            file_put_contents($logFile, "[$time] ERROR: " . $e->getMessage() . "\n", FILE_APPEND);
        }

        return true;
    }

    private static function moveUploadedFile( $oldName, $newName ) {
        if ( empty($oldName) || $oldName === $newName || $oldName === "File:" ) return;
        $oldTitle = Title::newFromText( $oldName );
        $newTitle = Title::newFromText( $newName );
        if ( $oldTitle && $newTitle && $oldTitle->exists() ) {
            $user = RequestContext::getMain()->getUser(); // Use current user for simplicity in this step
            $movePage = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $oldTitle, $newTitle );
            $movePage->move( $user, "Heritage Auto-Rename", false );
        }
    }

    /**
     * SetTargetName: We force the name here to ensure the Page Move happens.
     */
    public static function onPFSetTargetName( $formName, &$target_name, $wikitext = null ) {
        if ( is_string($wikitext) && preg_match('/^\|\s*Label\s*=\s*(.*)$/m', $wikitext, $m) ) {
            $label = trim($m[1]);
            if ( !empty($label) && $label !== 'GENERATING...' ) {
                $target_name = "Asset:" . $label;
            }
        }
        return true;
    }
}

7.4 Extension structure

 /var/www/mediawiki/extensions/AssetLifecycle/extension.json
 /var/www/mediawiki/extensions/AssetLifecycle/LegacyHooks.php

8. Immutability Rules

After a successful save, the following fields MUST NOT change:

  • Asset identifier (page name)
  • Label
  • Chapter
  • Place / Organisation
  • File
  • OriginalFilename

Other metadata MAY change.

Deletion of an Asset/File pair is sysop-only.


9. Status

Asset v2.1 is partially functionnal

File upload and move: OK Asset move: NOK Automatic numbering combination: OK for Chapter-Place-Number (Organisation not tested yet) Automatic counter increment: NOK OriginalFIleName filled in: NOK Identifier filled in: OK

Further work and testing needs to be done.