Merge branch 'master' of git://github.com/fguillot/miniflux

Conflicts:
	miniflux/templates/read_item.php
	miniflux/templates/unread_items.php
This commit is contained in:
Dysosmus 2013-05-18 16:49:07 +02:00
commit 2003f47c4c
43 changed files with 1630 additions and 869 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
update*
make-archive.sh
*.sqlite
*.db

View File

@ -14,26 +14,33 @@ Features
- Import/Export OPML feeds
- Feeds update by a cronjob or with the user interface in one click
- Protected by a login/password (only one possible user)
- Use secure headers (only external images are allowed)
- Use secure headers (only external images and Youtube/Vimeo videos are allowed)
- Open external links inside a new tab with a `rel="noreferrer"` attribute
- Mobile CSS (responsive design)
- Keyboard shortcuts
Todo
----
Todo and known bugs
-------------------
- Remove older items from the database
- See Issues: <https://github.com/fguillot/miniflux/issues>
License
-------
- AGPL: <http://www.gnu.org/licenses/agpl-3.0.txt>
Authors
-------
- Original author: [Frédéric Guillot](http://fredericguillot.com/)
- Contributors: [Pull requesters](https://github.com/fguillot/miniflux/pulls?direction=desc&page=1&sort=created&state=closed) and [Bug reporters](https://github.com/fguillot/miniflux/issues?page=1&state=closed)
Requirements
------------
- PHP >= 5.3.7
- PHP XML extensions (SimpleXML, DOM...)
- PHP Sqlite extensions
- PHP Sqlite extension
Libraries used
--------------
@ -81,6 +88,11 @@ Your life is cluttered.
Miniflux is a minimalist software. Less is more.
### Why there is no favourites?
Use the right tool for the right job.
Your browser already have bookmarks, if you don't like it there is many online tools for that.
### I found a bug, what next?
Report the bug to the [issues tracker](https://github.com/fguillot/miniflux/issues) and I will fix it.

View File

@ -24,13 +24,13 @@ body {
a {
color: #3366CC;
border: 1px solid rgba(255, 255, 255, 0);
}
a:focus {
outline: 0;
color: red;
text-decoration: none;
padding: 3px;
border: 1px dotted #aaa;
}
@ -63,6 +63,12 @@ blockquote {
font-family: Georgia, serif;
}
q {
color: purple;
font-family: Georgia, serif;
font-style: italic;
}
blockquote + p {
color: #555;
font-style: italic;
@ -127,8 +133,6 @@ form {
label {
cursor: pointer;
display: block;
float: left;
width: 10em;
}
input {
@ -144,7 +148,8 @@ input[type="text"] {
line-height: 15px;
width: 250px;
font-size: 99%;
margin-bottom: 15px;
margin-bottom: 10px;
margin-top: 5px;
}
input[type="email"]:focus,
@ -196,14 +201,14 @@ textarea.form-error {
.form-errors {
color: #b94a48;
margin-left: 10em;
margin-left: 15px;
list-style-type: none;
}
.form-help {
font-size: 0.9em;
color: brown;
display: inline;
margin-bottom: 15px;
}
/* alerts */
@ -234,6 +239,12 @@ textarea.form-error {
border-color: #bce8f1;
}
.alert-normal {
color: #333;
background-color: #f0f0f0;
border-color: #ddd;
}
/* buttons */
.btn {
@ -340,8 +351,7 @@ nav .active a {
.page-header li {
font-size: 90%;
display: inline;
padding-left: 10px;
padding-right: 10px;
padding-right: 5px;
border-right: 1px dotted #ccc;
}
@ -389,17 +399,25 @@ nav .active a {
font-family: Georgia, serif;
}
.items #current-item {
border: 2px dashed #d14;
padding: 5px;
}
/* item */
.item {
color: #333;
padding-left: 5px;
padding-right: 5px;
padding-bottom: 50px;
color: #555;
}
.item p,
.item li {
font-family: Georgia, serif;
line-height: 1.6em;
font-size: 1.1em;
}
.item h2,
@ -443,6 +461,44 @@ nav .active a {
color: purple;
}
.item nav span,
.item nav a,
.item nav a:visited {
color: #3366CC;
}
.item nav {
border-top: 1px dotted #ddd;
padding-top: 8px;
margin-top: 50px;
width: 100%;
}
.nav-left {
width: 30%;
display: block;
float: left;
}
.nav-middle {
text-align: center;
float: left;
width: 40%;
}
.nav-right {
text-align: right;
margin-left: 70%;
width: 30%;
display: block;
}
/* other pages */
section li {
margin-left: 15px;
list-style-type: square;
}
/* mobile design */
@media only screen and (max-width: 480px) {
@ -460,6 +516,7 @@ nav .active a {
.logo {
display: block;
float: none;
border-bottom: 1px dotted #ddd;
}
header {
@ -479,11 +536,19 @@ nav .active a {
line-height: 25px;
}
nav ul {
padding-top: 5px;
}
.page {
clear: both;
padding-top: 20px;
}
.page li {
margin-left: 5px;
}
.item {
font-size: 0.8em;
}
@ -496,4 +561,4 @@ nav .active a {
font-size: 0.9em;
padding: 0;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

View File

@ -5,21 +5,48 @@
var queue_length = 5;
function switch_status(item_id)
{
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState === 4 && is_listing()) {
var response = JSON.parse(request.responseText);
if (response.status == "read" || response.status == "unread") {
find_next_item();
remove_item(response.item_id);
}
}
}
request.open("POST", "?action=change-item-status&id=" + item_id, true);
request.send();
}
function mark_as_read(item_id)
{
var request = new XMLHttpRequest();
request.onload = function() {
var article = document.getElementById("item-" + item_id);
if (article) {
article.style.display = "none";
}
remove_item(item_id);
};
request.open("POST", "?action=read&id=" + item_id, true);
request.open("POST", "?action=mark-item-read&id=" + item_id, true);
request.send();
}
function mark_as_unread(item_id)
{
var request = new XMLHttpRequest();
request.open("POST", "?action=mark-item-unread&id=" + item_id, true);
request.send();
}
@ -60,28 +87,31 @@
var request = new XMLHttpRequest();
request.onload = function() {
request.onreadystatechange = function() {
hide_refresh_icon(feed_id);
if (request.readyState === 4) {
try {
hide_refresh_icon(feed_id);
var response = JSON.parse(this.responseText);
try {
if (callback) {
var response = JSON.parse(this.responseText);
callback(response);
}
if (! response.result) {
//window.alert('Unable to refresh this feed: ' + feed_id);
if (callback) {
callback(response);
}
if (! response.result) {
//window.alert('Unable to refresh this feed: ' + feed_id);
}
}
catch (e) {}
}
catch (e) {}
};
request.open("GET", "?action=ajax-refresh-feed&feed_id=" + feed_id, true);
request.open("POST", "?action=refresh-feed&feed_id=" + feed_id, true);
request.send();
return true;
@ -137,6 +167,161 @@
}
function remove_item(item_id)
{
var item = document.getElementById("item-" + item_id);
if (item) item.parentNode.removeChild(item);
}
function open_original_item()
{
var link = document.getElementById("original-item");
if (link) {
if (link.getAttribute("data-action") == "mark-read") {
mark_as_read(link.getAttribute("data-item-id"));
find_next_item();
}
link.click();
}
}
function open_item()
{
var link = document.getElementById("open-item");
if (link) link.click();
}
function open_next_item()
{
var link = document.getElementById("next-item");
if (link) {
link.click();
}
else if (is_listing()) {
find_next_item();
}
}
function open_previous_item()
{
var link = document.getElementById("previous-item");
if (link) {
link.click();
}
else if (is_listing()) {
find_previous_item();
}
}
function change_item_status()
{
var item = document.getElementById("current-item");
if (item) switch_status(item.getAttribute("data-item-id"));
}
function set_links_item(item_id)
{
var link = document.getElementById("original-item");
if (link) link.id = "original-" + link.getAttribute("data-item-id");
var link = document.getElementById("open-item");
if (link) link.id = "open-" + link.getAttribute("data-item-id");
var link = document.getElementById("original-" + item_id);
if (link) link.id = "original-item";
var link = document.getElementById("open-" + item_id);
if (link) link.id = "open-item";
}
function find_next_item()
{
var items = document.getElementsByTagName("article");
if (! document.getElementById("current-item")) {
items[0].id = "current-item";
set_links_item(items[0].getAttribute("data-item-id"));
}
else {
for (var i = 0, ilen = items.length; i < ilen; i++) {
if (items[i].id == "current-item") {
items[i].id = "item-" + items[i].getAttribute("data-item-id");
if (i + 1 < ilen) {
items[i + 1].id = "current-item";
set_links_item(items[i + 1].getAttribute("data-item-id"));
}
break;
}
}
}
}
function find_previous_item()
{
var items = document.getElementsByTagName("article");
if (! document.getElementById("current-item")) {
items[items.length - 1].id = "current-item";
set_links_item(items[items.length - 1].getAttribute("data-item-id"));
}
else {
for (var i = items.length - 1; i >= 0; i--) {
if (items[i].id == "current-item") {
items[i].id = "item-" + items[i].getAttribute("data-item-id");
if (i - 1 >= 0) {
items[i - 1].id = "current-item";
set_links_item(items[i - 1].getAttribute("data-item-id"));
}
break;
}
}
}
}
function is_listing()
{
if (document.getElementById("listing")) {
return true;
}
return false;
}
document.onclick = function(e) {
var action = e.target.getAttribute("data-action");
@ -161,4 +346,25 @@
}
};
document.onkeypress = function(e) {
switch (e.keyCode || e.which) {
case 112:
open_previous_item();
break;
case 110:
open_next_item();
break;
case 118:
open_original_item();
break;
case 111:
open_item();
break;
case 109:
change_item_status();
break;
}
};
})();

View File

@ -3,20 +3,33 @@
require 'check_setup.php';
require 'vendor/password.php';
require 'vendor/PicoTools/Dependency_Injection.php';
require 'vendor/PicoTools/Translator.php';
require 'vendor/PicoDb/Database.php';
require 'vendor/PicoDb/Table.php';
require 'schema.php';
require 'model.php';
const DB_VERSION = 4;
const APP_VERSION = 'master';
const APP_USERAGENT = 'Miniflux - http://miniflux.net';
const HTTP_TIMEOUT = 5;
function get_db_filename()
{
return 'data/db.sqlite';
}
PicoTools\container('db', function() {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => 'data/db.sqlite'
'filename' => get_db_filename()
));
if ($db->schema()->check(1)) {
if ($db->schema()->check(DB_VERSION)) {
return $db;
}

BIN
miniflux/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -22,11 +22,30 @@ Router\before(function($action) {
if ($action !== 'login' && ! isset($_SESSION['user'])) {
PicoFarad\Response\redirect('?action=login');
Response\redirect('?action=login');
}
$language = 'en_US';
if (isset($_SESSION['user']['language'])) {
$language = $_SESSION['user']['language'];
}
else if (isset($_COOKIE['language'])) {
$language = $_COOKIE['language'];
}
if ($language !== 'en_US') {
PicoTools\Translator\load($language);
}
setcookie('language', $language, time()+365*24*3600, dirname($_SERVER['PHP_SELF']));
Response\csp(array(
'img-src' => '*'
'img-src' => '*',
'frame-src' => 'http://www.youtube.com https://www.youtube.com http://player.vimeo.com https://player.vimeo.com'
));
Response\xframe();
@ -74,31 +93,85 @@ Router\post_action('login', function() {
});
Router\get_action('read', function() {
Router\get_action('show', function() {
$id = Request\param('id');
Model\set_item_read($id);
Response\html(Template\layout('read_item', array(
'item' => Model\get_item($id)
)));
});
Router\post_action('read', function() {
Router\get_action('read', function() {
$id = Request\param('id');
$item = Model\get_item($id);
$nav = Model\get_nav_item($item); // must be placed before set_item_read()
Model\set_item_read($id);
Response\html(Template\layout('read_item', array(
'item' => $item,
'item_nav' => $nav
)));
});
Router\get_action('mark-item-read', function() {
$id = Request\param('id');
Model\set_item_read($id);
Response\Redirect('?action=default');
});
Router\get_action('mark-item-unread', function() {
$id = Request\param('id');
Model\set_item_unread($id);
Response\Redirect('?action=history');
});
Router\get_action('mark-item-removed', function() {
$id = Request\param('id');
Model\set_item_removed($id);
Response\Redirect('?action=history');
});
Router\post_action('mark-item-read', function() {
$id = Request\param('id');
Model\set_item_read($id);
Response\json(array('Ok'));
});
Router\post_action('mark-item-unread', function() {
$id = Request\param('id');
Model\set_item_unread($id);
Response\json(array('Ok'));
});
Router\post_action('change-item-status', function() {
$id = Request\param('id');
Response\json(array(
'item_id' => urlencode($id),
'status' => Model\switch_item_status($id)
));
});
Router\get_action('history', function() {
Response\html(Template\layout('read_items', array(
Response\html(Template\layout('history', array(
'items' => Model\get_read_items(),
'menu' => 'history'
)));
@ -122,11 +195,11 @@ Router\get_action('remove', function() {
if ($id && Model\remove_feed($id)) {
Session\flash('This subscription has been removed successfully');
Session\flash(t('This subscription has been removed successfully.'));
}
else {
Session\flash_error('Unable to remove this subscription');
Session\flash_error(t('Unable to remove this subscription.'));
}
Response\redirect('?action=feeds');
@ -146,7 +219,7 @@ Router\get_action('refresh-feed', function() {
});
Router\get_action('ajax-refresh-feed', function() {
Router\post_action('refresh-feed', function() {
$id = Request\int_param('feed_id');
@ -159,13 +232,21 @@ Router\get_action('ajax-refresh-feed', function() {
});
Router\get_action('flush-unread', function() {
Router\get_action('mark-as-read', function() {
Model\flush_unread();
Model\mark_as_read();
Response\redirect('?action=unread');
});
Router\get_action('confirm-flush-history', function() {
Response\html(Template\layout('confirm_flush', array(
'menu' => 'history'
)));
});
Router\get_action('flush-history', function() {
Model\flush_read();
@ -176,7 +257,7 @@ Router\get_action('flush-history', function() {
Router\get_action('refresh-all', function() {
Model\update_feeds();
Session\flash('Your subscriptions are updated');
Session\flash(t('Your subscriptions are updated'));
Response\redirect('?action=unread');
});
@ -205,26 +286,32 @@ Router\post_action('add', function() {
if (Model\import_feed($_POST['url'])) {
Session\flash('Subscription added successfully.');
Session\flash(t('Subscription added successfully.'));
Response\redirect('?action=feeds');
}
else {
Session\flash_error('Unable to find a subscription.');
Session\flash_error(t('Unable to find a subscription.'));
}
Response\html(Template\layout('add', array(
'values' => array('url' => $_POST['url']),
'errors' => array('url' => 'Unable to find a news feed.'),
'menu' => 'feeds'
)));
});
Router\get_action('optimize-db', function() {
\PicoTools\singleton('db')->getConnection()->exec('VACUUM');
Response\redirect('?action=config');
});
Router\get_action('download-db', function() {
Response\force_download('db.sqlite.gz');
Response\binary(gzencode(file_get_contents('data/db.sqlite')));
Response\binary(gzencode(file_get_contents(get_db_filename())));
});
@ -248,14 +335,14 @@ Router\post_action('import', function() {
if (Model\import_feeds(Request\file_content('file'))) {
Session\flash('Your feeds are imported.');
Session\flash(t('Your feeds have been imported.'));
}
else {
Session\flash_error('Unable to import your OPML file.');
Session\flash_error(t('Unable to import your OPML file.'));
}
Response\redirect('?action=feeds');
Response\redirect('?action=import');
});
@ -264,6 +351,8 @@ Router\get_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => array(),
'values' => Model\get_config(),
'db_size' => filesize(get_db_filename()),
'languages' => Model\get_languages(),
'menu' => 'config'
)));
});
@ -278,11 +367,11 @@ Router\post_action('config', function() {
if (Model\save_config($values)) {
Session\flash('Your preferences are updated.');
Session\flash(t('Your preferences are updated.'));
}
else {
Session\flash_error('Unable to update your preferences.');
Session\flash_error(t('Unable to update your preferences.'));
}
Response\redirect('?action=config');
@ -291,6 +380,8 @@ Router\post_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => $errors,
'values' => $values,
'db_size' => filesize(get_db_filename()),
'languages' => Model\get_languages(),
'menu' => 'config'
)));
});

View File

@ -0,0 +1,87 @@
<?php
return array(
'French' => 'Français',
'English' => 'Anglais',
'unread' => 'non lus',
'history' => 'historique',
'subscriptions' => 'abonnements',
'Subscriptions' => 'Abonnements',
'preferences' => 'préférences',
'Preferences' => 'Préférences',
'logout' => 'déconnexion',
'Username' => 'Utilisateur',
'Password' => 'Mot de passe',
'Confirmation' => 'Confirmation',
'Language' => 'Langue',
'Update' => 'Mettre à jour',
'More informations' => 'Plus d\'informations',
'Database' => 'Base de données',
'Database size:' => 'Taille de la base de données :',
'Optimize the database' => 'Optimiser la base de données',
'(VACUUM command)' => '(commande SQL VACUUM)',
'Download the entire database' => 'Télécharger la base de données complète',
'(Gzip compressed Sqlite file)' => '(Fichier Sqlite compressé en Gzip)',
'Keyboard shortcuts' => 'Raccourcis clavier',
'Previous item' => 'Élément précédent',
'Next item' => 'Élément suivant',
'Mark as read or unread' => 'Marquer comme lu ou non lu',
'Open original link' => 'Ouvrir le lien original',
'Open item' => 'Ouvrir un élément',
'About' => 'A propos',
'Miniflux version:' => 'Version de Miniflux :',
'Nothing to read' => 'Rien à lire',
'Unread items' => 'Éléments non lus',
'mark all as read' => 'tout marquer comme lu',
'original link' => 'lien original',
'mark as read' => 'marquer comme lu',
'No history' => 'Aucun historique',
'mark as unread' => 'marquer comme non lu',
'History' => 'Historique',
'flush these items' => 'supprimer ces éléments',
'Item not found' => 'Élément introuvable',
'Unread items' => 'Éléments non lus',
'Next' => 'Suivant',
'Previous' => 'Précédent',
'Sign in' => 'Connexion',
'feeds' => 'abonnements',
'add' => 'ajouter',
'import' => 'importer',
'export' => 'exporter',
'OPML Import' => 'Importation OPML',
'OPML file' => 'Fichier OPML',
'Import' => 'Importer',
'refresh all' => 'actualiser',
'No subscription' => 'Aucun abonnement',
'remove' => 'supprimer',
'refresh' => 'actualiser',
'feed link' => 'lien du flux',
'New subscription' => 'Nouvel abonnement',
'Website or Feed URL' => 'URL du site ou du flux',
'Add' => 'Ajouter',
'http://website/' => 'http://siteweb/',
'Yes' => 'Oui',
'cancel' => 'annuler',
'or' => 'ou',
'Official website:' => 'Site officiel :',
'Bad username or password' => 'Mauvais utilisateur ou mot de passe',
'Unable to update your preferences.' => 'Impossible de mettre à jour vos préférences.',
'Your preferences are updated.' => 'Vos préférences ont été mises à jour.',
'Unable to import your OPML file.' => 'Impossible d\'importer votre fichier OPML',
'Your feeds have been imported.' => 'Vos abonnements ont été importés avec succès.',
'Unable to find a subscription.' => 'Impossible de trouver un abonnement.',
'Subscription added successfully.' => 'Abonnement ajouté avec succès.',
'Your subscriptions are updated' => 'Vos abonnements ont été mis à jour',
'Unable to remove this subscription.' => 'Impossible de supprimer cet abonnement.',
'This subscription has been removed successfully.' => 'L\'abonnement a été supprimé avec succès.',
'The user name is required' => 'Le nom d\'utilisateur est obligatoire',
'The maximum length is 50 characters' => 'La longueur maximale est de 50 caractères',
'The password is required' => 'Le mot de passe est obligatoire',
'The minimum length is 6 characters' => 'La longueur minimale est de 6 caractères',
'The confirmation is required' => 'La confirmation est obligatoire',
'Passwords doesn\'t match' => 'Les mots de passe ne sont pas identique',
'Do you really want to remove these items from your history?' => 'Voulez-vous vraiment supprimer les éléments de votre historique ?',
'Do you really want to remove this subscription: "%s"?' => 'Voulez-vous vraiment supprimer cet abonnement : "%s" ?',
'Nothing to read, do you want to <a href="?action=refresh-all" data-action="refresh-all">update your subscriptions?</a>' =>
'Il n\'y a rien à lire, voulez-vous <a href="?action=refresh-all" data-action="refresh-all">mettre à jour vos abonnements ?</a>'
);

View File

@ -4,7 +4,6 @@ namespace Model;
require_once 'vendor/PicoFeed/Export.php';
require_once 'vendor/PicoFeed/Import.php';
require_once 'vendor/PicoFeed/Parser.php';
require_once 'vendor/PicoFeed/Reader.php';
require_once 'vendor/SimpleValidator/Validator.php';
require_once 'vendor/SimpleValidator/Base.php';
@ -22,6 +21,15 @@ use PicoFeed\Reader;
use PicoFeed\Export;
function get_languages()
{
return array(
'en_US' => t('English'),
'fr_FR' => t('French')
);
}
function export_feeds()
{
$opml = new Export(get_feeds());
@ -64,7 +72,7 @@ function import_feeds($content)
function import_feed($url)
{
$reader = new Reader;
$reader->download($url);
$resource = $reader->download($url, '', '', HTTP_TIMEOUT, APP_USERAGENT);
$parser = $reader->getParser();
@ -72,10 +80,14 @@ function import_feed($url)
$feed = $parser->execute();
if ($feed === false) return false;
if (! $feed->title || ! $feed->url) return false;
$db = \PicoTools\singleton('db');
if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) {
// Etag and LastModified are added the next update
$rs = $db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->url,
@ -96,6 +108,65 @@ function import_feed($url)
}
function update_feeds()
{
foreach (get_feeds_id() as $feed_id) {
update_feed($feed_id);
}
// Auto-vacuum for people using the cronjob
\PicoTools\singleton('db')->getConnection()->exec('VACUUM');
}
function update_feed($feed_id)
{
$feed = get_feed($feed_id);
$reader = new Reader;
$resource = $reader->download(
$feed['feed_url'],
$feed['last_modified'],
$feed['etag'],
HTTP_TIMEOUT,
APP_USERAGENT
);
if (! $resource->isModified()) {
return true;
}
$parser = $reader->getParser();
if ($parser !== false) {
$feed = $parser->execute();
if ($feed !== false) {
update_feed_cache_infos($feed_id, $resource->getLastModified(), $resource->getEtag());
update_items($feed_id, $feed->items);
return true;
}
}
return false;
}
function get_feeds_id()
{
return \PicoTools\singleton('db')
->table('feeds')
->asc('updated')
->listing('id', 'id');
}
function get_feeds()
{
return \PicoTools\singleton('db')
@ -114,10 +185,21 @@ function get_feed($feed_id)
}
function update_feed_cache_infos($feed_id, $last_modified, $etag)
{
\PicoTools\singleton('db')
->table('feeds')
->eq('id', $feed_id)
->save(array(
'last_modified' => $last_modified,
'etag' => $etag
));
}
function remove_feed($feed_id)
{
$db = \PicoTools\singleton('db');
$db->table('items')->eq('feed_id', $feed_id)->remove();
return $db->table('feeds')->eq('id', $feed_id)->remove();
@ -157,6 +239,44 @@ function get_item($id)
}
function get_nav_item($item)
{
$unread_items = \PicoTools\singleton('db')
->table('items')
->columns('items.id')
->eq('status', 'unread')
->desc('updated')
->findAll();
$next_item = null;
$previous_item = null;
for ($i = 0, $ilen = count($unread_items); $i < $ilen; $i++) {
if ($unread_items[$i]['id'] == $item['id']) {
if ($i > 0) $previous_item = $unread_items[$i - 1];
if ($i < ($ilen - 1)) $next_item = $unread_items[$i + 1];
break;
}
}
return array(
'next' => $next_item,
'previous' => $previous_item
);
}
function set_item_removed($id)
{
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'removed'));
}
function set_item_read($id)
{
\PicoTools\singleton('db')
@ -166,6 +286,55 @@ function set_item_read($id)
}
function set_item_unread($id)
{
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
}
function switch_item_status($id)
{
$item = \PicoTools\singleton('db')
->table('items')
->columns('status')
->eq('id', $id)
->findOne();
if ($item['status'] == 'unread') {
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
return 'read';
}
else {
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
return 'unread';
}
return '';
}
function mark_as_read()
{
\PicoTools\singleton('db')
->table('items')
->eq('status', 'unread')
->save(array('status' => 'read'));
}
function flush_unread()
{
\PicoTools\singleton('db')
@ -184,63 +353,50 @@ function flush_read()
}
function update_feeds()
{
foreach (get_feeds() as $feed) {
$reader = new Reader;
$reader->download($feed['feed_url']);
$parser = $reader->getParser();
if ($parser !== false) {
update_items($feed['id'], $parser->execute()->items);
}
}
}
function update_feed($feed_id)
{
$feed = get_feed($feed_id);
$reader = new Reader;
$reader->download($feed['feed_url']);
$parser = $reader->getParser();
if ($parser !== false) {
update_items($feed['id'], $parser->execute()->items);
return true;
}
return false;
}
function update_items($feed_id, array $items)
{
$items_in_feed = array();
$db = \PicoTools\singleton('db');
$db->startTransaction();
foreach ($items as $item) {
if ($item->id && ! $db->table('items')->eq('id', $item->id)->count()) {
// Item parsed correctly?
if ($item->id) {
$db->table('items')->save(array(
'id' => $item->id,
'title' => $item->title,
'url' => $item->url,
'updated' => $item->updated,
'author' => $item->author,
'content' => $item->content,
'status' => 'unread',
'feed_id' => $feed_id
));
// Insert only new item
if ($db->table('items')->eq('id', $item->id)->count() !== 1) {
$db->table('items')->save(array(
'id' => $item->id,
'title' => $item->title,
'url' => $item->url,
'updated' => $item->updated,
'author' => $item->author,
'content' => $item->content,
'status' => 'unread',
'feed_id' => $feed_id
));
}
// Items inside this feed
$items_in_feed[] = $item->id;
}
}
// Remove from the database items marked as "removed"
// and not present inside the feed
if (! empty($items_in_feed)) {
\PicoTools\singleton('db')
->table('items')
->notin('id', $items_in_feed)
->eq('status', 'removed')
->eq('feed_id', $feed_id)
->remove();
}
$db->closeTransaction();
}
@ -249,7 +405,7 @@ function get_config()
{
return \PicoTools\singleton('db')
->table('config')
->columns('username', 'history')
->columns('username', 'language')
->findOne();
}
@ -258,7 +414,7 @@ function get_user()
{
return \PicoTools\singleton('db')
->table('config')
->columns('username', 'password')
->columns('username', 'password', 'language')
->findOne();
}
@ -266,9 +422,9 @@ function get_user()
function validate_login(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', 'The user name is required'),
new Validators\MaxLength('username', 'The maximum length is 50 characters', 50),
new Validators\Required('password', 'The password is required')
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required'))
));
$result = $v->execute();
@ -280,12 +436,13 @@ function validate_login(array $values)
if ($user && \password_verify($values['password'], $user['password'])) {
unset($user['password']);
$_SESSION['user'] = $user;
}
else {
$result = false;
$errors['login'] = 'Bad username or password';
$errors['login'] = t('Bad username or password');
}
}
@ -301,19 +458,19 @@ function validate_config_update(array $values)
if (! empty($values['password'])) {
$v = new Validator($values, array(
new Validators\Required('username', 'The user name is required'),
new Validators\MaxLength('username', 'The maximum length is 50 characters', 50),
new Validators\Required('password', 'The password is required'),
new Validators\MinLength('password', 'The minimum length is 6 characters', 6),
new Validators\Required('confirmation', 'The confirmation is required'),
new Validators\Equals('password', 'confirmation', 'Passwords doesn\'t match')
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match'))
));
}
else {
$v = new Validator($values, array(
new Validators\Required('username', 'The user name is required'),
new Validators\MaxLength('username', 'The maximum length is 50 characters', 50)
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50)
));
}
@ -337,5 +494,10 @@ function save_config(array $values)
unset($values['confirmation']);
$_SESSION['user']['language'] = $values['language'];
unset($_COOKIE['language']);
\PicoTools\Translator\load($values['language']);
return \PicoTools\singleton('db')->table('config')->update($values);
}

View File

@ -2,13 +2,32 @@
namespace Schema;
function version_4($pdo)
{
$pdo->exec("CREATE INDEX idx_status ON items(status)");
}
function version_3($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN language TEXT DEFAULT 'en_US'");
}
function version_2($pdo)
{
$pdo->exec('ALTER TABLE feeds ADD COLUMN last_modified TEXT');
$pdo->exec('ALTER TABLE feeds ADD COLUMN etag TEXT');
}
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
username TEXT DEFAULT 'admin',
password TEXT,
history INTEGER DEFAULT '15'
password TEXT
)
");

View File

@ -1,16 +1,16 @@
<div class="page-header">
<h2>New subscription</h2>
<h2><?= t('New subscription') ?></h2>
<ul>
<li><a href="?action=feeds">feeds</a></li>
<li><a href="?action=import">import</a></li>
<li><a href="?action=export">export</a></li>
<li><a href="?action=feeds"><?= t('feeds') ?></a></li>
<li><a href="?action=import"><?= t('import') ?></a></li>
<li><a href="?action=export"><?= t('export') ?></a></li>
</ul>
</div>
<form method="post" action="?action=add" autocomplete="off">
<label for="url">Site or Feed URL</label>
<input type="text" name="url" id="url" placeholder="http://website/" autofocus required/>
<label for="url"><?= t('Website or Feed URL') ?></label>
<input type="text" name="url" id="url" placeholder="<?= t('http://website/') ?>" autofocus required/>
<div class="form-actions">
<button type="submit" class="btn btn-blue">Add</button>
<button type="submit" class="btn btn-blue"><?= t('Add') ?></button>
</div>
</form>

View File

@ -2,21 +2,27 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width">
<link rel="icon" type="image/png" href="./assets/img/favicon.png">
<link rel="shortcut icon" href="favicon.ico">
<link rel="apple-touch-icon" href="./assets/img/touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="72x72" href="./assets/img/touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="114x114" href="./assets/img/touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png">
<title>miniflux</title>
<link href="./assets/css/app.css?v2" rel="stylesheet" media="screen">
<script type="text/javascript" src="./assets/js/app.js?v1" defer></script>
<link href="./assets/css/app.css?v<?= filemtime('assets/css/app.css') ?>" rel="stylesheet" media="screen">
<script type="text/javascript" src="./assets/js/app.js?v<?= filemtime('assets/js/app.js') ?>" defer></script>
</head>
<body>
<header>
<nav>
<a class="logo" href="?">mini<span>flux</span></a>
<ul>
<li <?= isset($menu) && $menu === 'unread' ? 'class="active"' : '' ?>><a href="?action=default">unread</a></li>
<li <?= isset($menu) && $menu === 'history' ? 'class="active"' : '' ?>><a href="?action=history">history</a></li>
<li <?= isset($menu) && $menu === 'feeds' ? 'class="active"' : '' ?>><a href="?action=feeds">subscriptions</a></li>
<li <?= isset($menu) && $menu === 'config' ? 'class="active"' : '' ?>><a href="?action=config">preferences</a></li>
<li><a href="?action=logout">logout</a></li>
<li <?= isset($menu) && $menu === 'unread' ? 'class="active"' : '' ?>><a href="?action=default"><?= t('unread') ?></a></li>
<li <?= isset($menu) && $menu === 'history' ? 'class="active"' : '' ?>><a href="?action=history"><?= t('history') ?></a></li>
<li <?= isset($menu) && $menu === 'feeds' ? 'class="active"' : '' ?>><a href="?action=feeds"><?= t('subscriptions') ?></a></li>
<li <?= isset($menu) && $menu === 'config' ? 'class="active"' : '' ?>><a href="?action=config"><?= t('preferences') ?></a></li>
<li><a href="?action=logout"><?= t('logout') ?></a></li>
</ul>
</nav>
</header>

View File

@ -1,26 +1,54 @@
<div class="page-header">
<h2>Preferences</h2>
<h2><?= t('Preferences') ?></h2>
</div>
<section>
<form method="post" action="?action=config" autocomplete="off">
<?= Helper\form_label('Username', 'username') ?>
<?= Helper\form_label(t('Username'), 'username') ?>
<?= Helper\form_text('username', $values, $errors, array('required')) ?><br/>
<?= Helper\form_label('Password', 'password') ?>
<?= Helper\form_password('password', $values, $errors) ?>
<span class="form-help">Don't use the same password everywhere!</span><br/>
<?= Helper\form_label(t('Password'), 'password') ?>
<?= Helper\form_password('password', $values, $errors) ?><br/>
<?= Helper\form_label('Confirmation', 'confirmation') ?>
<?= Helper\form_label(t('Confirmation'), 'confirmation') ?>
<?= Helper\form_password('confirmation', $values, $errors) ?><br/>
<?= Helper\form_label(t('Language'), 'language') ?>
<?= Helper\form_select('language', $languages, $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="Update" class="btn btn-blue"/>
<input type="submit" value="<?= t('Update') ?>" class="btn btn-blue"/>
</div>
</form>
</section>
<div class="page-section">
<h2>My data</h2>
<h2><?= t('More informations') ?></h2>
</div>
<p><a href="?action=download-db">Download the entire database</a> (Gzip compressed Sqlite file).</p>
<section>
<div class="alert alert-normal">
<h3><?= t('Database') ?></h3>
<ul>
<li><?= t('Database size:') ?> <?= Helper\format_bytes($db_size) ?></li>
<li><a href="?action=optimize-db"><?= t('Optimize the database') ?></a> <?= t('(VACUUM command)') ?></li>
<li><a href="?action=download-db"><?= t('Download the entire database') ?></a> <?= t('(Gzip compressed Sqlite file)') ?></li>
</ul>
</div>
<div class="alert alert-normal">
<h3><?= t('Keyboard shortcuts') ?></h3>
<ul>
<li><?= t('Previous item') ?> = <strong>p</strong></li>
<li><?= t('Next item') ?> = <strong>n</strong></li>
<li><?= t('Mark as read or unread') ?> = <strong>m</strong></li>
<li><?= t('Open original link') ?> = <strong>v</strong></li>
<li><?= t('Open item') ?> = <strong>o</strong></li>
</ul>
</div>
<div class="alert alert-normal">
<h3><?= t('About') ?></h3>
<ul>
<li><?= t('Miniflux version:') ?> <strong><?= APP_VERSION ?></strong></li>
<li><?= t('Official website:') ?> <a href="http://miniflux.net" target="_blank">http://miniflux.net</a></li>
</ul>
</div>
</section>

View File

@ -0,0 +1,10 @@
<div class="page-header">
<h2><?= t('Confirmation') ?></h2>
</div>
<p class="alert alert-info"><?= t('Do you really want to remove these items from your history?') ?></p>
<div class="form-actions">
<a href="?action=flush-history" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?action=history"><?= t('cancel') ?></a>
</div>

View File

@ -1,10 +1,10 @@
<div class="page-header">
<h2>Confirmation</h2>
<h2><?= t('Confirmation') ?></h2>
</div>
<p class="alert alert-info">Do you really want to remove this subscription: "<?= Helper\escape($feed['title']) ?>"?</p>
<p class="alert alert-info"><?= t('Do you really want to remove this subscription: "%s"?', Helper\escape($feed['title'])) ?></p>
<div class="form-actions">
<a href="?action=remove&amp;feed_id=<?= $feed['id'] ?>" class="btn btn-red">Yes</a>
or <a href="?action=feeds">cancel</a>
<a href="?action=remove&amp;feed_id=<?= $feed['id'] ?>" class="btn btn-red"><?= t('Yes') ?></a>
<?= t('or') ?> <a href="?action=feeds"><?= t('cancel') ?></a>
</div>

View File

@ -1,21 +1,21 @@
<div class="page-header">
<h2>Subscriptions</h2>
<h2><?= t('Subscriptions') ?></h2>
<ul>
<li><a href="?action=add">add</a></li>
<li><a href="?action=import">import</a></li>
<li><a href="?action=export">export</a></li>
<li><a href="?action=refresh-all" data-action="refresh-all">refresh all</a></li>
<li><a href="?action=add"><?= t('add') ?></a></li>
<li><a href="?action=import"><?= t('import') ?></a></li>
<li><a href="?action=export"><?= t('export') ?></a></li>
<li><a href="?action=refresh-all" data-action="refresh-all"><?= t('refresh all') ?></a></li>
</ul>
</div>
<?php if (empty($feeds)): ?>
<p class="alert alert-info">No subscription.</p>
<p class="alert alert-info"><?= t('No subscription') ?></p>
<?php else: ?>
<?php if ($nothing_to_read): ?>
<p class="alert">Nothing to read, do you want to <a href="?action=refresh-all" data-action="refresh-all">update your subscriptions?</a></p>
<p class="alert"><?= t('Nothing to read, do you want to <a href="?action=refresh-all" data-action="refresh-all">update your subscriptions?</a>') ?></p>
<?php endif ?>
<section class="items">
@ -26,10 +26,10 @@
<a href="<?= $feed['site_url'] ?>" rel="noreferrer" target="_blank"><?= Helper\escape($feed['title']) ?></a>
</h2>
<p>
<?= Helper\get_host_from_url($feed['site_url']) ?> |
<a href="<?= Helper\escape($feed['feed_url']) ?>">feed link</a> |
<a href="?action=confirm-remove&amp;feed_id=<?= $feed['id'] ?>">remove</a> |
<a href="?action=refresh-feed&amp;feed_id=<?= $feed['id'] ?>" data-feed-id="<?= $feed['id'] ?>" data-action="refresh-feed">refresh</a>
<a href="<?= $feed['site_url'] ?>" rel="noreferrer" target="_blank"><?= Helper\get_host_from_url($feed['site_url']) ?></a> |
<a href="<?= Helper\escape($feed['feed_url']) ?>" rel="noreferrer" target="_blank"><?= t('feed link') ?></a> |
<a href="?action=confirm-remove&amp;feed_id=<?= $feed['id'] ?>"><?= t('remove') ?></a> |
<a href="?action=refresh-feed&amp;feed_id=<?= $feed['id'] ?>" data-feed-id="<?= $feed['id'] ?>" data-action="refresh-feed"><?= t('refresh') ?></a>
</p>
</article>
<?php endforeach ?>

View File

@ -0,0 +1,44 @@
<?php if (empty($items)): ?>
<p class="alert alert-info"><?= t('No history') ?></p>
<?php else: ?>
<div class="page-header">
<h2><?= t('History') ?></h2>
<ul>
<li><a href="?action=confirm-flush-history"><?= t('flush these items') ?></a></li>
</ul>
</div>
<section class="items" id="listing">
<?php foreach ($items as $item): ?>
<article id="item-<?= urlencode($item['id']) ?>" data-item-id="<?= urlencode($item['id']) ?>">
<h2>
<a
href="?action=show&amp;id=<?= urlencode($item['id']) ?>"
id="open-<?= urlencode($item['id']) ?>"
>
<?= Helper\escape($item['title']) ?>
</a>
</h2>
<p>
<?= Helper\get_host_from_url($item['url']) ?> |
<?= dt('%A %e %B %Y %k:%M', $item['updated']) ?> |
<a href="?action=mark-item-unread&amp;id=<?= urlencode($item['id']) ?>"><?= t('mark as unread') ?></a> |
<a href="?action=mark-item-removed&amp;id=<?= urlencode($item['id']) ?>"><?= t('remove') ?></a> |
<a
href="<?= $item['url'] ?>"
id="original-<?= urlencode($item['id']) ?>"
rel="noreferrer"
target="_blank"
data-item-id="<?= urlencode($item['id']) ?>"
>
<?= t('original link') ?>
</a>
</p>
</article>
<?php endforeach ?>
</section>
<?php endif ?>

View File

@ -1,16 +1,16 @@
<div class="page-header">
<h2>OPML Import</h2>
<h2><?= t('OPML Import') ?></h2>
<ul>
<li><a href="?action=feeds">feeds</a></li>
<li><a href="?action=add">add</a></li>
<li><a href="?action=export">export</a></li>
<li><a href="?action=feeds"><?= t('feeds') ?></a></li>
<li><a href="?action=add"><?= t('add') ?></a></li>
<li><a href="?action=export"><?= t('export') ?></a></li>
</ul>
</div>
<form method="post" action="?action=import" enctype="multipart/form-data">
<label for="file">OPML file</label>
<label for="file"><?= t('OPML file') ?></label>
<input type="file" name="file" required/>
<div class="form-actions">
<button type="submit" class="btn btn-blue">Import</button>
<button type="submit" class="btn btn-blue"><?= t('Import') ?></button>
</div>
</form>

View File

@ -3,13 +3,19 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="./assets/img/favicon.png">
<link rel="shortcut icon" href="favicon.ico">
<link rel="apple-touch-icon" href="./assets/img/touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="72x72" href="./assets/img/touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="114x114" href="./assets/img/touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png">
<title>miniflux</title>
<link href="./assets/css/app.css?v1" rel="stylesheet" media="screen">
<link href="./assets/css/app.css?v<?= filemtime('assets/css/app.css') ?>" rel="stylesheet" media="screen">
</head>
<body>
<div class="page-header">
<h1>Login</h1>
<h1><?= t('Sign in') ?></h1>
</div>
<?php if (isset($errors['login'])): ?>
@ -20,14 +26,14 @@
<form method="post" action="?action=login">
<?= Helper\form_label('Username', 'username') ?>
<?= Helper\form_label(t('Username'), 'username') ?>
<?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
<?= Helper\form_label('Password', 'password') ?>
<?= Helper\form_label(t('Password'), 'password') ?>
<?= Helper\form_password('password', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="Login" class="btn btn-blue"/>
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div>
</form>
</body>

View File

@ -1,20 +1,51 @@
<?php if (empty($item)): ?>
<p class="alert alert-info">Article not found.</p>
<p class="alert alert-info"><?= t('Item not found') ?></p>
<?php else: ?>
<article class="item">
<article class="item" id="current-item" data-item-id="<?= urlencode($item['id']) ?>">
<h1>
<a href="<?= $item['url'] ?>" rel="noreferrer" target="_blank"><?= Helper\escape($item['title']) ?></a>
<a href="<?= $item['url'] ?>" rel="noreferrer" target="_blank" id="original-item"><?= Helper\escape($item['title']) ?></a>
</h1>
<p class="infos">
<?= Helper\get_host_from_url($item['url']) ?> |
<?= date('l, j F Y H:i', $item['updated']) ?>
<?= dt('%A %e %B %Y %k:%M', $item['updated']) ?>
</p>
<?= $item['content'] ?>
<?php if (isset($item_nav)): ?>
<nav>
<span class="nav-left">
<?php if ($item_nav['previous']): ?>
<a href="?action=read&amp;id=<?= urlencode($item_nav['previous']['id']) ?>" id="previous-item">« <?= t('Previous') ?></a>
<?php else: ?>
« <?= t('Previous') ?>
<?php endif ?>
</span>
<span class="nav-middle">
<?php if ($item_nav['previous'] && $item_nav['next']): ?>
<a href="?action=default#item-<?= urlencode($item_nav['next']['id']) ?>"><?= t('Unread items') ?></a>
<?php elseif ($item_nav['previous'] && ! $item_nav['next']): ?>
<a href="?action=default#item-<?= urlencode($item_nav['previous']['id']) ?>"><?= t('Unread items') ?></a>
<?php elseif (! $item_nav['previous'] && $item_nav['next']): ?>
<a href="?action=default#item-<?= urlencode($item_nav['next']['id']) ?>"><?= t('Unread items') ?></a>
<?php elseif (! $item_nav['previous'] && ! $item_nav['next']): ?>
<a href="?action=default"><?= t('Unread items') ?></a>
<?php endif ?>
</span>
<span class="nav-right">
<?php if ($item_nav['next']): ?>
<a href="?action=read&amp;id=<?= urlencode($item_nav['next']['id']) ?>" id="next-item"><?= t('Next') ?> »</a>
<?php else: ?>
<?= t('Next') ?> »
<?php endif ?>
</span>
</nav>
<?php endif ?>
</article>
<?php endif ?>

View File

@ -1,27 +0,0 @@
<?php if (empty($items)): ?>
<p class="alert alert-info">No history.</p>
<?php else: ?>
<div class="page-header">
<h2>History</h2>
<ul>
<li><a href="?action=flush-history">flush</a></li>
</ul>
</div>
<section class="items">
<?php foreach ($items as $item): ?>
<article>
<h2><a href="?action=read&amp;id=<?= urlencode($item['id']) ?>"><?= Helper\escape($item['title']) ?></a></h2>
<p>
<?= Helper\get_host_from_url($item['url']) ?> |
<?= date('l, j F Y H:i', $item['updated']) ?> |
<a href="<?= $item['url'] ?>" rel="noreferrer" target="_blank">direct link</a>
</p>
</article>
<?php endforeach ?>
</section>
<?php endif ?>

View File

@ -1,28 +1,43 @@
<?php if (empty($items)): ?>
<p class="alert alert-info">Nothing to read.</p>
<p class="alert alert-info"><?= t('Nothing to read') ?></p>
<?php else: ?>
<div class="page-header">
<h2>Unread items</h2>
<h2><?= t('Unread items') ?></h2>
<ul>
<li><a href="?action=flush-unread">flush</a></li>
<li><a href="?action=mark-as-read"><?= t('mark all as read') ?></a></li>
</ul>
</div>
<section class="items">
<section class="items" id="listing">
<?php foreach ($items as $item): ?>
<article id="item-<?= urlencode($item['id']) ?>">
<h2><a href="?action=read&amp;id=<?= urlencode($item['id']) ?>"><?= Helper\escape($item['title']) ?></a></h2>
<a href="?action" class="mark-read">mark read</a>
<article id="item-<?= urlencode($item['id']) ?>" data-item-id="<?= urlencode($item['id']) ?>">
<h2>
<a
href="?action=read&amp;id=<?= urlencode($item['id']) ?>"
id="open-<?= urlencode($item['id']) ?>"
>
<?= Helper\escape($item['title']) ?>
</a>
</h2>
<p class="preview">
<?= Helper\escape(Helper\summary(strip_tags($item['content']), 50, 300)) ?>
</p>
<p>
<?= Helper\get_host_from_url($item['url']) ?> |
<a href="<?= $item['url'] ?>" rel="noreferrer" target="_blank" data-item-id="<?= urlencode($item['id']) ?>" data-action="mark-read">direct link</a> |
<a href="#read" rel="noreferrer" data-item-id="<?= urlencode($item['id']) ?>" data-action="mark-read">mark read</a>
<a href="?action=mark-item-read&amp;id=<?= urlencode($item['id']) ?>"><?= t('mark as read') ?></a> |
<a
href="<?= $item['url'] ?>"
id="original-<?= urlencode($item['id']) ?>"
rel="noreferrer"
target="_blank"
data-item-id="<?= urlencode($item['id']) ?>"
data-action="mark-read"
>
<?= t('original link') ?>
</a>
</p>
</article>
<?php endforeach ?>

View File

@ -63,7 +63,14 @@ class Table
$this->conditions()
);
return false !== $this->db->execute($sql, $values);
$result = $this->db->execute($sql, $values);
if ($result !== false && $result->rowCount() > 0) {
return true;
}
return false;
}
@ -290,6 +297,17 @@ class Table
}
break;
case 'notin':
if (is_array($arguments[1])) {
$sql = sprintf(
'%s NOT IN (%s)',
$this->db->escapeIdentifier($column),
implode(', ', array_fill(0, count($arguments[1]), '?'))
);
}
break;
case 'like':
$sql = sprintf('%s LIKE ?', $this->db->escapeIdentifier($column));
break;

View File

@ -6,6 +6,12 @@ class Export
{
private $content = array();
public $required_fields = array(
'title',
'site_url',
'feed_url'
);
public function __construct(array $content)
{
@ -24,6 +30,19 @@ class Export
foreach ($this->content as $feed) {
$valid = true;
foreach ($this->required_fields as $field) {
if (! isset($feed[$field])) {
$valid = false;
break;
}
}
if (! $valid) continue;
$outline = $body->addChild('outline');
$outline->addAttribute('xmlUrl', $feed['feed_url']);
$outline->addAttribute('htmlUrl', $feed['site_url']);

View File

@ -7,11 +7,9 @@ class Filter
private $data = '';
private $url = '';
private $input = '';
private $empty_tag = false;
private $empty_tags = array();
private $strip_content = false;
public $ignored_tags = array();
public $allowed_tags = array(
'dt' => array(),
'dd' => array(),
@ -45,7 +43,9 @@ class Filter
'figcaption' => array(),
'cite' => array(),
'time' => array('datetime'),
'abbr' => array('title')
'abbr' => array('title'),
'iframe' => array('width', 'height', 'frameborder', 'src'),
'q' => array('cite')
);
public $strip_tags_content = array(
@ -56,8 +56,11 @@ class Filter
'http://',
'https://',
'ftp://',
'mailto://',
'//'
'mailto:',
'//',
'data:image/png;base64,',
'data:image/gif;base64,',
'data:image/jpg;base64,'
);
public $protocol_attributes = array(
@ -67,25 +70,46 @@ class Filter
public $blacklist_media = array(
'feeds.feedburner.com',
'feedsportal.com',
'da.feedsportal.com',
'rss.feedsportal.com',
'res.feedsportal.com',
'pi.feedsportal.com',
'rss.nytimes.com',
'feeds.wordpress.com',
'stats.wordpress.com'
'stats.wordpress.com',
'rss.cnn.com',
'twitter.com/home?status=',
'twitter.com/share',
'twitter_icon_large.png',
'www.facebook.com/sharer.php',
'facebook_icon_large.png',
'plus.google.com/share',
'www.gstatic.com/images/icons/gplus-16.png',
'www.gstatic.com/images/icons/gplus-32.png',
'www.gstatic.com/images/icons/gplus-64.png'
);
public $required_attributes = array(
'a' => array('href'),
'img' => array('src')
'img' => array('src'),
'iframe' => array('src')
);
public $add_attributes = array(
'a' => 'rel="noreferrer" target="_blank"'
);
public $iframe_allowed_resources = array(
'http://www.youtube.com/',
'https://www.youtube.com/',
'http://player.vimeo.com/',
'https://player.vimeo.com/'
);
public function __construct($data, $url)
public function __construct($data, $site_url)
{
$this->url = $url;
$this->url = $site_url;
// Convert bad formatted documents to XML
$dom = new \DOMDocument;
@ -104,7 +128,7 @@ class Filter
if (! xml_parse($parser, $this->input, true)) {
var_dump($this->input);
//var_dump($this->input);
die(xml_get_current_line_number($parser).'|'.xml_error_string(xml_get_error_code($parser)));
}
@ -116,12 +140,12 @@ class Filter
public function startTag($parser, $name, $attributes)
{
$this->empty_tag = false;
$empty_tag = false;
$this->strip_content = false;
if ($this->isPixelTracker($name, $attributes)) {
$this->empty_tag = true;
$empty_tag = true;
}
else if ($this->isAllowedTag($name)) {
@ -130,17 +154,33 @@ class Filter
foreach ($attributes as $attribute => $value) {
if ($this->isAllowedAttribute($name, $attribute)) {
if ($value != '' && $this->isAllowedAttribute($name, $attribute)) {
if ($this->isResource($attribute)) {
if ($this->isRelativePath($value)) {
if ($name === 'iframe') {
if ($this->isAllowedIframeResource($value)) {
$attr_data .= ' '.$attribute.'="'.$value.'"';
$used_attributes[] = $attribute;
}
}
else if ($this->isRelativePath($value)) {
$attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"';
$used_attributes[] = $attribute;
}
else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistMedia($value)) {
if ($attribute == 'src' &&
isset($attributes['data-src']) &&
$this->isAllowedProtocol($attributes['data-src']) &&
! $this->isBlacklistMedia($attributes['data-src'])) {
$value = $attributes['data-src'];
}
$attr_data .= ' '.$attribute.'="'.$value.'"';
$used_attributes[] = $attribute;
}
@ -153,45 +193,46 @@ class Filter
}
}
// Check for required attributes
if (isset($this->required_attributes[$name])) {
foreach ($this->required_attributes[$name] as $required_attribute) {
if (! in_array($required_attribute, $used_attributes)) {
$this->empty_tag = true;
$empty_tag = true;
break;
}
}
}
if (! $this->empty_tag) {
if (! $empty_tag) {
$this->data .= '<'.$name.$attr_data;
// Add custom attributes
if (isset($this->add_attributes[$name])) {
$this->data .= ' '.$this->add_attributes[$name].' ';
}
// If img or br, we don't close it here
if ($name !== 'img' && $name !== 'br') $this->data .= '>';
}
}
else {
$this->ignored_tags[] = $name;
}
if (in_array($name, $this->strip_tags_content)) {
$this->strip_content = true;
}
$this->empty_tags[] = $empty_tag;
}
public function endTag($parser, $name)
{
if (! $this->empty_tag && $this->isAllowedTag($name)) {
if (! array_pop($this->empty_tags) && $this->isAllowedTag($name)) {
$this->data .= $name !== 'img' && $name !== 'br' ? '</'.$name.'>' : '/>';
}
@ -216,7 +257,6 @@ class Filter
else {
// Relative path
$url_path = $components['path'];
if ($url_path{strlen($url_path) - 1} !== '/') {
@ -236,6 +276,8 @@ class Filter
public function isRelativePath($value)
{
if (strpos($value, 'data:') === 0) return false;
return strpos($value, '://') === false && strpos($value, '//') !== 0;
}
@ -258,6 +300,20 @@ class Filter
}
public function isAllowedIframeResource($value)
{
foreach ($this->iframe_allowed_resources as $url) {
if (strpos($value, $url) === 0) {
return true;
}
}
return false;
}
public function isAllowedProtocol($value)
{
foreach ($this->allowed_protocols as $protocol) {

View File

@ -14,6 +14,7 @@ abstract class Parser
public $title = '';
public $updated = '';
public $items = array();
public $debug = false;
abstract public function execute();
@ -37,253 +38,25 @@ abstract class Parser
return $content;
}
}
class Atom extends Parser
{
public function execute()
public function displayXmlErrors()
{
try {
foreach(\libxml_get_errors() as $error) {
\libxml_use_internal_errors(true);
$xml = new \SimpleXMLElement($this->content);
$this->url = $this->getUrl($xml);
$this->title = (string) $xml->title;
$this->id = (string) $xml->id;
$this->updated = strtotime((string) $xml->updated);
$author = (string) $xml->author->name;
foreach ($xml->entry as $entry) {
if (isset($entry->author->name)) {
$author = $entry->author->name;
}
$item = new \StdClass;
$item->id = (string) $entry->id;
$item->title = (string) $entry->title;
$item->url = $this->getUrl($entry);
$item->updated = strtotime((string) $entry->updated);
$item->author = $author;
$item->content = $this->filterHtml($this->getContent($entry), $item->url);
$this->items[] = $item;
}
printf("Message: %s\nLine: %d\nColumn: %d\nCode: %d\n",
$error->message,
$error->line,
$error->column,
$error->code
);
}
catch (\Exception $e) {
}
return $this;
}
public function getContent($entry)
// Dirty quickfix before XML parsing
public function normalizeData($data)
{
if (isset($entry->content) && ! empty($entry->content)) {
if (count($entry->content->children())) {
return (string) $entry->content->asXML();
}
else {
return (string) $entry->content;
}
}
else if (isset($entry->summary) && ! empty($entry->summary)) {
return (string) $entry->summary;
}
return '';
}
public function getUrl($xml)
{
foreach ($xml->link as $link) {
if ((string) $link['type'] === 'text/html') {
return (string) $link['href'];
}
}
return (string) $xml->link['href'];
return str_replace("\xc3\x20", '', $data);
}
}
class Rss20 extends Parser
{
public function execute()
{
try {
\libxml_use_internal_errors(true);
$xml = new \SimpleXMLElement($this->content);
$ns = $xml->getNamespaces(true);
$this->title = (string) $xml->channel->title;
$this->url = (string) $xml->channel->link;
$this->id = $this->url;
$this->updated = isset($xml->channel->pubDate) ? (string) $xml->channel->pubDate : (string) $xml->channel->lastBuildDate;
if ($this->updated) {
$this->updated = strtotime($this->updated);
}
else {
$this->updated = time();
}
foreach ($xml->channel->item as $entry) {
$author = '';
$content = '';
$pubdate = '';
$link = '';
if (isset($ns['feedburner'])) {
$ns_fb = $entry->children($ns['feedburner']);
$link = $ns_fb->origLink;
}
if (isset($ns['dc'])) {
$ns_dc = $entry->children($ns['dc']);
$author = (string) $ns_dc->creator;
$pubdate = (string) $ns_dc->date;
}
if (isset($ns['content'])) {
$ns_content = $entry->children($ns['content']);
$content = (string) $ns_content->encoded;
}
if ($content === '' && isset($entry->description)) {
$content = (string) $entry->description;
}
if ($author === '') {
if (isset($entry->author)) {
$author = (string) $entry->author;
}
else if (isset($xml->channel->webMaster)) {
$author = (string) $xml->channel->webMaster;
}
}
$item = new \StdClass;
$item->title = (string) $entry->title;
$item->url = $link ?: (string) $entry->link;
$item->id = isset($entry->guid) ? (string) $entry->guid : $item->url;
$item->updated = strtotime($pubdate ?: (string) $entry->pubDate) ?: $this->updated;
$item->content = $this->filterHtml($content, $item->url);
$item->author = $author;
$this->items[] = $item;
}
}
catch (\Exception $e) {
}
return $this;
}
}
class Rss10 extends Parser
{
public function execute()
{
try {
\libxml_use_internal_errors(true);
$xml = new \SimpleXMLElement($this->content);
$ns = $xml->getNamespaces(true);
$this->title = (string) $xml->channel->title;
$this->url = (string) $xml->channel->link;
$this->id = $this->url;
if (isset($ns['dc'])) {
$ns_dc = $xml->channel->children($ns['dc']);
$this->updated = isset($ns_dc->date) ? strtotime($ns_dc->date) : time();
}
else {
$this->updated = time();
}
foreach ($xml->item as $entry) {
$author = '';
$content = '';
$pubdate = '';
$link = '';
if (isset($ns['feedburner'])) {
$ns_fb = $entry->children($ns['feedburner']);
$link = $ns_fb->origLink;
}
if (isset($ns['dc'])) {
$ns_dc = $entry->children($ns['dc']);
$author = (string) $ns_dc->creator;
$pubdate = (string) $ns_dc->date;
}
if (isset($ns['content'])) {
$ns_content = $entry->children($ns['content']);
$content = (string) $ns_content->encoded;
}
if ($content === '' && isset($entry->description)) {
$content = (string) $entry->description;
}
$item = new \StdClass;
$item->title = (string) $entry->title;
$item->url = $link ?: (string) $entry->link;
$item->id = $item->url;
$item->updated = $pubdate ? strtotime($pubdate) : time();
$item->content = $this->filterHtml($content, $item->url);
$item->author = $author ?: (string) $xml->channel->webMaster;
$this->items[] = $item;
}
}
catch (\Exception $e) {
}
return $this;
}
}
class Rss92 extends Rss20 {}
class Rss91 extends Rss20 {}

View File

@ -0,0 +1,85 @@
<?php
namespace PicoFeed;
class Atom extends Parser
{
public function execute()
{
$this->content = $this->normalizeData($this->content);
\libxml_use_internal_errors(true);
$xml = \simplexml_load_string($this->content);
if ($xml === false) {
if ($this->debug) $this->displayXmlErrors();
return false;
}
$this->url = $this->getUrl($xml);
$this->title = (string) $xml->title;
$this->id = (string) $xml->id;
$this->updated = strtotime((string) $xml->updated);
$author = (string) $xml->author->name;
foreach ($xml->entry as $entry) {
if (isset($entry->author->name)) {
$author = $entry->author->name;
}
$item = new \StdClass;
$item->id = (string) $entry->id;
$item->title = (string) $entry->title;
$item->url = $this->getUrl($entry);
$item->updated = strtotime((string) $entry->updated);
$item->author = $author;
$item->content = $this->filterHtml($this->getContent($entry), $item->url);
if (empty($item->title)) $item->title = $item->url;
$this->items[] = $item;
}
return $this;
}
public function getContent($entry)
{
if (isset($entry->content) && ! empty($entry->content)) {
if (count($entry->content->children())) {
return (string) $entry->content->asXML();
}
else {
return (string) $entry->content;
}
}
else if (isset($entry->summary) && ! empty($entry->summary)) {
return (string) $entry->summary;
}
return '';
}
public function getUrl($xml)
{
foreach ($xml->link as $link) {
if ((string) $link['type'] === 'text/html' || (string) $link['type'] === 'application/xhtml+xml') {
return (string) $link['href'];
}
}
return (string) $xml->link['href'];
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace PicoFeed;
class Rss10 extends Parser
{
public function execute()
{
$this->content = $this->normalizeData($this->content);
\libxml_use_internal_errors(true);
$xml = \simplexml_load_string($this->content);
if ($xml === false) {
if ($this->debug) $this->displayXmlErrors();
return false;
}
$namespaces = $xml->getNamespaces(true);
$this->title = (string) $xml->channel->title;
$this->url = (string) $xml->channel->link;
$this->id = $this->url;
if (isset($namespaces['dc'])) {
$ns_dc = $xml->channel->children($namespaces['dc']);
$this->updated = isset($ns_dc->date) ? strtotime($ns_dc->date) : time();
}
else {
$this->updated = time();
}
foreach ($xml->item as $entry) {
$item = new \StdClass;
$item->title = (string) $entry->title;
$item->url = '';
$item->author= '';
$item->updated = '';
$item->content = '';
foreach ($namespaces as $name => $url) {
$namespace = $entry->children($namespaces[$name]);
if (! $item->url && ! empty($namespace->origLink)) $item->url = (string) $namespace->origLink;
if (! $item->author && ! empty($namespace->creator)) $item->author = (string) $namespace->creator;
if (! $item->updated && ! empty($namespace->date)) $item->updated = strtotime((string) $namespace->date);
if (! $item->updated && ! empty($namespace->updated)) $item->updated = strtotime((string) $namespace->updated);
if (! $item->content && ! empty($namespace->encoded)) $item->content = (string) $namespace->encoded;
}
if (empty($item->url)) $item->url = (string) $entry->link;
if (empty($item->updated)) $item->updated = $this->updated;
if (empty($item->content)) {
$item->content = isset($entry->description) ? (string) $entry->description : '';
}
if (empty($item->author)) {
if (isset($entry->author)) {
$item->author = (string) $entry->author;
}
else if (isset($xml->channel->webMaster)) {
$item->author = (string) $xml->channel->webMaster;
}
}
if (empty($item->title)) $item->title = $item->url;
$item->id = $item->url;
$item->content = $this->filterHtml($item->content, $item->url);
$this->items[] = $item;
}
return $this;
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace PicoFeed;
class Rss20 extends Parser
{
public function execute()
{
$this->content = $this->normalizeData($this->content);
\libxml_use_internal_errors(true);
$xml = \simplexml_load_string($this->content);
if ($xml === false) {
if ($this->debug) $this->displayXmlErrors();
return false;
}
$namespaces = $xml->getNamespaces(true);
if ($xml->channel->link->count() > 1) {
foreach ($xml->channel->link as $xml_link) {
$link = (string) $xml_link;
if ($link !== '') {
$this->url = (string) $link;
break;
}
}
}
else {
$this->url = (string) $xml->channel->link;
}
$this->title = (string) $xml->channel->title;
$this->id = $this->url;
$this->updated = isset($xml->channel->pubDate) ? (string) $xml->channel->pubDate : (string) $xml->channel->lastBuildDate;
$this->updated = $this->updated ? strtotime($this->updated) : time();
foreach ($xml->channel->item as $entry) {
$item = new \StdClass;
$item->title = (string) $entry->title;
$item->url = '';
$item->author= '';
$item->updated = '';
$item->content = '';
foreach ($namespaces as $name => $url) {
$namespace = $entry->children($namespaces[$name]);
if (! $item->url && ! empty($namespace->origLink)) $item->url = (string) $namespace->origLink;
if (! $item->author && ! empty($namespace->creator)) $item->author = (string) $namespace->creator;
if (! $item->updated && ! empty($namespace->date)) $item->updated = strtotime((string) $namespace->date);
if (! $item->updated && ! empty($namespace->updated)) $item->updated = strtotime((string) $namespace->updated);
if (! $item->content && ! empty($namespace->encoded)) $item->content = (string) $namespace->encoded;
}
if (empty($item->url)) $item->url = (string) $entry->link;
if (empty($item->updated)) $item->updated = strtotime((string) $entry->pubDate) ?: $this->updated;
if (empty($item->content)) {
$item->content = isset($entry->description) ? (string) $entry->description : '';
}
if (empty($item->author)) {
if (isset($entry->author)) {
$item->author = (string) $entry->author;
}
else if (isset($xml->channel->webMaster)) {
$item->author = (string) $xml->channel->webMaster;
}
}
if (isset($entry->guid) && isset($entry->guid['isPermaLink']) && (string) $entry->guid['isPermaLink'] != 'false') {
$item->id = (string) $entry->guid;
}
else {
$item->id = $item->url;
}
if (empty($item->title)) $item->title = $item->url;
$item->content = $this->filterHtml($item->content, $item->url);
$this->items[] = $item;
}
return $this;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace PicoFeed;
require_once __DIR__.'/Rss20.php';
class Rss91 extends Rss20 {}

View File

@ -0,0 +1,7 @@
<?php
namespace PicoFeed;
require_once __DIR__.'/Rss20.php';
class Rss92 extends Rss20 {}

View File

@ -2,6 +2,9 @@
namespace PicoFeed;
require_once __DIR__.'/Parser.php';
require_once __DIR__.'/RemoteResource.php';
class Reader
{
private $url = '';
@ -16,17 +19,22 @@ class Reader
}
public function download($url)
public function download($url, $last_modified = '', $etag = '', $timeout = 5, $user_agent = 'PicoFeed (https://github.com/fguillot/picoFeed)')
{
if (strpos($url, 'http') !== 0) {
$url = 'http://'.$url;
}
$this->url = $url;
$this->content = @file_get_contents($this->url);
$resource = new RemoteResource($url, $timeout, $user_agent);
$resource->setLastModified($last_modified);
$resource->setEtag($etag);
$resource->execute();
return $this;
$this->content = $resource->getContent();
$this->url = $resource->getUrl();
return $resource;
}
@ -67,24 +75,32 @@ class Reader
{
$first_tag = $this->getFirstTag($this->content);
if (strpos($first_tag, '<feed ') !== false) {
if (strpos($first_tag, '<feed') !== false) {
require_once __DIR__.'/Parsers/Atom.php';
return new Atom($this->content);
}
else if (strpos($first_tag, '<rss ') !== false && strpos($first_tag, 'version="2.0"') !== false) {
else if (strpos($first_tag, '<rss') !== false &&
(strpos($first_tag, 'version="2.0"') !== false || strpos($first_tag, 'version=\'2.0\'') !== false)) {
require_once __DIR__.'/Parsers/Rss20.php';
return new Rss20($this->content);
}
else if (strpos($first_tag, '<rss ') !== false && strpos($first_tag, 'version="0.92"') !== false) {
else if (strpos($first_tag, '<rss') !== false &&
(strpos($first_tag, 'version="0.92"') !== false || strpos($first_tag, 'version=\'0.92\'') !== false)) {
require_once __DIR__.'/Parsers/Rss92.php';
return new Rss92($this->content);
}
else if (strpos($first_tag, '<rss ') !== false && strpos($first_tag, 'version="0.91"') !== false) {
else if (strpos($first_tag, '<rss') !== false &&
(strpos($first_tag, 'version="0.91"') !== false || strpos($first_tag, 'version=\'0.91\'') !== false)) {
require_once __DIR__.'/Parsers/Rss91.php';
return new Rss91($this->content);
}
else if (strpos($first_tag, '<rdf:') !== false && strpos($first_tag, 'xmlns="http://purl.org/rss/1.0/"') !== false) {
require_once __DIR__.'/Parsers/Rss10.php';
return new Rss10($this->content);
}
else if ($discover === true) {

View File

@ -0,0 +1,166 @@
<?php
namespace PicoFeed;
class RemoteResource
{
public $user_agent;
public $timeout;
public $url;
public $etag;
public $last_modified;
public $is_modified = true;
public $content = '';
public function __construct($url, $timeout = 5, $user_agent = 'PicoFeed (https://github.com/fguillot/picoFeed)')
{
$this->url = $url;
$this->timeout = $timeout;
$this->user_agent = $user_agent;
return $this;
}
public function setLastModified($last_modified)
{
$this->last_modified = $last_modified;
return $this;
}
public function getLastModified()
{
return $this->last_modified;
}
public function setEtag($etag)
{
$this->etag = $etag;
return $this;
}
public function getEtag()
{
return $this->etag;
}
public function getUrl()
{
return $this->url;
}
public function getContent()
{
return $this->content;
}
public function isModified()
{
return $this->is_modified;
}
public function execute()
{
$response = $this->makeRequest();
$this->etag = isset($response['headers']['ETag']) ? $response['headers']['ETag'] : '';
$this->last_modified = isset($response['headers']['Last-Modified']) ? $response['headers']['Last-Modified'] : '';
if ($response['status'] == 304) {
$this->is_modified = false;
}
else if ($response['status'] == 301 || $response['status'] == 302) {
if (isset($response['headers']['Location'])) {
$this->url = $response['headers']['Location'];
}
else if (isset($response['headers']['location'])) {
$this->url = $response['headers']['location'];
}
$this->execute();
}
else {
$this->content = $response['body'];
}
}
public function makeRequest()
{
$http_code = 200;
$http_body = '';
$http_headers = array();
if (! function_exists('curl_init')) {
$http_body = @file_get_contents($this->url);
}
else {
$headers = array('Connection: close');
if ($this->etag) $headers[] = 'If-None-Match: '.$this->etag;
if ($this->last_modified) $headers[] = 'If-Modified-Since: '.$this->last_modified;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
$http_response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$http_body = '';
$http_headers = array();
curl_close($ch);
$lines = explode("\r\n", $http_response);
$body_start = 0;
$i = 0;
foreach ($lines as $line) {
if ($line === '') {
$body_start = $i;
break;
}
else if (($p = strpos($line, ':')) !== false) {
$key = substr($line, 0, $p);
$value = substr($line, $p + 1);
$http_headers[trim($key)] = trim($value);
}
$i++;
}
$http_body = implode("\r\n", array_splice($lines, $i + 1));
}
return array(
'status' => $http_code,
'body' => $http_body,
'headers' => $http_headers
);
}
}

View File

@ -1,100 +0,0 @@
<?php
/*
* This file is part of picoTools.
*
* (c) Frédéric Guillot http://fredericguillot.com
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PicoTools;
/**
* Chrono, tool for benchmarking
* Calculate the duration of your code
*
* @author Frédéric Guillot
*/
class Chrono
{
/**
* Chronos values
*
* @access private
* @static
* @var array
*/
private static $chronos = array();
/**
* Start a chrono
*
* @access public
* @static
* @param string $name Chrono name
*/
public static function start($name)
{
self::$chronos[$name] = array(
'start' => microtime(true),
'finish' => 0
);
}
/**
* Stop a chrono
*
* @access public
* @static
* @param string $name Chrono name
*/
public static function stop($name)
{
if (! isset(self::$chronos[$name])) {
throw new \RuntimeException('Chrono not started!');
}
self::$chronos[$name]['finish'] = microtime(true);
}
/**
* Get a duration of a chrono
*
* @access public
* @static
* @return float
*/
public static function duration($name)
{
if (! isset(self::$chronos[$name])) {
throw new \RuntimeException('Chrono not started!');
}
return self::$chronos[$name]['finish'] - self::$chronos[$name]['start'];
}
/**
* Show all durations
*
* @access public
* @static
*/
public static function show()
{
foreach (self::$chronos as $name => $values) {
echo $name.' = ';
echo round($values['finish'] - $values['start'], 2).'s';
echo PHP_EOL;
}
}
}

View File

@ -1,36 +0,0 @@
<?php
/*
* This file is part of PicoTools.
*
* (c) Frédéric Guillot http://fredericguillot.com
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PicoTools\Crypto;
/**
* Generate a random token
*
* @return string Random token
*/
function token()
{
return hash('sha256', uniqid('', true).microtime());
}
/**
* Generate a signature with a key
*
* @param string $data Data
* @param string $key Key
* @return string Signature
*/
function signature($data, $key)
{
return hash_hmac('sha256', $data, $key);
}

View File

@ -49,6 +49,15 @@ function flash_error($html)
}
function format_bytes($size, $precision = 2)
{
$base = log($size) / log(1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
}
function get_host_from_url($url)
{
return escape(parse_url($url, PHP_URL_HOST));
@ -185,6 +194,18 @@ function form_password($name, $values = array(), array $errors = array(), array
}
function form_email($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="email" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_textarea($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);

View File

@ -1,243 +0,0 @@
<?php
/*
* This file is part of picoTools.
*
* (c) Frédéric Guillot http://fredericguillot.com
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PicoTools;
/**
* A wrapper around exiv2 command line utility
* You can write and read IPTC, XMP and EXIF metadata inside a picture
*
* @author Frédéric Guillot
*/
class Pixtag implements \ArrayAccess, \Iterator
{
/**
* Filename
*
* @access private
* @var string
*/
private $filename;
/**
* Container
*
* @access private
* @var array
*/
private $container = array();
/**
* Constructor
*
* @access public
* @param string $filename Path to the picture
*/
public function __construct($filename)
{
$this->filename = $filename;
}
/**
* Read metadata from the picture
*
* @access public
*/
public function read()
{
$c = new Command('exiv2 -PIEXkt '.$this->filename);
$c->execute();
$this->parse($c->getStdout());
}
/**
* Parse metadata bloc from exiv2 output command
*
* @access public
* @param string $data Raw command output of exiv2
*/
public function parse($data)
{
$lines = explode("\n", trim($data));
foreach ($lines as $line) {
$results = preg_split('/ /', $line, -1, PREG_SPLIT_OFFSET_CAPTURE);
if (isset($results[0][0])) {
$key = $results[0][0];
$value = '';
for ($i = 1, $ilen = count($results); $i < $ilen; ++$i) {
if ($results[$i][0] !== '') {
$value = substr($line, $results[$i][1]);
break;
}
}
if ($value === '(Binary value suppressed)') {
$value = '';
}
$this->container[$key] = $value;
}
}
}
/**
* Write metadata to the picture
* This method erase all keys and then add them to the picture
*
* @access public
*/
public function write()
{
$commands = array();
foreach ($this->container as $key => $value) {
$commands[] = sprintf('-M "del %s"', $key);
$commands[] = sprintf('-M "add %s %s"', $key, $value);
}
$c = new Command(sprintf(
'exiv2 %s %s',
implode(' ', $commands),
$this->filename
));
$c->execute();
if ($c->getReturnValue() !== 0) {
throw new \RuntimeException('Unable to write metadata');
}
}
/**
* Set a metadata
*
* @access public
* @param string $offset Key name, see exiv2 documentation for keys list
* @param string $value Key value
*/
public function offsetSet($offset, $value)
{
$this->container[$offset] = $value;
}
/**
* Check if a key exists
*
* @access public
* @param string $offset Key name, see exiv2 documentation for keys list
* @return boolean True if the key exists
*/
public function offsetExists($offset)
{
return isset($this->container[$offset]);
}
/**
* Remove a metadata
*
* @access public
* @param string $offset Key name, see exiv2 documentation for keys list
*/
public function offsetUnset($offset)
{
unset($this->container[$offset]);
}
/**
* Get a metadata
*
* @access public
* @param string $offset Key name, see exiv2 documentation for keys list
* @return string Key value
*/
public function offsetGet($offset)
{
return isset($this->container[$offset]) ? $this->container[$offset] : null;
}
/**
* Reset the position of the container
*
* @access public
*/
public function rewind()
{
reset($this->container);
}
/**
* Current
*
* @access public
* @return string Current value
*/
public function current()
{
return current($this->container);
}
/**
* Key
*
* @access public
* @return string Current key
*/
public function key()
{
return key($this->container);
}
/**
* Next
*
* @access public
*/
public function next()
{
next($this->container);
}
/**
* Valid
*
* @access public
* @return boolean True if the current key is valid
*/
public function valid()
{
return isset($this->container[key($this->container)]);
}
}

View File

@ -19,7 +19,7 @@ namespace PicoTools\Translator {
$args = \func_get_args();
\array_shift($args);
\array_unshift($args, get($identifier));
\array_unshift($args, get($identifier, $identifier));
return \call_user_func_array(
'sprintf',
@ -61,6 +61,12 @@ namespace PicoTools\Translator {
}
function datetime($format, $timestamp)
{
return strftime($format, $timestamp);
}
function get($identifier, $default = '')
{
$locales = container();
@ -78,6 +84,8 @@ namespace PicoTools\Translator {
function load($language)
{
setlocale(LC_TIME, $language.'.UTF-8');
$path = PATH.$language;
$locales = array();
@ -102,7 +110,7 @@ namespace PicoTools\Translator {
{
static $values = array();
if ($locales) {
if ($locales !== null) {
$values = $locales;
}
@ -118,4 +126,10 @@ namespace {
return call_user_func_array('\PicoTools\Translator\translate', func_get_args());
}
function dt() {
return call_user_func_array('\PicoTools\Translator\datetime', func_get_args());
}
}