-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathModule.php
More file actions
246 lines (232 loc) · 10.4 KB
/
Copy pathModule.php
File metadata and controls
246 lines (232 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
<?php
declare(strict_types=1);
namespace IwacVisualizations;
use IwacVisualizations\Mvc\EmbedFramingListener;
use IwacVisualizations\Sentiment\Centralite;
use IwacVisualizations\Sentiment\Polarite;
use IwacVisualizations\Sentiment\Subjectivite;
use Laminas\EventManager\Event;
use Laminas\EventManager\SharedEventManagerInterface;
use Laminas\Mvc\MvcEvent;
use Omeka\Module\AbstractModule;
/**
* IWAC Visualizations module.
*
* Asset loading: a block template declares WHAT it needs through
* `view/common/iwac-assets.phtml` (stylesheets, CDN libraries, shared JS
* modules, panels, orchestrator) and that partial emits them — templates
* never call $this->headLink / headScript themselves. We deliberately do
* NOT attach a controller listener that blanket-loads ECharts/MapLibre on
* every Item and ItemSet view — doing so cost ~600 KB of unused JavaScript
* on every Article page, even when no Visualizations block was configured.
* Per-block declaration keeps the cost contained to pages that actually
* render a block, and the on-view lazy loader in that partial defers even
* those until a block nears the viewport.
*
* Sentiment properties: the article dashboard renders its AI sentiment
* panel from Omeka item metadata (iwac:<model><Axis>) rather than the
* precomputed HF dataset. To keep the default item page clean we
* attach a `rep.resource.display_values` listener that strips every
* sentiment property from the default metadata table. This mirrors
* the pattern of the standalone `IwacSentiment` module whose logic is
* now rolled into this module (v0.11.0+). See
* `src/Site/ResourcePageBlockLayout/SentimentExtractor.php` for how an
* item's properties become the panel's rows, and `src/Sentiment/` for the
* three enums that map controlled-vocabulary item ids to labels and to
* their position on each scale.
*
* If you add a new block: register it in `IwacVisualizations\Site\BlockRegistry`
* (slug, label, description), add a `BlockLayout` subclass declaring that
* slug, wire the invokable in config/module.config.php, and model the
* template on `view/common/block-layout/press-bylines.phtml` — a call to
* `common/iwac-block-shell` with an `assets` array. `npm run lint:blocks`
* checks those four sites still agree.
*/
class Module extends AbstractModule
{
/**
* Every annotator family in the `iwac:` sentiment vocabulary, as the
* camelCase stem its six properties share.
*
* Deliberately wider than the models the article panel renders
* (`SentimentExtractor::MODELS`): the vocabulary also holds the
* January–February 2026 generation-1 slots and a retired DeepSeek
* preview that still carries ~11.5k real annotations. Every one of
* them must stay out of the default metadata table — listing only the
* models currently on display would dump 20-odd raw rating rows back
* onto every article page the moment the panel's model set changes.
*
* This list must gain a stem BEFORE that model's first annotation
* lands upstream, not after. A stem missing here is not a quiet
* degradation: the six raw rating rows appear on every article page
* the run has reached, justification prose included.
*/
const SENTIMENT_MODEL_STEMS = [
// Generation 1 — vendor slots, read-only, being retired upstream.
'gemini', 'chatgpt', 'mistral',
// Generation 2 — keyed by model. The five the panel renders,
// plus the families whose properties exist but hold no (or
// superseded) values.
'gpt56Luna', 'mistralSmall2603', 'deepseekV4Flash0731', 'gemma431bIt',
'qwen3827b',
'deepseekV4Flash', 'gemini35FlashLite', 'gemini36Flash',
'qwen35A3b', 'qwen35A10b',
];
/** The six property suffixes each annotator family carries. */
const SENTIMENT_AXIS_SUFFIXES = [
'Centralite',
'CentraliteJustification',
'Polarite',
'PolariteJustification',
'SubjectiviteScore',
'SubjectiviteJustification',
];
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
/**
* Grant public (unauthenticated and every role) access to the embed
* controller. Omeka denies access to module controllers by default,
* so the standalone iframe endpoint needs an explicit allow or it
* would 403 for anonymous site visitors.
*/
public function onBootstrap(MvcEvent $event): void
{
parent::onBootstrap($event);
$acl = $this->getServiceLocator()->get('Omeka\Acl');
// Use the registered controller service name (what Omeka adds as the
// ACL resource), NOT the class FQCN — passing the FQCN throws
// "Resource '...EmbedController' not found" and 500s the whole site.
$acl->allow(null, ['IwacVisualizations\Controller\Site\Embed']);
// Admin data-sync page (issue #7): GLOBAL admins only. The resource
// name must match config `navigation.resource` and the controller
// service name, same service-name rule as above.
//
// Not `site_admin`: the job this page dispatches replaces the whole
// `files/iwac-visualizations/` tree, which every site on the
// installation reads. A site admin's authority is over one site, and
// this is not a per-site operation — it is a filesystem swap on
// shared state, with a several-hundred-megabyte download in front of
// it.
$acl->allow(
['global_admin'],
['IwacVisualizations\Controller\Admin\Data']
);
// Allow the embed widget to be framed cross-origin (slides, project
// sites, blog posts, …). On the /iwac-embed routes only, swap the
// site's X-Frame-Options for a permissive CSP frame-ancestors:
// X-Frame-Options only understands DENY / SAMEORIGIN — it cannot
// allowlist origins — so a SAMEORIGIN hardening default (common in
// nginx) renders the embed iframe blank on every other origin.
// Modern browsers honour CSP frame-ancestors over X-Frame-Options.
// See relaxEmbedFraming(). Attached on the *application* event
// manager (not the shared one) because MvcEvent::FINISH is an
// application lifecycle event.
$event->getApplication()->getEventManager()->attach(
MvcEvent::EVENT_FINISH,
[$this, 'relaxEmbedFraming'],
100
);
}
/**
* Kept as a method on the module because `onBootstrap` attaches it and
* `tests/integration/omeka_boot.php` calls it by name; the ~100 lines of
* header parsing it used to carry now live in `EmbedFramingListener`
* (Tier 8 / H4).
*/
public function relaxEmbedFraming(MvcEvent $event): void
{
(new EmbedFramingListener())($event);
}
/**
* @deprecated Call `EmbedFramingListener::relaxFrameAncestorsPolicies()`.
* Kept because `tests/php/run.php` covers the pure CSP composition
* through this name.
*/
public static function relaxFrameAncestorsPolicies(array $headerValues): array
{
return EmbedFramingListener::relaxFrameAncestorsPolicies($headerValues);
}
public function attachListeners(SharedEventManagerInterface $sharedEventManager): void
{
// Strip sentiment properties from the default metadata table on
// every item representation. The article dashboard still reads
// them via $item->value() — this listener only cleans up the
// rendered property list the public theme iterates over.
$sharedEventManager->attach(
'Omeka\Api\Representation\ItemRepresentation',
'rep.resource.display_values',
[$this, 'filterSentimentValues']
);
}
/**
* Every `iwac:<model><Axis>` property term, one per model family ×
* axis. Built rather than spelled out: the vocabulary has grown from
* 18 to 66 sentiment properties across two annotation generations,
* and a hand-maintained list is exactly what falls behind.
*
* @return string[]
*/
public static function sentimentProperties(): array
{
$terms = [];
foreach (self::SENTIMENT_MODEL_STEMS as $model) {
foreach (self::SENTIMENT_AXIS_SUFFIXES as $axis) {
$terms[] = "iwac:{$model}{$axis}";
}
}
return $terms;
}
/**
* Drop the sentiment properties from the `values` array passed to
* the default resource-page metadata loop. Other modules / themes
* that want to display them can still reach them via
* `$item->value('iwac:gpt56LunaPolarite')` directly.
*/
public function filterSentimentValues(Event $event): void
{
$values = $event->getParam('values');
foreach (self::sentimentProperties() as $prop) {
unset($values[$prop]);
}
$event->setParam('values', $values);
}
/**
* The three sentiment axes are enums now.
*
* `CENTRALITE_ITEMS` / `POLARITE_ITEMS` / `SUBJECTIVITE_ITEMS` mapped
* item id → label, and `CENTRALITE_VALUES` / `POLARITE_VALUES` mapped
* label → ordinal, with nothing but convention keeping the two key sets
* aligned. `src/Sentiment/{Polarite,Centralite,Subjectivite}.php` now
* own both, as `match` expressions over a closed set of cases, so a new
* vocabulary value cannot be added with a label and no ordinal
* (Tier 8 / H4). The `@translate` markers moved with the labels;
* `extract-pot.js` scans `src/`, so every msgid is unchanged.
*
* The five lookups below stay because `SentimentExtractor` and
* `view/.../article.phtml` call them statically, and threading a module
* instance into a view partial to reach an enum would be a worse trade
* than five one-line shims.
*/
public static function getCentraliteLabel(?int $itemId): ?string
{
return Centralite::fromItemId($itemId)?->label();
}
public static function getPolariteLabel(?int $itemId): ?string
{
return Polarite::fromItemId($itemId)?->label();
}
public static function getSubjectiviteInfo(?int $itemId): ?array
{
return Subjectivite::fromItemId($itemId)?->info();
}
public static function getCentraliteNumeric(?string $label): int
{
return Centralite::ordinalForLabel($label);
}
public static function getPolariteNumeric(?string $label): int
{
return Polarite::ordinalForLabel($label);
}
}