yannstatic/static/2023/07/13/Tiny-Tiny-RSS_ttrss.html

2369 lines
212 KiB
HTML
Raw Normal View History

2024-10-31 20:18:37 +01:00
<!DOCTYPE html><html lang="fr">
<head><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"><title>Tiny Tiny RSS (ttrss) - YannStatic</title>
<meta name="description" content="Un agrégateur de flux RSS est un outil permettant de rassembler automatiquement, sur une même interface, les articles parus sur différents sites. Utilisé pou...">
<link rel="canonical" href="https://static.rnmkcy.eu/2023/07/13/Tiny-Tiny-RSS_ttrss.html"><link rel="alternate" type="application/rss+xml" title="YannStatic" href="/feed.xml">
<!-- - include head/favicon.html - -->
<link rel="shortcut icon" type="image/png" href="/assets/favicon/favicon.png"><link rel="stylesheet" href="/assets/css/main.css"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" ><!-- start custom head snippets --><link rel="stylesheet" href="/assets/css/expand.css">
<!-- end custom head snippets --><script>(function() {
window.isArray = function(val) {
return Object.prototype.toString.call(val) === '[object Array]';
};
window.isString = function(val) {
return typeof val === 'string';
};
window.hasEvent = function(event) {
return 'on'.concat(event) in window.document;
};
window.isOverallScroller = function(node) {
return node === document.documentElement || node === document.body || node === window;
};
window.isFormElement = function(node) {
var tagName = node.tagName;
return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
};
window.pageLoad = (function () {
var loaded = false, cbs = [];
window.addEventListener('load', function () {
var i;
loaded = true;
if (cbs.length > 0) {
for (i = 0; i < cbs.length; i++) {
cbs[i]();
}
}
});
return {
then: function(cb) {
cb && (loaded ? cb() : (cbs.push(cb)));
}
};
})();
})();
(function() {
window.throttle = function(func, wait) {
var args, result, thisArg, timeoutId, lastCalled = 0;
function trailingCall() {
lastCalled = new Date;
timeoutId = null;
result = func.apply(thisArg, args);
}
return function() {
var now = new Date,
remaining = wait - (now - lastCalled);
args = arguments;
thisArg = this;
if (remaining <= 0) {
clearTimeout(timeoutId);
timeoutId = null;
lastCalled = now;
result = func.apply(thisArg, args);
} else if (!timeoutId) {
timeoutId = setTimeout(trailingCall, remaining);
}
return result;
};
};
})();
(function() {
var Set = (function() {
var add = function(item) {
var i, data = this._data;
for (i = 0; i < data.length; i++) {
if (data[i] === item) {
return;
}
}
this.size ++;
data.push(item);
return data;
};
var Set = function(data) {
this.size = 0;
this._data = [];
var i;
if (data.length > 0) {
for (i = 0; i < data.length; i++) {
add.call(this, data[i]);
}
}
};
Set.prototype.add = add;
Set.prototype.get = function(index) { return this._data[index]; };
Set.prototype.has = function(item) {
var i, data = this._data;
for (i = 0; i < data.length; i++) {
if (this.get(i) === item) {
return true;
}
}
return false;
};
Set.prototype.is = function(map) {
if (map._data.length !== this._data.length) { return false; }
var i, j, flag, tData = this._data, mData = map._data;
for (i = 0; i < tData.length; i++) {
for (flag = false, j = 0; j < mData.length; j++) {
if (tData[i] === mData[j]) {
flag = true;
break;
}
}
if (!flag) { return false; }
}
return true;
};
Set.prototype.values = function() {
return this._data;
};
return Set;
})();
window.Lazyload = (function(doc) {
var queue = {js: [], css: []}, sources = {js: {}, css: {}}, context = this;
var createNode = function(name, attrs) {
var node = doc.createElement(name), attr;
for (attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
node.setAttribute(attr, attrs[attr]);
}
}
return node;
};
var end = function(type, url) {
var s, q, qi, cbs, i, j, cur, val, flag;
if (type === 'js' || type ==='css') {
s = sources[type], q = queue[type];
s[url] = true;
for (i = 0; i < q.length; i++) {
cur = q[i];
if (cur.urls.has(url)) {
qi = cur, val = qi.urls.values();
qi && (cbs = qi.callbacks);
for (flag = true, j = 0; j < val.length; j++) {
cur = val[j];
if (!s[cur]) {
flag = false;
}
}
if (flag && cbs && cbs.length > 0) {
for (j = 0; j < cbs.length; j++) {
cbs[j].call(context);
}
qi.load = true;
}
}
}
}
};
var load = function(type, urls, callback) {
var s, q, qi, node, i, cur,
_urls = typeof urls === 'string' ? new Set([urls]) : new Set(urls), val, url;
if (type === 'js' || type ==='css') {
s = sources[type], q = queue[type];
for (i = 0; i < q.length; i++) {
cur = q[i];
if (_urls.is(cur.urls)) {
qi = cur;
break;
}
}
val = _urls.values();
if (qi) {
callback && (qi.load || qi.callbacks.push(callback));
callback && (qi.load && callback());
} else {
q.push({
urls: _urls,
callbacks: callback ? [callback] : [],
load: false
});
for (i = 0; i < val.length; i++) {
node = null, url = val[i];
if (s[url] === undefined) {
(type === 'js' ) && (node = createNode('script', { src: url }));
(type === 'css') && (node = createNode('link', { rel: 'stylesheet', href: url }));
if (node) {
node.onload = (function(type, url) {
return function() {
end(type, url);
};
})(type, url);
(doc.head || doc.body).appendChild(node);
s[url] = false;
}
}
}
}
}
};
return {
js: function(url, callback) {
load('js', url, callback);
},
css: function(url, callback) {
load('css', url, callback);
}
};
})(this.document);
})();
</script><script>
(function() {
var TEXT_VARIABLES = {
version: '2.2.6',
sources: {
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css',
jquery: '/assets/js/jquery.min.js',
leancloud_js_sdk: '//cdn.jsdelivr.net/npm/leancloud-storage@3.13.2/dist/av-min.js',
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js',
gitalk: {
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js',
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
},
valine: 'https://unpkg.com/valine/dist/Valine.min.js'
},
site: {
toc: {
selectors: 'h1,h2,h3'
}
},
paths: {
search_js: '/assets/search.js'
}
};
window.TEXT_VARIABLES = TEXT_VARIABLES;
})();
</script>
</head>
<body>
<div class="root" data-is-touch="false">
<div class="layout--page js-page-root"><!----><div class="page__main js-page-main page__viewport hide-footer has-aside has-aside cell cell--auto">
<div class="page__main-inner"><div class="page__header d-print-none"><header class="header"><div class="main">
<div class="header__title">
<div class="header__brand"><svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="478.9473684210526" viewBox="0, 0, 400,478.9473684210526"><g id="svgg"><path id="path0" d="M308.400 56.805 C 306.970 56.966,303.280 57.385,300.200 57.738 C 290.906 58.803,278.299 59.676,269.200 59.887 L 260.600 60.085 259.400 61.171 C 258.010 62.428,256.198 63.600,255.645 63.600 C 255.070 63.600,252.887 65.897,252.598 66.806 C 252.460 67.243,252.206 67.600,252.034 67.600 C 251.397 67.600,247.206 71.509,247.202 72.107 C 247.201 72.275,246.390 73.190,245.400 74.138 C 243.961 75.517,243.598 76.137,243.592 77.231 C 243.579 79.293,241.785 83.966,240.470 85.364 C 239.176 86.740,238.522 88.365,237.991 91.521 C 237.631 93.665,236.114 97.200,235.554 97.200 C 234.938 97.200,232.737 102.354,232.450 104.472 C 232.158 106.625,230.879 109.226,229.535 110.400 C 228.933 110.926,228.171 113.162,226.434 119.500 C 226.178 120.435,225.795 121.200,225.584 121.200 C 225.373 121.200,225.200 121.476,225.200 121.813 C 225.200 122.149,224.885 122.541,224.500 122.683 C 223.606 123.013,223.214 123.593,223.204 124.600 C 223.183 126.555,220.763 132.911,219.410 134.562 C 218.443 135.742,217.876 136.956,217.599 138.440 C 217.041 141.424,215.177 146.434,214.532 146.681 C 214.240 146.794,214.000 147.055,214.000 147.261 C 214.000 147.467,213.550 148.086,213.000 148.636 C 212.450 149.186,212.000 149.893,212.000 150.208 C 212.000 151.386,208.441 154.450,207.597 153.998 C 206.319 153.315,204.913 150.379,204.633 147.811 C 204.365 145.357,202.848 142.147,201.759 141.729 C 200.967 141.425,199.200 137.451,199.200 135.974 C 199.200 134.629,198.435 133.224,196.660 131.311 C 195.363 129.913,194.572 128.123,193.870 125.000 C 193.623 123.900,193.236 122.793,193.010 122.540 C 190.863 120.133,190.147 118.880,188.978 115.481 C 188.100 112.928,187.151 111.003,186.254 109.955 C 185.358 108.908,184.518 107.204,183.847 105.073 C 183.280 103.273,182.497 101.329,182.108 100.753 C 181.719 100.177,180.904 98.997,180.298 98.131 C 179.693 97.265,178.939 95.576,178.624 94.378 C 178.041 92.159,177.125 90.326,175.023 87.168 C 174.375 86.196,173.619 84.539,173.342 83.486 C 172.800 81.429,171.529 79.567,170.131 78.785 C 169.654 78.517,168.697 77.511,168.006 76.549 C 167.316 75.587,166.594 74.800,166.402 74.800 C 166.210 74.800,164.869 73.633,163.421 72.206 C 160.103 68.936,161.107 69.109,146.550 69.301 C 133.437 69.474,128.581 70.162,126.618 72.124 C 126.248 72.495,125.462 72.904,124.872 73.033 C 124.282 73.163,123.088 73.536,122.219 73.863 C 121.349 74.191,119.028 74.638,117.061 74.858 C 113.514 75.254,109.970 76.350,108.782 77.419 C 107.652 78.436,100.146 80.400,97.388 80.400 C 95.775 80.400,93.167 81.360,91.200 82.679 C 90.430 83.195,89.113 83.804,88.274 84.031 C 85.875 84.681,78.799 90.910,74.400 96.243 L 73.400 97.456 73.455 106.028 C 73.526 117.055,74.527 121.238,77.820 124.263 C 78.919 125.273,80.400 127.902,80.400 128.842 C 80.400 129.202,81.075 130.256,81.900 131.186 C 83.563 133.059,85.497 136.346,86.039 138.216 C 86.233 138.886,87.203 140.207,88.196 141.153 C 89.188 142.098,90.000 143.104,90.000 143.388 C 90.000 144.337,92.129 148.594,92.869 149.123 C 93.271 149.410,93.600 149.831,93.600 150.059 C 93.600 150.286,93.932 150.771,94.337 151.136 C 94.743 151.501,95.598 153.004,96.237 154.475 C 96.877 155.947,97.760 157.351,98.200 157.596 C 98.640 157.841,99.900 159.943,101.000 162.267 C 102.207 164.817,103.327 166.644,103.825 166.876 C 104.278 167.087,105.065 168.101,105.573 169.130 C 107.658 173.348,108.097 174.093,110.006 176.647 C 111.103 178.114,112.000 179.725,112.000 180.227 C 112.000 181.048,113.425 183.163,114.678 184.200 C 115.295 184.711,117.396 188.733,117.720 190.022 C 117.855 190.562,118.603 191.633,119.381 192.402 C 120.160 193.171,121.496 195.258,122.351 197.039 C 123.206 198.820,124.167 200.378,124.487 200.501 C 124.807 200.624,125.953 202.496,127.034 204.662 C 128.114 206.828,129.676 209.299,130.505 210.153 C 131.333 211.007,132.124 212.177,132.262 212.753 C 132.618 214.239,134.291 217.048,136.288 219.5
" href="/">YannStatic</a></div><!--<button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button>--><!-- <li><button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button></li> -->
<!-- Champ de recherche -->
<div id="searchbox" class="search search--dark" style="visibility: visible">
<div class="main">
<div class="search__header"></div>
<div class="search-bar">
<div class="search-box js-search-box">
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
<input id="search-input" type="text" />
<!-- <div class="search-box__icon-clear js-icon-clear">
<a><i class="fas fa-times"></i></a>
</div> -->
</div>
</div>
</div>
</div>
<!-- Script pointing to search-script.js -->
<script>/*!
* Simple-Jekyll-Search
* Copyright 2015-2020, Christian Fei
* Licensed under the MIT License.
*/
(function(){
'use strict'
var _$Templater_7 = {
compile: compile,
setOptions: setOptions
}
const options = {}
options.pattern = /\{(.*?)\}/g
options.template = ''
options.middleware = function () {}
function setOptions (_options) {
options.pattern = _options.pattern || options.pattern
options.template = _options.template || options.template
if (typeof _options.middleware === 'function') {
options.middleware = _options.middleware
}
}
function compile (data) {
return options.template.replace(options.pattern, function (match, prop) {
const value = options.middleware(prop, data[prop], options.template)
if (typeof value !== 'undefined') {
return value
}
return data[prop] || match
})
}
'use strict';
function fuzzysearch (needle, haystack) {
var tlen = haystack.length;
var qlen = needle.length;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
return needle === haystack;
}
outer: for (var i = 0, j = 0; i < qlen; i++) {
var nch = needle.charCodeAt(i);
while (j < tlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
var _$fuzzysearch_1 = fuzzysearch;
'use strict'
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
function FuzzySearchStrategy () {
this.matches = function (string, crit) {
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
}
}
'use strict'
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
function LiteralSearchStrategy () {
this.matches = function (str, crit) {
if (!str) return false
str = str.trim().toLowerCase()
crit = crit.trim().toLowerCase()
return crit.split(' ').filter(function (word) {
return str.indexOf(word) >= 0
}).length === crit.split(' ').length
}
}
'use strict'
var _$Repository_4 = {
put: put,
clear: clear,
search: search,
setOptions: __setOptions_4
}
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
function NoSort () {
return 0
}
const data = []
let opt = {}
opt.fuzzy = false
opt.limit = 10
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = NoSort
opt.exclude = []
function put (data) {
if (isObject(data)) {
return addObject(data)
}
if (isArray(data)) {
return addArray(data)
}
return undefined
}
function clear () {
data.length = 0
return data
}
function isObject (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
}
function addObject (_data) {
data.push(_data)
return data
}
function addArray (_data) {
const added = []
clear()
for (let i = 0, len = _data.length; i < len; i++) {
if (isObject(_data[i])) {
added.push(addObject(_data[i]))
}
}
return added
}
function search (crit) {
if (!crit) {
return []
}
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
}
function __setOptions_4 (_opt) {
opt = _opt || {}
opt.fuzzy = _opt.fuzzy || false
opt.limit = _opt.limit || 10
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = _opt.sort || NoSort
opt.exclude = _opt.exclude || []
}
function findMatches (data, crit, strategy, opt) {
const matches = []
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
const match = findMatchesInObject(data[i], crit, strategy, opt)
if (match) {
matches.push(match)
}
}
return matches
}
function findMatchesInObject (obj, crit, strategy, opt) {
for (const key in obj) {
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
return obj
}
}
}
function isExcluded (term, excludedTerms) {
for (let i = 0, len = excludedTerms.length; i < len; i++) {
const excludedTerm = excludedTerms[i]
if (new RegExp(excludedTerm).test(term)) {
return true
}
}
return false
}
/* globals ActiveXObject:false */
'use strict'
var _$JSONLoader_2 = {
load: load
}
function load (location, callback) {
const xhr = getXHR()
xhr.open('GET', location, true)
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
xhr.send()
}
function createStateChangeListener (xhr, callback) {
return function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
callback(null, JSON.parse(xhr.responseText))
} catch (err) {
callback(err, null)
}
}
}
}
function getXHR () {
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
}
'use strict'
var _$OptionsValidator_3 = function OptionsValidator (params) {
if (!validateParams(params)) {
throw new Error('-- OptionsValidator: required options missing')
}
if (!(this instanceof OptionsValidator)) {
return new OptionsValidator(params)
}
const requiredOptions = params.required
this.getRequiredOptions = function () {
return requiredOptions
}
this.validate = function (parameters) {
const errors = []
requiredOptions.forEach(function (requiredOptionName) {
if (typeof parameters[requiredOptionName] === 'undefined') {
errors.push(requiredOptionName)
}
})
return errors
}
function validateParams (params) {
if (!params) {
return false
}
return typeof params.required !== 'undefined' && params.required instanceof Array
}
}
'use strict'
var _$utils_9 = {
merge: merge,
isJSON: isJSON
}
function merge (defaultParams, mergeParams) {
const mergedOptions = {}
for (const option in defaultParams) {
mergedOptions[option] = defaultParams[option]
if (typeof mergeParams[option] !== 'undefined') {
mergedOptions[option] = mergeParams[option]
}
}
return mergedOptions
}
function isJSON (json) {
try {
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
return true
}
return false
} catch (err) {
return false
}
}
var _$src_8 = {};
(function (window) {
'use strict'
let options = {
searchInput: null,
resultsContainer: null,
json: [],
success: Function.prototype,
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
templateMiddleware: Function.prototype,
sortMiddleware: function () {
return 0
},
noResultsText: 'No results found',
limit: 10,
fuzzy: false,
debounceTime: null,
exclude: []
}
let debounceTimerHandle
const debounce = function (func, delayMillis) {
if (delayMillis) {
clearTimeout(debounceTimerHandle)
debounceTimerHandle = setTimeout(func, delayMillis)
} else {
func.call()
}
}
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
/* removed: const _$Templater_7 = require('./Templater') */;
/* removed: const _$Repository_4 = require('./Repository') */;
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
const optionsValidator = _$OptionsValidator_3({
required: requiredOptions
})
/* removed: const _$utils_9 = require('./utils') */;
window.SimpleJekyllSearch = function (_options) {
const errors = optionsValidator.validate(_options)
if (errors.length > 0) {
throwError('You must specify the following required options: ' + requiredOptions)
}
options = _$utils_9.merge(options, _options)
_$Templater_7.setOptions({
template: options.searchResultTemplate,
middleware: options.templateMiddleware
})
_$Repository_4.setOptions({
fuzzy: options.fuzzy,
limit: options.limit,
sort: options.sortMiddleware,
exclude: options.exclude
})
if (_$utils_9.isJSON(options.json)) {
initWithJSON(options.json)
} else {
initWithURL(options.json)
}
const rv = {
search: search
}
typeof options.success === 'function' && options.success.call(rv)
return rv
}
function initWithJSON (json) {
_$Repository_4.put(json)
registerInput()
}
function initWithURL (url) {
_$JSONLoader_2.load(url, function (err, json) {
if (err) {
throwError('failed to get JSON (' + url + ')')
}
initWithJSON(json)
})
}
function emptyResultsContainer () {
options.resultsContainer.innerHTML = ''
}
function appendToResultsContainer (text) {
options.resultsContainer.innerHTML += text
}
function registerInput () {
options.searchInput.addEventListener('input', function (e) {
if (isWhitelistedKey(e.which)) {
emptyResultsContainer()
debounce(function () { search(e.target.value) }, options.debounceTime)
}
})
}
function search (query) {
if (isValidQuery(query)) {
emptyResultsContainer()
render(_$Repository_4.search(query), query)
}
}
function render (results, query) {
const len = results.length
if (len === 0) {
return appendToResultsContainer(options.noResultsText)
}
for (let i = 0; i < len; i++) {
results[i].query = query
appendToResultsContainer(_$Templater_7.compile(results[i]))
}
}
function isValidQuery (query) {
return query && query.length > 0
}
function isWhitelistedKey (key) {
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
}
function throwError (message) {
throw new Error('SimpleJekyllSearch --- ' + message)
}
})(window)
}());
</script>
<!-- Configuration -->
<script>
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
json: '/search.json',
//searchResultTemplate: '<li><a href="https://static.rnmkcy.eu{url}">{date}&nbsp;{title}</a></li>'
searchResultTemplate: '<li><a href="{url}">{date}&nbsp;{title}</a></li>'
})
</script>
<!-- Fin déclaration champ de recherche --></div><nav class="navigation">
<ul><li class="navigation__item"><a href="/archive.html">Etiquettes</a></li><li class="navigation__item"><a href="/htmldoc.html">Documents</a></li><li class="navigation__item"><a href="/liens_ttrss.html">Liens</a></li><li class="navigation__item"><a href="/aide-jekyll-text-theme.html">Aide</a></li></ul>
</nav></div>
</header>
</div><div class="page__content"><div class ="main"><div class="grid grid--reverse">
<div class="col-main cell cell--auto"><!-- start custom main top snippet --><div id="results-container" class="search-result js-search-result"></div><!-- end custom main top snippet -->
<article itemscope itemtype="http://schema.org/Article"><div class="article__header"><header><h1 style="color:Tomato;">Tiny Tiny RSS (ttrss)</h1></header></div><meta itemprop="headline" content="Tiny Tiny RSS (ttrss)"><div class="article__info clearfix"><ul class="left-col menu"><li>
<a class="button button--secondary button--pill button--sm"
href="/archive.html?tag=rss">rss</a>
</li></ul><ul class="right-col menu"><li>
<i class="far fa-calendar-alt"></i>&nbsp;<span title="Création" style="color:#FF00FF">13&nbsp;juil.&nbsp;2023</span>
<span title="Modification" style="color:#00FF7F">17&nbsp;déc.&nbsp;&nbsp;2023</span></li></ul></div><meta itemprop="datePublished" content="2023-12-17T00:00:00+01:00">
<meta itemprop="keywords" content="rss"><div class="js-article-content">
<div class="layout--article"><!-- start custom article top snippet -->
<style>
#myBtn {
display: none;
position: fixed;
bottom: 10px;
right: 10px;
z-index: 99;
font-size: 12px;
font-weight: bold;
border: none;
outline: none;
background-color: white;
color: black;
cursor: pointer;
padding: 5px;
border-radius: 4px;
}
#myBtn:hover {
background-color: #555;
}
</style>
<button onclick="topFunction()" id="myBtn" title="Haut de page">&#8679;</button>
<script>
//Get the button
var mybutton = document.getElementById("myBtn");
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function() {scrollFunction()};
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
mybutton.style.display = "block";
} else {
mybutton.style.display = "none";
}
}
// When the user clicks on the button, scroll to the top of the document
function topFunction() {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
</script>
<!-- end custom article top snippet -->
<div class="article__content" itemprop="articleBody"><details>
<summary><b>Afficher/cacher Sommaire</b></summary>
<!-- affichage sommaire -->
<div class="toc-aside js-toc-root"></div>
</details><p><em>Un agrégateur de flux RSS est un outil permettant de rassembler automatiquement, sur une même interface, les articles parus sur différents sites. Utilisé pour la veille et la curation, il facilite le classement de linformation récupérée sur les blogs, médias en ligne, mais aussi sur les réseaux sociaux.</em></p>
<h2 id="tiny-tiny-rss-avec-nginx-php-fpm-et-mariadb">Tiny Tiny RSS avec Nginx, PHP-FPM et MariaDB</h2>
<p><img src="/images/rss-07552-128x128.png" alt="RSS" /></p>
<ul>
<li><a href="https://howto.wared.fr/tiny-tiny-rss-ubuntu-nginx-php-fpm-mysql/">Ubuntu 18.04 LTS Installation de Tiny Tiny RSS avec Nginx, PHP-FPM et MariaDB</a></li>
<li><a href="https://framacloud.org/fr/cultiver-son-jardin/ttrss.html">Framacloud - Installation de TinyTinyRSS</a></li>
</ul>
<p>Tiny Tiny RSS avantages et fonctionnalités :</p>
<ul>
<li>Interface fluide et responsive</li>
<li>Multi-utilisateurs</li>
<li>Mode hors-connexion pour continuer à lire vos feeds</li>
<li>Multilingue</li>
<li>Gestion des tags et catégories</li>
<li>Moteur de recherche</li>
<li>Raccourcis clavier</li>
<li>Support du format OPML</li>
<li>Podcasts</li>
<li>Personnalisations via des plugins et des thèmes</li>
<li>Applications Android et iOS</li>
<li>Gratuit</li>
</ul>
<p><em>Côté client, seul un navigateur est nécessaire, côté serveur, Tiny Tiny RSS a besoin dun serveur web (Nginx), de PHP, dune interface permettant la communication entre le serveur web et PHP (PHP-FPM) et dune base de données (MariaDB). Amélioration des performances de tt-rss grâce à OPCache ,sécurisation des échanges grâce à un certificat SSL/TLS.</em></p>
<h3 id="prérequis">Prérequis</h3>
<p><strong>nginx</strong>, <strong>PHP8.x</strong> et <strong>mariadb</strong> sont installés</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx version: nginx/1.24.0
PHP 8.2.13 (cli) (built: Nov 24 2023 13:10:42) (NTS)
mysql Ver 15.1 Distrib 10.11.4-MariaDB, for debian-linux-gnu (x86_64) using EditLine wrapper
</code></pre></div></div>
<h3 id="installation-tiny-tiny-rss">Installation Tiny Tiny RSS</h3>
<ul>
<li>Upstream app code repository: <a href="https://gitlab.tt-rss.org/tt-rss/tt-rss">https://gitlab.tt-rss.org/tt-rss/tt-rss</a></li>
</ul>
<p>Passage en super utilisateur</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo -s
</code></pre></div></div>
<p>Git</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /var/www
git clone https://gitea.xoyize.xyz/yako/ttrss
</code></pre></div></div>
<p>Pour des raisons évidentes de sécurité, il est donc recommandé de cloisonner ces utilisateurs et davoir un utilisateur dédié à la gestion du dossier ttrss. Cet utilisateur aura des droits aussi restreints que possible à ce répertoire.</p>
<p>Modifier le propriétaire du répertoire <strong>/var/www/ttrss</strong> et lattribuer à un nouvel utilisateur dédié <em>ttrss</em><br />
Nginx est lancé sous lutilisateur <em>www-data</em> et doit avoir accès en lecture au répertoire <strong>/var/www/ttrss</strong> pour lire les ressources statiques (HTML, CSS, JS, etc.).<br />
Attribuer le répertoire <strong>/var/www/ttrss</strong> au groupe <strong>www-data</strong>.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>useradd ttrss <span class="c"># création utilisateur dédié ttrss</span>
<span class="nb">chown</span> <span class="nt">-R</span> ttrss:www-data /var/www/ttrss <span class="c"># changement de propriétaire par ttrss et groupe par www-data</span>
</code></pre></div></div>
<p>Retirer toutes les permissions de ce répertoire aux autres utilisateurs.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod -R o-rwx /var/www/ttrss
</code></pre></div></div>
<h3 id="php-et-ses-modules">PHP et ses modules</h3>
<p>Tiny Tiny RSS nécessite certains modules PHP pour fonctionner :</p>
<ul>
<li>PHP PDO : connecteur pour MariaDB</li>
<li>PHP XML : opérations sur la lecture des flux RSS</li>
<li>PHP mbstring : support des caractères multi-octets</li>
<li>PHP fileinfo : améliore les performances danalyse de fichiers</li>
<li>PHP CURL : nécessaire pour certains plugins</li>
<li>PHP POSIX : nécessaire pour le processus de mise à jour des flux</li>
<li>PHP GD : opérations sur les images présentes dans les flux</li>
<li>PHP INTL : support de linternationalisation.</li>
</ul>
<p>Installer les paquets suivants : php-fileinfo php-posix</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt install php8.2 php8.2-fpm php8.2-cli php8.2-mysql php8.2-xml php8.2-mbstring php8.2-curl php8.2-gd php8.2-intl
</code></pre></div></div>
<h3 id="pool-php-fpm">Pool PHP-FPM</h3>
<p><em>Le module PHP-FPM permet la communication entre le serveur Nginx et PHP, basée sur le protocole FastCGI. Ce module, écoutant sur le port 9000 par défaut ou sur un socket UNIX, permet notamment lexécution de scripts PHP dans un processus indépendant de Nginx avec des UID et GID différents. Il sera alors possible, dans le cas de la gestion de plusieurs applications sur un même serveur, de créer et configurer un groupe (appelé aussi pool) par application. Un pool définit notamment le UID/GID des processus PHP et le nombre de processus minimum, maximum ou encore le nombre de processus en attente à lancer.</em></p>
<p><strong>Création du pool dédié à Tiny Tiny RSS</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano /etc/php/8.2/fpm/pool.d/ttrss.conf
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ttrss]
listen = /run/php/php8.2-fpm-ttrss.sock
listen.owner = ttrss
listen.group = www-data
user = ttrss
group = www-data
pm = ondemand
pm.max_children = 6
pm.process_idle_timeout = 60s
pm.max_requests = 500
</code></pre></div></div>
<p>Redémarrer le service php-fpm afin dactiver le nouveau pool ttrss :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl restart php8.2-fpm.service
</code></pre></div></div>
<p><strong>Description des paramètres</strong></p>
<ul>
<li><strong>[ttrss]</strong> : nom du pool. Il est possible de créer plusieurs pools par fichier. Chaque pool doit commencer par cette directive.</li>
<li><strong>listen</strong> : interface découte des requêtes. Les syntaxes acceptées sont ADRESSE_IP:PORT (exemple : listen = 128.2.0.1:9000) et /path/to/unix/socket (exemple : listen = /var/run/ttrss.sock). Le socket est représenté comme un simple fichier sur le système et permet dinterfacer des processus entre eux sans passer par la couche réseau du système, ce qui est inutile lorsque Nginx et PHP-FPM sont hébergés sur le même serveur. Je vous conseille donc dutiliser un socket.</li>
<li><strong>listen.owner</strong> &amp; <strong>listen.group</strong> : affecte lutilisateur et le groupe au socket Unix si utilisé. Ces deux paramètres peuvent être associés au paramètre listen.mode qui définit les permissions du socket (660 par défaut). Il est important que Nginx ait les droits de lecture sur le socket Unix.</li>
<li><strong>user</strong> &amp; <strong>group</strong> : utilisateur et groupe sous lesquels le pool de processus sera exécuté. Cet utilisateur et ce groupe doivent bien sûr exister sur votre système et surtout accéder aux fichiers PHP de votre tt-rss. Cela veut dire aussi que chaque fichier et répertoire créé dans tt-rss appartiendra à cet utilisateur et à ce groupe. Comme nous lavons vu dans le chapitre dédié aux droits Unix, chaque fichier devra appartenir à lutilisateur ttrss et au groupe www-data.</li>
<li><strong>pm</strong> : directive acceptant les 3 valeurs suivantes : static, dynamic et ondemand.
<ul>
<li><em>static</em> : les processus, au nombre de pm.max_children, sont continuellement actifs (quelle que soit la charge et laffluence de votre tt-rss) et sont susceptibles de consommer de la mémoire inutilement. Cette directive est recommandée si tt-rss est lunique application de votre serveur.</li>
<li><em>dynamic</em> : le nombre de processus fils pourra varier suivant la charge. Cependant, nous gardons le contrôle sur le nombre de processus fils à créer au démarrage du serveur, le nombre de processus maximum, en attente de requêtes, etc. Les directives suivantes deviennent obligatoires : pm.max_children, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers. Cette directive est recommandée si vous avez plusieurs pools avec un fort trafic (plus de 10 000 requêtes/jour).
* <em>ondemand</em> : aucun processus fils nest lancé au démarrage du serveur, les processus sactivent à la demande et auront une durée de vie définie par la directive pm.process_idle_timeout. Lintérêt de cette directive est de libérer de la mémoire en cas de faible charge mais celle-ci peut légèrement augmenter le temps de réponse de votre tt-rss. Cette directive est recommandée si vous avez plusieurs pools avec potentiellement une faible affluence.</li>
</ul>
</li>
</ul>
<blockquote>
<p><em>Sachant que lutilisation de tt-rss est personnelle et souvent limitée à quelques utilisateurs, nous choisirons et détaillerons ici la directive ondemand.</em></p>
</blockquote>
<ul>
<li><strong>pm.process_idle_timeout</strong> : durée en secondes avant quun processus fils inactif soit détruit.</li>
<li><strong>pm.max_requests</strong> : nombre de requêtes que chaque processus fils devra exécuter avant dêtre détruit. Cette valeur ne doit pas être trop élevée afin de contourner déventuelles fuites mémoires, ni trop faible pour ne pas solliciter régulièrement le CPU à chaque création de processus fils. 500 reste une valeur recommandée.</li>
<li><strong>pm.max_children</strong> : nombre maximum de processus fils. La valeur du paramètre pm.max_children varie dun système à lautre.</li>
</ul>
<p><strong>Procédure pour déterminer la valeur du paramètre <code class="language-plaintext highlighter-rouge">pm.max_children</code></strong></p>
<p>Arrêtez le service php-fpm :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> systemctl stop php8.2-fpm.service
</code></pre></div></div>
<p>Affichez la mémoire disponible (colonne available) sur votre système :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> free -m
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> total utilisé libre partagé tamp/cache disponible
Mem: 11850 1027 6231 2 4907 10822
Échange: 975 0 975
</code></pre></div></div>
<p>Sur cet exemple, le système dispose de 10822Mo de RAM disponible. La quantité de RAM que vous souhaitez allouer au maximum à tt-rss dépend de vous et des autres services actifs que vous disposez sur ce même système. Dans notre exemple, nous partirons du principe que nous souhaitons allouer au maximum 256Mo de RAM à ttrss.<br />
Affichez la mémoire utilisée par un processus fils php-fpm :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> systemctl start php8.2-fpm.service &amp;&amp; ps --no-headers -o "rss,cmd" -C php-fpm8.2 | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/NR/1024,"M") }'
</code></pre></div></div>
<p>Donne le résultat suivant</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 25M
</code></pre></div></div>
<p>Déterminez le nombre de <code class="language-plaintext highlighter-rouge">pm.max_children</code> en appliquant la méthode de calcul suivante :<br />
<code class="language-plaintext highlighter-rouge">pm.max_children</code> = mémoire allouée (en Mo) / mémoire utilisée par un processus fils<br />
Dans notre exemple : 256 / 25 = 10.24 soit 10</p>
<h3 id="opcache">OPcache</h3>
<p><em>OPcache (qui signifie Optimizer Plus Cache) est introduit depuis la version 5.5.0 de PHP. Il sert à cacher lopcode de PHP, cest-à-dire les instructions de bas niveau générées par la machine virtuelle PHP lors de lexécution dun script. Autrement dit, le code pré-compilé est stocké en mémoire. Cela évite ainsi létape de compilation à chaque requête PHP.</em></p>
<p>Vérifier et/ou activer option opcache</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano /etc/php/8.2/fpm/php.ini
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=1
</code></pre></div></div>
<p>Redémarrer le service php-fpm pour la prise en charge</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl restart php8.2-fpm.service
</code></pre></div></div>
<h3 id="création-de-la-base-de-données-sous-mariadb">Création de la base de données sous MariaDB</h3>
<p>Créer la base de données ttrss en mode su</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mariadb -e "CREATE DATABASE ttrss;"
</code></pre></div></div>
<p>Tout comme pour la gestion du répertoire ttrss et pour plus de sécurité, vous allez tout dabord créer un utilisateur MySQL ttrss dédié à la base de données ttrss, renseigner un mot de passe et ensuite lui donner les droits sur cette base de données :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mariadb -e "CREATE USER 'ttrss'@'localhost';
SET password FOR 'ttrss'@'localhost' = password('DiurnalBurineAugureBiplace');
GRANT ALL PRIVILEGES ON ttrss.* TO 'ttrss'@'localhost' IDENTIFIED BY 'DiurnalBurineAugureBiplace';
FLUSH PRIVILEGES;"
</code></pre></div></div>
<p>Importer la structure</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mariadb -uttrss -pDiurnalBurineAugureBiplace ttrss &lt; /var/www/ttrss/schema/ttrss_schema_mysql.sql
</code></pre></div></div>
<h3 id="virtualhost-rssbullsvmloc">Virtualhost rss.bullsvm.loc</h3>
<p>On utilise une configuration existante pour le HTTP2 et les certificats</p>
<blockquote>
<p>IMPORTANT : Les certificats SSL Lets Encrypt sont valables pour tous les sous-domaines de cinay.pw</p>
</blockquote>
<p>Créer le virtualhost nginx (port 443 , HTTP2 et certificats SSL)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano /etc/nginx/conf.d/rss.bullsvm.loc.conf
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name rss.bullsvm.loc;
ssl_certificate /etc/ssl/certs/bullsvm-cert.pem;
ssl_certificate_key /etc/ssl/private/bullsvm-key.pem;
root /var/www/ttrss/ ;
index index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.2-fpm-ttrss.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
}
access_log /var/log/nginx/rss.bullsvm.loc-access.log;
error_log /var/log/nginx/rss.bullsvm.loc-error.log;
}
</code></pre></div></div>
<p>Vérification</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx -t
</code></pre></div></div>
<p>La nouvelle configuration sera prise en compte après rechargement du service Nginx :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl reload nginx.service
</code></pre></div></div>
<h3 id="configuration-php-ttrss">Configuration php ttrss</h3>
<p>Modifier le fichier de configuration ttrss : <code class="language-plaintext highlighter-rouge">/var/www/ttrss/config.php</code></p>
<p><img src="/images/tiny1.png" alt="tiny" /></p>
<h3 id="première-connexion">Première connexion</h3>
<p><a href="https://rss.bullsvm.loc">https://rss.bullsvm.loc</a></p>
<p>Première connexion, login <em>admin</em> et mot de passe <em>password</em>.<br />
Changer ces valeurs par défaut du compte administrateur dans <strong>Actions… → Configuration… → Utilisateurs</strong>.<br />
Créer un utilisateur <strong>yannick</strong></p>
<blockquote>
<p>Pour mettre à jour vos flux, il vous suffit de double-cliquer sur les catégories, Tiny Tiny RSS ne se met pas à jour automatiquement.</p>
</blockquote>
<h3 id="mise-à-jour-automatique-des-flux">Mise à jour automatique des flux</h3>
<p>Processus en arrière plan, créer un service qui mettra automatiquement à jour les flux.<br />
Créer le service <strong>/etc/systemd/system/ttrss-backend.service</strong> :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano /etc/systemd/system/ttrss-backend.service
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Unit]
Description=TTRSS: News feed reader and aggregator
After=network.target mysql.service
[Service]
Type=simple
User=ttrss
Group=ttrss
WorkingDirectory=/var/www/ttrss/
ExecStart=/usr/bin/php8.2 /var/www/ttrss/update_daemon2.php
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
</code></pre></div></div>
<p>Activer et lancer le service <strong>ttrss-backend</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl daemon-reload
systemctl enable ttrss-backend
systemctl start ttrss-backend
</code></pre></div></div>
<h3 id="thème-tiny-rss">Thème tiny RSS</h3>
<p>Les thèmes sur le <a href="https://github.com/levito/tt-rss-feedly-theme">site</a><br />
Cloner les thèmes dans le dossier ttrss/themes.local</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/levito/tt-rss-feedly-theme
<span class="nb">chown</span> <span class="nt">-R</span> ttrss:www-data tt-rss-feedly-theme
<span class="nb">mv </span>tt-rss-feedly-theme/<span class="k">*</span> /var/www/ttrss/themes.local/
<span class="nb">rm</span> <span class="nt">-r</span> tt-rss-feedly-theme
</code></pre></div></div>
<ul>
<li><code class="language-plaintext highlighter-rouge">feedly*.css</code> et le répertoire feedly sont nécessaires pour obtenir toutes les variantes du thème</li>
<li><code class="language-plaintext highlighter-rouge">local-overrides.js</code> est optionnel, il fournit des polyfills pour Safari et prépare les vues utilitaires pour un style adapté aux mobiles.</li>
<li><code class="language-plaintext highlighter-rouge">local-overrides.css</code> est facultatif, mais dépend de local-overrides.js pour personnaliser les vues utilitaires.</li>
</ul>
<p>Allez dans vos préférences TT-RSS et sélectionnez le thème <strong>feedly</strong>.<br />
Installez/activez les plugins recommandés</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">toggle_sidebar</code> pour réduire la barre latérale du support de flux en cliquant sur le côté gauche de lécran</li>
<li><code class="language-plaintext highlighter-rouge">close_button</code> pour permettre de fermer le détail de larticle en vue fractionnée, important pour les mobiles</li>
<li><code class="language-plaintext highlighter-rouge">shorten_expanded</code> pour tronquer les articles longs en vue combinée</li>
</ul>
<h2 id="yunohost-tiny-tiny-rss-ttrss">Yunohost Tiny Tiny RSS (ttrss)</h2>
<p><img src="/images/ttrss-logo-a.png" alt="image" width="50px" /></p>
<h3 id="domaine-rssxoyazxyz">Domaine rss.xoyaz.xyz</h3>
<p>Ajout domaine et certificats rss.xoyaz.xyz en utilisant ladministrateur web yunohost</p>
<p>Paramétrage en mode administration web, emails sortants et entrants à Non <br />
<img src="/images/xoyaz-yunohost-003.png" alt="" /></p>
<h3 id="installer-tiny-tiny-rss">Installer Tiny Tiny RSS</h3>
<p>En ligne de commande, le dépôt : <a href="https://github.com/YunoHost-Apps/ttrss_ynh/tree/testing">https://github.com/YunoHost-Apps/ttrss_ynh/tree/testing</a></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install</span>
<span class="nb">sudo </span>yunohost app <span class="nb">install </span>https://github.com/YunoHost-Apps/ttrss_ynh/tree/testing <span class="nt">--debug</span>
<span class="c"># upgrade</span>
<span class="nb">sudo </span>yunohost app upgrade ttrss <span class="nt">-u</span> https://github.com/YunoHost-Apps/ttrss_ynh/tree/testing <span class="nt">--debug</span>
</code></pre></div></div>
<p>Installation application Tiny Tiny RSS en utilisant ladministrateur web yunohost</p>
<p>Domaine: rss.xoyaz.xyz<br />
Chemin: /<br />
Visiteurs anonymes (public): Oui</p>
<p><img src="/images/rss.xoyaz.xyz01.png" alt="" /><br />
<img src="/images/rss.xoyaz.xyz02.png" alt="" /></p>
<h3 id="authentification-par-certificat-client">Authentification par certificat client</h3>
<p><img src="/images/certificat-a.png" alt="image" width="80px" /></p>
<h4 id="a---configurer-une-autorité-de-certification">A - Configurer une Autorité de Certification</h4>
<p><a href="/2022/05/13/Mettre_en_place_et_configurer_une_autorite_de_certification_AC_avec_Easy-RSA.html">Comment mettre en place et configurer une autorité de certification (AC) avec Easy-RSA</a><br />
<u>Sur un serveur Debian</u> :</p>
<ol>
<li>Il faut ajouter copier lautorité de certification dans le fichier <code class="language-plaintext highlighter-rouge">/usr/local/share/ca-certificates/ca-easy-rsa.crt</code></li>
<li>Mettre à jour les certificats , <code class="language-plaintext highlighter-rouge">update-ca-certificates</code> qui génére le fichier <code class="language-plaintext highlighter-rouge">/etc/ssl/certs/ca-easy-rsa.pem</code></li>
</ol>
<h4 id="b---créer-un-certificat-client">B - Créer un certificat client</h4>
<p>On utilise les fichiers ca.crt (ca-easy-rsa.crt) et ca.key (ca-easy-rsa.key) de lautorité de certification<br />
Exemple, créer un certificat client “yannick”</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># clé RSA avec pass phrase</span>
openssl genrsa <span class="nt">-des3</span> <span class="nt">-out</span> yannick.key 4096
openssl req <span class="nt">-new</span> <span class="nt">-key</span> yannick.key <span class="nt">-out</span> yannick.csr
<span class="c"># demande signature certificat (CSR)</span>
openssl req <span class="nt">-new</span> <span class="nt">-key</span> yannick.key <span class="nt">-out</span> yannick.csr
Enter pass phrase <span class="k">for </span>yannick.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter <span class="s1">'.'</span>, the field will be left blank.
<span class="nt">-----</span>
Country Name <span class="o">(</span>2 letter code<span class="o">)</span> <span class="o">[</span>AU]:FR
State or Province Name <span class="o">(</span>full name<span class="o">)</span> <span class="o">[</span>Some-State]:
Locality Name <span class="o">(</span>eg, city<span class="o">)</span> <span class="o">[]</span>:
Organization Name <span class="o">(</span>eg, company<span class="o">)</span> <span class="o">[</span>Internet Widgits Pty Ltd]:
Organizational Unit Name <span class="o">(</span>eg, section<span class="o">)</span> <span class="o">[]</span>:
Common Name <span class="o">(</span>e.g. server FQDN or YOUR name<span class="o">)</span> <span class="o">[]</span>:Yannick
Email Address <span class="o">[]</span>:
Please enter the following <span class="s1">'extra'</span> attributes
to be sent with your certificate request
A challenge password <span class="o">[]</span>:
An optional company name <span class="o">[]</span>:
<span class="c"># Le CSR doit maintenant être signé par le CA</span>
openssl x509 <span class="nt">-req</span> <span class="nt">-days</span> 365 <span class="nt">-in</span> yannick.csr <span class="nt">-CA</span> ca.crt <span class="nt">-CAkey</span> private/ca.key <span class="nt">-set_serial</span> 01 <span class="nt">-out</span> yannick.crt
<span class="c"># Créer un fichier pfx pour l'importation dans les navigateurs firefox et chrome , un "Export Password" est exigé</span>
openssl pkcs12 <span class="nt">-export</span> <span class="nt">-out</span> yannick.pfx <span class="nt">-inkey</span> yannick.key <span class="nt">-in</span> yannick.crt <span class="nt">-certfile</span> ca.crt
</code></pre></div></div>
<p>Le fichier pfx sera utilisé pour limportation dans firefox et chrome<br />
<img src="/images/certificat-client-firefox-a.png" alt="" /> <br />
<img src="/images/certificat-client-firefox-b.png" alt="" /></p>
<h4 id="c---configuration-nginx-certificat-client">C - Configuration nginx certificat client</h4>
<p>Modifier le fichier de configuration nginx <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf</code> pour la prise en compte du certificat client
Ajouter ce qui suit au début du fichier</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Authentification par certificat client
ssl_client_certificate /etc/ssl/certs/ca-easy-rsa.pem;
# Authentification uniquement par certificat
# ssl_verify_client on;
# Authentification par certificat ou par mot de passe
ssl_verify_client optional;
</code></pre></div></div>
<p>Ajouter ce qui suit après la ligne <code class="language-plaintext highlighter-rouge">fastcgi_param SCRIPT_FILENAME $request_filename;</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # Authentification par certificat client
fastcgi_param SSL_CLIENT_M_SERIAL $ssl_client_serial;
fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
fastcgi_param SSL_CLIENT_V_START $ssl_client_v_start;
fastcgi_param SSL_CLIENT_V_END $ssl_client_v_end;
</code></pre></div></div>
<p>Le fichier <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf</code> après les modifications</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Authentification par certificat client
ssl_client_certificate /etc/ssl/certs/ca-easy-rsa.pem;
# Authentification uniquement par certificat
# ssl_verify_client on;
# Authentification par certificat ou par mot de passe
ssl_verify_client optional;
#sub_path_only rewrite ^/$ / permanent;
location / {
# Path to source
alias /var/www/ttrss/ ;
index index.php;
# Common parameter to increase upload size limit in conjunction with dedicated php-fpm file
#client_max_body_size 50M;
try_files $uri $uri/ index.php;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass unix:/var/run/php/php8.0-fpm-ttrss.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $request_filename;
# Authentification par certificat client
fastcgi_param SSL_CLIENT_M_SERIAL $ssl_client_serial;
fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
fastcgi_param SSL_CLIENT_V_START $ssl_client_v_start;
fastcgi_param SSL_CLIENT_V_END $ssl_client_v_end;
}
# Include SSOWAT user panel.
include conf.d/yunohost_panel.conf.inc;
}
</code></pre></div></div>
<p>Relever le nouveau ckecksum du fichier</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>md5sum /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf
</code></pre></div></div>
<p>bee3f701092e6c2dd066e37d4f0154c6 /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf</p>
<p>Modifier le ckecksum existant dans le fichier <code class="language-plaintext highlighter-rouge">/etc/yunohost/apps/ttrss/settings.yml</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod 600 /etc/yunohost/apps/ttrss/settings.yml # accessible en écriture
nano /etc/yunohost/apps/ttrss/settings.yml # édition
</code></pre></div></div>
<p>Mettre le nouveau checksum dans la ligne <code class="language-plaintext highlighter-rouge">checksum__etc_nginx_conf.d_rss.xoyaz.xyz.d_ttrss.conf:</code><br />
Puis verrouiller laccès en écriture</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod 400 /etc/yunohost/apps/ttrss/settings.yml
</code></pre></div></div>
<p>Recharger nginx</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl reload nginx
</code></pre></div></div>
<p>Mettre à jour le fichier de configuration de TT-RSS <code class="language-plaintext highlighter-rouge">/var/www/ttrss/config.php</code> pour ajouter <code class="language-plaintext highlighter-rouge">auth_remote</code> à la constante PLUGINS (vers la fin du fichier) par les instructions suivantes en mode su</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>600 /var/www/ttrss/config.php
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/putenv('TTRSS_PLUGINS=auth_internal, note')</span><span class="se">\;</span><span class="s2">/putenv('TTRSS_PLUGINS=auth_internal, auth_remote, note')</span><span class="se">\;</span><span class="s2">/g"</span> /var/www/ttrss/config.php
<span class="nb">chmod </span>400 /var/www/ttrss/config.php
</code></pre></div></div>
<p>Il faut ajouter le certificat client aux navigateurs sinon<br />
<img src="/images/ttrss-err-certif.png" alt="" /></p>
<p>Au premier passage, une authentification login mot de passe est demandée</p>
<h3 id="mise-à-jour-ttrss">Mise à jour ttrss</h3>
<p><em>La configuration de base est modifiée pour une utilisation des certificats clients en authentification et à chaque mise à jour de lapplication , des messages davertissement sont émis, le mode fonctionnement de base est rétabli</em></p>
<p>Créer un script <code class="language-plaintext highlighter-rouge">ttrss_auth_cert.sh</code> à exécuter, en mode su, après chaque mise à jour de ttrss</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
<span class="c"># le script modifie le fichier de configuration nginx ttrss.conf pour une authentification par certificat</span>
<span class="c"># modifie le checksum dans le fichier paramétrage settings.yml</span>
<span class="c"># active l'authentification dans la configuration ttrss config.php</span>
<span class="c"># Vérifier si utilisateur "root"</span>
<span class="k">if</span> <span class="o">[</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span> <span class="o">!=</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s2">"Erreur : Vous devez être root pour exécuter ce script"</span>
<span class="nb">exit </span>1
<span class="k">fi</span>
<span class="c"># fichier de configuration nginx ttrss : /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf</span>
<span class="c"># sauvegarde</span>
<span class="nb">cp</span> /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf.old
<span class="c"># On arrête le service</span>
systemctl stop ttrss
<span class="c"># Modification fichier nginx ttrss.conf</span>
<span class="c"># pour y ajouter l'authentification par certificat</span>
<span class="c"># Insertion avant ligne "location / {"</span>
gawk <span class="nt">-i</span> inplace <span class="s1">'
/location \/ \{/ {
print "# Authentification par certificat client"
print "ssl_client_certificate /etc/ssl/certs/ac-yako.pem;"
print "# Authentification uniquement par certificat"
print "# ssl_verify_client on;"
print "# Authentification par certificat ou par mot de passe"
print "ssl_verify_client optional;"
}
{ print }
'</span> /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf
<span class="c"># Insertion après ligne "fastcgi_param SCRIPT_FILENAME $request_filename;"</span>
gawk <span class="nt">-i</span> inplace <span class="s1">'
{ print }
/fastcgi_param \SCRIPT_FILENAME \$request_filename;/ {
print " # Authentification par certificat client"
print " fastcgi_param SSL_CLIENT_M_SERIAL $ssl_client_serial;"
print " fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;"
print " fastcgi_param SSL_CLIENT_V_START $ssl_client_v_start;"
print " fastcgi_param SSL_CLIENT_V_END $ssl_client_v_end;"
}
'</span> /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf
<span class="c"># Modification checksum correspondant à ttrss.conf dans le fichier settings.yml </span>
<span class="nb">cp</span> /etc/yunohost/apps/ttrss/settings.yml /etc/yunohost/apps/ttrss/settings.yml.old
<span class="nb">chmod </span>600 /etc/yunohost/apps/ttrss/settings.yml <span class="c"># accessible en écriture</span>
<span class="nb">sed</span> <span class="nt">-E</span> <span class="nt">-i</span> <span class="s2">"s/^(checksum__etc_php_8.2_fpm_pool.d_ttrss.conf: ).*/</span><span class="se">\1</span><span class="si">$(</span><span class="nb">echo</span> <span class="si">$(</span><span class="nb">md5sum</span> /etc/nginx/conf.d/rss.xoyaz.xyz.d/ttrss.conf<span class="si">)</span> |awk <span class="s1">'{print $1}'</span><span class="si">)</span><span class="s2">/"</span> /etc/yunohost/apps/ttrss/settings.yml
<span class="nb">chmod </span>400 /etc/yunohost/apps/ttrss/settings.yml
<span class="c"># Mettre à jour le fichier de configuration de TT-RSS `/var/www/ttrss/config.php` </span>
<span class="c"># pour ajouter `auth_remote` à la constante PLUGINS </span>
<span class="nb">cp</span> /var/www/ttrss/config.php /var/www/ttrss/config.php.old
<span class="nb">chmod </span>600 /var/www/ttrss/config.php
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/TTRSS_PLUGINS=auth_internal/TTRSS_PLUGINS=auth_internal, auth_remote/g"</span> /var/www/ttrss/config.php
<span class="nb">chmod </span>400 /var/www/ttrss/config.php
<span class="c"># recharger nginx</span>
systemctl reload nginx
<span class="c"># On démarre le service</span>
systemctl start ttrss
</code></pre></div></div>
<p>Le rendre exécutable</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chmod +x ttrss_auth_cert.sh
</code></pre></div></div>
</div>
<div class="d-print-none"><footer class="article__footer"><meta itemprop="dateModified" content="2023-07-13T00:00:00+02:00"><!-- start custom article footer snippet -->
<!-- end custom article footer snippet -->
<!--
<div align="right"><a type="application/rss+xml" href="/feed.xml" title="S'abonner"><i class="fa fa-rss fa-2x"></i></a>
&emsp;</div>
-->
</footer>
<div class="article__section-navigator clearfix"><div class="previous"><span>PRÉCÉDENT</span><a href="/2023/07/01/Yubikey.html">Yubico - YubiKey 5 NFC</a></div><div class="next"><span>SUIVANT</span><a href="/2023/08/02/EndeavourOS_XFCE_-_Environnements_de_bureau_LightDM.html">EndeavourOS XFCE - LightDM sur les systèmes multi-affichages</a></div></div></div>
</div>
<script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
$(function() {
var $this ,$scroll;
var $articleContent = $('.js-article-content');
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
var scroll = hasSidebar ? '.js-page-main' : 'html, body';
$scroll = $(scroll);
$articleContent.find('.highlight').each(function() {
$this = $(this);
$this.attr('data-lang', $this.find('code').attr('data-lang'));
});
$articleContent.find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
$this = $(this);
$this.append($('<a class="anchor d-print-none" aria-hidden="true"></a>').html('<i class="fas fa-anchor"></i>'));
});
$articleContent.on('click', '.anchor', function() {
$scroll.scrollToAnchor('#' + $(this).parent().attr('id'), 400);
});
});
});
})();
</script>
</div><section class="page__comments d-print-none"></section></article><!-- start custom main bottom snippet -->
<!-- end custom main bottom snippet -->
</div>
</div></div></div></div>
</div><script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
var $body = $('body'), $window = $(window);
var $pageRoot = $('.js-page-root'), $pageMain = $('.js-page-main');
var activeCount = 0;
function modal(options) {
var $root = this, visible, onChange, hideWhenWindowScroll = false;
var scrollTop;
function setOptions(options) {
var _options = options || {};
visible = _options.initialVisible === undefined ? false : show;
onChange = _options.onChange;
hideWhenWindowScroll = _options.hideWhenWindowScroll;
}
function init() {
setState(visible);
}
function setState(isShow) {
if (isShow === visible) {
return;
}
visible = isShow;
if (visible) {
activeCount++;
scrollTop = $(window).scrollTop() || $pageMain.scrollTop();
$root.addClass('modal--show');
$pageMain.scrollTop(scrollTop);
activeCount === 1 && ($pageRoot.addClass('show-modal'), $body.addClass('of-hidden'));
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.on('scroll', hide);
$window.on('keyup', handleKeyup);
} else {
activeCount > 0 && activeCount--;
$root.removeClass('modal--show');
$window.scrollTop(scrollTop);
activeCount === 0 && ($pageRoot.removeClass('show-modal'), $body.removeClass('of-hidden'));
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.off('scroll', hide);
$window.off('keyup', handleKeyup);
}
onChange && onChange(visible);
}
function show() {
setState(true);
}
function hide() {
setState(false);
}
function handleKeyup(e) {
// Char Code: 27 ESC
if (e.which === 27) {
hide();
}
}
setOptions(options);
init();
return {
show: show,
hide: hide,
$el: $root
};
}
$.fn.modal = modal;
});
})();
</script><div class="modal modal--overflow page__search-modal d-print-none js-page-search-modal"><script>
(function () {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
// search panel
var search = (window.search || (window.search = {}));
var useDefaultSearchBox = window.useDefaultSearchBox === undefined ?
true : window.useDefaultSearchBox ;
var $searchModal = $('.js-page-search-modal');
var $searchToggle = $('.js-search-toggle');
var searchModal = $searchModal.modal({ onChange: handleModalChange, hideWhenWindowScroll: true });
var modalVisible = false;
search.searchModal = searchModal;
var $searchBox = null;
var $searchInput = null;
var $searchClear = null;
function getModalVisible() {
return modalVisible;
}
search.getModalVisible = getModalVisible;
function handleModalChange(visible) {
modalVisible = visible;
if (visible) {
search.onShow && search.onShow();
useDefaultSearchBox && $searchInput[0] && $searchInput[0].focus();
} else {
search.onShow && search.onHide();
useDefaultSearchBox && $searchInput[0] && $searchInput[0].blur();
setTimeout(function() {
useDefaultSearchBox && ($searchInput.val(''), $searchBox.removeClass('not-empty'));
search.clear && search.clear();
window.pageAsideAffix && window.pageAsideAffix.refresh();
}, 400);
}
}
$searchToggle.on('click', function() {
modalVisible ? searchModal.hide() : searchModal.show();
});
// Char Code: 83 S, 191 /
$(window).on('keyup', function(e) {
if (!modalVisible && !window.isFormElement(e.target || e.srcElement) && (e.which === 83 || e.which === 191)) {
modalVisible || searchModal.show();
}
});
if (useDefaultSearchBox) {
$searchBox = $('.js-search-box');
$searchInput = $searchBox.children('input');
$searchClear = $searchBox.children('.js-icon-clear');
search.getSearchInput = function() {
return $searchInput.get(0);
};
search.getVal = function() {
return $searchInput.val();
};
search.setVal = function(val) {
$searchInput.val(val);
};
$searchInput.on('focus', function() {
$(this).addClass('focus');
});
$searchInput.on('blur', function() {
$(this).removeClass('focus');
});
$searchInput.on('input', window.throttle(function() {
var val = $(this).val();
if (val === '' || typeof val !== 'string') {
search.clear && search.clear();
} else {
$searchBox.addClass('not-empty');
search.onInputNotEmpty && search.onInputNotEmpty(val);
}
}, 400));
$searchClear.on('click', function() {
$searchInput.val(''); $searchBox.removeClass('not-empty');
search.clear && search.clear();
});
}
});
})();
</script><div class="search search--dark">
<div class="main">
<div class="search__header">Recherche</div>
<div class="search-bar">
<div class="search-box js-search-box">
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
<input id="search-input" type="text" />
<div class="search-box__icon-clear js-icon-clear">
<a><i class="fas fa-times"></i></a>
</div>
</div>
<button class="button button--theme-dark button--pill search__cancel js-search-toggle">
Annuler</button>
</div>
<div id="results-container" class="search-result js-search-result"></div>
</div>
</div>
<!-- Script pointing to search-script.js -->
<script>/*!
* Simple-Jekyll-Search
* Copyright 2015-2020, Christian Fei
* Licensed under the MIT License.
*/
(function(){
'use strict'
var _$Templater_7 = {
compile: compile,
setOptions: setOptions
}
const options = {}
options.pattern = /\{(.*?)\}/g
options.template = ''
options.middleware = function () {}
function setOptions (_options) {
options.pattern = _options.pattern || options.pattern
options.template = _options.template || options.template
if (typeof _options.middleware === 'function') {
options.middleware = _options.middleware
}
}
function compile (data) {
return options.template.replace(options.pattern, function (match, prop) {
const value = options.middleware(prop, data[prop], options.template)
if (typeof value !== 'undefined') {
return value
}
return data[prop] || match
})
}
'use strict';
function fuzzysearch (needle, haystack) {
var tlen = haystack.length;
var qlen = needle.length;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
return needle === haystack;
}
outer: for (var i = 0, j = 0; i < qlen; i++) {
var nch = needle.charCodeAt(i);
while (j < tlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
var _$fuzzysearch_1 = fuzzysearch;
'use strict'
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
function FuzzySearchStrategy () {
this.matches = function (string, crit) {
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
}
}
'use strict'
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
function LiteralSearchStrategy () {
this.matches = function (str, crit) {
if (!str) return false
str = str.trim().toLowerCase()
crit = crit.trim().toLowerCase()
return crit.split(' ').filter(function (word) {
return str.indexOf(word) >= 0
}).length === crit.split(' ').length
}
}
'use strict'
var _$Repository_4 = {
put: put,
clear: clear,
search: search,
setOptions: __setOptions_4
}
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
function NoSort () {
return 0
}
const data = []
let opt = {}
opt.fuzzy = false
opt.limit = 10
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = NoSort
opt.exclude = []
function put (data) {
if (isObject(data)) {
return addObject(data)
}
if (isArray(data)) {
return addArray(data)
}
return undefined
}
function clear () {
data.length = 0
return data
}
function isObject (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
}
function addObject (_data) {
data.push(_data)
return data
}
function addArray (_data) {
const added = []
clear()
for (let i = 0, len = _data.length; i < len; i++) {
if (isObject(_data[i])) {
added.push(addObject(_data[i]))
}
}
return added
}
function search (crit) {
if (!crit) {
return []
}
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
}
function __setOptions_4 (_opt) {
opt = _opt || {}
opt.fuzzy = _opt.fuzzy || false
opt.limit = _opt.limit || 10
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = _opt.sort || NoSort
opt.exclude = _opt.exclude || []
}
function findMatches (data, crit, strategy, opt) {
const matches = []
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
const match = findMatchesInObject(data[i], crit, strategy, opt)
if (match) {
matches.push(match)
}
}
return matches
}
function findMatchesInObject (obj, crit, strategy, opt) {
for (const key in obj) {
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
return obj
}
}
}
function isExcluded (term, excludedTerms) {
for (let i = 0, len = excludedTerms.length; i < len; i++) {
const excludedTerm = excludedTerms[i]
if (new RegExp(excludedTerm).test(term)) {
return true
}
}
return false
}
/* globals ActiveXObject:false */
'use strict'
var _$JSONLoader_2 = {
load: load
}
function load (location, callback) {
const xhr = getXHR()
xhr.open('GET', location, true)
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
xhr.send()
}
function createStateChangeListener (xhr, callback) {
return function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
callback(null, JSON.parse(xhr.responseText))
} catch (err) {
callback(err, null)
}
}
}
}
function getXHR () {
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
}
'use strict'
var _$OptionsValidator_3 = function OptionsValidator (params) {
if (!validateParams(params)) {
throw new Error('-- OptionsValidator: required options missing')
}
if (!(this instanceof OptionsValidator)) {
return new OptionsValidator(params)
}
const requiredOptions = params.required
this.getRequiredOptions = function () {
return requiredOptions
}
this.validate = function (parameters) {
const errors = []
requiredOptions.forEach(function (requiredOptionName) {
if (typeof parameters[requiredOptionName] === 'undefined') {
errors.push(requiredOptionName)
}
})
return errors
}
function validateParams (params) {
if (!params) {
return false
}
return typeof params.required !== 'undefined' && params.required instanceof Array
}
}
'use strict'
var _$utils_9 = {
merge: merge,
isJSON: isJSON
}
function merge (defaultParams, mergeParams) {
const mergedOptions = {}
for (const option in defaultParams) {
mergedOptions[option] = defaultParams[option]
if (typeof mergeParams[option] !== 'undefined') {
mergedOptions[option] = mergeParams[option]
}
}
return mergedOptions
}
function isJSON (json) {
try {
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
return true
}
return false
} catch (err) {
return false
}
}
var _$src_8 = {};
(function (window) {
'use strict'
let options = {
searchInput: null,
resultsContainer: null,
json: [],
success: Function.prototype,
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
templateMiddleware: Function.prototype,
sortMiddleware: function () {
return 0
},
noResultsText: 'No results found',
limit: 10,
fuzzy: false,
debounceTime: null,
exclude: []
}
let debounceTimerHandle
const debounce = function (func, delayMillis) {
if (delayMillis) {
clearTimeout(debounceTimerHandle)
debounceTimerHandle = setTimeout(func, delayMillis)
} else {
func.call()
}
}
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
/* removed: const _$Templater_7 = require('./Templater') */;
/* removed: const _$Repository_4 = require('./Repository') */;
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
const optionsValidator = _$OptionsValidator_3({
required: requiredOptions
})
/* removed: const _$utils_9 = require('./utils') */;
window.SimpleJekyllSearch = function (_options) {
const errors = optionsValidator.validate(_options)
if (errors.length > 0) {
throwError('You must specify the following required options: ' + requiredOptions)
}
options = _$utils_9.merge(options, _options)
_$Templater_7.setOptions({
template: options.searchResultTemplate,
middleware: options.templateMiddleware
})
_$Repository_4.setOptions({
fuzzy: options.fuzzy,
limit: options.limit,
sort: options.sortMiddleware,
exclude: options.exclude
})
if (_$utils_9.isJSON(options.json)) {
initWithJSON(options.json)
} else {
initWithURL(options.json)
}
const rv = {
search: search
}
typeof options.success === 'function' && options.success.call(rv)
return rv
}
function initWithJSON (json) {
_$Repository_4.put(json)
registerInput()
}
function initWithURL (url) {
_$JSONLoader_2.load(url, function (err, json) {
if (err) {
throwError('failed to get JSON (' + url + ')')
}
initWithJSON(json)
})
}
function emptyResultsContainer () {
options.resultsContainer.innerHTML = ''
}
function appendToResultsContainer (text) {
options.resultsContainer.innerHTML += text
}
function registerInput () {
options.searchInput.addEventListener('input', function (e) {
if (isWhitelistedKey(e.which)) {
emptyResultsContainer()
debounce(function () { search(e.target.value) }, options.debounceTime)
}
})
}
function search (query) {
if (isValidQuery(query)) {
emptyResultsContainer()
render(_$Repository_4.search(query), query)
}
}
function render (results, query) {
const len = results.length
if (len === 0) {
return appendToResultsContainer(options.noResultsText)
}
for (let i = 0; i < len; i++) {
results[i].query = query
appendToResultsContainer(_$Templater_7.compile(results[i]))
}
}
function isValidQuery (query) {
return query && query.length > 0
}
function isWhitelistedKey (key) {
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
}
function throwError (message) {
throw new Error('SimpleJekyllSearch --- ' + message)
}
})(window)
}());
</script>
<!-- Configuration -->
<script>
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
noResultsText: '<p>Aucun résultat!</p>',
json: '/search.json',
searchResultTemplate: '<li><a href="{url}">{date}&nbsp;{title}</a>&nbsp;(Création {create})</li>'
})
</script>
</div></div>
<script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function scrollToAnchor(anchor, duration, callback) {
var $root = this;
$root.animate({ scrollTop: $(anchor).position().top }, duration, function() {
window.history.replaceState(null, '', window.location.href.split('#')[0] + anchor);
callback && callback();
});
}
$.fn.scrollToAnchor = scrollToAnchor;
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function affix(options) {
var $root = this, $window = $(window), $scrollTarget, $scroll,
offsetBottom = 0, scrollTarget = window, scroll = window.document, disabled = false, isOverallScroller = true,
rootTop, rootLeft, rootHeight, scrollBottom, rootBottomTop,
hasInit = false, curState;
function setOptions(options) {
var _options = options || {};
_options.offsetBottom && (offsetBottom = _options.offsetBottom);
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
_options.scroll && (scroll = _options.scroll);
_options.disabled !== undefined && (disabled = _options.disabled);
$scrollTarget = $(scrollTarget);
isOverallScroller = window.isOverallScroller($scrollTarget[0]);
$scroll = $(scroll);
}
function preCalc() {
top();
rootHeight = $root.outerHeight();
rootTop = $root.offset().top + (isOverallScroller ? 0 : $scrollTarget.scrollTop());
rootLeft = $root.offset().left;
}
function calc(needPreCalc) {
needPreCalc && preCalc();
scrollBottom = $scroll.outerHeight() - offsetBottom - rootHeight;
rootBottomTop = scrollBottom - rootTop;
}
function top() {
if (curState !== 'top') {
$root.removeClass('fixed').css({
left: 0,
top: 0
});
curState = 'top';
}
}
function fixed() {
if (curState !== 'fixed') {
$root.addClass('fixed').css({
left: rootLeft + 'px',
top: 0
});
curState = 'fixed';
}
}
function bottom() {
if (curState !== 'bottom') {
$root.removeClass('fixed').css({
left: 0,
top: rootBottomTop + 'px'
});
curState = 'bottom';
}
}
function setState() {
var scrollTop = $scrollTarget.scrollTop();
if (scrollTop >= rootTop && scrollTop <= scrollBottom) {
fixed();
} else if (scrollTop < rootTop) {
top();
} else {
bottom();
}
}
function init() {
if(!hasInit) {
var interval, timeout;
calc(true); setState();
// run calc every 100 millisecond
interval = setInterval(function() {
calc();
}, 100);
timeout = setTimeout(function() {
clearInterval(interval);
}, 45000);
window.pageLoad.then(function() {
setTimeout(function() {
clearInterval(interval);
clearTimeout(timeout);
}, 3000);
});
$scrollTarget.on('scroll', function() {
disabled || setState();
});
$window.on('resize', function() {
disabled || (calc(true), setState());
});
hasInit = true;
}
}
setOptions(options);
if (!disabled) {
init();
}
$window.on('resize', window.throttle(function() {
init();
}, 200));
return {
setOptions: setOptions,
refresh: function() {
calc(true, { animation: false }); setState();
}
};
}
$.fn.affix = affix;
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function toc(options) {
var $root = this, $window = $(window), $scrollTarget, $scroller, $tocUl = $('<ul class="toc toc--ellipsis"></ul>'), $tocLi, $headings, $activeLast, $activeCur,
selectors = 'h1,h2,h3', container = 'body', scrollTarget = window, scroller = 'html, body', disabled = false,
headingsPos, scrolling = false, hasRendered = false, hasInit = false;
function setOptions(options) {
var _options = options || {};
_options.selectors && (selectors = _options.selectors);
_options.container && (container = _options.container);
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
_options.scroller && (scroller = _options.scroller);
_options.disabled !== undefined && (disabled = _options.disabled);
$headings = $(container).find(selectors).filter('[id]');
$scrollTarget = $(scrollTarget);
$scroller = $(scroller);
}
function calc() {
headingsPos = [];
$headings.each(function() {
headingsPos.push(Math.floor($(this).position().top));
});
}
function setState(element, disabled) {
var scrollTop = $scrollTarget.scrollTop(), i;
if (disabled || !headingsPos || headingsPos.length < 1) { return; }
if (element) {
$activeCur = element;
} else {
for (i = 0; i < headingsPos.length; i++) {
if (scrollTop >= headingsPos[i]) {
$activeCur = $tocLi.eq(i);
} else {
$activeCur || ($activeCur = $tocLi.eq(i));
break;
}
}
}
$activeLast && $activeLast.removeClass('active');
($activeLast = $activeCur).addClass('active');
}
function render() {
if(!hasRendered) {
$root.append($tocUl);
$headings.each(function() {
var $this = $(this);
$tocUl.append($('<li></li>').addClass('toc-' + $this.prop('tagName').toLowerCase())
.append($('<a></a>').text($this.text()).attr('href', '#' + $this.prop('id'))));
});
$tocLi = $tocUl.children('li');
$tocUl.on('click', 'a', function(e) {
e.preventDefault();
var $this = $(this);
scrolling = true;
setState($this.parent());
$scroller.scrollToAnchor($this.attr('href'), 400, function() {
scrolling = false;
});
});
}
hasRendered = true;
}
function init() {
var interval, timeout;
if(!hasInit) {
render(); calc(); setState(null, scrolling);
// run calc every 100 millisecond
interval = setInterval(function() {
calc();
}, 100);
timeout = setTimeout(function() {
clearInterval(interval);
}, 45000);
window.pageLoad.then(function() {
setTimeout(function() {
clearInterval(interval);
clearTimeout(timeout);
}, 3000);
});
$scrollTarget.on('scroll', function() {
disabled || setState(null, scrolling);
});
$window.on('resize', window.throttle(function() {
if (!disabled) {
render(); calc(); setState(null, scrolling);
}
}, 100));
}
hasInit = true;
}
setOptions(options);
if (!disabled) {
init();
}
$window.on('resize', window.throttle(function() {
init();
}, 200));
return {
setOptions: setOptions
};
}
$.fn.toc = toc;
});
})();
/*(function () {
})();*/
</script><script>
/* toc must before affix, since affix need to konw toc' height. */(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
var TOC_SELECTOR = window.TEXT_VARIABLES.site.toc.selectors;
window.Lazyload.js(SOURCES.jquery, function() {
var $window = $(window);
var $articleContent = $('.js-article-content');
var $tocRoot = $('.js-toc-root'), $col2 = $('.js-col-aside');
var toc;
var tocDisabled = false;
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
var hasToc = $articleContent.find(TOC_SELECTOR).length > 0;
function disabled() {
return $col2.css('display') === 'none' || !hasToc;
}
tocDisabled = disabled();
toc = $tocRoot.toc({
selectors: TOC_SELECTOR,
container: $articleContent,
scrollTarget: hasSidebar ? '.js-page-main' : null,
scroller: hasSidebar ? '.js-page-main' : null,
disabled: tocDisabled
});
$window.on('resize', window.throttle(function() {
tocDisabled = disabled();
toc && toc.setOptions({
disabled: tocDisabled
});
}, 100));
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
var $window = $(window), $pageFooter = $('.js-page-footer');
var $pageAside = $('.js-page-aside');
var affix;
var tocDisabled = false;
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
affix = $pageAside.affix({
offsetBottom: $pageFooter.outerHeight(),
scrollTarget: hasSidebar ? '.js-page-main' : null,
scroller: hasSidebar ? '.js-page-main' : null,
scroll: hasSidebar ? $('.js-page-main').children() : null,
disabled: tocDisabled
});
$window.on('resize', window.throttle(function() {
affix && affix.setOptions({
disabled: tocDisabled
});
}, 100));
window.pageAsideAffix = affix;
});
})();
</script><!---->
</div>
<script>(function () {
var $root = document.getElementsByClassName('root')[0];
if (window.hasEvent('touchstart')) {
$root.dataset.isTouch = true;
document.addEventListener('touchstart', function(){}, false);
}
})();
</script>
</body>
</html>