mirror of https://gitlab.crans.org/nounous/nixos
Merge branch 'wiki' into 'main'
Draft: Wiki See merge request nounous/nixos!60merge-requests/60/merge
commit
2f2b5e6c6c
|
|
@ -1 +1,2 @@
|
|||
result
|
||||
*.qcow2
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@
|
|||
specialArgs = inputs;
|
||||
modules = [ ./hosts/vm/livre ] ++ baseModules;
|
||||
};
|
||||
|
||||
mediakiwi = nixosSystem {
|
||||
specialArgs = inputs;
|
||||
modules = [ ./hosts/vm/mediakiwi ] ++ baseModules;
|
||||
};
|
||||
|
||||
neo = nixosSystem {
|
||||
specialArgs = inputs;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
$magicWords = [];
|
||||
$magicWords['en'] = [
|
||||
'calendar' => [ 0, 'calendar' ],
|
||||
];
|
||||
$magicWords['fr'] = [
|
||||
'calendar' => [ 0, 'calendrier' ],
|
||||
];
|
||||
?>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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 ) {
|
||||
|
|
@ -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
|
||||
];
|
||||
```
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
@ -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 doesn’t 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 doesn’t 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
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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 ];
|
||||
};
|
||||
}
|
||||
|
|
@ -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'; # C’est 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 l’utilisateurice
|
||||
$wgEnotifRevealEditorAddress = false; # Pas de champ répondre avec l’adresse mail de l’utilisateurice
|
||||
$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 l’envoie 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 s’occupe des du contrôle d’accè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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ let
|
|||
collabora = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFa2D9fREtO2r2oIx6q9JAKFUHtxGbgEPMjkx09DQSU8 root@collabora";
|
||||
jitsi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB6jVMIZ5y2oXX9HOkw7r5UUjw95MlFaFuu7FnEC0Q8z root@jitsi";
|
||||
livre = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEVfKNokHG6ig32hhQxTep+fKFmKahlDClPrX/dP4/gb root@livre";
|
||||
mediakiwi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFfcKtim7Aj+strHdg847D7KCCdoOAuasxx/2yi2TY1n root@mediakiwi";
|
||||
neo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMGfSvxqC2PJYRrxJaivVDujwlwCZ6AwH8hOSA9ktZ1V root@neo";
|
||||
nextcloud = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgSP9UmuJw8Bi2ML07WHsWvxN8akkc9XZxXyOgdjXkq root@nextcloud";
|
||||
periodique = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTdfSIL3AWIv0mjRDam6E/qsjoqwJ8QSm1Cb0xqs1s1 root@periodique";
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
¢Bá.áZñ§—3PÀ5¸ÇwÔ!™)Ñ<>}Ú[UèB™ö±ÛråNØÚI^
|
||||
|
|
@ -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
|
||||
2Õ‹r};‰„Ï,"¾èÃvß&êÀÂd¬¼}™Ž×8Uê_¼¨VY¸µÅn§h€ZúħÈgê`Nf`
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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ë™CR‘L"8§<38>ń *ePžćńá—±<E28094><C2B1>˘Řţľľ)ťu=GNTŇéâĂawť_żČ’<C48C> Mˇkií±@é>
|
||||
Binary file not shown.
Loading…
Reference in New Issue