Commit 2c77039b authored by yPhil's avatar yPhil
Browse files

reset

parent 6546cd23
Pipeline #495718 passed with stage
in 18 seconds
# Changelog
## v1.5.0
- Pétrolette can now find/discover all the feeds in the URI
- New feature: Find/discover all the feeds in the URI #3ba52686
- New feature: Feeds suggestions #86213901
- Hardened security #1e3a51a
- Errors (in both the feeds and the discovery) are now much better handled, with a direct link to the error type docs #a4e7d83c
- More data (img, video & audio) types managed #1325ab5
- The search input text field is now un-focused on pressing escape #e36feb4c
- All dependancies are now installed & managed with NPM #90af60e
- All dependancies are now installed & managed with NPM #efdebe2e => #e22524d2
- New "Insecure items in this feed were not loaded" notification #1e3a51a
- Default feeds update #4da8bdf
- New tests (static files, error codes, etc.) #e32f2d07
### Bugfixes
- Complex URLs containing query strings are now properly handled #1325ab5
- The custom name of the feed is no longer overriden with the default one #38f99386
- Complex URLs containing query strings were not properly handled #1325ab5
- The custom name of the feed was sometimes overriden with the default one #38f99386
- The feed was sometimes deleted when closing the feedPrefs dialog by clicking on the overlay #38f9938/#ee1fe05
- The return key misbehaved in the feedPrefs dialog #f6e7f06
- Feed timestamp: `substr()` is now deprecated (?) so heck, full timestamp for now #83a22f6
......
......@@ -9,8 +9,8 @@
### Library / dependancies upgrade conlicts - read before editing package.json
- Intro.js is now installed with NPM and linked directly, since the Bower archive is missing files ; Considering doing this for all (client-side) Bower deps.
- The `node-fetch` 3.n branch requires that the whole Pétrolette project be ported to ESM ; help needed
- As of #v1.5.5 all dependancies are now installed with NPM ;
- The `node-fetch` 3.n branch requires that the whole Pétrolette project be ported to ESM ; **help wanted please** ;
- The `helmet` 5.n branch apparently breaks CORS for images, investigating.
## Conventions & style guide
......@@ -85,4 +85,4 @@ Extract and copy the relevant files:
For any question, please [use the repo](https://framagit.org/yphil/petrolette/-/issues/new?issue%5Bmilestone_id%5D=) itself.
Thank you **very much** for any love you give to this project.
Thank you **very much** for [any love you give](https://liberapay.com/yPhil/) to this project.
......@@ -2,6 +2,7 @@
[![pipeline status](https://framagit.org/yphil/petrolette/badges/master/pipeline.svg)](https://framagit.org/yphil/petrolette/-/pipelines)
[![website](https://img.shields.io/website?down_message=down&up_color=brightgreen&up_message=up&url=https%3A%2F%2Fpetrolette.space)](https://petrolette.space)
[![License GPLv3](https://img.shields.io/badge/license-GPL_v3-green.svg)](http://www.gnu.org/licenses/gpl-3.0.html)
[![Liberapay](https://img.shields.io/liberapay/receives/yPhil?logo=liberapay)](https://liberapay.com/yPhil/donate)
[![Liberapay](https://img.shields.io/liberapay/goal/yPhil?logo=liberapay)](https://liberapay.com/yPhil/donate)
[![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?label=buy+me+a&style=flat&logo=ko-fi&logoColor=white)](https://ko-fi.com/yphil/tiers)
......@@ -40,10 +41,13 @@ Pétrolette is fully self-contained, makes **no external call** whatsoever, and
- Bookmarklet to **add the feed of any website to Pétrolette** on the fly
- Compatible with [FLOSS browsers](https://www.gnu.org/software/librejs/) (links to all uncompressed executable files, licenses available directly and in standard format)
## What's new?
### What's new?
Read [the changelog](https://framagit.org/yphil/petrolette/-/blob/master/CHANGELOG.md).
## Usage
Pétrolette can be use both locally on an http, or a https server.
### Install
......@@ -57,20 +61,20 @@ npm start
`xdg-open http://localhost:8000` or `xdg-open https://localhost:8001`
#### Update
### Update
```sh
cd petrolette
git pull
npm update
```
#### Test
### Test
```javascript
npm test
```
### Contributing
## Contributing
- Read the [development guidelines](https://framagit.org/yphil/petrolette/-/blob/master/DEV.md)
- Pétrolette is [Free, Libre, and Open-Source Software](https://framagit.org/yphil/petrolette/-/blob/master/LICENSE). However the development requires [a lot of time](https://www.youtube.com/watch?v=JlbMEx9H6FE) and [a lot of work](https://framagit.org/yphil/petrolette/-/commits/master). In order to keep developing it with new features I need your help ; Please consider to [support the Pétrolette project](https://liberapay.com/yPhil/donate) by sending a donation. Even the smallest amount will help a lot.
......
{
"name": "petrolette",
"description": "The news page that doesn't know you",
"version": "1.5.6",
"version": "v1.5.6",
"private": true,
"license": "GPL-3.0-only",
"HTTP_PORT": 8000,
......
const express = require('express'),
path = require('path'),
router = require('./routes/router'),
fs = require('fs'),
pjson = require('./package.json'),
bodyParser = require('body-parser'),
app = express(),
helmet = require("helmet"),
compression = require('compression');
path = require('path'),
router = require('./routes/router'),
fs = require('fs'),
pjson = require('./package.json'),
bodyParser = require('body-parser'),
app = express(),
helmet = require("helmet"),
compression = require('compression');
fs.mkdir(path.join(__dirname, pjson.FAVICONS_CACHE_DIR), {
recursive: true
recursive: true
}, (err) => {
if (err) {
return console.error(err);
}
return true;
if (err) {
return console.error(err);
}
return true;
});
app.set('views', path.join(__dirname, 'views'));
......@@ -27,12 +27,11 @@ app.use(compression());
app.use(
helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'", "https://www.googleapis.com", "https://api.dropboxapi.com", "https://content.dropboxapi.com", "https:"],
imgSrc: ["'self'", "'unsafe-inline'", "https:", "data:"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
scriptSrcAttr: null
}
})
......
......@@ -427,31 +427,29 @@ PTL.dialog = {
.attr('class', 'feedsGroupLi')
.appendTo($tabUl).hide();
$tabUl
.append($('<li>')
.attr('class', 'tabTitle closed hover')
.append($('<strong>')
.attr('class', 'tabLi')
.text(tab.name)
)
.on('click', function() {
if ($(this).hasClass('open')) {
$('li.feedsGroupLi').hide();
$(this).addClass('closed')
.removeClass('open');
} else {
$('li.feedsGroupLi').hide();
$(this).addClass('open')
.removeClass('closed')
.siblings('li.feedsGroupLi')
.slideDown('fast');
}
$tabUl.append($('<li>')
.attr('class', 'tabTitle closed hover')
.append($('<strong>')
.attr('class', 'tabLi')
.text(tab.name))
.on('click', function() {
}));
if ($(this).hasClass('open')) {
$('li.feedsGroupLi').slideUp('fast');
$(this).addClass('closed')
.removeClass('open');
} else {
$('li.feedsGroupLi').hide();
$(this).addClass('open')
.removeClass('closed')
.siblings('li.feedsGroupLi')
.slideDown('fast');
}
}));
$.each(tab.columns, function(i, col) {
$.each(col, function(i, feed) {
$feedsGroupLi.append(PTL.dialog.feedsListItem(feed));
});
......@@ -459,7 +457,6 @@ PTL.dialog = {
});
$feedsGroupLi.appendTo($tabUl);
// $feedsMasterUl.append($feedsGroupLi);
});
......@@ -711,16 +708,12 @@ PTL.dialog = {
$dialog.dialog('open');
});
},
killTab: function($button) {
killTab: function($button, tabName, $selectedTab, $selectedPanel, numberOfFeeds) {
$('div#ptlDialogs').load('/static/templates/dialogs.html #questionDialog', function() {
const $dialog = $(this),
$tabs = $('#tabs'),
$a = $button.prev('a.ui-tabs-anchor'),
tabId = $a.attr('href'),
$selectedTab = $a.parent(),
$selectedPanel = $tabs.find(tabId),
$icon = $dialog.find('div#icon > i'),
selectedTabIndex = $tabs.tabs('option', 'active'),
previousTabIndex = selectedTabIndex === 0 ? 0 : selectedTabIndex - 1;
......@@ -768,10 +761,9 @@ PTL.dialog = {
.addClass('dangerous')
.text(PTL.tr('This action cannot be undone.'))
.next('h2').text(PTL.tr('Name'))
.next('p').text($a.text())
.next('p').text(tabName)
.next('h2').text(PTL.tr('Number of feeds'))
.next('p').text($selectedPanel.find('li.feed').length);
.next('p').text(numberOfFeeds);
}
});
......
......@@ -577,71 +577,57 @@ PTL.feed = {
$refreshButton.addClass('spin');
let fetchFeed = await PTL.feed.fetchFeed(feedUrl, feedLastItem);
try {
let fetchFeed = await PTL.feed.fetchFeed(feedUrl, feedLastItem);
if (fetchFeed.error) {
if (fetchFeed.error) {
$feedBody
.empty()
.append(PTL.feed.errorFeed(fetchFeed.error, feedUrl))
.css('height', '');
$feedLink.addClass('danger');
$refreshButton.removeClass('spin');
} else {
let lastItems = await PTL.feed.lastItems(fetchFeed.feedItems, $dataStore);
$feedBody
.empty()
.append(PTL.feed.errorFeed(fetchFeed.error, feedUrl))
.css('height', '');
$feedLink.addClass('danger');
$refreshButton.removeClass('spin');
let isInsecureLinks = lastItems[2];
} else {
const feedName = fetchFeed.feedTitle;
let lastItems = await PTL.feed.lastItems(fetchFeed.feedItems, $dataStore);
let $insecureIcon = $('<i>')
.attr('class', 'icon-lock-open insecureIcon warning')
.attr('title', PTL.tr("Some linked elements (image, audio or video) within this feed's items could not be loaded because they were served insecurely"));
if (isInsecureLinks) $warningIconSpan.html($insecureIcon);
$feedBody.html(lastItems[0]);
let isInsecureLinks = lastItems[2];
$refreshButton
.attr('title', PTL.tr('Refresh this feed') + ' (' + feedUrl + ', ' + timeStamp + ')')
.removeClass('spin');
$feedLink
.attr('href', fetchFeed.feedLink)
.attr('title', feedName)
.text(feedName);
if ($dataStore.data('name') == '') $dataStore.data('name', feedName);
if (fetchFeed.totalNewItems > 0) {
$dataStore.data('lastitem', fetchFeed.lastItem);
PTL.tab.saveTabs();
$badge
.text(fetchFeed.totalNewItems)
.attr('title', PTL.tr('There are %1 new items in this feed', fetchFeed.totalNewItems))
.fadeIn('slow');
} else {
$badge.fadeOut('slow');
}
const feedName = fetchFeed.feedTitle;
}
} catch (error) {
let $insecureIcon = $('<i>')
.attr('class', 'icon-lock warning')
.attr('title', PTL.tr("Some linked elements (image, audio or video) within this feed's items could not be loaded because they were served insecurely"));
if (isInsecureLinks) $warningIconSpan.html($insecureIcon);
$feedBody.html(lastItems[0]);
console.error('Error: %s (%s)', error);
$refreshButton
.attr('title', PTL.tr('Refresh this feed') + ' (' + feedUrl + ', ' + timeStamp + ')')
.removeClass('spin');
$feedBody
.empty()
.append(PTL.feed.errorFeed(fetchFeed.error, feedUrl))
.css('height', '');
$feedLink.addClass('danger');
$refreshButton.removeClass('spin');
$feedLink
.attr('href', fetchFeed.feedLink)
.attr('title', feedName)
.text(feedName);
if ($dataStore.data('name') == '') $dataStore.data('name', feedName);
if (fetchFeed.totalNewItems > 0) {
$dataStore.data('lastitem', fetchFeed.lastItem);
PTL.tab.saveTabs();
$badge
.text(fetchFeed.totalNewItems)
.attr('title', PTL.tr('There are %1 new items in this feed', fetchFeed.totalNewItems))
.fadeIn('slow');
} else {
$badge.fadeOut('slow');
}
}
} else {
$feedBody.addClass('folded');
......
......@@ -106,6 +106,8 @@ PTL.i18n.translations = {
"The news page that doesn't know you" : { fr: "La page d'actu qui ne sait rien de toi", ja: "あなたを知らないニュースページ", es: "La página de noticias que no te conoce" },
"There are %1 new items in this feed" : { fr: "Il y a %1 nouveaux articles dans ce flux", ja: "このフィードには%1つの新しいアイテムがあります", es: "Hay %1 artículos nuevos en este feed." },
"Name the feed of this website, if it is not informative enough ; leave blank to get the default feed title." : { fr: "Renommez le flux de ce site Web, s'il n'est pas assez informatif ; laisser vide pour obtenir le titre du flux par défaut." , ja: "それが十分に報知されていない場合、このウェブサイトのフィードに名前を付けます。 デフォルトフィードのタイトルを取得するには、空白のままにします", es: "Nombra la feed de este sitio web, si no es lo suficientemente informativo; Deje en blanco para obtener el título de feed predeterminado." },
......
......@@ -34,7 +34,23 @@ PTL.tab = {
});
$tabs.on("click", "i.tabCloser", function() {
PTL.dialog.killTab($(this));
const $tabs = $('#tabs'),
$a = $(this).prev('a.ui-tabs-anchor'),
tabName = $a.text(),
tabId = $a.attr('href'),
$selectedTab = $a.parent(),
$selectedPanel = $tabs.find(tabId),
numberOfFeeds = $selectedPanel.find('li.feed').length;
if (numberOfFeeds) {
PTL.dialog.killTab($(this), $a.text(), $selectedTab, $selectedPanel, numberOfFeeds);
} else {
$selectedTab.remove();
$selectedPanel.remove();
PTL.tab.saveTabs();
}
});
if (PTL.util.isMobile()) {
......
......@@ -5,7 +5,7 @@ PTL.util = {
return $($('.ui-tabs-active')
.find('a')
.attr('href'))
.find('.column').first()
.find('.column').first();
},
XMLtoJSON: function() {
......@@ -20,317 +20,318 @@ PTL.util = {
xmlDoc.async = "false";
}
var json_str = jsontoStr(setJsonObj(xmlDoc));
return (typeof (rstr) == 'undefined') ? JSON.parse(json_str) : json_str;
};
var setJsonObj = function(xml) {
var js_obj = {};
if (xml.nodeType == 1) {
if (xml.attributes.length > 0) {
js_obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
js_obj["@attributes"][attribute.nodeName] = attribute.value;
}
return (typeof (rstr) == 'undefined') ? JSON.parse(json_str) : json_str;
};
var setJsonObj = function(xml) {
var js_obj = {};
if (xml.nodeType == 1) {
if (xml.attributes.length > 0) {
js_obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++) {
var attribute = xml.attributes.item(j);
js_obj["@attributes"][attribute.nodeName] = attribute.value;
}
} else if (xml.nodeType == 3) {
js_obj = xml.nodeValue;
}
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof (js_obj[nodeName]) == "undefined") {
js_obj[nodeName] = setJsonObj(item);
} else {
if (typeof (js_obj[nodeName].push) == "undefined") {
var old = js_obj[nodeName];
js_obj[nodeName] = [];
js_obj[nodeName].push(old);
}
js_obj[nodeName].push(setJsonObj(item));
} else if (xml.nodeType == 3) {
js_obj = xml.nodeValue;
}
if (xml.hasChildNodes()) {
for (var i = 0; i < xml.childNodes.length; i++) {
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof (js_obj[nodeName]) == "undefined") {
js_obj[nodeName] = setJsonObj(item);
} else {
if (typeof (js_obj[nodeName].push) == "undefined") {
var old = js_obj[nodeName];
js_obj[nodeName] = [];
js_obj[nodeName].push(old);
}
js_obj[nodeName].push(setJsonObj(item));
}
}
return js_obj;
};
}
return js_obj;
};
var jsontoStr = function(js_obj) {
var rejsn = JSON.stringify(js_obj, undefined, 2)
var jsontoStr = function(js_obj) {
var rejsn = JSON.stringify(js_obj, undefined, 2)
.replace(/(\\t|\\r|\\n)/g, '')
.replace(/"",[\n\t\r\s]+""[,]*/g, '')
.replace(/(\n[\t\s\r]*\n)/g, '')
.replace(/[\s\t]{2,}""[,]{0,1}/g, '')
.replace(/"[\s\t]{1,}"[,]{0,1}/g, '')
.replace(/\[[\t\s]*\]/g, '""');
return (rejsn.indexOf('"parsererror": {') == -1) ? rejsn : 'Invalid XML format';
};
},
isNV: function(xml) {
return xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>');
},
importNV: function(xml) {
var xml2json = new PTL.util.XMLtoJSON(),
return (rejsn.indexOf('"parsererror": {') == -1) ? rejsn : 'Invalid XML format';
};
},
isNV: function(xml) {
return xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>');
},
importNV: function(xml) {
var xml2json = new PTL.util.XMLtoJSON(),
objson = xml2json.fromStr(xml),
dbparsed = JSON.stringify(objson),
all = objson.opml.body.outline,
allTabs = [];
var totalNbOfFeed = 0;
var totalNbOfFeed = 0;
for (var nbOfTabs in all) {
var tabs = all[nbOfTabs],
for (var nbOfTabs in all) {
var tabs = all[nbOfTabs],
feeds = tabs.outline;
for (var key in tabs.outline[0]) {
var tab = tabs[key],
for (var key in tabs.outline[0]) {
var tab = tabs[key],
thisTab = {},
thisTabFeeds = [],
thisColFeeds = [];
for (var nbOfFeeds in feeds) {
var thisFeed = {};
totalNbOfFeed++;
thisFeed.status = "off";
thisFeed.limit = 220;
thisFeed.type = "mixed";
thisFeed.url = DOMPurify.sanitize(feeds[nbOfFeeds]["@attributes"].xmlUrl);
thisIndex = DOMPurify.sanitize(Number(feeds[nbOfFeeds]["@attributes"].col)) - 1;
if (!thisColFeeds[thisIndex]) thisColFeeds[thisIndex] = [];
thisColFeeds[thisIndex].push(thisFeed);
}
thisTabFeeds.push(thisColFeeds);
thisTab.columns = thisColFeeds;
thisTab.name = tab.title;
allTabs.push(thisTab);
for (var nbOfFeeds in feeds) {
var thisFeed = {};
var thisIndex;
totalNbOfFeed++;
thisFeed.status = "off";
thisFeed.limit = 220;
thisFeed.type = "mixed";
thisFeed.url = DOMPurify.sanitize(feeds[nbOfFeeds]["@attributes"].xmlUrl);
thisIndex = DOMPurify.sanitize(Number(feeds[nbOfFeeds]["@attributes"].col)) - 1;
if (!thisColFeeds[thisIndex]) thisColFeeds[thisIndex] = [];
thisColFeeds[thisIndex].push(thisFeed);
}
thisTabFeeds.push(thisColFeeds);
thisTab.columns = thisColFeeds;
thisTab.name = tab.title;
allTabs.push(thisTab);
}
}
PTL.tab.empty(function() {
PTL.tab.populate(allTabs, true);
PTL.util.say(PTL.tr('Data structure OK: %1 tab(s) containing %2 feed(s)', Number(nbOfTabs) + 1, Number(totalNbOfFeed)), 'success', true);
});
},
beg: function() {
PTL.tab.empty(function() {
PTL.tab.populate(allTabs, true);
PTL.util.say(PTL.tr('Data structure OK: %1 tab(s) containing %2 feed(s)', Number(nbOfTabs) + 1, Number(totalNbOfFeed)), 'success', true);
});
const dateNow = Date.now(),
nextNag = PTL.prefs.readConfig('nextNag');
},
beg: function() {
PTL.util.say(PTL.tr('Pétrolette needs you'), 'success');
const dateNow = Date.now(),
nextNag = PTL.prefs.readConfig('nextNag');
if (nextNag === 0) {
PTL.dialog.beg();
PTL.prefs.writeConfig('nextNag', dateNow + 86400000); // 24 hours
}
PTL.util.say(PTL.tr('Pétrolette needs you'), 'success');
if (dateNow > nextNag) {
PTL.prefs.writeConfig('nextNag', dateNow + 43200000);
PTL.dialog.beg();
}
},
say: function(text, type, notify, title) {
if (nextNag === 0) {
PTL.dialog.beg();
PTL.prefs.writeConfig('nextNag', dateNow + 86400000); // 24 hours
}
const $lines = $('#console div'),
d = new Date();
if (dateNow > nextNag) {
PTL.prefs.writeConfig('nextNag', dateNow + 43200000);
PTL.dialog.beg();
}
const dialogTitle = title ? title : PTL.tr(type[0].toUpperCase() + type.substring(1));
},
say: function(text, type, notify, title) {