Merge branch 'wiki' into 'main'

Draft: Wiki

See merge request nounous/nixos!60
merge-requests/60/merge
Pyjacpp 2026-08-16 06:20:08 +02:00
commit 2f2b5e6c6c
32 changed files with 33070 additions and 0 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
result result
*.qcow2

View File

@ -60,6 +60,11 @@
modules = [ ./hosts/vm/livre ] ++ baseModules; modules = [ ./hosts/vm/livre ] ++ baseModules;
}; };
mediakiwi = nixosSystem {
specialArgs = inputs;
modules = [ ./hosts/vm/mediakiwi ] ++ baseModules;
};
neo = nixosSystem { neo = nixosSystem {
specialArgs = inputs; specialArgs = inputs;
modules = [ ./hosts/vm/neo ] ++ baseModules; modules = [ ./hosts/vm/neo ] ++ baseModules;

View File

@ -0,0 +1,9 @@
<?php
$magicWords = [];
$magicWords['en'] = [
'calendar' => [ 0, 'calendar' ],
];
$magicWords['fr'] = [
'calendar' => [ 0, 'calendrier' ],
];
?>

View File

@ -0,0 +1,45 @@
{
"name": "FullCalendar",
"version": "0.1.0",
"author": [
"Crans"
],
"url": "",
"license-name": "GPL-3.0+",
"description": "",
"type": "parserhook",
"requires": {
"MediaWiki": ">= 1.39.0"
},
"ResourceModules": {
"ext.fullcalendar": {
"scripts": [
"ical.es5.cjs",
"fullcalendar7/fullcalendar.global.js",
"fullcalendar7/themes/breezy/global.js",
"fullcalendar7/locales-all/global.js",
"fullcalendar7/fullcalendar-ical.js",
"init.js"
],
"styles": [
"fullcalendar7/skeleton.css",
"fullcalendar7/themes/breezy/theme.css",
"fullcalendar7/themes/breezy/palettes/rose.css"
]
}
},
"ResourceFileModulePaths": {
"localBasePath": "resources",
"remoteExtPath": "fullcalendar/resources"
},
"AutoloadClasses": {
"FullCalendar": "includes/FullCalendar.php"
},
"ExtensionMessagesFiles": {
"FullCalendarMagic": "FullCalendarMagic.i18n.php"
},
"Hooks": {
"ParserFirstCallInit": "FullCalendar::onParserFirstCallInit"
},
"manifest_version": 2
}

View File

@ -0,0 +1,21 @@
<?php
use MediaWiki\Html\Html;
class FullCalendar {
// Register any render callbacks with the parser
public static function onParserFirstCallInit( Parser $parser ) {
// Create a function hook associating the <code>example</code> magic word with renderExample()
$parser->setFunctionHook( 'calendar', [ self::class, 'renderCalendar' ] );
}
public static function renderCalendar( Parser $parser, $param1 = '', ) {
$parser->getOutput()->addModules( [ 'ext.fullcalendar' ] );
$resultHtml = Html::element( 'div', [ 'class' => 'mw-crans-fullcalendar', 'data-ics' => $param1 ] );
return [ $resultHtml, 'noparse' => true, 'isHTML' => true ];
}
}
?>

View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 Adam Shaw
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,237 @@
/*!
FullCalendar iCalendar Plugin v7.0.1
Docs & License: https://fullcalendar.io/docs/icalendar
(c) 2026 Adam Shaw
*/
(function (ICAL) {
'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var ICAL__default = /*#__PURE__*/_interopDefaultLegacy(ICAL);
function addDays(m, n) {
let a = dateToUtcArray(m);
a[2] += n;
return arrayToUtcDate(a);
}
function dateToUtcArray(date) {
return [
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds(),
];
}
function arrayToUtcDate(a) {
// according to web standards (and Safari), a month index is required.
// massage if only given a year.
if (a.length === 1) {
a = a.concat([0]);
}
return new Date(Date.UTC(...a));
}
/* eslint-disable */
class IcalExpander {
constructor(opts) {
this.maxIterations = opts.maxIterations != null ? opts.maxIterations : 1000;
this.skipInvalidDates = opts.skipInvalidDates != null ? opts.skipInvalidDates : false;
this.jCalData = ICAL__default["default"].parse(opts.ics);
this.component = new ICAL__default["default"].Component(this.jCalData);
this.events = this.component.getAllSubcomponents('vevent').map(vevent => new ICAL__default["default"].Event(vevent));
if (this.skipInvalidDates) {
this.events = this.events.filter((evt) => {
try {
evt.startDate.toJSDate();
evt.endDate.toJSDate();
return true;
}
catch (err) {
// skipping events with invalid time
return false;
}
});
}
}
between(after, before) {
function isEventWithinRange(startTime, endTime) {
return (!after || endTime >= after.getTime()) &&
(!before || startTime <= before.getTime());
}
function getTimes(eventOrOccurrence) {
const startTime = eventOrOccurrence.startDate.toJSDate().getTime();
let endTime = eventOrOccurrence.endDate.toJSDate().getTime();
// If it is an all day event, the end date is set to 00:00 of the next day
// So we need to make it be 23:59:59 to compare correctly with the given range
if (eventOrOccurrence.endDate.isDate && (endTime > startTime)) {
endTime -= 1;
}
return { startTime, endTime };
}
const exceptions = [];
this.events.forEach((event) => {
if (event.isRecurrenceException())
exceptions.push(event);
});
const ret = {
events: [],
occurrences: [],
};
this.events.filter(e => !e.isRecurrenceException()).forEach((event) => {
const exdates = [];
event.component.getAllProperties('exdate').forEach((exdateProp) => {
const exdate = exdateProp.getFirstValue();
exdates.push(exdate.toJSDate().getTime());
});
// Recurring event is handled differently
if (event.isRecurring()) {
const iterator = event.iterator();
let next;
let i = 0;
do {
i += 1;
next = iterator.next();
if (next) {
const occurrence = event.getOccurrenceDetails(next);
const { startTime, endTime } = getTimes(occurrence);
const isOccurrenceExcluded = exdates.indexOf(startTime) !== -1;
// TODO check that within same day?
const exception = exceptions.find(ex => ex.uid === event.uid && ex.recurrenceId.toJSDate().getTime() === occurrence.startDate.toJSDate().getTime());
// We have passed the max date, stop
if (before && startTime > before.getTime())
break;
// Check that we are within our range
if (isEventWithinRange(startTime, endTime)) {
if (exception) {
ret.events.push(exception);
}
else if (!isOccurrenceExcluded) {
ret.occurrences.push(occurrence);
}
}
}
} while (next && (!this.maxIterations || i < this.maxIterations));
return;
}
// Non-recurring event:
const { startTime, endTime } = getTimes(event);
if (isEventWithinRange(startTime, endTime))
ret.events.push(event);
});
return ret;
}
before(before) {
return this.between(undefined, before);
}
after(after) {
return this.between(after);
}
all() {
return this.between();
}
}
const eventSourceDef = {
parseMeta(refined) {
if (refined.url && refined.format === 'ics') {
return {
url: refined.url,
format: 'ics',
};
}
return null;
},
fetch(arg, successCallback, // any
errorCallback) {
let meta = arg.eventSource.meta;
let { internalState } = meta;
/*
NOTE: isRefetch is a HACK. we would do the recurring-expanding in a separate plugin hook,
but we couldn't leverage built-in allDay-guessing, among other things.
*/
if (!internalState || arg.isRefetch) {
internalState = meta.internalState = {
response: null,
iCalExpanderPromise: fetch(meta.url, { method: 'GET' }).then((response) => {
return response.text().then((icsText) => {
internalState.response = response;
return new IcalExpander({
ics: icsText,
skipInvalidDates: true,
});
});
}),
};
}
internalState.iCalExpanderPromise.then((iCalExpander) => {
successCallback({
rawEvents: expandICalEvents(iCalExpander, arg.range),
response: internalState.response,
});
}, errorCallback);
},
};
function expandICalEvents(iCalExpander, range) {
// expand the range. because our `range` is timeZone-agnostic UTC
// or maybe because ical.js always produces dates in local time? i forget
let rangeStart = addDays(range.start, -1);
let rangeEnd = addDays(range.end, 1);
let iCalRes = iCalExpander.between(rangeStart, rangeEnd); // end inclusive. will give extra results
let expanded = [];
// TODO: instead of using startDate/endDate.toString to communicate allDay,
// we can query startDate/endDate.isDate. More efficient to avoid formatting/reparsing.
// single events
for (let iCalEvent of iCalRes.events) {
expanded.push({
...buildNonDateProps(iCalEvent),
start: iCalEvent.startDate.toString(),
end: (specifiesEnd(iCalEvent) && iCalEvent.endDate)
? iCalEvent.endDate.toString()
: null,
});
}
// recurring event instances
for (let iCalOccurence of iCalRes.occurrences) {
let iCalEvent = iCalOccurence.item;
expanded.push({
...buildNonDateProps(iCalEvent),
start: iCalOccurence.startDate.toString(),
end: (specifiesEnd(iCalEvent) && iCalOccurence.endDate)
? iCalOccurence.endDate.toString()
: null,
});
}
return expanded;
}
function buildNonDateProps(iCalEvent) {
return {
title: iCalEvent.summary,
url: extractEventUrl(iCalEvent),
extendedProps: {
location: iCalEvent.location,
organizer: iCalEvent.organizer,
description: iCalEvent.description,
},
};
}
function extractEventUrl(iCalEvent) {
let urlProp = iCalEvent.component.getFirstProperty('url');
return urlProp ? urlProp.getFirstValue() : '';
}
function specifiesEnd(iCalEvent) {
return Boolean(iCalEvent.component.getFirstProperty('dtend')) ||
Boolean(iCalEvent.component.getFirstProperty('duration'));
}
var plugin = {
name: 'icalendar',
eventSourceDefs: [eventSourceDef],
};
FullCalendar.globalPlugins.push(plugin);
})(ICAL);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,469 @@
:root {
--fc-sticky-header-footer-z: 3;
--fc-popover-z: 4;
}
.fc-11 {
z-index: var(--fc-popover-z) !important;
}
.fc-HY {
isolation: isolate;
}
/*
This will prevent all ancestors from customizing their box size unless they use .contentBox
*/
.fc-jz,
.fc-jz *,
.fc-jz *:before,
.fc-jz *:after {
box-sizing: border-box !important;
}
/* classes attached to <body> */
.fc-j2,
.fc-j2 * {
cursor: not-allowed !important;
}
.fc-Ki {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
.fc-Ki::-webkit-scrollbar {
display: none !important;
}
.fc-ST {
flex-shrink: 0 !important;
}
.fc-Z9 .fc-1g {
display: flex !important;
flex-direction: row !important;
flex-wrap: wrap !important;
}
/* HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
https://stackoverflow.com/a/60256345 */
.fc-HC .fc-1g > * {
float: left !important;
}
[dir=rtl] .fc-HC .fc-1g > *,
.fc-HC[dir=rtl] .fc-1g > * {
float: right !important;
}
.fc-HC .fc-1g::after {
content: "" !important;
display: block !important;
clear: both !important;
}
.fc-ZB {
cursor: pointer !important;
}
.fc-Ld {
cursor: n-resize !important;
}
.fc-lJ {
cursor: s-resize !important;
}
.fc-Zm {
cursor: w-resize !important;
}
[dir=rtl] .fc-Zm {
cursor: e-resize !important;
}
.fc-n5 {
cursor: e-resize !important;
}
[dir=rtl] .fc-n5 {
cursor: w-resize !important;
}
.fc-Px {
cursor: col-resize !important;
}
.fc-QK,
.fc-eh,
.fc-M9 {
position: absolute !important;
box-sizing: content-box !important;
width: 100% !important;
height: 100% !important;
}
.fc-QK {
padding: 10px !important;
margin: -10px !important;
}
.fc-eh {
padding-left: 10px !important;
padding-right: 10px !important;
margin-left: -10px !important;
margin-right: -10px !important;
}
.fc-M9 {
padding-top: 10px !important;
padding-bottom: 10px !important;
margin-top: -10px !important;
margin-bottom: -10px !important;
}
/* QUESTION: why not use -left -right instead of negative margins/padding for ALL? */
.fc-SC {
position: absolute;
top: 0;
bottom: 0;
left: -5px;
right: -5px;
}
.fc-hi {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.fc-F0 {
visibility: hidden;
}
/* Border Utils */
/* ------------------------------------------------------------------------------------------------- */
/* TODO: break-out to x and y. don't have "only" all */
/* TODO: instead of "border" utils, call them "borderless" to subtract? */
.fc-6s {
border: 0 !important;
}
.fc-8B {
border-left: 0 !important;
border-right: 0 !important;
border-bottom: 0 !important;
}
.fc-RJ {
border-top: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
}
.fc-Hm,
.fc-ge {
border-top: 0 !important;
border-bottom: 0 !important;
}
.fc-Hm {
border-inline-end: 0 !important;
}
.fc-ge {
border-inline-start: 0 !important;
}
.fc-KC {
border-left: 0 !important;
border-right: 0 !important;
}
.fc-mE {
border-top: 0 !important;
border-bottom: 0 !important;
}
/* for matching cell start-border, assumed to be 1px, which can't be guaranteed */
.fc-FA {
border-inline-start: 1px solid transparent !important;
}
/* Flexbox Utils */
/* ------------------------------------------------------------------------------------------------- */
.fc-5g {
display: flex !important;
flex-direction: row !important;
}
.fc-Fa {
display: flex !important;
flex-direction: column !important;
}
.fc-16 {
flex-grow: 1 !important;
}
.fc-Im {
flex-grow: 1 !important;
flex-basis: 0 !important;
min-width: 0 !important;
min-height: 0 !important;
}
.fc-tZ {
min-height: 0 !important;
}
/*
TODO: use liquidX/Y elsewhere because frees up min other dimension; less collisions
*/
.fc-Pw {
flex-grow: 1 !important;
flex-basis: 0 !important;
min-width: 0 !important;
}
/* Print-Safe Utils (media:screen ONLY) */
/* ------------------------------------------------------------------------------------------------- */
.fc-Z9 .fc-kE,
.fc-Z9 .fc-Xg {
display: flex !important;
flex-direction: column !important;
}
/* Table Utils */
/* ------------------------------------------------------------------------------------------------- */
.fc-np {
padding: 0 !important;
}
.fc-gd {
margin: 0 !important;
}
.fc-hB {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.fc-uk {
margin-left: 0 !important;
margin-right: 0 !important;
}
.fc-he {
white-space: nowrap !important;
}
.fc-Bv {
white-space: pre !important;
}
/* Misc Utils */
/* ------------------------------------------------------------------------------------------------- */
.fc-U2 {
overflow-anchor: none;
}
.fc-bB {
overflow: hidden !important;
}
/*
TODO: eventually use this on daygrid/timegrid events' inner, time, and title, FOR PRINT
Needed to prevent wrapping to multiple lines, increasing height, throwing off print-positioning,
which can't rely on dynamic height detection after print-flag activated.
*/
.fc-pp {
white-space: nowrap !important;
overflow: hidden !important;
}
.fc-8A {
position: relative !important;
}
.fc-7t {
position: absolute !important;
}
.fc-pK {
inset-inline-start: 0 !important;
}
.fc-wP {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
}
.fc-yW,
.fc-5j {
position: absolute !important;
left: 0 !important;
right: 0 !important;
}
.fc-1K,
.fc-bH {
position: absolute !important;
top: 0 !important;
bottom: 0 !important;
}
.fc-yW {
top: 0 !important;
}
.fc-bH {
left: 0 !important;
right: 0 !important;
width: 0 !important;
}
@media not print {
.fc-dY {
position: sticky !important;
}
.fc-aX {
position: sticky !important;
top: 0 !important;
}
/* Sticks to either left or right (the flex "start") depending on ltr/rtl */
.fc-pZ {
position: sticky !important;
inset-inline-start: 0 !important;
}
.fc-Mw {
position: sticky !important;
top: 0 !important;
z-index: var(--fc-sticky-header-footer-z) !important;
}
}
/* Only needed when padding/border must be separate from width/height */
.fc-CU {
box-sizing: content-box !important;
}
.fc-jm {
position: absolute !important;
left: -10000px !important;
}
.fc-oO {
align-items: center !important;
}
.fc-MM {
align-items: flex-start !important;
}
.fc-Qk {
align-items: flex-end !important;
}
/* Footer Scrollbar */
/* ------------------------------------------------------------------------------------------------- */
.fc-Q6 {
position: sticky !important;
bottom: 0 !important;
z-index: var(--fc-sticky-header-footer-z) !important;
}
.fc-bD > * {
margin-top: -1px !important;
}
.fc-bD > * > * {
height: 1px !important;
}
/* Print-Safe Utils */
/* ------------------------------------------------------------------------------------------------- */
.fc-HC .fc-lF {
-moz-column-break-inside: avoid !important;
break-inside: avoid !important;
}
.fc-HC .fc-kE {
display: table !important;
table-layout: fixed !important;
width: 100% !important;
border-spacing: 0 !important;
border-collapse: separate !important;
}
.fc-HC .fc-Xg {
display: table-header-group !important;
-moz-column-break-inside: avoid !important;
break-inside: avoid !important;
background: #fff !important;
z-index: 9999 !important;
position: relative !important;
}
.fc-Vf {
/* min-height when multiple rows coexist, provides sane aspect-ratio for cells */
min-height: 6em !important;
}
/* Z-index */
/* ------------------------------------------------------------------------------------------------- */
.fc-BP {
z-index: 0;
}
.fc-XV {
z-index: 1;
}
.fc-Ah:focus-visible {
z-index: 2;
}
/* INTERNAL MARKER -- used to mark an element for internal significance */
/* ------------------------------------------------------------------------------------------------- */
.fc-vz {}
.fc-4B {}
.fc-D7 {}
.fc-wp {}
.fc-Ex {}
.fc-wl {}
.fc-yM {}
.fc-2f {}
.fc-zR {}
.fc-0j {}
.fc-M7 {}
.fc-tb {}
.fc-ew {}
.fc-IZ {}
.fc-UH {}

View File

@ -0,0 +1,387 @@
/*!
FullCalendar (Vanilla JS) v7.0.1
Docs & License: https://fullcalendar.io
(c) 2026 Adam Shaw
*/
(function ({ H: joinClassNames, u, S, G: globalPlugins }) {
// usually 11px font / 12px line-height
const xxsTextClass = "fc-breezy-vQz";
// outline
const outlineWidthClass = "fc-breezy-0Bj";
const outlineWidthFocusClass = "fc-breezy-uqo";
const outlineWidthGroupFocusClass = "fc-breezy-nPR";
const outlineOffsetClass = "fc-breezy-3Xj";
const primaryOutlineColorClass = "fc-breezy-yg7";
const primaryOutlineFocusClass = `${outlineWidthFocusClass} ${primaryOutlineColorClass}`;
// neutral buttons
const strongSolidPressableClass = joinClassNames("fc-breezy-hJa", "fc-breezy-AT7", "fc-breezy-b3B");
const mutedHoverClass = "fc-breezy-qiG";
const mutedHoverPressableClass = `${mutedHoverClass} fc-breezy-WfX`;
const faintHoverClass = "fc-breezy-1EL";
const faintHoverPressableClass = `${faintHoverClass} fc-breezy-qWF fc-breezy-UTk`;
// controls
const selectedClass = `fc-breezy-GfU fc-breezy-Oiq ${primaryOutlineFocusClass}`;
const unselectedClass = `fc-breezy-t4l fc-breezy-hnE ${primaryOutlineFocusClass}`;
// primary
const primaryClass = "fc-breezy-Anp fc-breezy-ECg";
const primaryPressableClass = `${primaryClass} fc-breezy-8mf`;
const primaryPressableGroupClass = `${primaryClass} fc-breezy-t8l`;
const primaryButtonClass = `${primaryPressableClass} fc-breezy-d0j ${primaryOutlineFocusClass} ${outlineOffsetClass}`;
// secondary
const secondaryClass = "fc-breezy-nyY fc-breezy-NK3";
const secondaryPressableClass = `${secondaryClass} fc-breezy-StK`;
const secondaryButtonClass = `${secondaryPressableClass} fc-breezy-nuP ${primaryOutlineFocusClass} fc-breezy-07j`;
const secondaryButtonIconClass = "fc-breezy-XUJ fc-breezy-J7Y fc-breezy-b7A fc-breezy-tRE";
// event content
const eventMutedFgClass = "fc-breezy-WLU";
const eventFaintBgClass = "fc-breezy-GjO";
const eventFaintPressableClass = joinClassNames(eventFaintBgClass, "fc-breezy-yE8", "fc-breezy-x2T");
// interactive neutral foregrounds
const mutedFgPressableGroupClass = "fc-breezy-t4l fc-breezy-Wxh fc-breezy-x7E";
// transparent resizer for mouse
const blockPointerResizerClass = "fc-breezy-1EY fc-breezy-pps fc-breezy-vs6";
const rowPointerResizerClass = `${blockPointerResizerClass} fc-breezy-AWB fc-breezy-hza`;
const columnPointerResizerClass = `${blockPointerResizerClass} fc-breezy-MaV fc-breezy-uuA`;
// circle resizer for touch
const blockTouchResizerClass = "fc-breezy-1EY fc-breezy-3wQ fc-breezy-wsy fc-breezy-lNM fc-breezy-gmc fc-breezy-AAA";
const rowTouchResizerClass = `${blockTouchResizerClass} fc-breezy-ERR fc-breezy-Dq8`;
const columnTouchResizerClass = `${blockTouchResizerClass} fc-breezy-1V6 fc-breezy-F99`;
const getNormalDayHeaderBorderClass = (info) => joinClassNames(!info.inPopover && (info.isMajor ? "fc-breezy-wsy fc-breezy-OFc" :
!info.isNarrow && "fc-breezy-wsy fc-breezy-EAo"));
const getMutedDayHeaderBorderClass = (info) => joinClassNames(!info.inPopover && (info.isMajor ? "fc-breezy-wsy fc-breezy-OFc" :
!info.isNarrow && "fc-breezy-wsy fc-breezy-tTN"));
const getNormalDayCellBorderColorClass = (info) => (info.isMajor ? "fc-breezy-OFc" : "fc-breezy-EAo");
const getMutedDayCellBorderColorClass = (info) => (info.isMajor ? "fc-breezy-OFc" : "fc-breezy-tTN");
const tallDayCellBottomClass = "fc-breezy-mhE";
const getShortDayCellBottomClass = (info) => joinClassNames(!info.isNarrow && "fc-breezy-toR");
const mutedHoverButtonClass = joinClassNames(mutedHoverPressableClass, outlineWidthFocusClass, primaryOutlineColorClass);
const dayRowCommonClasses = {
/* Day Row > List-Item Event
----------------------------------------------------------------------------------------------- */
listItemEventClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-7A6", info.isNarrow
? "fc-breezy-148 fc-breezy-Fvv"
: "fc-breezy-rVY fc-breezy-KzJ", info.isSelected
? "fc-breezy-pZQ"
: info.isInteractive
? mutedHoverPressableClass
: mutedHoverClass),
listItemEventInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-N2M", info.isNarrow
? `fc-breezy-z5u ${xxsTextClass}`
: "fc-breezy-2rx fc-breezy-a3B"),
listItemEventTimeClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-F1o" : "fc-breezy-oQ2", "fc-breezy-t4l fc-breezy-NPw fc-breezy-TZ4 fc-breezy-pKG fc-breezy-1Zl"),
listItemEventTitleClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-F1o" : "fc-breezy-oQ2", "fc-breezy-Oiq fc-breezy-1OT fc-breezy-TZ4 fc-breezy-pKG fc-breezy-OLq", info.timeText && "fc-breezy-IPx"),
/* Day Row > Row Event
----------------------------------------------------------------------------------------------- */
rowEventClass: (info) => joinClassNames(info.isStart && (info.isNarrow ? "fc-breezy-Jzj" : "fc-breezy-Wga"), info.isEnd && (info.isNarrow ? "fc-breezy-3e1" : "fc-breezy-KYn")),
rowEventInnerClass: (info) => info.isNarrow ? "fc-breezy-z5u" : "fc-breezy-2rx",
/* Day Row > More-Link
----------------------------------------------------------------------------------------------- */
rowMoreLinkClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-wsy", info.isNarrow
? "fc-breezy-148 fc-breezy-UIT fc-breezy-Fvv"
: "fc-breezy-sI7 fc-breezy-rVY fc-breezy-d0j fc-breezy-KzJ", mutedHoverPressableClass),
rowMoreLinkInnerClass: (info) => joinClassNames(info.isNarrow
? `fc-breezy-7A6 ${xxsTextClass}`
: "fc-breezy-KUX fc-breezy-a3B", "fc-breezy-Oiq"),
};
var index = {
name: "theme-breezy",
optionDefaults: {
className: (info) => joinClassNames("fc-breezy-gmc fc-breezy-n5m", !(info.borderlessTop || info.borderlessBottom || info.borderlessX) && "fc-breezy-hny"),
viewClass: (info) => {
const hasBorderTop = !info.options.headerToolbar && !info.borderlessTop;
const hasBorderBottom = !info.options.footerToolbar && !info.borderlessBottom;
const hasBorderX = !info.borderlessX;
return joinClassNames("fc-breezy-EAo", hasBorderTop && "fc-breezy-ku3", hasBorderBottom && "fc-breezy-zi1", hasBorderX && "fc-breezy-1Wx", (hasBorderTop && hasBorderX) && "fc-breezy-wko", (hasBorderBottom && hasBorderX) && "fc-breezy-L2o", !info.isHeightAuto && "fc-breezy-pKG");
},
/* Toolbar
--------------------------------------------------------------------------------------------- */
toolbarClass: (info) => joinClassNames("fc-breezy-KRz fc-breezy-OEz fc-breezy-nYK fc-breezy-dl1 fc-breezy-1sP fc-breezy-dNl fc-breezy-XpK fc-breezy-N2M fc-breezy-Pms fc-breezy-pKG fc-breezy-EAo", !info.borderlessX && "fc-breezy-1Wx"),
headerToolbarClass: (info) => joinClassNames("fc-breezy-zi1", !info.borderlessTop && "fc-breezy-ku3", !(info.borderlessTop || info.borderlessX) && "fc-breezy-wko"),
footerToolbarClass: (info) => joinClassNames("fc-breezy-ku3", !info.borderlessBottom && "fc-breezy-zi1", !(info.borderlessBottom || info.borderlessX) && "fc-breezy-L2o"),
toolbarSectionClass: "fc-breezy-yi0 fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-Pms",
toolbarTitleClass: "fc-breezy-9ZS fc-breezy-C8a fc-breezy-Oiq",
buttonGroupClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", !info.hasSelection && "fc-breezy-KzJ fc-breezy-eSM"),
buttonClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-dl6 fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-9yp fc-breezy-Z9U", info.isIconOnly ? "fc-breezy-Nca" : "fc-breezy-Apf", info.buttonGroup?.hasSelection ? joinClassNames("fc-breezy-KzJ fc-breezy-1OT", info.isSelected
? selectedClass
: unselectedClass) : joinClassNames("fc-breezy-C8a", info.isPrimary
? primaryButtonClass
: secondaryButtonClass, info.buttonGroup
? "fc-breezy-Ps8 fc-breezy-H1W fc-breezy-g3A fc-breezy-7ss fc-breezy-JIC"
: "fc-breezy-KzJ fc-breezy-eSM fc-breezy-wsy")),
buttons: {
prev: {
iconContent: () => chevronDown(joinClassNames(secondaryButtonIconClass, "fc-breezy-z44 fc-breezy-keW")),
},
next: {
iconContent: () => chevronDown(joinClassNames(secondaryButtonIconClass, "fc-breezy-KxI fc-breezy-ZW3")),
},
prevYear: {
iconContent: () => chevronDoubleLeft(joinClassNames(secondaryButtonIconClass, "fc-breezy-asP"))
},
nextYear: {
iconContent: () => chevronDoubleLeft(joinClassNames(secondaryButtonIconClass, "fc-breezy-jmT fc-breezy-jY6"))
},
},
/* Abstract Event
--------------------------------------------------------------------------------------------- */
eventShortHeight: 50,
eventColor: "var(--fc-breezy-event)",
eventContrastColor: "var(--fc-breezy-event-contrast)",
eventClass: (info) => joinClassNames(info.isDragging && "fc-breezy-n5m", info.event.url && "fc-breezy-JiE", info.isSelected
? joinClassNames(outlineWidthClass, info.isDragging && "fc-breezy-tkw")
: outlineWidthFocusClass, primaryOutlineColorClass),
/* Background Event
--------------------------------------------------------------------------------------------- */
backgroundEventColor: "var(--fc-breezy-background-event)",
backgroundEventClass: "fc-breezy-aPk fc-breezy-jsy fc-breezy-DO7",
backgroundEventTitleClass: (info) => joinClassNames("fc-breezy-lMo fc-breezy-L1Y", info.isNarrow
? `fc-breezy-iS4 ${xxsTextClass}`
: "fc-breezy-3N5 fc-breezy-a3B", "fc-breezy-sI1"),
/* Block Event
--------------------------------------------------------------------------------------------- */
blockEventClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-eYX fc-breezy-vwH fc-breezy-d0j fc-breezy-DO7", info.isInteractive ? eventFaintPressableClass : eventFaintBgClass, (info.isDragging && !info.isSelected) && "fc-breezy-iTG"),
blockEventInnerClass: eventMutedFgClass,
blockEventTimeClass: "fc-breezy-TZ4 fc-breezy-pKG fc-breezy-1Zl",
blockEventTitleClass: "fc-breezy-TZ4 fc-breezy-pKG fc-breezy-OLq",
/* Row Event
--------------------------------------------------------------------------------------------- */
rowEventClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-JIC", info.isStart && joinClassNames("fc-breezy-3J4", info.isNarrow ? "fc-breezy-kmj" : "fc-breezy-QUg"), info.isEnd && joinClassNames("fc-breezy-USt", info.isNarrow ? "fc-breezy-Skl" : "fc-breezy-RNO")),
rowEventBeforeClass: (info) => joinClassNames(info.isStartResizable && joinClassNames(info.isSelected ? rowTouchResizerClass : rowPointerResizerClass, "fc-breezy-11a")),
rowEventAfterClass: (info) => joinClassNames(info.isEndResizable && joinClassNames(info.isSelected ? rowTouchResizerClass : rowPointerResizerClass, "fc-breezy-Tuc")),
rowEventInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", info.isNarrow ? xxsTextClass : "fc-breezy-a3B"),
rowEventTimeClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-a7i" : "fc-breezy-C2j", "fc-breezy-1OT"),
rowEventTitleClass: (info) => (info.isNarrow ? "fc-breezy-oQ2" : "fc-breezy-aCI"),
/* Column Event
--------------------------------------------------------------------------------------------- */
columnEventClass: (info) => joinClassNames("fc-breezy-1Wx fc-breezy-A3h fc-breezy-9Iz", info.isStart && joinClassNames("fc-breezy-ku3 fc-breezy-wko", info.isNarrow ? "fc-breezy-sEX" : "fc-breezy-zZM"), info.isEnd && joinClassNames("fc-breezy-zi1 fc-breezy-L2o", info.isNarrow ? "fc-breezy-Ika" : "fc-breezy-vUo")),
columnEventBeforeClass: (info) => joinClassNames(info.isStartResizable && joinClassNames(info.isSelected ? columnTouchResizerClass : columnPointerResizerClass, "fc-breezy-YDC")),
columnEventAfterClass: (info) => joinClassNames(info.isEndResizable && joinClassNames(info.isSelected ? columnTouchResizerClass : columnPointerResizerClass, "fc-breezy-fJL")),
columnEventInnerClass: (info) => joinClassNames("fc-breezy-dl1", info.isShort
? "fc-breezy-1sP fc-breezy-XpK fc-breezy-NWN fc-breezy-iS4"
: joinClassNames("fc-breezy-sgX", info.isNarrow ? "fc-breezy-aCI fc-breezy-2rx" : "fc-breezy-Nca fc-breezy-Jhn"), (info.isShort || info.isNarrow) ? xxsTextClass : "fc-breezy-a3B"),
columnEventTimeClass: (info) => (!info.isShort && (info.isNarrow ? "fc-breezy-166" : "fc-breezy-4dx")),
columnEventTitleClass: (info) => joinClassNames(!info.isShort && (info.isNarrow ? "fc-breezy-2rx" : "fc-breezy-Jhn"), "fc-breezy-C8a"),
/* More-Link
--------------------------------------------------------------------------------------------- */
moreLinkClass: `${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
moreLinkInnerClass: "fc-breezy-TZ4 fc-breezy-pKG",
columnMoreLinkClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-SEP" : "fc-breezy-V9v", `fc-breezy-wsy fc-breezy-d0j fc-breezy-4MR fc-breezy-KzJ ${strongSolidPressableClass} fc-breezy-vwH fc-breezy-A3h fc-breezy-9Iz`),
columnMoreLinkInnerClass: (info) => joinClassNames(info.isNarrow
? `fc-breezy-KUX ${xxsTextClass}`
: "fc-breezy-iS4 fc-breezy-a3B", "fc-breezy-sI1"),
/* Day Header
--------------------------------------------------------------------------------------------- */
dayHeaderAlign: (info) => info.inPopover ? "start" : "center",
dayHeaderClass: (info) => joinClassNames("fc-breezy-E9P", info.inPopover && "fc-breezy-zi1 fc-breezy-EAo fc-breezy-nYK"),
dayHeaderInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", (!info.dayNumberText && !info.inPopover)
? joinClassNames("fc-breezy-Jhn fc-breezy-Fvv fc-breezy-a3B", info.isNarrow
? "fc-breezy-aCI fc-breezy-gMS fc-breezy-t4l"
: "fc-breezy-ZrE fc-breezy-bvX fc-breezy-C8a fc-breezy-sI1", info.hasNavLink && mutedHoverButtonClass)
: (info.isToday && info.dayNumberText && !info.inPopover)
? joinClassNames("fc-breezy-bCs fc-breezy-bvX fc-breezy-hS8", info.isNarrow ? "fc-breezy-TFV" : "fc-breezy-I1A")
: joinClassNames("fc-breezy-Fvv", info.inPopover
? "fc-breezy-bvX fc-breezy-aCI fc-breezy-2rx"
: joinClassNames("fc-breezy-fn8 fc-breezy-TFV fc-breezy-ZrE", info.isNarrow ? "fc-breezy-2tF" : "fc-breezy-X6C"), info.hasNavLink && mutedHoverButtonClass)),
dayHeaderContent: (info) => ((!info.dayNumberText && !info.inPopover) ? (u(S, { children: info.text })) : (u(S, { children: info.textParts.map((textPart, i) => (u("span", { className: joinClassNames("fc-breezy-jm6", info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp", textPart.type === "day"
? joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", !info.isNarrow && "fc-breezy-C8a", (info.isToday && !info.inPopover)
? joinClassNames("fc-breezy-cKZ fc-breezy-AAA fc-breezy-E9P", info.isNarrow ? "fc-breezy-MSG" : "fc-breezy-n6w", info.hasNavLink
? `${primaryPressableGroupClass} ${outlineWidthGroupFocusClass} ${outlineOffsetClass} ${primaryOutlineColorClass}`
: primaryClass)
: "fc-breezy-Oiq")
: "fc-breezy-t4l"), children: textPart.value }, i))) }))),
/* Day Cell
--------------------------------------------------------------------------------------------- */
dayCellClass: (info) => joinClassNames("fc-breezy-wsy", ((info.isOther || info.isDisabled) && !info.options.businessHours) && "fc-breezy-nYK"),
dayCellTopClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-84e" : "fc-breezy-p7s", "fc-breezy-dl1 fc-breezy-1sP"),
dayCellTopInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-E9P fc-breezy-TZ4", info.isNarrow
? `fc-breezy-SEP fc-breezy-oM6 ${xxsTextClass}`
: "fc-breezy-V9v fc-breezy-TFV fc-breezy-a3B", info.isToday
? joinClassNames("fc-breezy-AAA fc-breezy-C8a", info.isNarrow ? "fc-breezy-qvL" : "fc-breezy-Wga", info.text === info.dayNumberText
? (info.isNarrow ? "fc-breezy-79F" : "fc-breezy-ilz")
: (info.isNarrow ? "fc-breezy-aCI" : "fc-breezy-Nca"), info.hasNavLink
? `${primaryPressableClass} ${outlineOffsetClass}`
: primaryClass)
: joinClassNames("fc-breezy-Skl", info.isNarrow ? "fc-breezy-aCI" : "fc-breezy-Nca", info.hasNavLink && mutedHoverPressableClass, info.isOther
? "fc-breezy-mNS"
: (info.monthText ? "fc-breezy-sI1" : "fc-breezy-t4l"), info.monthText && "fc-breezy-DIS")),
dayCellInnerClass: (info) => joinClassNames(info.inPopover && "fc-breezy-3N5"),
/* Popover
--------------------------------------------------------------------------------------------- */
popoverClass: "fc-breezy-uhF fc-breezy-wsy fc-breezy-LDJ fc-breezy-hny fc-breezy-pKG fc-breezy-1kP fc-breezy-gMS fc-breezy-aNc fc-breezy-n5m",
popoverCloseClass: `fc-breezy-bCs fc-breezy-1EY fc-breezy-SKv fc-breezy-aYN fc-breezy-KUX fc-breezy-Fvv ${mutedHoverButtonClass} fc-breezy-Z9U`,
popoverCloseContent: () => x(`fc-breezy-XUJ ${mutedFgPressableGroupClass}`),
/* Lane
--------------------------------------------------------------------------------------------- */
dayLaneClass: (info) => joinClassNames("fc-breezy-wsy", info.isMajor ? "fc-breezy-OFc" : "fc-breezy-tTN", info.isDisabled && "fc-breezy-nYK"),
dayLaneInnerClass: (info) => (info.isStack
? "fc-breezy-gMS"
: info.isNarrow ? "fc-breezy-148" : "fc-breezy-rVY"),
slotLaneClass: (info) => joinClassNames("fc-breezy-wsy fc-breezy-tTN", info.isMinor && "fc-breezy-TN2"),
/* List Day
--------------------------------------------------------------------------------------------- */
listDaysClass: "fc-breezy-8T7 fc-breezy-g0K fc-breezy-6u7 fc-breezy-E7b fc-breezy-KRz",
listDayClass: (info) => joinClassNames(!info.isLast && "fc-breezy-zi1 fc-breezy-tTN", "fc-breezy-dl1 fc-breezy-1sP fc-breezy-EF4 fc-breezy-tgZ"),
listDayHeaderClass: "fc-breezy-SEP fc-breezy-yi0 fc-breezy-vVE fc-breezy-kMV fc-breezy-MKw fc-breezy-dl1 fc-breezy-sgX fc-breezy-EF4",
listDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-cJ3 fc-breezy-2rx fc-breezy-Nca fc-breezy-PtF fc-breezy-AAA fc-breezy-9yp", !info.level
? joinClassNames(info.isToday
? joinClassNames("fc-breezy-C8a", info.hasNavLink ? primaryPressableClass : primaryClass)
: joinClassNames("fc-breezy-1OT fc-breezy-Oiq", info.hasNavLink && mutedHoverPressableClass))
: joinClassNames("fc-breezy-mNS", info.hasNavLink && `${mutedHoverPressableClass} fc-breezy-i3P`)),
listDayBodyClass: "fc-breezy-9Qs fc-breezy-1El fc-breezy-2KU fc-breezy-wsy fc-breezy-EAo fc-breezy-KzJ",
/* Single Month (in Multi-Month)
--------------------------------------------------------------------------------------------- */
singleMonthClass: (info) => joinClassNames(info.multiMonthColumns > 1 && "fc-breezy-jD5", (info.multiMonthColumns === 1 && !info.isLast) && "fc-breezy-zi1 fc-breezy-EAo"),
singleMonthHeaderClass: (info) => joinClassNames(info.multiMonthColumns > 1
? "fc-breezy-x96"
: "fc-breezy-End fc-breezy-gmc fc-breezy-zi1 fc-breezy-EAo", "fc-breezy-XpK"),
singleMonthHeaderInnerClass: (info) => joinClassNames("fc-breezy-Jhn fc-breezy-Nca fc-breezy-KzJ fc-breezy-9yp fc-breezy-Oiq fc-breezy-C8a", info.hasNavLink && mutedHoverPressableClass),
/* Misc Table
--------------------------------------------------------------------------------------------- */
tableHeaderClass: "fc-breezy-gmc",
fillerClass: "fc-breezy-wsy fc-breezy-tTN",
dayNarrowWidth: 100,
dayHeaderRowClass: "fc-breezy-wsy fc-breezy-tTN",
dayRowClass: "fc-breezy-wsy fc-breezy-EAo",
slotHeaderRowClass: "fc-breezy-wsy fc-breezy-EAo",
slotHeaderInnerClass: "fc-breezy-mNS fc-breezy-XHd",
/* Misc Content
--------------------------------------------------------------------------------------------- */
navLinkClass: `${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
inlineWeekNumberClass: (info) => joinClassNames("fc-breezy-1EY fc-breezy-n9G fc-breezy-iD1 fc-breezy-gmc fc-breezy-t4l fc-breezy-TZ4 fc-breezy-4In fc-breezy-zi1 fc-breezy-k8g fc-breezy-3J4 fc-breezy-Hhp", info.isNarrow
? `fc-breezy-KUX ${xxsTextClass}`
: "fc-breezy-XJa fc-breezy-a3B", info.hasNavLink
? `${mutedHoverPressableClass} fc-breezy-07j`
: mutedHoverClass),
highlightClass: "fc-breezy-xAy",
nonBusinessHoursClass: "fc-breezy-nYK",
nowIndicatorLineClass: "fc-breezy-CH7 fc-breezy-qQW fc-breezy-D9l",
nowIndicatorDotClass: "fc-breezy-aAW fc-breezy-Vpk fc-breezy-D9l fc-breezy-63n fc-breezy-AAA fc-breezy-GBJ fc-breezy-9Iz",
/* Resource Day Header
--------------------------------------------------------------------------------------------- */
resourceDayHeaderAlign: "center",
resourceDayHeaderClass: "fc-breezy-wsy",
resourceDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-bvX fc-breezy-sI1 fc-breezy-C8a", info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp"),
/* Resource Data Grid
--------------------------------------------------------------------------------------------- */
resourceColumnHeaderClass: "fc-breezy-wsy fc-breezy-tTN fc-breezy-E9P",
resourceColumnHeaderInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
resourceColumnResizerClass: "fc-breezy-1EY fc-breezy-AWB fc-breezy-4Tv fc-breezy-dnf",
resourceGroupHeaderClass: "fc-breezy-wsy fc-breezy-EAo fc-breezy-pZQ",
resourceGroupHeaderInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
resourceCellClass: "fc-breezy-wsy fc-breezy-tTN",
resourceCellInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
resourceIndentClass: "fc-breezy-Wga fc-breezy-p9t fc-breezy-E9P",
resourceExpanderClass: `fc-breezy-bCs fc-breezy-KUX fc-breezy-AAA ${mutedHoverPressableClass} ${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
resourceExpanderContent: (info) => chevronDown(joinClassNames(`fc-breezy-XUJ ${mutedFgPressableGroupClass}`, !info.isExpanded && "fc-breezy-KxI fc-breezy-ZW3")),
resourceHeaderRowClass: "fc-breezy-wsy fc-breezy-EAo",
resourceRowClass: "fc-breezy-wsy fc-breezy-EAo",
resourceColumnDividerClass: "fc-breezy-USt fc-breezy-OFc",
/* Timeline Lane
--------------------------------------------------------------------------------------------- */
resourceGroupLaneClass: "fc-breezy-wsy fc-breezy-EAo fc-breezy-pZQ",
resourceLaneClass: "fc-breezy-wsy fc-breezy-EAo",
resourceLaneBottomClass: (info) => joinClassNames(info.options.eventOverlap && "fc-breezy-uuA"),
timelineBottomClass: "fc-breezy-uuA",
},
views: {
dayGrid: {
...dayRowCommonClasses,
dayHeaderClass: getNormalDayHeaderBorderClass,
dayHeaderDividerClass: "fc-breezy-zi1 fc-breezy-OFc",
dayCellClass: getNormalDayCellBorderColorClass,
dayCellBottomClass: getShortDayCellBottomClass,
backgroundEventInnerClass: "fc-breezy-dl1 fc-breezy-1sP fc-breezy-LMv",
},
multiMonth: {
...dayRowCommonClasses,
dayHeaderClass: getNormalDayHeaderBorderClass,
dayHeaderDividerClass: (info) => joinClassNames(info.multiMonthColumns === 1 && "fc-breezy-zi1 fc-breezy-OFc fc-breezy-qNs"),
dayCellClass: getNormalDayCellBorderColorClass,
dayCellBottomClass: getShortDayCellBottomClass,
tableBodyClass: (info) => joinClassNames(info.multiMonthColumns > 1 && "fc-breezy-wsy fc-breezy-EAo fc-breezy-KzJ fc-breezy-eSM fc-breezy-pKG"),
},
timeGrid: {
...dayRowCommonClasses,
dayHeaderClass: getMutedDayHeaderBorderClass,
dayHeaderDividerClass: (info) => joinClassNames("fc-breezy-zi1", info.options.allDaySlot
? "fc-breezy-EAo"
: "fc-breezy-OFc fc-breezy-rf6"),
dayCellClass: getMutedDayCellBorderColorClass,
dayCellBottomClass: tallDayCellBottomClass,
/* TimeGrid > Week Number Header
------------------------------------------------------------------------------------------- */
weekNumberHeaderClass: "fc-breezy-XpK fc-breezy-LMv",
weekNumberHeaderInnerClass: (info) => joinClassNames("fc-breezy-cGD fc-breezy-TFV fc-breezy-ZrE fc-breezy-t4l fc-breezy-Fvv fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", info.hasNavLink && mutedHoverPressableClass, info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp"),
/* TimeGrid > All-Day Header
------------------------------------------------------------------------------------------- */
allDayHeaderClass: "fc-breezy-XpK",
allDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-rUb fc-breezy-mNS", info.isNarrow ? xxsTextClass : "fc-breezy-a3B"),
allDayDividerClass: "fc-breezy-zi1 fc-breezy-OFc fc-breezy-rf6",
/* TimeGrid > Slot Header
------------------------------------------------------------------------------------------- */
slotHeaderClass: "fc-breezy-LMv",
slotHeaderInnerClass: (info) => joinClassNames("fc-breezy-eYX fc-breezy-GFf fc-breezy-2tF", info.isNarrow
? `fc-breezy-Cy2 ${xxsTextClass}`
: "fc-breezy-uqG fc-breezy-a3B", info.isFirst && "fc-breezy-pps"),
slotHeaderDividerClass: "fc-breezy-USt fc-breezy-tTN",
},
list: {
/* List-View > List-Item Event
------------------------------------------------------------------------------------------- */
listItemEventClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-lqx fc-breezy-XpK fc-breezy-wwb", !info.isLast && "fc-breezy-zi1 fc-breezy-tTN", info.isInteractive
? faintHoverPressableClass
: faintHoverClass),
listItemEventBeforeClass: "fc-breezy-5JF fc-breezy-lNM fc-breezy-AAA",
listItemEventInnerClass: "fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-wwb fc-breezy-9yp",
listItemEventTimeClass: "fc-breezy-yi0 fc-breezy-roZ fc-breezy-kMV fc-breezy-TZ4 fc-breezy-pKG fc-breezy-IPx fc-breezy-t4l",
listItemEventTitleClass: (info) => joinClassNames("fc-breezy-1El fc-breezy-2KU fc-breezy-1OT fc-breezy-TZ4 fc-breezy-pKG fc-breezy-sI1", info.event.url && "fc-breezy-Ogp"),
/* No-Events Screen
------------------------------------------------------------------------------------------- */
noEventsClass: "fc-breezy-1El fc-breezy-dl1 fc-breezy-sgX fc-breezy-XpK fc-breezy-E9P",
noEventsInnerClass: "fc-breezy-P9h fc-breezy-t4l",
},
resourceDayGrid: {
resourceDayHeaderClass: (info) => (info.isMajor
? "fc-breezy-OFc"
: "fc-breezy-EAo"),
},
resourceTimeGrid: {
resourceDayHeaderClass: (info) => (info.isMajor
? "fc-breezy-OFc"
: "fc-breezy-tTN"),
},
timeline: {
/* Timeline > Row Event
------------------------------------------------------------------------------------------- */
rowEventClass: (info) => info.isEnd && "fc-breezy-9hC",
rowEventInnerClass: (info) => info.options.eventOverlap ? "fc-breezy-Jhn" : "fc-breezy-dl6",
/* Timeline > More-Link
------------------------------------------------------------------------------------------- */
rowMoreLinkClass: `fc-breezy-9hC fc-breezy-Ika fc-breezy-wsy fc-breezy-d0j fc-breezy-4MR fc-breezy-KzJ ${strongSolidPressableClass} fc-breezy-vwH`,
rowMoreLinkInnerClass: "fc-breezy-iS4 fc-breezy-sI1 fc-breezy-a3B",
/* Timeline > Slot Header
------------------------------------------------------------------------------------------- */
slotHeaderAlign: (info) => info.isTime ? "start" : "center",
slotHeaderClass: (info) => joinClassNames(info.level > 0 && "fc-breezy-wsy fc-breezy-tTN", "fc-breezy-LMv"),
slotHeaderInnerClass: (info) => joinClassNames("fc-breezy-GFf fc-breezy-2tF fc-breezy-a3B", info.isTime && joinClassNames("fc-breezy-eYX fc-breezy-OAt", info.isFirst && "fc-breezy-pps"), info.hasNavLink && "fc-breezy-Eu0"),
slotHeaderDividerClass: "fc-breezy-zi1 fc-breezy-OFc fc-breezy-qNs",
},
},
};
/* SVGs
------------------------------------------------------------------------------------------------- */
function chevronDown(className) {
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { fillRule: "evenodd", d: "M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z", clipRule: "evenodd" }) });
}
function chevronDoubleLeft(className) {
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { fillRule: "evenodd", d: "M4.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L6.31 10l3.72-3.72a.75.75 0 1 0-1.06-1.06L4.72 9.47Zm9.25-4.25L9.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L11.31 10l3.72-3.72a.75.75 0 0 0-1.06-1.06Z", clipRule: "evenodd" }) });
}
function x(className) {
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { d: "M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" }) });
}
globalPlugins.push(index);
})(FullCalendar.Shared);

View File

@ -0,0 +1,132 @@
:root {
/* primary */
--fc-breezy-primary: #AD1F1F;
--fc-breezy-primary-over: #AD1F1F;
--fc-breezy-primary-foreground: #ffffff;
/* secondary */
--fc-breezy-secondary: var(--fc-breezy-background);
--fc-breezy-secondary-over: var(--fc-breezy-faint);
--fc-breezy-secondary-border: var(--fc-breezy-strong-border);
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
--fc-breezy-secondary-icon: var(--fc-breezy-faint-foreground);
--fc-breezy-secondary-icon-over: var(--fc-breezy-muted-foreground);
/* calendar content */
--fc-breezy-event: #1FAD95;
--fc-breezy-event-contrast: #fff;
--fc-breezy-background-event: #1F95AD;
--fc-breezy-highlight: #ef444414;
--fc-breezy-now: #ef4444;
/* controls */
--fc-breezy-selected: var(--fc-breezy-strong);
/* popover */
--fc-breezy-popover: var(--fc-breezy-background);
--fc-breezy-popover-border: var(--fc-breezy-strong-border);
/* neutral backgrounds */
--fc-breezy-background: #ffffff;
--fc-breezy-faint: #00000005;
--fc-breezy-muted: #0000000D;
--fc-breezy-strong: #0000001A;
--fc-breezy-stronger: #00000024;
--fc-breezy-strongest: #0000002E;
/* neutral foregrounds */
--fc-breezy-foreground: #374151;
--fc-breezy-faint-foreground: #9ca3af;
--fc-breezy-muted-foreground: #6b7280;
--fc-breezy-strong-foreground: #111827;
/* neutral borders */
--fc-breezy-border: #e5e7eb;
--fc-breezy-muted-border: #f3f4f6;
--fc-breezy-strong-border: #d1d5db;
}
@media not print {
:root.skin-theme-clientpref-night {
/* secondary */
--fc-breezy-secondary: var(--fc-breezy-muted);
--fc-breezy-secondary-over: var(--fc-breezy-strong);
--fc-breezy-secondary-border: var(--fc-breezy-muted-border);
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
--fc-breezy-secondary-icon: var(--fc-breezy-muted-foreground);
--fc-breezy-secondary-icon-over: var(--fc-breezy-foreground);
/* calendar content */
--fc-breezy-highlight: #f8717114;
/* controls */
--fc-breezy-selected: var(--fc-breezy-muted);
/* popovers */
--fc-breezy-popover: #1f2937;
--fc-breezy-popover-border: var(--fc-breezy-border);
/* neutral backgrounds */
--fc-breezy-background: #111827;
--fc-breezy-faint: #ffffff08;
--fc-breezy-muted: #ffffff12;
--fc-breezy-strong: #ffffff1F;
--fc-breezy-stronger: #ffffff29;
--fc-breezy-strongest: #ffffff33;
/* neutral foregrounds */
--fc-breezy-foreground: #d1d5db;
--fc-breezy-muted-foreground: #9ca3af;
--fc-breezy-faint-foreground: #6b7280;
--fc-breezy-strong-foreground: #ffffff;
/* neutral borders */
--fc-breezy-border: #ffffff1A;
--fc-breezy-muted-border: #ffffff0D;
--fc-breezy-strong-border: #ffffff26;
}
}
@media not print and (prefers-color-scheme:dark){
/* Copie du thème night */
:root.skin-theme-clientpref-os {
/* secondary */
--fc-breezy-secondary: var(--fc-breezy-muted);
--fc-breezy-secondary-over: var(--fc-breezy-strong);
--fc-breezy-secondary-border: var(--fc-breezy-muted-border);
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
--fc-breezy-secondary-icon: var(--fc-breezy-muted-foreground);
--fc-breezy-secondary-icon-over: var(--fc-breezy-foreground);
/* calendar content */
--fc-breezy-highlight: #f8717114;
/* controls */
--fc-breezy-selected: var(--fc-breezy-muted);
/* popovers */
--fc-breezy-popover: #1f2937;
--fc-breezy-popover-border: var(--fc-breezy-border);
/* neutral backgrounds */
--fc-breezy-background: #111827;
--fc-breezy-faint: #ffffff08;
--fc-breezy-muted: #ffffff12;
--fc-breezy-strong: #ffffff1F;
--fc-breezy-stronger: #ffffff29;
--fc-breezy-strongest: #ffffff33;
/* neutral foregrounds */
--fc-breezy-foreground: #d1d5db;
--fc-breezy-muted-foreground: #9ca3af;
--fc-breezy-faint-foreground: #6b7280;
--fc-breezy-strong-foreground: #ffffff;
/* neutral borders */
--fc-breezy-border: #ffffff1A;
--fc-breezy-muted-border: #ffffff0D;
--fc-breezy-strong-border: #ffffff26;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
console.log("Youpi Calendar!");
mw.hook('wikipage.content').add(function() {
console.log("Content loaded?");
console.log(FullCalendar);
var elems = document.getElementsByClassName('mw-crans-fullcalendar');
for (let el of elems) {
try {
const url = new URL(el.dataset.ics);
let calendar = new FullCalendar.Calendar(el, {
headerToolbar: {
start: 'prev,today,next',
center: 'title',
end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek,multiMonthYear',
},
locale: 'fr',
events: {
url: url.href,
format: 'ics'
}
});
calendar.render();
} catch (e) {
console.error(e);
continue;
}
}
console.log(elems);
});

View File

@ -0,0 +1,49 @@
diff --git a/sql/postgres/table_wsoauth_multiauth_mappings.sql b/sql/postgres/table_wsoauth_multiauth_mappings.sql
index d2917db..b903ba7 100644
--- a/sql/postgres/table_wsoauth_multiauth_mappings.sql
+++ b/sql/postgres/table_wsoauth_multiauth_mappings.sql
@@ -1,5 +1,5 @@
CREATE TABLE /*_*/wsoauth_multiauth_mappings (
- wsoauth_user int unsigned NOT NULL,
+ wsoauth_user int NOT NULL,
wsoauth_remote_name varchar(512) NOT NULL,
wsoauth_provider_id varchar(255) NOT NULL,
PRIMARY KEY (wsoauth_remote_name, wsoauth_provider_id)
diff --git a/src/WSOAuth.php b/src/WSOAuth.php
index 3a94c87..e077b9e 100644
--- a/src/WSOAuth.php
+++ b/src/WSOAuth.php
@@ -308,11 +308,12 @@ class WSOAuth extends PluggableAuth {
// Set $realname and $email to the values returned from the authentication provider, if they are available
$realname = $remoteUserInfo['realname'] ?? null;
$email = $remoteUserInfo['email'] ?? null;
+ $username = ucfirst( $remoteUserInfo['name'] );
- $remoteUsername = ucfirst( $remoteUserInfo['name'] );
- $localUserId = $this->getLocalAccountID( $remoteUsername );
+ $remoteUserId = $remoteUserInfo['remoteUserId'] ?? $username;
+ $localUserId = $this->getLocalAccountID( $remoteUserId );
- $this->session->set( self::WSOAUTH_REMOTE_USERNAME_SESSION_KEY, $remoteUsername );
+ $this->session->set( self::WSOAUTH_REMOTE_USERNAME_SESSION_KEY, $remoteUserId );
$this->session->save();
if ( $localUserId !== 0 ) {
@@ -326,7 +327,7 @@ class WSOAuth extends PluggableAuth {
$currentUser = RequestContext::getMain()->getUser();
$currentUserId = $currentUser->getId();
- $this->createMapping( $currentUserId, $remoteUsername );
+ $this->createMapping( $currentUserId, $remoteUserId );
// Log the account in like normal
$username = $currentUser->getName();
@@ -339,7 +340,7 @@ class WSOAuth extends PluggableAuth {
throw new ContinuationException( wfMessage( "wsoauth-remote-only-accounts-disabled" )->parse() );
}
- $desiredLocalUsername = $this->useRealNameAsUsername && $realname !== null ? $realname : $remoteUsername;
+ $desiredLocalUsername = $this->useRealNameAsUsername && $realname !== null ? $realname : $username;
$userId = User::newFromName( $desiredLocalUsername )->idForName();
if ( $userId > 0 && $this->migrateUsersByUsername ) {

View File

@ -0,0 +1,14 @@
# NoteKfetAuth
Extension pour médiawiki pour ajouter le support de l'autentification par Note
via l'extension WSOAuth.
## Installation
Il faut enregistrer l'extension comme provider
```
$wgOAuthCustomAuthProviders = [
'note' => WSOAuth\AuthenticationProvider\NoteKfetAuth::class
];
```

View File

@ -0,0 +1,25 @@
{
"name": "WSONoteKfetAuth",
"author": [
"Pyjacpp"
],
"url": "https://gitlab.crans.org/nounous/nixos/-/tree/main/hosts/vm/mediakiwi/WSONoteKfetAuth",
"description": "Implementation of the NoteKfet OAuth2 for the WSOAuth extension",
"type": "other",
"requires": {
"MediaWiki": ">= 1.35.0",
"extensions": {
"WSOAuth": ">= 9.0"
}
},
"AutoloadNamespaces": {
"WSOAuth\\AuthenticationProvider\\": "src/"
},
"config": {
"NoteKfetUrl": {
"description": "The url of the NoteKfet.",
"value": "https://note.crans.org/"
}
},
"manifest_version": 2
}

View File

@ -0,0 +1,149 @@
<?php
/**
* Copyright 2020 Marijn van Wezel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace WSOAuth\AuthenticationProvider;
use MediaWiki\User\UserIdentity;
class NoteKfetAuth extends AuthProvider {
/**
* @var string
*/
private $clientId;
/**
* @var string
*/
private $clientSecret;
/**
* @inheritDoc
*/
public function __construct(
string $clientId,
string $clientSecret,
?string $authUri,
?string $redirectUri,
array $extensionData = []
) {
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
}
/**
* @inheritDoc
*/
public function login( ?string &$key, ?string &$secret, ?string &$authUrl ): bool {
// This state is used to prevent CSRF, i.e., ensuring that authentification request
// were initiated on our website.
$state = random_int(PHP_INT_MIN, PHP_INT_MAX);
$secret = "$state";
$authUrl = $GLOBALS['wgNoteKfetUrl'] . "o/authorize/?" . http_build_query([
'client_id' => $this->clientId,
'response_type' => 'code',
'scope' => '1_1',
'state' => $secret,
]);
return true;
}
/**
* @inheritDoc
*/
public function logout( UserIdentity &$user ): void {
}
/**
* @inheritDoc
*/
public function getUser( string $key, string $secret, &$errorMessage ) {
if ( !isset( $_GET['code'] ) ) {
return false;
}
if ( !isset( $_GET['state'] ) || empty( $_GET['state'] ) || ( $_GET['state'] !== $secret ) ) {
return false;
}
try {
$token = $this->getAccessTokens( $_GET['code'] );
$userInfos = $this->getUserInfos( $token );
return [
'name' => $this->sanitizeName( "$userInfos->normalized_name (note)" ),
'realname' => $userInfos->username,
'email' => $userInfos->email,
'remoteUserId' => $userInfos->id,
];
} catch ( \Exception $e ) {
return false;
}
}
private function sanitizeName( string $name ) {
// We replace forbidden chars.
$res = preg_replace('/[#\/:<>=@\|]/', '-', $name);
$res = preg_replace(['/[\[{]/', '/[\]}]/'], ['(', ')'], $res);
$res = str_replace('_', ' ', $res);
// We remove the last controls chars possibly remaining.
return preg_replace('/[^a-zA-Z0-9 !\"$%&\'()*+,\-.;?\\\^`~]/', '', $res);
}
private function getAccessTokens( string $code ) {
$data = [
'grant_type' => 'authorization_code',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'code' => $code,
];
$options = [
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => http_build_query($data),
],
];
$context = stream_context_create($options);
$response = file_get_contents($GLOBALS['wgNoteKfetUrl'] . 'o/token/', false, $context);
$tokens = json_decode($response);
return $tokens->access_token;
}
private function getUserInfos( string $token ) {
$options = [
'http' => [
'method' => 'GET',
'header' => "Authorization: Bearer $token",
],
];
$context = stream_context_create($options);
$response = file_get_contents($GLOBALS['wgNoteKfetUrl'] . 'api/me/', false, $context);
return json_decode($response);
}
/**
* @inheritDoc
*/
public function saveExtraAttributes( int $id ): void {
}
}

View File

@ -0,0 +1,94 @@
diff --git a/CategoryLockdown.php b/CategoryLockdown.php
index 1e17ec7..8108615 100644
--- a/CategoryLockdown.php
+++ b/CategoryLockdown.php
@@ -16,6 +16,8 @@ class CategoryLockdown {
*/
public static function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
global $wgCategoryLockdown;
+ global $wgCategoryGroupLockdown;
+ global $wgCategoryLockdownWhitelist;
$explicitGroups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserGroups( $user );
$implicitGroups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserImplicitGroups( $user );
@@ -26,6 +28,11 @@ class CategoryLockdown {
return;
}
+ // Rules doesnt apply to the whitelist
+ if ( in_array( $title, $wgCategoryLockdownWhitelist ) ) {
+ return;
+ }
+
$categories = array_keys( $title->getParentCategories() );
// Apply rules to the category page itself
@@ -33,16 +40,11 @@ class CategoryLockdown {
$categories[] = $title->getFullText();
}
+ // Support "Category:Top_secret", "Category:Top secret", "Top_secret" and "Top secret"
+ $categories = array_map( fn($c) => str_replace( '_', ' ', substr( $c, strpos( $c, ':' ) + 1 ) ), $categories );
$combinedGroups = [];
foreach ( $categories as $category ) {
- // Support "Category:Top_secret", "Category:Top secret", "Top_secret" and "Top secret"
- $category = substr( $category, strpos( $category, ':' ) + 1 );
- $category = str_replace( '_', ' ', $category );
$permissions = $wgCategoryLockdown[ $category ] ?? null;
- if ( !$permissions ) {
- $category = str_replace( ' ', '_', $category );
- $permissions = $wgCategoryLockdown[ $category ] ?? null;
- }
if ( !$permissions ) {
continue;
}
@@ -57,15 +59,44 @@ class CategoryLockdown {
$combinedGroups[] = $allowedGroup;
}
}
- if ( $combinedGroups ) {
- foreach ( $userGroups as $userGroup ) {
- if ( in_array( $userGroup, $combinedGroups ) ) {
- return;
- }
+
+ $allow = false;
+ foreach ( $userGroups as $userGroup ) {
+ if ( in_array( $userGroup, $combinedGroups ) ) {
+ $allow = true;
+ break;
}
+ }
+ if ( $combinedGroups && !$allow ) {
$result = [ 'categorylockdown-error', implode( ', ', $combinedGroups ) ];
return false;
}
+
+ $allow = true;
+ foreach ( $wgCategoryGroupLockdown as $group => $groupCategories ) {
+ if ( str_starts_with( $group, "!") ?
+ in_array( substr($group, 1), $userGroups ) :
+ !in_array( $group, $userGroups ) ) {
+ continue; # Skip if this group rule doesnt match the user
+ }
+
+ $requiredCat = $groupCategories[ $action ] ?? null;
+ if ( $requiredCat == null ) {
+ continue;
+ }
+
+ $groupLocked = true;
+ foreach ( $requiredCat as $c ) {
+ if ( in_array( $c, $categories ) ) {
+ $groupLocked = false;
+ break; # One of the category is present, we can grant this action.
+ }
+ }
+ if ( $groupLocked ) {
+ $result = [ 'categorylockdown-error', implode( ', ', $requiredCat ) ];
+ return false; # This group of users need a category from groupCategories on this page to perform $action
+ }
+ }
}
/**

View File

@ -0,0 +1,28 @@
{ ... }:
{
imports = [
./hardware-configuration.nix
./mediawiki.nix
../../../modules
../../../modules/crans/nullmailer.nix
];
networking.hostName = "mediakiwi";
boot.loader.grub.devices = [ "/dev/sda" ];
crans = {
enable = true;
networking = {
id = 144;
srvNat.enable = true;
};
# Enable when deploying the real mediakiwi
resticClient.when = "06:47";
};
system.stateVersion = "25.05";
}

View File

@ -0,0 +1,24 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/45e1f37b-5bf2-47fc-86ef-e79b062c2b3c";
fsType = "ext4";
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}

View File

@ -0,0 +1,852 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
mkDefault
mkEnableOption
mkPackageOption
mkForce
mkIf
mkMerge
mkOption
;
inherit (lib)
concatStringsSep
literalExpression
mapAttrsToList
optional
optionals
optionalString
types
;
cfg = config.services.mediawiki;
fpm = config.services.phpfpm.pools.mediawiki;
user = "mediawiki";
group =
if cfg.webserver == "apache" then
config.services.httpd.group
else if cfg.webserver == "nginx" then
config.services.nginx.group
else
"mediawiki";
cacheDir = "/var/cache/mediawiki";
stateDir = "/var/lib/mediawiki";
toolsPath = pkgs.symlinkJoin {
name = "mediawiki-path";
paths = cfg.path;
};
pkg = pkgs.stdenv.mkDerivation rec {
pname = "mediawiki-full";
inherit (src) version;
src = cfg.package;
installPhase = ''
mkdir -p $out
cp -r * $out/
substituteInPlace $out/share/mediawiki/includes/config-schema.php \
--replace-fail "/usr/bin/" "${toolsPath}/bin/" \
--replace-fail "\$path/" "${toolsPath}/bin/"
# try removing directories before symlinking to allow overwriting any builtin extension or skin
${concatStringsSep "\n" (
mapAttrsToList (k: v: ''
rm -rf $out/share/mediawiki/skins/${k}
ln -s ${v} $out/share/mediawiki/skins/${k}
'') cfg.skins
)}
${concatStringsSep "\n" (
mapAttrsToList (k: v: ''
rm -rf $out/share/mediawiki/extensions/${k}
ln -s ${
if v != null then v else "$src/share/mediawiki/extensions/${k}"
} $out/share/mediawiki/extensions/${k}
'') cfg.extensions
)}
'';
};
mediawikiScripts =
pkgs.runCommand "mediawiki-scripts"
{
nativeBuildInputs = [ pkgs.makeWrapper ];
preferLocalBuild = true;
}
''
mkdir -p $out/bin
makeWrapper ${cfg.phpPackage}/bin/php $out/bin/mediawiki-maintenance \
--set MEDIAWIKI_CONFIG ${mediawikiConfig} \
--add-flags ${pkg}/share/mediawiki/maintenance/run.php
for i in changePassword createAndPromote deleteUserEmail renameUser resetUserEmail userOptions edit nukePage update importDump run; do
script="$out/bin/mediawiki-$i"
cat <<'EOF' >"$script"
#!${pkgs.runtimeShell}
become=(exec)
if [[ "$(id -u)" != ${user} ]]; then
become=(exec /run/wrappers/bin/sudo -u ${user} --)
fi
"${"$"}{become[@]}" ${placeholder "out"}/bin/mediawiki-maintenance \
EOF
if [[ "$i" != "run" ]]; then
echo " ${pkg}/share/mediawiki/maintenance/$i.php \"\$@\"" >>"$script"
else
echo " ${pkg}/share/mediawiki/maintenance/\$1.php \"\''${@:2}\"" >>"$script"
fi
chmod +x "$script"
done
'';
dbAddr =
if cfg.database.type == "postgres" then
"${if cfg.database.socket == null then cfg.database.host else cfg.database.socket}"
else if cfg.database.socket == null then
"${cfg.database.host}:${toString cfg.database.port}"
else if cfg.database.type == "mysql" then
"${cfg.database.host}:${cfg.database.socket}"
else
throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
mediawikiConfig = pkgs.writeTextFile {
name = "LocalSettings.php";
checkPhase = ''
${cfg.phpPackage}/bin/php --syntax-check "$target"
'';
text =
let
dbSettings =
if cfg.database.type == "sqlite" then
''
$wgSQLiteDataDir = "${cfg.database.path}";
''
else
''
$wgDBserver = "${dbAddr}";
$wgDBport = "${toString cfg.database.port}";
$wgDBuser = "${cfg.database.user}";
${optionalString (
cfg.database.passwordFile != null
) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
# MySQL specific settings
$wgDBprefix = "${cfg.database.tablePrefix}";
''}
${optionalString (cfg.database.type == "mysql") ''
# MySQL table options to use during installation or update
$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
''}
'';
in
''
<?php
# Protect against web entry
if ( !defined( 'MEDIAWIKI' ) ) {
exit;
}
$wgSitename = "${cfg.name}";
$wgMetaNamespace = false;
## The URL base path to the directory containing the wiki;
## defaults for all runtime URL paths are based off of this.
## For more information on customizing the URLs
## (like /w/index.php/Page_title to /wiki/Page_title) please see:
## https://www.mediawiki.org/wiki/Manual:Short_URL
$wgScriptPath = "${lib.optionalString (cfg.webserver == "nginx") "/w"}";
## The protocol and server name to use in fully-qualified URLs
$wgServer = "${cfg.url}";
## The URL path to static resources (images, scripts, etc.)
$wgResourceBasePath = $wgScriptPath;
${lib.optionalString (cfg.webserver == "nginx") ''
$wgArticlePath = "/wiki/$1";
$wgUsePathInfo = true;
''}
## The URL path to the logo. Make sure you change this from the default,
## or else you'll overwrite your logo when you upgrade!
$wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
## UPO means: this is also a user preference option
$wgEnableEmail = true;
$wgEnableUserEmail = true; # UPO
$wgPasswordSender = "${cfg.passwordSender}";
$wgEnotifUserTalk = false; # UPO
$wgEnotifWatchlist = false; # UPO
$wgEmailAuthentication = true;
## Database settings
$wgDBtype = "${cfg.database.type}";
$wgDBname = "${cfg.database.name}";
${dbSettings}
## Shared memory settings
$wgMainCacheType = CACHE_NONE;
$wgMemCachedServers = [];
${optionalString (cfg.uploadsDir != null) ''
$wgEnableUploads = true;
$wgUploadDirectory = "${cfg.uploadsDir}";
''}
$wgUseImageMagick = true;
# InstantCommons allows wiki to use images from https://commons.wikimedia.org
$wgUseInstantCommons = false;
# Periodically send a pingback to https://www.mediawiki.org/ with basic data
# about this MediaWiki instance. The Wikimedia Foundation shares this data
# with MediaWiki developers to help guide future development efforts.
$wgPingback = true;
## If you use ImageMagick (or any other shell command) on a
## Linux server, this will need to be set to the name of an
## available UTF-8 locale
$wgShellLocale = "C.UTF-8";
## Set $wgCacheDirectory to a writable directory on the web server
## to make your wiki go slightly faster. The directory should not
## be publicly accessible from the web.
$wgCacheDirectory = "${cacheDir}";
# Site language code, should be one of the list in ./languages/data/Names.php
$wgLanguageCode = "en";
$wgSecretKey = file_get_contents("${stateDir}/secret.key");
# Changing this will log out all existing sessions.
$wgAuthenticationTokenVersion = "";
## For attaching licensing metadata to pages, and displaying an
## appropriate copyright notice / icon. GNU Free Documentation
## License and Creative Commons licenses are supported so far.
$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
$wgRightsUrl = "";
$wgRightsText = "";
$wgRightsIcon = "";
# Enable APCU caching
$wgMainCacheType = CACHE_ACCEL;
# Enabled skins.
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
# Enabled extensions.
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
# End of automatically generated settings.
# Add more configuration options below.
${cfg.extraConfig}
'';
};
withTrailingSlash = str: if lib.hasSuffix "/" str then str else "${str}/";
in
{
options = {
services.mediawiki = {
enable = mkEnableOption "MediaWiki";
package = mkPackageOption pkgs "mediawiki" { };
# https://www.mediawiki.org/wiki/Compatibility#PHP
phpPackage = mkPackageOption pkgs "php" { } // {
default = pkgs.php83.buildEnv {
extensions = { all, enabled }: enabled ++ (with all; [ apcu ]);
};
};
finalPackage = mkOption {
type = types.package;
readOnly = true;
default = pkg;
defaultText = literalExpression "pkg";
description = ''
The final package used by the module. This is the package that will have extensions and skins installed.
'';
};
name = mkOption {
type = types.str;
default = "MediaWiki";
example = "Foobar Wiki";
description = "Name of the wiki.";
};
url = mkOption {
type = types.str;
default =
if cfg.webserver == "apache" then
"${
if
cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL
then
"https"
else
"http"
}://${cfg.httpd.virtualHost.hostName}"
else if cfg.webserver == "nginx" then
let
hasSSL = host: host.forceSSL || host.addSSL;
in
"${
if hasSSL config.services.nginx.virtualHosts.${cfg.nginx.hostName} then "https" else "http"
}://${cfg.nginx.hostName}"
else
"http://localhost";
defaultText = ''
if "mediawiki uses ssl" then "{"https" else "http"}://''${cfg.hostName}" else "http://localhost";
'';
example = "https://wiki.example.org";
description = "URL of the wiki.";
};
uploadsDir = mkOption {
type = types.nullOr types.path;
default = "${stateDir}/uploads";
description = ''
This directory is used for uploads of pictures. The directory passed here is automatically
created and permissions adjusted as required.
'';
};
passwordFile = mkOption {
type = types.path;
description = ''
A file containing the initial password for the administrator account "admin".
'';
example = "/run/keys/mediawiki-password";
};
passwordSender = mkOption {
type = types.str;
default =
if cfg.webserver == "apache" then
if cfg.httpd.virtualHost.adminAddr != null then
cfg.httpd.virtualHost.adminAddr
else
config.services.httpd.adminAddr
else
"root@localhost";
defaultText = literalExpression ''
if cfg.webserver == "apache" then
if cfg.httpd.virtualHost.adminAddr != null then
cfg.httpd.virtualHost.adminAddr
else
config.services.httpd.adminAddr else "root@localhost"
'';
description = "Contact address for password reset.";
};
path = mkOption {
type = types.listOf types.package;
defaultText = lib.literalExpression "with pkgs; [ diffutils imagemagick ]";
example = lib.literalExpression "with pkgs; [ librsvg ]";
description = "Extra packages to add to the PATH of phpfpm-pool.";
};
skins = mkOption {
default = { };
type = types.attrsOf types.path;
description = ''
Attribute set of paths whose content is copied to the {file}`skins`
subdirectory of the MediaWiki installation in addition to the default skins.
'';
};
extensions = mkOption {
default = { };
type = types.attrsOf (types.nullOr types.path);
description = ''
Attribute set of paths whose content is copied to the {file}`extensions`
subdirectory of the MediaWiki installation and enabled in configuration.
Use `null` instead of path to enable extensions that are part of MediaWiki.
'';
example = literalExpression ''
{
Matomo = pkgs.fetchzip {
url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
};
ParserFunctions = null;
}
'';
};
webserver = mkOption {
type = types.enum [
"apache"
"none"
"nginx"
];
default = "apache";
description = "Webserver to use.";
};
database = {
type = mkOption {
type = types.enum [
"mysql"
"postgres"
"mssql"
"oracle"
"sqlite"
];
default = "mysql";
description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
};
host = mkOption {
type = types.nullOr types.str;
default = if cfg.database.type == "sqlite" then null else "localhost";
defaultText = ''"localhost"'';
description = "Database host address. Used only if database type is not SQLite.";
};
port = mkOption {
type = types.nullOr types.port;
default =
if cfg.database.type == "mysql" then
3306
else if cfg.database.type != "sqlite" then
5432
else
null;
defaultText = literalExpression "3306";
description = "Database host port. Used only if database type is not SQLite.";
};
name = mkOption {
type = types.str;
default = "mediawiki";
description = "Database name.";
};
user = mkOption {
type = types.nullOr types.str;
default = if cfg.database.type != "sqlite" then "mediawiki" else null;
defaultText = literalExpression ''"mediawiki"'';
description = "Database user. Used only if database type is not SQLite.";
};
path = mkOption {
type = types.nullOr types.path;
default = if cfg.database.type == "sqlite" then "${stateDir}/data" else null;
defaultText = literalExpression ''"${stateDir}/data"'';
description = "Path to store the MediaWiki database in if using SQLite.";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
example = "/run/keys/mediawiki-dbpassword";
description = ''
A file containing the password corresponding to
{option}`database.user`. Used only if database type is not SQLite.
'';
};
tablePrefix = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
If you only have access to a single database and wish to install more than
one version of MediaWiki, or have other applications that also use the
database, you can give the table names a unique prefix to stop any naming
conflicts or confusion. Only used if database type is MySQL.
See <https://www.mediawiki.org/wiki/Manual:$wgDBprefix>.
'';
};
socket = mkOption {
type = types.nullOr types.path;
default =
if (cfg.database.type == "mysql" && cfg.database.createLocally) then
"/run/mysqld/mysqld.sock"
else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
"/run/postgresql"
else
null;
defaultText = literalExpression "/run/mysqld/mysqld.sock";
description = "Path to the unix socket file to use for authentication. Used only if database type is not SQLite.";
};
createLocally = mkOption {
type = types.bool;
default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
defaultText = literalExpression "true";
description = ''
Create the database and database user locally.
This currently only applies if database type "mysql" or "postgres" is selected.
'';
};
};
nginx.hostName = mkOption {
type = types.str;
example = literalExpression "wiki.example.com";
default = "localhost";
description = ''
The hostname to use for the nginx virtual host.
This is used to generate the nginx configuration.
'';
};
httpd.virtualHost = mkOption {
type = types.submodule { }; # (import ../web-servers/apache-httpd/vhost-options.nix);
example = literalExpression ''
{
hostName = "mediawiki.example.org";
adminAddr = "webmaster@example.org";
forceSSL = true;
enableACME = true;
}
'';
description = ''
Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
See [](#opt-services.httpd.virtualHosts) for further information.
'';
};
poolConfig = mkOption {
type =
with types;
attrsOf (oneOf [
str
int
bool
]);
default = {
"pm" = "dynamic";
"pm.max_children" = 32;
"pm.start_servers" = 2;
"pm.min_spare_servers" = 2;
"pm.max_spare_servers" = 4;
"pm.max_requests" = 500;
};
description = ''
Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf`
for details on configuration directives.
'';
};
extraConfig = mkOption {
type = types.lines;
description = ''
Any additional text to be appended to MediaWiki's
LocalSettings.php configuration file. For configuration
settings, see <https://www.mediawiki.org/wiki/Manual:Configuration_settings>.
'';
default = "";
example = ''
$wgEnableEmail = false;
'';
};
};
};
imports = [
(lib.mkRenamedOptionModule
[ "services" "mediawiki" "virtualHost" ]
[ "services" "mediawiki" "httpd" "virtualHost" ]
)
];
config = mkIf cfg.enable {
assertions = [
{
assertion =
cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
}
{
assertion =
cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user;
message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
}
{
assertion = cfg.database.createLocally -> cfg.database.socket != null;
message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
}
{
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
}
];
warnings =
lib.optional
(
cfg.database.type == "sqlite"
&& (
cfg.database.host != null
|| cfg.database.port != null
|| cfg.database.user != null
|| cfg.database.passwordFile != null
|| cfg.database.socket != null
)
)
''
The services.mediawiki.database options host, port, user, passwordFile, and socket will be ignored because services.mediawiki.database.type is "sqlite".
''
++ lib.optional (cfg.database.type != "sqlite" && cfg.database.path != null) ''
The services.mediawiki.database.path option will be ignored because services.mediawiki.database.type is not "sqlite".
''
++ lib.optional (cfg.database.type != "mysql" && cfg.database.tablePrefix != null) ''
The services.mediawiki.database.tablePrefix option has no effect when the services.mediawiki.database.type is not "mysql".
'';
services.mediawiki = {
path = with pkgs; [
diffutils
imagemagick
];
skins = {
MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
Vector = "${cfg.package}/share/mediawiki/skins/Vector";
};
};
services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
enable = true;
package = mkDefault pkgs.mariadb;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensurePermissions = {
"${cfg.database.name}.*" = "ALL PRIVILEGES";
};
}
];
};
services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensureDBOwnership = true;
}
];
};
services.phpfpm.pools.mediawiki = {
inherit user group;
phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
phpPackage = cfg.phpPackage;
settings =
(
if (cfg.webserver == "apache") then
{
"listen.owner" = config.services.httpd.user;
"listen.group" = config.services.httpd.group;
}
else if (cfg.webserver == "nginx") then
{
"listen.owner" = config.services.nginx.user;
"listen.group" = config.services.nginx.group;
}
else
{
"listen.owner" = user;
"listen.group" = group;
}
)
// cfg.poolConfig;
};
services.httpd = lib.mkIf (cfg.webserver == "apache") {
enable = true;
extraModules = [ "proxy_fcgi" ];
virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
cfg.httpd.virtualHost
{
documentRoot = mkForce "${pkg}/share/mediawiki";
extraConfig = ''
<Directory "${pkg}/share/mediawiki">
<FilesMatch "\.php$">
<If "-f %{REQUEST_FILENAME}">
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
</If>
</FilesMatch>
Require all granted
DirectoryIndex index.php
AllowOverride All
</Directory>
''
+ optionalString (cfg.uploadsDir != null) ''
Alias "/images" "${cfg.uploadsDir}"
<Directory "${cfg.uploadsDir}">
Require all granted
</Directory>
'';
}
];
};
# inspired by https://www.mediawiki.org/wiki/Manual:Short_URL/Nginx
services.nginx = lib.mkIf (cfg.webserver == "nginx") {
enable = true;
virtualHosts.${config.services.mediawiki.nginx.hostName} = {
root = "${pkg}/share/mediawiki";
locations = {
"~ ^/w/(index|load|api|thumb|opensearch_desc|rest|img_auth)\\.php$".extraConfig = ''
rewrite ^/w/(.*) /$1 break;
include ${config.services.nginx.package}/conf/fastcgi.conf;
fastcgi_index index.php;
fastcgi_pass unix:${config.services.phpfpm.pools.mediawiki.socket};
'';
"/w/images/".alias = withTrailingSlash cfg.uploadsDir;
# Deny access to deleted images folder
"/w/images/deleted".extraConfig = ''
deny all;
'';
# MediaWiki assets (usually images)
"~ ^/w/resources/(assets|lib|src)".extraConfig = ''
rewrite ^/w(/.*) $1 break;
add_header Cache-Control "public";
expires 7d;
'';
# Assets, scripts and styles from skins and extensions
"~ ^/w/(skins|extensions)/.+\\.(css|js|gif|jpg|jpeg|png|svg|wasm|ttf|woff|woff2)$".extraConfig = ''
rewrite ^/w(/.*) $1 break;
add_header Cache-Control "public";
expires 7d;
'';
# Handling for Mediawiki REST API, see [[mw:API:REST_API]]
"/w/rest.php/".tryFiles = "$uri $uri/ /w/rest.php?$query_string";
# Handling for the article path (pretty URLs)
"/wiki/".extraConfig = ''
rewrite ^/wiki/(?<pagename>.*)$ /w/index.php;
'';
# Explicit access to the root website, redirect to main page (adapt as needed)
"= /".extraConfig = ''
return 301 /wiki/;
'';
# Every other entry point will be disallowed.
# Add specific rules for other entry points/images as needed above this
"/".extraConfig = ''
return 404;
'';
};
};
};
systemd.tmpfiles.rules = [
"d '${stateDir}' 0750 ${user} ${group} - -"
"d '${cacheDir}' 0750 ${user} ${group} - -"
]
++ optionals (cfg.uploadsDir != null) [
"d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
"Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
];
systemd.services.mediawiki-init = {
wantedBy = [ "multi-user.target" ];
before = [ "phpfpm-mediawiki.service" ];
after =
optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.target";
script =
let
dbOptions =
if cfg.database.type == "sqlite" then
''
--dbpath ${lib.escapeShellArg cfg.database.path} \
--dbname ${lib.escapeShellArg cfg.database.name} \
''
else
''
--dbserver ${lib.escapeShellArg dbAddr} \
--dbport ${toString cfg.database.port} \
--dbname ${lib.escapeShellArg cfg.database.name} \
${
optionalString (
cfg.database.tablePrefix != null
) "--dbprefix ${lib.escapeShellArg cfg.database.tablePrefix}"
} \
--dbuser ${lib.escapeShellArg cfg.database.user} \
${
optionalString (
cfg.database.passwordFile != null
) "--dbpassfile ${lib.escapeShellArg cfg.database.passwordFile}"
} \
'';
in
''
if ! test -e "${stateDir}/secret.key"; then
tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
fi
${optionalString (cfg.database.type == "sqlite") "mkdir -p ${cfg.database.path}"}
echo "exit( \$this->getPrimaryDB()->tableExists( 'user' ) ? 1 : 0 );" | \
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/run.php eval --conf ${mediawikiConfig} && \
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/run.php ${pkg}/share/mediawiki/maintenance/install.php \
--confpath /tmp \
--scriptpath / \
${dbOptions} \
--dbtype ${cfg.database.type} \
--passfile ${lib.escapeShellArg cfg.passwordFile} \
${lib.escapeShellArg cfg.name} \
admin
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick --skip-external-dependencies
'';
serviceConfig = {
Type = "oneshot";
User = user;
Group = group;
PrivateTmp = true;
};
};
systemd.services.httpd.after =
optional (
cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql"
) "mysql.service"
++ optional (
cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres"
) "postgresql.target";
users.users.${user} = {
inherit group;
isSystemUser = true;
};
users.groups.${group} = { };
environment.systemPackages = [ mediawikiScripts ];
};
}

View File

@ -0,0 +1,371 @@
{
pkgs,
config,
...
}:
let
phpExtensions = config.services.mediawiki.phpPackage.extensions;
in
{
# Mauvaise config posgresql, on fixe à la mano temporairement
disabledModules = [ "services/web-apps/mediawiki.nix" ];
imports = [ ./mediawiki-patch.nix ];
age.secrets.mediawiki-admin-passwd = {
file = ../../../secrets/mediakiwi/mediawiki-admin-passwd.age;
owner = "mediawiki";
};
age.secrets.mediawiki-ldap = {
file = ../../../secrets/mediakiwi/mediawiki-ldap.age;
owner = "mediawiki";
};
age.secrets.mediawiki-oauth = {
file = ../../../secrets/mediakiwi/mediawiki-oauth.age;
owner = "mediawiki";
};
# Attention: il ne faut pas mettre de retour à la ligne à la fin du mot de passe!
age.secrets.mediawiki-db = {
file = ../../../secrets/mediakiwi/mediawiki-db-pass.age;
owner = "mediawiki";
};
environment.systemPackages = with pkgs; [
imagemagick
# For the PdfHandler extension
ghostscript
poppler-utils
# For the SyntaxHighlighting extension
python3Packages.pygments
];
services.phpfpm.pools.mediawiki.phpOptions = ''
upload_max_filesize = 512M
post_max_size = 512M
max_execution_time = 1000
max_input_time = 2000
default_socket_timeout = 2000
extension = ${phpExtensions.mbstring}/lib/php/extensions/mbstring.so
extension = ${phpExtensions.luasandbox}/lib/php/extensions/luasandbox.so
'';
services.nginx.clientMaxBodySize = "512M";
services.syslogd.enable = true;
# Tâches lourdes à gérer au fond https://www.mediawiki.org/wiki/Manual:Job_queue
systemd.services.mediawiki-jobs =
let
# Hack pour retrouver la conf
conf = config.services.phpfpm.pools.mediawiki.phpEnv.MEDIAWIKI_CONFIG;
php = "${config.services.mediawiki.phpPackage}/bin/php";
maintenanceScript = "${config.services.mediawiki.finalPackage}/share/mediawiki/maintenance/run.php";
runJobs = "MEDIAWIKI_CONFIG='${conf}' ${php} ${maintenanceScript} runJobs --maxtime=3600";
in
{
description = "Tâches lourdes de Mediawiki";
wantedBy = [ "multi-user.target" ];
after = [ "phpfpm-mediawiki.service" ];
path = [ pkgs.mediawiki ];
# Inspiré de https://www.mediawiki.org/wiki/Manual:Job_queue
script = ''
while true; do
${runJobs} --type="enotifNotify"
# --wait pour ne pas boucler pour rien
${runJobs} --wait --maxjobs=20
# Au bout de 20 jobs on attend pour éviter de surcharger le serveur
# avec des tâches de fond
sleep 5
done
'';
serviceConfig = {
Restart = "always";
RestartSec = "60s";
User = "mediawiki";
Nice = 10;
ProtectSystem = "full";
OOMScoreAdjust = 200;
};
};
services.mediawiki = {
enable = true;
name = "Wiki Crans";
nginx.hostName = "mediawiki.crans.org";
webserver = "nginx";
passwordFile = config.age.secrets.mediawiki-admin-passwd.path;
database = {
createLocally = false;
host = "tealc.adm.crans.org";
name = "mediawiki";
user = "mediawiki";
passwordFile = config.age.secrets.mediawiki-db.path;
type = "postgres";
};
extraConfig = ''
# TODO remove log error in test
error_reporting( -1 );
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
# Server settings
$wgFavicon = 'https://www.crans.org/images/favicon.ico';
$wgLogo = 'https://www.crans.org/images/crans.svg';
$wgJobRunRate = 0; # On fait les jobs manuellement dans un systemd.
$wgEnableEditRecovery = true; # Pour pouvoir récuppérer des changements non sauvegardées (1 mois par défaut en local)
# Files and Uploads
$wgMaxUploadSize = 512 * 1024 * 1024;
$wgUseImageMagick = true;
$wgImageMagickConvertCommand = '${pkgs.imagemagick}/bin/convert';
$wgStrictFileExtensions = false; # On autorise toutes les extensions sauf https://www.mediawiki.org/wiki/Manual:$wgProhibitedFileExtensions
$wgFileExtensions[] = 'pdf';
$wgFileExtensions[] = 'svg';
# Limite mémoire, quand on fait de gros importi (9M de xml), le parser
# prends plus que 50M
$wgMemoryLimit = 512 * 1024 * 1024;
# E-mail settings
$wgEnableEmail = true;
$wgSMTP = [
'host' => 'smtp.adm.crans.org',
'IDHost' => 'crans.org',
'localhost' => 'crans.org',
'port' => 25,
'auth' => false,
];
$wgPasswordSender = 'wiki@crans.org'; # Cest un alias de root (pas de réponse)
$wgEmergencyContact = 'contact@crans.org';
$wgNoReplyAddress = 'contact@crans.org'; # On reçoit les réponses aux mails
$wgEnableUserEmail = true; # On active les emails aux utilisateurices mais restreint via les ACLs
$wgAllowHTMLEmail = true;
$wgEnotifUseRealName = true; # On active les noms réels pour plus de lisibilité avec les comptes note
$wgEnotifFromEditor = false; # On ne se fait pas passer par lutilisateurice
$wgEnotifRevealEditorAddress = false; # Pas de champ répondre avec ladresse mail de lutilisateurice
$wgEnotifUserTalk = true;
$wgEnotifMinorEdits = true;
$wgEnotifWatchlist = true;
# Peut-être utilisé pour les Wikistes
$wgUsersNotifiedOnAllChanges = [ 'Ninja_wiki_notifs' ];
# Auth
$wgPluggableAuth_EnableLocalLogin = false;
$LDAPAuthentication2AllowLocalLogin = true;
$LDAPProviderDomainConfigs = "${config.age.secrets.mediawiki-ldap.path}";
$wgOAuthCustomAuthProviders = [
'note' => WSOAuth\AuthenticationProvider\NoteKfetAuth::class
];
$wgPluggableAuth_Config = [
"Compte Crans" => [
'plugin' => 'LDAPAuthentication2',
'data' => [
'domain' => 'crans'
]
],
"Note BDE" => [
'plugin' => 'WSOAuth',
'data' => require('${config.age.secrets.mediawiki-oauth.path}'),
]
];
# Theme
$wgDefaultSkin = 'citizen';
$wgCitizenThemeColor = '#AD1F1F';
$wgCitizenEnableARFonts = true;
$wgCitizenEnableCJKFonts = true;
$wgLanguageCode = 'fr';
$wgLocaltimezone = 'Europe/Paris';
$wgDefaultUserOptions['timecorrection'] = 'ZoneInfo|0|' . $wgLocaltimezone;
# Access Control
$wgGroupPermissions['*']['edit'] = false; # Restrict edition for anonymous user
$wgGroupPermissions['*']['createaccount'] = false; # Restrict the creation of account to sysop only
$wgGroupPermissions['*']['autocreateaccount'] = true; # Création de comptes depuis LDAP et Note mais pas local
$wgGroupPermissions['*']['delete'] = true; # Création de comptes depuis LDAP et Note mais pas local
$wgGroupPermissions['user']['sendemail'] = false;
$wgGroupPermissions['sysop']['sendemail'] = true; # On réserve lenvoie de mail aux admins
$wgCategoryLockdownWhitelist = [
"Spécial:Connexion",
"Spécial:Déconnexion",
"Spécial:Connexion/return",
"Spécial:PluggableAuthLogin",
"Spécial:Recherche",
"MediaWiki:Common.css",
"MediaWiki:Common.js"
];
$wgCategoryGroupLockdown["!user"]["read"] = [ "Page Publique" ]; # Restrict read for non-user (i.e. anonymous) on execpt for Page Publique
$wgFeed = false; # Pas de flux car ils ne sont pas authentifiés :(.
# Extensions
$wgWikiEditorRealtimePreview = true;
$wgCiteBookReferencing = true;
$wgPdfProcessor = '${pkgs.ghostscript}/bin/gs';
$wgPdfPostProcessor = $wgImageMagickConvertCommand;
$wgPdfInfo = '${pkgs.poppler-utils}/bin/pdfinfo';
$wgPdftoText = '${pkgs.poppler-utils}/bin/pdftotext';
$wgScribuntoDefaultEngine = 'luasandbox';
# 4 pour les catégories, 3 pour le reste https://www.mediawiki.org/wiki/Extension:CategoryTree
$wgCategoryTreeMaxDepth = [10 => 3, 20 => 3, 0 => 4, 100 => 3];
# SyntaxHighlight extension
$wgPygmentizePath = '${pkgs.python3Packages.pygments}/bin/pygmentize';
# Custom Namespaces
define("NS_ARCHIVE", 3000);
define("NS_ARCHIVE_TALK", 3001);
$wgExtraNamespaces = [
NS_ARCHIVE => "Archive",
NS_ARCHIVE_TALK => "Discussion_archive",
];
# Sous pages
$wgNamespacesWithSubpages[NS_MAIN] = true;
$wgNamespacesWithSubpages[NS_ARCHIVE] = true;
$wgNamespacesWithSubpages[NS_CATEGORY] = true;
$wgVisualEditorAvailableNamespaces[NS_ARCHIVE] = true;
# Pour les Popups
$wgContentNamespaces[] = NS_ARCHIVE;
$wgContentNamespaces[] = NS_USER;
# Pour la recherche
$wgNamespacesToBeSearchedDefault[NS_ARCHIVE] = true; # Temporairement pour la migration?
$wgNamespacesToBeSearchedDefault[NS_USER] = true;
'';
skins = {
Citizen = pkgs.fetchFromGitHub {
name = "Citizen";
owner = "StarCitizenTools";
repo = "mediawiki-skins-Citizen";
tag = "v3.19.0";
sha256 = "sha256-iQS0lyvOYH45gI6fMKYV4ERK9JO35J8ktLNrQi6UsH0=";
};
};
extensions = {
# Enables embedded extensions
AbuseFilter = null; # pour faire de la modération
CategoryTree = null; # pour faire des arbres de catégories
Cite = null; # pour faire des références/footnotes
CiteThisPage = null; # pour avoir la citation (à la bibtex) d'une page
CodeEditor = null; # pour éditer des macros/scripts wiki
DiscussionTools = null; # pour des pages de discussion intéractives
Echo = null; # pour le système de notification du wiki
Gadgets = null; # pour avoir un système d'outils activable par les users
ImageMap = null; # pour mettre des widgets sur des images
Linter = null; # requis pour DiscussionTools
Math = null; # pour avoir des maths LaTeX
MultimediaViewer = null; # pour avoir un affichage sympa des images
Nuke = null; # pour purger des pages
PageImages = null; # pour set la bonne image représentant un article
ParserFunctions = null; # pour la logique et les fonctions de base du templating
PdfHandler = null; # pour afficher les pdfs
Poem = null; # pour afficher des blocks de texte respectant l'espacement (poèmes, écrits)
ReplaceText = null; # pour du méga-renommage à travers le wiki
Scribunto = null; # pour faire du scripting et des templates pour le wiki
SyntaxHighlight_GeSHi = null; # pour avoir de la coloration syntaxique
TemplateData = null; # pour faire de la doc sur les templates
TemplateStyles = null; # pour styliser les templates (bundlé pour la prochaine version de mediawiki)
TextExtracts = null; # pour set le bon text représentant un article
Thanks = null; # pour remercier des gens en notif
TitleBlacklist = null; # pour faire de la modération
VisualEditor = null; # pour éditer visuellement les pages
WikiEditor = null; # pour éditer le code wiki des pages
CategoryLockdown = pkgs.applyPatches {
src = pkgs.fetchFromGitHub {
name = "CategoryLockdown";
owner = "wikimedia";
repo = "mediawiki-extensions-CategoryLockdown";
rev = "d29e734c87697590af80f91d0cdbb4275bc3f072";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-iYRzCzWu3hndPoMIkFZOaYJkbVoAnVv/ZMZTSAcQdKo=";
};
patches = [
# Cette extension soccupe des du contrôle daccès du Wiki
# et a été beaucoup patché. Pensez à vérifier les changements
# et révisez le patch le cas échéant.
"${./category-lockdown.patch}"
];
};
# FullCalendar
FullCalendar = ./FullCalendar;
# Popups
Popups = pkgs.fetchFromGitHub {
name = "Popups";
owner = "wikimedia";
repo = "mediawiki-extensions-Popups";
rev = "82742d6e42750bf5b8000c90fc894b304aa95235";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-mqiDU7w3B3imMLqoTvwgnyk/vCDWeAiTEX1y4dWmNDk=";
};
# Auth
PluggableAuth = pkgs.fetchFromGitHub {
name = "PluggableAuth";
owner = "wikimedia";
repo = "mediawiki-extensions-PluggableAuth";
rev = "3b9428bd8832aeb8ef8ff1ea61b7a4afca7f0ecb";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-9PUf7KC0oz4XeN/m/5KbefNpsgRwV2NswMQZl7a/+I0=";
};
LDAPProvider = pkgs.fetchFromGitHub {
name = "LDAPProvider";
owner = "wikimedia";
repo = "mediawiki-extensions-LDAPProvider";
tag = "3.1.0";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-9/5Uph5C6VPWT7NzrPsYpM9pxZPIQneKZDPFCaYm1e8=";
};
LDAPAuthentication2 = pkgs.fetchFromGitHub {
name = "LDAPAuthentication2";
owner = "wikimedia";
repo = "mediawiki-extensions-LDAPAuthentication2";
tag = "3.1.0";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-/9VISvpvhaVq/sAHE10INlTju7+PTirFO/6U68WBOgM=";
};
LDAPGroup = pkgs.fetchFromGitHub {
name = "LDAPGroup";
owner = "wikimedia";
repo = "mediawiki-extensions-LDAPGroups";
tag = "3.1.0";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-aW1nVechxy4CfyT953LfNgeUOXZGeIkV3KwPJJwlyug=";
};
WSOAuth = pkgs.applyPatches {
src = pkgs.fetchFromGitHub {
name = "WSOAuth";
owner = "wikimedia";
repo = "mediawiki-extensions-WSOAuth";
rev = "4b8a12d26196b323be5da93480b6a75fda25b6d0";
# Le SHA doit être changé à chaque nouveau commit de traduction.
# Pas de meilleure solution à ma connaissance pour suivre les releases.
sha256 = "sha256-W2SL14U5nVUcf/aTkWuVk9+vQGd3RCtuR3bAhBMN5iY=";
};
patches = [ "${./WSOAuth.patch}" ];
};
WSONoteKfetAuth = "${./WSONoteKfetAuth}";
};
};
}

View File

@ -40,6 +40,7 @@ let
collabora = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFa2D9fREtO2r2oIx6q9JAKFUHtxGbgEPMjkx09DQSU8 root@collabora"; collabora = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFa2D9fREtO2r2oIx6q9JAKFUHtxGbgEPMjkx09DQSU8 root@collabora";
jitsi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB6jVMIZ5y2oXX9HOkw7r5UUjw95MlFaFuu7FnEC0Q8z root@jitsi"; jitsi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB6jVMIZ5y2oXX9HOkw7r5UUjw95MlFaFuu7FnEC0Q8z root@jitsi";
livre = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEVfKNokHG6ig32hhQxTep+fKFmKahlDClPrX/dP4/gb root@livre"; livre = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEVfKNokHG6ig32hhQxTep+fKFmKahlDClPrX/dP4/gb root@livre";
mediakiwi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFfcKtim7Aj+strHdg847D7KCCdoOAuasxx/2yi2TY1n root@mediakiwi";
neo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMGfSvxqC2PJYRrxJaivVDujwlwCZ6AwH8hOSA9ktZ1V root@neo"; neo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMGfSvxqC2PJYRrxJaivVDujwlwCZ6AwH8hOSA9ktZ1V root@neo";
nextcloud = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgSP9UmuJw8Bi2ML07WHsWvxN8akkc9XZxXyOgdjXkq root@nextcloud"; nextcloud = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgSP9UmuJw8Bi2ML07WHsWvxN8akkc9XZxXyOgdjXkq root@nextcloud";
periodique = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTdfSIL3AWIv0mjRDam6E/qsjoqwJ8QSm1Cb0xqs1s1 root@periodique"; periodique = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTdfSIL3AWIv0mjRDam6E/qsjoqwJ8QSm1Cb0xqs1s1 root@periodique";

Binary file not shown.

View File

@ -0,0 +1,33 @@
age-encryption.org/v1
-> ssh-ed25519 ZpDcxw Z/XNMwkMr7AXaMpDc/Q7ASWWwv/ID5pRpSUyx66R+2Y
8IWRR7cQVrrBFwAg0zHnbliWZWDKsa2PmdxHZsCFQLM
-> piv-p256 ewCc3w AkDTktwBj1dnAA63otR9BhIAyjcPoSww4G8/cP1zSX+j
i+ftraOs9yZ51gUumFA+zJZzgAPRtVyuPfUsA0hlXnI
-> piv-p256 6CL/Pw AsD6ZBVGl8HU3FQKnuskPk8qFHPgxK+9adJGuuzik88c
11sTCEgccvVjS29h2j3d7PwNZsdPqf4Y/9tagHbE2HM
-> ssh-ed25519 eOAUSg wdE8dGVe35YXCdpXfko+lLsJTgJUmal7OSII3r4Somw
nXbdTQ6KrGbb/FMSiELLuQexNut8qefOIcGyocbYbiA
-> ssh-rsa REaZBA
S8twRuHh+Oety2nRkiOyugu5ZgCaaoapLe4UF9CNCy0ldX87t+Y2va/WdAB2q/UU
evMdN8MOplM8UxkQy69I2BlT26M9QktFuo90ktXK35DrFwiaP8a4aKOyiKzXFdcZ
h4F7Af//lMlcwLYFARbAuXTIj5w8/Hd9IvhAbFoqtXo5+a2n6MgHdc5LvlLSjf9v
aP7+01jGcT2lSbmw8YS8+caYE3LVlUh2ThTeFQKYWm+nE78IP19/TOabmNnx8eBX
LtJggwnoNPrwmn53fn1tADI1+6QoIpTIrZOZLhAKnzDc5oasVNyjAY9NGC/U6A8e
9eaPn3KmIg6QBg8hXUCLH4JeGHFU0hIxCeLotoh1F1HEi+CY3tPuylpny08/J3AJ
i3+RvguyuJ3M5zwoMDvmavPw5mcV9tvWYhUSAWd6iZ6DIN1AHFsXQFXQuRn4P+3h
v9faikTZrt/sv4ZQgWKNoTJ6CDxcAnhDkTtlgG4afjreFvRuB7esQz8v+G6BhUcE
-> ssh-ed25519 J/iReg aY/7+a5eakXhTJcsTR6ES7dTNBbmgh5riO0fhHzN4V8
g4c9HRqdnw8oW3XZaDzUGl8FiVRe/Zwf/uewt8Henlk
-> ssh-ed25519 GNhSGw pBleL7cbEZQhQRPP6wSLIJ9SmU7xCjdanDzQvzVBuUg
d7QKEOygw4M8ne+iFiU9DxOwBGTC9JNS/2UPx9zjLXs
-> ssh-ed25519 eXMAtA PRMzXld8kf3oLoqORVVeO0THG2g6as9vTwGhznQPZBo
i+Ea7CfolHGjvoDuIeHl4bCF20718pV7WrlES+Sf2bE
-> ssh-ed25519 5hXocQ 0e9eAjPAQee/VZvL2PYm9RgoVBnkOgI/y0pblLvoRW4
PBfnrfOakv2ACKrDYs/to1zVaYldLiLldaUxn3GY/5k
-> ssh-ed25519 bRHVVA gX62eG+wJesfSBd4lXzTqNMI+FjPl6fnztXygR8HDx0
mML+owf1iLKGT9ZZkMUu2/l10eSPZEl5TwyAftOOyDA
-> ssh-ed25519 HgW9eA 3Qp1NEa89r/JsT4GKnqDPaTmJaEI1on9D5n5I84DzhE
hH9Kf6l1s1R3zT69iIXmKEB0qPE/i850WMpfb2lk3hE
--- AAABc4YYLcIgv6zjThJ9BhOkR24VZ4wMV+LPR9yTtOM
¢.áZñ§—3PÀ5¸ÇwÔ!™)Ñ<>}Ú[UèB™ö±ÛråNØÚI^

View File

@ -0,0 +1,33 @@
age-encryption.org/v1
-> ssh-ed25519 ZpDcxw EYeeHlk7G0Ce8Zjz7DOA6umVE58ZNeRb1ao86/romSA
iCLH6+fsNCeM5heK2QFh9kEY44BiDOQ8um4MCNUDeuI
-> piv-p256 ewCc3w A+QtLSWeA4j0756rlETtyvJRiZR3y70+UMs/X1/u0fqq
FslsT1yQ+NNHK8m/eBLWeNcsPu2BnubNO1h/AO95UxY
-> piv-p256 6CL/Pw AxHghSnLJmHJz9rXlP1IQDoigFTkFUQiE7h3c0EMeLsc
bmI1TbflKxJ6Axg/TgDEtCV0pw7tY6V/pMl4CJGjvL8
-> ssh-ed25519 eOAUSg TPhT5qmh2Y9b0eBa41uTdIDdZEcDZtR6tEV8Jo1MuCk
gBD5CQlnxMYohS13RvzoDDQeGDhYOEcS18Xv1S/5f+M
-> ssh-rsa REaZBA
NtWXPRN/qPJ5XUZNXt64Rt3brMfbXxP2ARvXo3wpnB0ed0QPnX29OTyVm/O2VNrY
5jPouFaxWwDBuwyJ/HyJJB5SdrE188u7du/M0Z8l1/H8i2lpkp98XS64AEXkvrHo
oWFsRloCWyeIU3m1MtUdqu/c3pnqyiE1SlCnajb6MUUrhDe2CdEEgJxFsIyMwXpW
mkwtHxe9ngVebRLY3vw9OVhtqrKeKrNvSGGCE+IFn6kWuNoGD/rDI52kx76sNa0D
531FFAdsiPt1VKZi0YBaUfRP407sc6rw8R258jFCeYpa58Zud62BWsT0d464gPkD
tJQUZecCPRW0jflpVWzqZHIVurtp3Dqjp/oovmMMJ303/ywL0VdZR6PeMndGOkf+
ame4zqTwOyQoxSkU+62D/KeSzM0QlVqAZhL+lMfYkFCYizZYQZSlLAHi7nF3704W
S+nrv+FNNxLYT6zlq0hHokP9fIfUct/eFfMmV2ff9E77Tow/79TpgQnfxxdtJWJG
-> ssh-ed25519 J/iReg ZAxHEQjYmqQkMFth5Lp344f0JqwfGh4CO6DEG+uMmys
qlLlf9eLNNw3/o++6LVpy2Anj3TWkNbcJoJ/rP4JKsI
-> ssh-ed25519 GNhSGw 44kEvp1o41BaNRwFnAjKB4K3otIYhN7QN9FgCd+eSQ4
fqZM/JCIderSDDrxckv4VLZu64Dfx275UVmfKsozpKU
-> ssh-ed25519 eXMAtA mPn1SPpt7RkhMBLqUZ4mWGvqNeGtUN+pcA9jjKKGPGU
bGyck1ZRHRlzk+HVmL3e99MiVFxjTuC7v/6WHjRjH8A
-> ssh-ed25519 5hXocQ bT0MsqVcYEcB/ckkuAC0zbH5+pfs4iPso9MHhVUyx2U
jZQOFmGWnZMtdTS5ePKIvnQFiOVsZTC/A09ZmXzveho
-> ssh-ed25519 bRHVVA NSnH4xuAWnDgUIpi+csZOvq0K6u99Dt1IUGDrzZGcgQ
L9WOetohzpDPAcF/1jGPmWnBhazSDyIU/0or+il2/ug
-> ssh-ed25519 HgW9eA LdB9v8WGmExJffyQa8KuudNr97nyxshWj9j0YMDJKlI
0BCRYHWmulVjwtlYUR2Xi4rdnLSRQTLL2jn8N5rubSo
--- y4i6KM8sEq07Sx594lC7WX9hEtq9ce4Vm9tTU8loJtc
r};‰„Ï,"¾èívß&ê­ÀÂd¬¼}™Ž×8Uê_¼¨VY¸µÅn§h€ZúħÈgê`Nf`

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
age-encryption.org/v1
-> ssh-ed25519 vZ8Vgw +B57yVih+Nm3HaJfX53NcVTIUChaN4QRHpIhNRh0wig
2Icnq7jf1+suEjUYnQ1ABx3gdTefT6eZJ6KVEupA3Xg
-> piv-p256 ewCc3w A2uBciAE9cSCBJWLowDc4d5D4GQsroJ1EX3BJsr0w8+T
FUJZET8ck71xPBh2eWF8930JacE89+R2n2i32hFhAlg
-> piv-p256 6CL/Pw AgCcZy/RvYFO4WZz5/Os29sXgkDGdDLSPl4ZBdJjONPk
J7eaYHcgnrLEm4PMVh8qXHB8g/qpx7huoRgHNgO2aXw
-> ssh-ed25519 I2EdxQ +7WH3VaqC48ABO8Wpcb7hlY04lhKetfyYm3+62JESFo
iZXrtZ0+UFdvcn75ceJ6gxdXc410BZdgnPSaJsG3muA
-> ssh-ed25519 J/iReg rUhw71duo5PP4M3ySN7xHW1ebsXJ6iYFdj3eZUriGgA
pSAHL6A9lCJ5qFqK26PJLG84d6nIs4psL3ea5Pd8Tac
-> ssh-ed25519 GNhSGw zRjF+gRLm7IibS6joIBlOInFygsHVo7vf10IsYWx20o
93aGm4IS9bc/noU/2l3sMAKJbf5EkDU5gDDxDDdomak
-> ssh-ed25519 eXMAtA 2QLLIa6MylYi7h0KOyeZCxQQuyMPtcRTWu5Mg8oRpSI
VwWCwogmcmAQs86ABxxbuWdK7XWh071HPdQdPeeS7I8
-> ssh-ed25519 5hXocQ AIMNW8H9VIA/wabPNGB54KGv2OT4iUtX4b6emWTpYk0
vIj14LjtUcJ2GVDrStg40xHjpkEAkv53qnXdtaweuAk
--- iqSZN2bLs1BukHdpv8L48ir+5W4DeJ4ZviSn2hj9Eqc
w?<3F>ă
pjć”sN®˙€ ý—úšĂćm¬)}Zś,Ť!|3Ľ±#ťKßÖ<C396>^ËČ&o,ő0ů/K¬1J« †Őč;ď'ńÁ-ÚQ±ŰśGş°
@Íă‰ďË˙Ż 1Ţą?ę“pďز ç.ˇ][řő÷5D$+ ´ÍÍ9Ű´Ä$ářPą…đ¨ĄjŮ_Ë“ÄÝTqźx9<19>sŚH8=;1ŇÎWSšjŞ+”×üÍů"ľHäéĆÝ<C486>\_Ą<5F>ú1y†ÔgG†PĽ<¸˝!1jĹů`ôćć2ë™CRL"8§<38>ń *ePžćńá—±<E28094><C2B1>˘Řţľľ)ťu=GNTŇéâĂawť_żČ<C48C> Mˇkií±@é>

Binary file not shown.