first commit

This commit is contained in:
Frederic Guillot 2013-02-17 21:48:21 -05:00
commit e3cf8fe802
62 changed files with 4785 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
src/data/

55
README.markdown Normal file
View File

@ -0,0 +1,55 @@
Miniflux - Minimalist Feed Reader
=================================
Miniflux is a minimalist web-based news reader.
Features
--------
- Host anywhere (shared hosting, vps or localhost)
- Easy setup => copy and paste and you are done!
- CSS optimized for readability
- Keep an history of read items
- Remove Feedburner Ads and analytics trackers
- 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)
Todo
----
- Remove older items from the database
- Mobile CSS
- Improve feeds update to use Ajax calls
Requirements
------------
- PHP >= 5.3
- XML extensions (SimpleXML, DOM...)
- Sqlite
Dependencies
------------
- [PicoFeed](https://github.com/fguillot/picoFeed)
- [PicoFarad](https://github.com/fguillot/picoFarad)
- [PicoTools](https://github.com/fguillot/picoTools)
- [PicoDb](https://github.com/fguillot/picoDb)
- [SimpleValidator](https://github.com/fguillot/simpleValidator)
Screenshots
-----------
![items](https://github.com/fguillot/miniflux/screenshots/items.png)
![item](https://github.com/fguillot/miniflux/screenshots/item.png)
![feeds](https://github.com/fguillot/miniflux/screenshots/feeds.png)
Installation
------------
In progress...

BIN
screenshots/feeds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
screenshots/item.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
screenshots/items.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

348
src/assets/css/app.css Normal file
View File

@ -0,0 +1,348 @@
li,
ul,
table,
tr,
td,
th,
p,
blockquote,
body {
margin: 0;
padding: 0;
font-size: 100%;
}
body {
margin: 0 auto;
max-width: 750px;
color: #333;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
a {
color: #3366CC;
}
a:focus {
outline: 0;
color: red;
text-decoration: none;
padding: 3px;
border: 1px dotted #aaa;
}
a:hover {
color: #333;
text-decoration: none;
}
h1, h2, h3 {
font-weight: normal;
color: #333;
}
h2 {
font-size: 1.6em;
}
h3 {
font-size: 1.2em;
}
/* forms */
form {
padding-top: 5px;
padding-bottom: 5px;
}
label {
cursor: pointer;
display: block;
float: left;
width: 10em;
}
input[type="email"],
input[type="tel"],
input[type="password"],
input[type="text"] {
border: 1px solid #ccc;
padding: 3px;
line-height: 15px;
width: 250px;
font-size: 99%;
margin-bottom: 15px;
}
input[type="email"]:focus,
input[type="tel"]:focus,
input[type="password"]:focus,
input[type="text"]:focus,
textarea:focus {
color: #000;
border-color: rgba(82, 168, 236, 0.8);
outline: 0;
box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
}
textarea {
border: 1px solid #ccc;
padding: 3px;
width: 400px;
height: 200px;
font-size: 99%;
}
select {
margin-bottom: 15px;
}
::-webkit-input-placeholder {
color: #bbb;
padding-top: 2px;
}
::-ms-input-placeholder {
color: #bbb;
padding-top: 2px;
}
:-moz-placeholder {
color: #bbb;
padding-top: 2px;
}
.form-actions {
margin-top: 40px;
}
input.form-error,
textarea.form-error {
border: 2px solid #b94a48;
}
.form-errors {
color: #b94a48;
margin-left: 10em;
list-style-type: none;
}
/* alerts */
.alert {
padding: 8px 35px 8px 14px;
margin-bottom: 20px;
color: #c09853;
background-color: #fcf8e3;
border: 1px solid #fbeed5;
border-radius: 4px;
}
.alert-success {
color: #468847;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.alert-error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}
.alert-info {
color: #3a87ad;
background-color: #d9edf7;
border-color: #bce8f1;
}
/* buttons */
.btn {
display: block;
color: #333;
border: 1px solid #ccc;
background: #efefef;
padding: 5px;
padding-left: 15px;
padding-right: 15px;
font-size: 90%;
cursor: pointer;
border-radius: 2px;
}
.btn-blue {
border-color: #3079ed;
background: #4d90fe;
color: #fff;
}
.btn-blue:hover,
.btn-blue:focus {
border-color: #2f5bb7;
background: #357ae8;
}
/* header */
header {
margin-bottom: 50px;
margin-top: 10px;
}
header ul {
text-align: right;
font-size: 90%;
}
header li {
display: inline;
padding-left: 30px;
}
header a {
color: #777;
text-decoration: none;
}
nav .active a {
color: #333;
font-weight: bold;
}
.logo {
color: #000;
letter-spacing: 1px;
float: left;
}
.logo span {
color: #339966;
}
.page-header {
margin-bottom: 30px;
}
.page-header h2 {
margin: 0;
padding: 0;
font-size: 130%;
border-bottom: 1px dotted #ccc;
}
.page-header ul {
text-align: right;
margin-top: 2px;
}
.page-header li {
display: inline;
padding-left: 10px;
padding-right: 10px;
border-right: 1px dotted #ccc;
}
.page-header li:last-child {
border: none;
padding-right: 0;
}
/* items listing */
.items article {
margin-bottom: 20px;
}
.items h2 {
font-size: 100%;
font-weight: bold;
margin: 0;
padding: 0;
padding-bottom: 2px;
}
.items a {
text-decoration: none;
}
.items a:hover,
.items :focus {
text-decoration: underline;
}
.items p {
color: #aaa;
font-size: 70%;
}
/* item */
.item {
font-size: 110%;
color: #444;
padding-bottom: 50px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
.item pre,
.item ul,
.item p {
margin-top: 15px;
}
.item p {
margin-bottom: 20px;
overflow: auto;
}
.item ul {
margin-left: 25px;
}
.item li {
margin-top: 10px;
}
.item pre {
border: 1px solid #ccc;
border-radius: 10px;
background: #f0f0f0;
padding: 20px;
overflow: auto;
color: brown;
}
.item img {
display: block;
margin-top: 15px;
margin-bottom: 15px;
}
.item code {
color: brown;
}
.infos {
padding-bottom: 30px;
color: #ccc;
}
.item h1 {
}
.item h1 a {
font-size: 150%;
text-decoration: none;
}
blockquote {
border-left: 4px solid #ddd;
padding-left: 25px;
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
color: #666;
line-height: 22px;
}

26
src/common.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require 'vendor/PicoTools/Dependency_Injection.php';
require 'vendor/PicoDb/Database.php';
require 'vendor/PicoDb/Table.php';
require 'vendor/PicoTools/Crypto.php';
require 'schema.php';
require 'model.php';
PicoTools\container('db', function() {
$db = new PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => 'data/db.sqlite'
));
if ($db->schema()->check(1)) {
return $db;
}
else {
die('Unable to migrate database schema.');
}
});

5
src/cronjob.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require 'common.php';
Model\update_feeds();

259
src/index.php Normal file
View File

@ -0,0 +1,259 @@
<?php
require 'common.php';
require 'vendor/PicoTools/Template.php';
require 'vendor/PicoTools/Helper.php';
require 'vendor/PicoFarad/Response.php';
require 'vendor/PicoFarad/Request.php';
require 'vendor/PicoFarad/Session.php';
require 'vendor/PicoFarad/Router.php';
use PicoFarad\Router;
use PicoFarad\Response;
use PicoFarad\Request;
use PicoFarad\Session;
use PicoTools\Template;
Session\open(dirname($_SERVER['PHP_SELF']));
Router\before(function($action) {
if ($action !== 'login' && ! isset($_SESSION['user'])) {
PicoFarad\Response\redirect('?action=login');
}
Response\csp(array(
'img-src' => '*'
));
Response\xframe();
Response\xss();
Response\nosniff();
});
Router\get_action('logout', function() {
Session\close();
Response\redirect('?action=login');
});
Router\get_action('login', function() {
if (isset($_SESSION['user'])) {
Response\redirect('./index.php');
}
Response\html(Template\load('login', array(
'errors' => array(),
'values' => array()
)));
});
Router\post_action('login', function() {
$values = Request\values();
list($valid, $errors) = Model\validate_login($values);
if ($valid) {
Response\redirect('?action=default');
}
Response\html(Template\load('login', array(
'errors' => $errors,
'values' => $values
)));
});
Router\get_action('read', function() {
$id = Request\param('id');
Model\set_item_read($id);
Response\html(Template\layout('read_item', array(
'item' => Model\get_item($id)
)));
});
Router\get_action('history', function() {
Response\html(Template\layout('read_items', array(
'items' => Model\get_read_items(),
'menu' => 'history'
)));
});
Router\get_action('remove', function() {
$id = Request\int_param('feed_id');
if ($id) {
Model\remove_feed($id);
}
Response\redirect('?action=feeds');
});
Router\get_action('refresh', function() {
$id = Request\int_param('feed_id');
if ($id) {
Model\update_feed($id);
}
Response\redirect('?action=unread');
});
Router\get_action('flush-unread', function() {
Model\flush_unread();
Response\redirect('?action=unread');
});
Router\get_action('flush-history', function() {
Model\flush_read();
Response\redirect('?action=history');
});
Router\get_action('refresh-all', function() {
Model\update_feeds();
Session\flash('Your subscriptions are updated');
Response\redirect('?action=unread');
});
Router\get_action('feeds', function() {
Response\html(Template\layout('feeds', array(
'feeds' => Model\get_feeds(),
'menu' => 'feeds'
)));
});
Router\get_action('add', function() {
Response\html(Template\layout('add', array(
'values' => array(),
'errors' => array(),
'menu' => 'feeds'
)));
});
Router\post_action('add', function() {
if (Model\import_feed($_POST['url'])) {
Session\flash('Subscription added successfully.');
Response\redirect('?action=feeds');
}
else {
Session\flash_error('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('export', function() {
Response\force_download('feeds.opml');
Response\xml(Model\export_feeds());
});
Router\get_action('import', function() {
Response\html(Template\layout('import', array(
'errors' => array(),
'menu' => 'feeds'
)));
});
Router\post_action('import', function() {
if (Model\import_feeds(Request\file_content('file'))) {
Session\flash('Your feeds are imported.');
}
else {
Session\flash_error('Unable to import your OPML file.');
}
Response\redirect('?action=feeds');
});
Router\get_action('config', function() {
Response\html(Template\layout('config', array(
'errors' => array(),
'values' => Model\get_config(),
'menu' => 'config'
)));
});
Router\post_action('config', function() {
$values = Request\values();
list($valid, $errors) = Model\validate_config_update($values);
if ($valid) {
if (Model\save_config($values)) {
Session\flash('Your preferences are updated.');
}
else {
Session\flash_error('Unable to update your preferences.');
}
Response\redirect('?action=config');
}
Response\html(Template\layout('config', array(
'errors' => $errors,
'values' => $values,
'menu' => 'config'
)));
});
Router\notfound(function() {
Response\html(Template\layout('unread_items', array(
'items' => Model\get_unread_items(),
'menu' => 'unread'
)));
});

331
src/model.php Normal file
View File

@ -0,0 +1,331 @@
<?php
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/PicoTools/Crypto.php';
require_once 'vendor/SimpleValidator/Validator.php';
require_once 'vendor/SimpleValidator/Base.php';
require_once 'vendor/SimpleValidator/Validators/Required.php';
require_once 'vendor/SimpleValidator/Validators/Unique.php';
require_once 'vendor/SimpleValidator/Validators/MaxLength.php';
require_once 'vendor/SimpleValidator/Validators/MinLength.php';
require_once 'vendor/SimpleValidator/Validators/Integer.php';
require_once 'vendor/SimpleValidator/Validators/Equals.php';
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoFeed\Import;
use PicoFeed\Reader;
use PicoFeed\Export;
function export_feeds()
{
$opml = new Export(get_feeds());
return $opml->execute();
}
function import_feeds($content)
{
$import = new Import($content);
$feeds = $import->execute();
if ($feeds) {
$db = \PicoTools\singleton('db');
$db->startTransaction();
foreach ($feeds as $feed) {
if (! $db->table('feeds')->eq('feed_url', $feed->feed_url)->count()) {
$db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->site_url,
'feed_url' => $feed->feed_url
));
}
}
$db->closeTransaction();
return true;
}
return false;
}
function import_feed($url)
{
$reader = new Reader;
$reader->download($url);
$parser = $reader->getParser();
if ($parser !== false) {
$feed = $parser->execute();
$db = \PicoTools\singleton('db');
if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) {
$rs = $db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->url,
'feed_url' => $reader->getUrl()
));
if ($rs) {
$feed_id = $db->getConnection()->getLastId();
update_items($feed_id, $feed->items);
}
}
return true;
}
return false;
}
function get_feeds()
{
return \PicoTools\singleton('db')
->table('feeds')
->asc('title')
->findAll();
}
function get_feed($feed_id)
{
return \PicoTools\singleton('db')
->table('feeds')
->eq('id', $feed_id)
->findOne();
}
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();
}
function get_unread_items()
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'feeds.site_url')
->join('feeds', 'id', 'feed_id')
->eq('status', 'unread')
->desc('updated')
->findAll();
}
function get_read_items()
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'feeds.site_url')
->join('feeds', 'id', 'feed_id')
->eq('status', 'read')
->desc('updated')
->findAll();
}
function get_item($id)
{
return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->findOne();
}
function set_item_read($id)
{
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
}
function flush_unread($id)
{
\PicoTools\singleton('db')
->table('items')
->eq('status', 'unread')
->save(array('status' => 'removed'));
}
function flush_read($id)
{
\PicoTools\singleton('db')
->table('items')
->eq('status', 'read')
->save(array('status' => 'removed'));
}
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);
}
}
function update_items($feed_id, array $items)
{
$db = \PicoTools\singleton('db');
$db->startTransaction();
foreach ($items as $item) {
if (! $db->table('items')->eq('id', $item->id)->count()) {
$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
));
}
}
$db->closeTransaction();
}
function get_config()
{
return \PicoTools\singleton('db')
->table('config')
->columns('username', 'history')
->findOne();
}
function get_user()
{
return \PicoTools\singleton('db')
->table('config')
->columns('username', 'password')
->findOne();
}
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')
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
$user = get_user();
if ($user && \PicoTools\Crypto\password_verify($values['password'], $user['password'])) {
$_SESSION['user'] = $user;
}
else {
$result = false;
$errors['login'] = 'Bad username or password';
}
}
return array(
$result,
$errors
);
}
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')
));
}
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)
));
}
return array(
$v->execute(),
$v->getErrors()
);
}
function save_config(array $values)
{
$values['password'] = \PicoTools\Crypto\password_hash($values['password']);
unset($values['confirmation']);
return \PicoTools\singleton('db')->table('config')->update($values);
}

43
src/schema.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace Schema;
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
username TEXT DEFAULT 'admin',
password TEXT,
history INTEGER DEFAULT '15'
)
");
$pdo->exec("
INSERT INTO config
(password)
VALUES ('".\PicoTools\Crypto\password_hash('admin')."')
");
$pdo->exec('
CREATE TABLE feeds (
id INTEGER PRIMARY KEY,
site_url TEXT,
feed_url TEXT UNIQUE,
title TEXT
)
');
$pdo->exec('
CREATE TABLE items (
id TEXT PRIMARY KEY,
url TEXT,
title TEXT,
author TEXT,
content TEXT,
updated TEXT,
status TEXT,
feed_id INTEGER,
FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
)
');
}

12
src/templates/add.php Normal file
View File

@ -0,0 +1,12 @@
<div class="page-header">
<h2>New subscription</h2>
<?php include __DIR__.'/feed_menu.php' ?>
</div>
<form method="post" action="?action=add">
<label for="url">Site or Feed URL</label>
<input type="text" name="url" id="url" placeholder="http://website/" autofocus required/>
<div class="form-actions">
<button type="submit" class="btn btn-blue">Add</button>
</div>
</form>

View File

@ -0,0 +1,3 @@
</section>
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>miniflux</title>
<link href="./assets/css/app.css?v1" rel="stylesheet" media="screen">
</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>
</ul>
</nav>
</header>
<section class="page">
<?= Helper\flash('<div class="alert alert-success">%s</div>') ?>
<?= Helper\flash_error('<div class="alert alert-error">%s</div>') ?>

19
src/templates/config.php Normal file
View File

@ -0,0 +1,19 @@
<div class="page-header">
<h2>Preferences</h2>
</div>
<form method="post" action="?action=config">
<?= Helper\form_label('Username', 'username', 'control-label') ?>
<?= Helper\form_text('username', $values, $errors, array('required')) ?><br/>
<?= Helper\form_label('Password', 'password', 'control-label') ?>
<?= Helper\form_password('password', $values, $errors) ?><br/>
<?= Helper\form_label('Confirmation', 'confirmation', 'control-label') ?>
<?= Helper\form_password('confirmation', $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="Update" class="btn btn-blue"/>
</div>
</form>

View File

@ -0,0 +1,7 @@
<ul>
<li><a href="?action=feeds">feeds</a></li>
<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">refresh all</a></li>
</ul>

25
src/templates/feeds.php Normal file
View File

@ -0,0 +1,25 @@
<div class="page-header">
<h2>Subscriptions</h2>
<?php include __DIR__.'/feed_menu.php' ?>
</div>
<?php if (empty($feeds)): ?>
<p class="alert alert-info">No subscriptions.</p>
<?php else: ?>
<section class="items">
<?php foreach ($feeds as $feed): ?>
<article>
<h2><a href="<?= $feed['site_url'] ?>" traget="_blank"><?= Helper\escape($feed['title']) ?></a></h2>
<p>
<?= Helper\escape(parse_url($feed['site_url'], PHP_URL_HOST)) ?> |
<a href="?action=remove&amp;feed_id=<?= $feed['id'] ?>">remove</a> |
<a href="?action=refresh&amp;feed_id=<?= $feed['id'] ?>">refresh</a>
</p>
</article>
<?php endforeach ?>
</section>
<?php endif ?>

12
src/templates/import.php Normal file
View File

@ -0,0 +1,12 @@
<div class="page-header">
<h2>OPML Import</h2>
<?php include __DIR__.'/feed_menu.php' ?>
</div>
<form method="post" action="?action=import" enctype="multipart/form-data">
<label for="file">OPML file</label>
<input type="file" name="file" required/>
<div class="form-actions">
<button type="submit" class="btn btn-blue">Import</button>
</div>
</form>

34
src/templates/login.php Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>miniflux</title>
<link href="./assets/css/app.css?v1" rel="stylesheet" media="screen">
</head>
<body>
<div class="page-header">
<h1>Login</h1>
</div>
<?php if (isset($errors['login'])): ?>
<p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p>
<?php endif ?>
<form method="post" action="?action=login">
<?= Helper\form_label('Username', 'username', 'control-label') ?>
<?= Helper\form_text('username', $values, $errors, array('autofocus', 'required')) ?><br/>
<?= Helper\form_label('Password', 'password', 'control-label') ?>
<?= Helper\form_password('password', $values, $errors, array('required')) ?>
<div class="form-actions">
<input type="submit" value="Login" class="btn btn-blue"/>
</div>
</form>
</body>
</html>

View File

@ -0,0 +1,20 @@
<?php if (empty($item)): ?>
<p class="alert alert-info">Article not found.</p>
<?php else: ?>
<article class="item">
<h1>
<a href="<?= $item['url'] ?>" target="_blank"><?= Helper\escape($item['title']) ?></a>
</h1>
<p class="infos">
<?= Helper\escape(parse_url($item['url'], PHP_URL_HOST)) ?> |
<?= date('l, j F Y H:i T', $item['updated']) ?>
</p>
<?= $item['content'] ?>
</article>
<?php endif ?>

View File

@ -0,0 +1,26 @@
<?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\escape(parse_url($item['site_url'], PHP_URL_HOST)) ?> |
<?= date('l, j F Y H:i T', $item['updated']) ?>
</p>
</article>
<?php endforeach ?>
</section>
<?php endif ?>

View File

@ -0,0 +1,25 @@
<?php if (empty($items)): ?>
<p class="alert alert-info">No unread items.</p>
<?php else: ?>
<div class="page-header">
<h2>Unread items</h2>
<ul>
<li><a href="?action=flush-unread">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\escape(parse_url($item['site_url'], PHP_URL_HOST)) ?>
</p>
</article>
<?php endforeach ?>
</section>
<?php endif ?>

114
src/vendor/PicoDb/Database.php vendored Normal file
View File

@ -0,0 +1,114 @@
<?php
namespace PicoDb;
class Database
{
private $logs = array();
private $pdo;
public function __construct(array $settings)
{
if (! isset($settings['driver'])) {
throw new \LogicException('You must define a database driver.');
}
switch ($settings['driver']) {
case 'sqlite':
require_once __DIR__.'/Drivers/Sqlite.php';
$this->pdo = new Sqlite($settings['filename']);
break;
/*
case 'mysql':
$this->pdo = new \PDO(
'mysql:host='.$settings['hostname'].';dbname='.$settings['dbname'],
$settings['username'],
$settings['password']
);
break;*/
default:
throw new \LogicException('This database driver is not supported.');
}
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
public function setLogMessage($message)
{
$this->logs[] = $message;
}
public function getLogMessages()
{
return implode(', ', $this->logs);
}
public function getConnection()
{
return $this->pdo;
}
public function escapeIdentifier($value)
{
return $this->pdo->escapeIdentifier($value);
}
public function execute($sql, array $values = array())
{
try {
$this->setLogMessage($sql);
$this->setLogMessage(implode(', ', $values));
$rq = $this->pdo->prepare($sql);
$rq->execute($values);
return $rq;
}
catch (\PDOException $e) {
$this->setLogMessage($e->getMessage());
return false;
}
}
public function startTransaction()
{
$this->pdo->beginTransaction();
}
public function closeTransaction()
{
$this->pdo->commit();
}
public function cancelTransaction()
{
$this->pdo->rollback();
}
public function table($table_name)
{
return new Table($this, $table_name);
}
public function schema()
{
require_once __DIR__.'/Schema.php';
return new Schema($this);
}
}

47
src/vendor/PicoDb/Drivers/Sqlite.php vendored Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace PicoDb;
class Sqlite extends \PDO {
public function __construct($filename)
{
parent::__construct('sqlite:'.$filename);
$this->exec('PRAGMA foreign_keys = ON');
}
public function getSchemaVersion()
{
$rq = $this->prepare('PRAGMA user_version');
$rq->execute();
$result = $rq->fetch(\PDO::FETCH_ASSOC);
if (isset($result['user_version'])) {
return $result['user_version'];
}
return 0;
}
public function setSchemaVersion($version)
{
$this->exec('PRAGMA user_version='.$version);
}
public function getLastId()
{
return $this->lastInsertId();
}
public function escapeIdentifier($value)
{
return '"'.$value.'"';
}
}

60
src/vendor/PicoDb/Schema.php vendored Normal file
View File

@ -0,0 +1,60 @@
<?php
namespace PicoDb;
class Schema
{
protected $db = null;
public function __construct(Database $db)
{
$this->db = $db;
}
public function check($last_version = 1)
{
$current_version = $this->db->getConnection()->getSchemaVersion();
if ($current_version < $last_version) {
return $this->migrateTo($current_version, $last_version);
}
return true;
}
public function migrateTo($current_version, $next_version)
{
try {
$this->db->startTransaction();
for ($i = $current_version + 1; $i <= $next_version; $i++) {
$function_name = '\Schema\version_'.$i;
if (function_exists($function_name)) {
call_user_func($function_name, $this->db->getConnection());
$this->db->getConnection()->setSchemaVersion($i);
}
else {
throw new \LogicException('To execute a database migration, you need to create this function: "'.$function_name.'".');
}
}
$this->db->closeTransaction();
}
catch (\PDOException $e) {
$this->db->cancelTransaction();
return false;
}
return true;
}
}

343
src/vendor/PicoDb/Table.php vendored Normal file
View File

@ -0,0 +1,343 @@
<?php
namespace PicoDb;
class Table
{
private $table_name = '';
private $sql_limit = '';
private $sql_offset = '';
private $sql_order = '';
private $joins = array();
private $conditions = array();
private $or_conditions = array();
private $is_or_condition = false;
private $columns = array();
private $values = array();
private $db;
public function __construct(Database $db, $table_name)
{
$this->db = $db;
$this->table_name = $table_name;
return $this;
}
public function save(array $data)
{
if (! empty($this->conditions)) {
return $this->update($data);
}
else {
return $this->insert($data);
}
}
public function update(array $data)
{
$columns = array();
$values = array();
foreach ($data as $column => $value) {
$columns[] = $this->db->escapeIdentifier($column).'=?';
$values[] = $value;
}
foreach ($this->values as $value) {
$values[] = $value;
}
$sql = sprintf(
'UPDATE %s SET %s %s',
$this->db->escapeIdentifier($this->table_name),
implode(', ', $columns),
$this->conditions()
);
return false !== $this->db->execute($sql, $values);
}
public function insert(array $data)
{
$columns = array();
foreach ($data as $column => $value) {
$columns[] = $this->db->escapeIdentifier($column);
}
$sql = sprintf(
'INSERT INTO %s (%s) VALUES (%s)',
$this->db->escapeIdentifier($this->table_name),
implode(', ', $columns),
implode(', ', array_fill(0, count($data), '?'))
);
return false !== $this->db->execute($sql, array_values($data));
}
public function remove()
{
$sql = sprintf(
'DELETE FROM %s %s',
$this->db->escapeIdentifier($this->table_name),
$this->conditions()
);
return false !== $this->db->execute($sql, $this->values);
}
public function listing($key, $value)
{
$this->columns($key, $value);
$listing = array();
$results = $this->findAll();
if ($results) {
foreach ($results as $result) {
$listing[$result[$key]] = $result[$value];
}
}
return $listing;
}
public function findAll()
{
$sql = sprintf(
'SELECT %s FROM %s %s %s %s %s %s',
empty($this->columns) ? '*' : implode(', ', $this->columns),
$this->db->escapeIdentifier($this->table_name),
implode(' ', $this->joins),
$this->conditions(),
$this->sql_order,
$this->sql_limit,
$this->sql_offset
);
$rq = $this->db->execute($sql, $this->values);
if (false === $rq) {
return false;
}
return $rq->fetchAll(\PDO::FETCH_ASSOC);
}
public function findOne()
{
$this->limit(1);
$result = $this->findAll();
return isset($result[0]) ? $result[0] : null;
}
public function count()
{
$sql = sprintf(
'SELECT COUNT(*) AS count FROM %s'.$this->conditions().$this->sql_order.$this->sql_limit.$this->sql_offset,
$this->db->escapeIdentifier($this->table_name)
);
$rq = $this->db->execute($sql, $this->values);
if (false === $rq) {
return false;
}
$result = $rq->fetch(\PDO::FETCH_ASSOC);
return isset($result['count']) ? (int) $result['count'] : 0;
}
public function join($table, $foreign_column, $local_column)
{
$this->joins[] = sprintf(
'LEFT JOIN %s ON %s=%s',
$this->db->escapeIdentifier($table),
$this->db->escapeIdentifier($table).'.'.$this->db->escapeIdentifier($foreign_column),
$this->db->escapeIdentifier($this->table_name).'.'.$this->db->escapeIdentifier($local_column)
);
return $this;
}
public function conditions()
{
if (! empty($this->conditions)) {
return ' WHERE '.implode(' AND ', $this->conditions);
}
else {
return '';
}
}
public function addCondition($sql)
{
if ($this->is_or_condition) {
$this->or_conditions[] = $sql;
}
else {
$this->conditions[] = $sql;
}
}
public function beginOr()
{
$this->is_or_condition = true;
$this->or_conditions = array();
return $this;
}
public function closeOr()
{
$this->is_or_condition = false;
if (! empty($this->or_conditions)) {
$this->conditions[] = '('.implode(' OR ', $this->or_conditions).')';
}
return $this;
}
public function asc($column)
{
$this->sql_order = ' ORDER BY '.$this->db->escapeIdentifier($column).' ASC';
return $this;
}
public function desc($column)
{
$this->sql_order = ' ORDER BY '.$this->db->escapeIdentifier($column).' DESC';
return $this;
}
public function limit($value)
{
$this->sql_limit = ' LIMIT '.(int) $value;
return $this;
}
public function offset($value)
{
$this->sql_offset = ' OFFSET '.(int) $value;
return $this;
}
public function columns()
{
$this->columns = \func_get_args();
return $this;
}
public function __call($name, array $arguments)
{
if (2 !== count($arguments)) {
throw new \LogicException('You must define a column and a value.');
}
$column = $arguments[0];
$sql = '';
switch ($name) {
case 'in':
if (is_array($arguments[1])) {
$sql = sprintf(
'%s 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;
case 'eq':
case 'equal':
case 'equals':
$sql = sprintf('%s = ?', $this->db->escapeIdentifier($column));
break;
case 'gt':
case 'greaterThan':
$sql = sprintf('%s > ?', $this->db->escapeIdentifier($column));
break;
case 'lt':
case 'lowerThan':
$sql = sprintf('%s < ?', $this->db->escapeIdentifier($column));
break;
case 'gte':
case 'greaterThanOrEquals':
$sql = sprintf('%s >= ?', $this->db->escapeIdentifier($column));
break;
case 'lte':
case 'lowerThanOrEquals':
$sql = sprintf('%s <= ?', $this->db->escapeIdentifier($column));
break;
}
if ('' !== $sql) {
$this->addCondition($sql);
if (is_array($arguments[1])) {
foreach ($arguments[1] as $value) {
$this->values[] = $value;
}
}
else {
$this->values[] = $arguments[1];
}
}
return $this;
}
}

50
src/vendor/PicoFarad/Request.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace PicoFarad\Request;
function param($name)
{
return isset($_GET[$name]) ? $_GET[$name] : null;
}
function int_param($name)
{
return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : null;
}
function values()
{
if (! empty($_POST)) {
return $_POST;
}
$result = json_decode(body(), true);
if ($result) {
return $result;
}
return array();
}
function body()
{
return file_get_contents('php://input');
}
function file_content($name)
{
if (isset($_FILES[$name])) {
return file_get_contents($_FILES[$name]['tmp_name']);
}
return '';
}

115
src/vendor/PicoFarad/Response.php vendored Normal file
View File

@ -0,0 +1,115 @@
<?php
namespace PicoFarad\Response;
function force_download($filename)
{
header('Content-Disposition: attachment; filename="'.$filename.'"');
}
function status($status_code)
{
if (strpos(php_sapi_name(), 'apache') !== false) {
header('HTTP/1.0 '.$status_code);
}
else {
header('Status: '.$status_code);
}
}
function redirect($url)
{
header('Location: '.$url);
exit;
}
function json(array $data, $status_code = 200)
{
status($status_code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function text($data, $status_code = 200)
{
status($status_code);
header('Content-Type: text/plain; charset=utf-8');
echo $data;
exit;
}
function html($data, $status_code = 200)
{
status($status_code);
header('Content-Type: text/html; charset=utf-8');
echo $data;
exit;
}
function xml($data, $status_code = 200)
{
status($status_code);
header('Content-Type: text/xml; charset=utf-8');
echo $data;
exit;
}
function csp(array $policies = array())
{
$policies['default-src'] = "'self'";
foreach (array('X-WebKit-CSP', 'X-Content-Security-Policy', 'Content-Security-Policy') as $header) {
$values = '';
foreach ($policies as $policy => $hosts) {
$values .= $policy.' '.$hosts.'; ';
}
header($header.': '.$values);
}
}
function nosniff()
{
header('X-Content-Type-Options: nosniff');
}
function xss()
{
header('X-XSS-Protection: 1; mode=block');
}
function hsts()
{
header('Strict-Transport-Security: max-age=31536000');
}
function xframe($mode = 'DENY', array $urls = array())
{
header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
}

138
src/vendor/PicoFarad/Router.php vendored Normal file
View File

@ -0,0 +1,138 @@
<?php
namespace PicoFarad\Router;
function before($value = null)
{
static $before_callback = null;
if (is_callable($value)) {
$before_callback = $value;
}
else if (is_callable($before_callback)) {
$before_callback($value);
}
}
function action($name, \Closure $callback)
{
$handler = isset($_GET['action']) ? $_GET['action'] : 'default';
if ($handler === $name) {
before($handler);
$callback();
}
}
function post_action($name, \Closure $callback)
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
action($name, $callback);
}
}
function get_action($name, \Closure $callback)
{
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
action($name, $callback);
}
}
function notfound(\Closure $callback)
{
before();
$callback();
}
function get($url, \Closure $callback)
{
find_route('GET', $url, $callback);
}
function post($url, \Closure $callback)
{
find_route('POST', $url, $callback);
}
function put($url, \Closure $callback)
{
find_route('PUT', $url, $callback);
}
function delete($url, \Closure $callback)
{
find_route('DELETE', $url, $callback);
}
function find_route($method, $route, \Closure $callback)
{
if ($_SERVER['REQUEST_METHOD'] === $method) {
if ($_SERVER['QUERY_STRING']) {
$url = substr($_SERVER['REQUEST_URI'], 0, -(strlen($_SERVER['QUERY_STRING']) + 1));
}
else {
$url = $_SERVER['REQUEST_URI'];
}
$params = array();
if (url_match($route, $url, $params)) {
before($handler);
\call_user_func_array($callback, $params);
exit;
}
}
}
function url_match($route_uri, $request_uri, array &$params)
{
if ($request_uri === $route_uri) return true;
if ($route_uri === '/' || $request_uri === '/') return false;
$route_uri = trim($route_uri, '/');
$request_uri = trim($request_uri, '/');
$route_items = explode('/', $route_uri);
$request_items = explode('/', $request_uri);
$nb_route_items = count($route_items);
if ($nb_route_items === count($request_items)) {
for ($i = 0; $i < $nb_route_items; ++$i) {
if ($route_items[$i][0] === ':') {
$params[substr($route_items[$i], 1)] = $request_items[$i];
}
else if ($route_items[$i] !== $request_items[$i]) {
$params = array();
return false;
}
}
return true;
}
return false;
}

30
src/vendor/PicoFarad/Session.php vendored Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace PicoFarad\Session;
const SESSION_LIFETIME = 2678400;
function open($base_path = '/')
{
session_set_cookie_params(SESSION_LIFETIME, $base_path, null, false, true);
session_start();
}
function close()
{
session_destroy();
}
function flash($message)
{
$_SESSION['flash_message'] = $message;
}
function flash_error($message)
{
$_SESSION['flash_error_message'] = $message;
}

39
src/vendor/PicoFeed/Export.php vendored Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace PicoFeed;
class Export
{
private $content = array();
public function __construct(array $content)
{
$this->content = $content;
}
public function execute()
{
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><opml/>');
$head = $xml->addChild('head');
$head->addChild('title', 'OPML Export');
$body = $xml->addChild('body');
foreach ($this->content as $feed) {
$outline = $body->addChild('outline');
$outline->addAttribute('xmlUrl', $feed['feed_url']);
$outline->addAttribute('htmlUrl', $feed['site_url']);
$outline->addAttribute('title', $feed['title']);
$outline->addAttribute('text', $feed['title']);
$outline->addAttribute('description', isset($feed['description']) ? $feed['description'] : $feed['title']);
$outline->addAttribute('type', 'rss');
$outline->addAttribute('version', 'RSS');
}
return $xml->asXML();
}
}

271
src/vendor/PicoFeed/Filter.php vendored Normal file
View File

@ -0,0 +1,271 @@
<?php
namespace PicoFeed;
class Filter
{
private $data = '';
private $url = '';
private $input = '';
private $empty_tag = false;
private $strip_content = false;
public $ignored_tags = array();
public $allowed_tags = array(
'h2' => array(),
'h3' => array(),
'h4' => array(),
'h5' => array(),
'h6' => array(),
'strong' => array(),
'em' => array(),
'code' => array(),
'pre' => array(),
'blockquote' => array(),
'p' => array(),
'ul' => array(),
'li' => array(),
'ol' => array(),
'br' => array(),
'del' => array(),
'a' => array('href'),
'img' => array('src', 'width', 'height')
);
public $strip_tags_content = array(
'script'
);
public $allowed_protocols = array(
'http://',
'https://',
'ftp://',
'mailto://',
'//'
);
public $protocol_attributes = array(
'src',
'href',
);
public $blacklist_media = array(
'feeds.feedburner.com',
'feedsportal.com',
'rss.nytimes.com',
'feeds.wordpress.com',
'stats.wordpress.com'
);
public $required_attributes = array(
'a' => array('href'),
'img' => array('src')
);
public function __construct($data, $url)
{
$this->url = $url;
$data = iconv("UTF-8", "ISO-8859-15//IGNORE", $data);
$dom = new \DOMDocument();
$dom->loadHTML($data);
$this->input = $dom->saveXML($dom->getElementsByTagName('body')->item(0));
}
public function execute()
{
$parser = xml_parser_create();
xml_set_object($parser, $this);
xml_set_element_handler($parser, 'startTag', 'endTag');
xml_set_character_data_handler($parser, 'dataTag');
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
if (! xml_parse($parser, $this->input, true)) {
var_dump($this->input);
die(xml_get_current_line_number($parser).'|'.xml_error_string(xml_get_error_code($parser)));
}
xml_parser_free($parser);
return $this->data;
}
public function startTag($parser, $name, $attributes)
{
$this->empty_tag = false;
$this->strip_content = false;
if ($this->isPixelTracker($name, $attributes)) {
$this->empty_tag = true;
}
else if ($this->isAllowedTag($name)) {
$attr_data = '';
$used_attributes = array();
foreach ($attributes as $attribute => $value) {
if ($this->isAllowedAttribute($name, $attribute)) {
if ($this->isResource($attribute)) {
if ($this->isRelativePath($value)) {
$attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"';
$used_attributes[] = $attribute;
}
else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistMedia($value)) {
$attr_data .= ' '.$attribute.'="'.$value.'"';
$used_attributes[] = $attribute;
}
}
else {
$attr_data .= ' '.$attribute.'="'.$value.'"';
$used_attributes[] = $attribute;
}
}
}
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;
break;
}
}
}
if (! $this->empty_tag) {
$this->data .= '<'.$name.$attr_data;
if ($name !== 'img' && $name !== 'br') $this->data .= '>';
}
}
else {
$this->ignored_tags[] = $name;
}
if (in_array($name, $this->strip_tags_content)) {
$this->strip_content = true;
}
}
public function endTag($parser, $name)
{
if (! $this->empty_tag && $this->isAllowedTag($name)) {
$this->data .= $name !== 'img' && $name !== 'br' ? '</'.$name.'>' : '/>';
}
}
public function dataTag($parser, $content)
{
if (! $this->strip_content) $this->data .= htmlspecialchars($content, ENT_QUOTES, 'UTF-8', false);
}
public function getAbsoluteUrl($path, $url)
{
$components = parse_url($url);
if ($path{0} === '/') {
// Absolute path
return $components['scheme'].'://'.$components['host'].$path;
}
else {
// Relative path
$url_path = $components['path'];
if ($url_path{strlen($url_path) - 1} !== '/') {
$url_path = dirname($url_path).'/';
}
if (substr($path, 0, 2) === './') {
$path = substr($path, 2);
}
return $components['scheme'].'://'.$components['host'].$url_path.$path;
}
}
public function isRelativePath($value)
{
return strpos($value, '://') === false && strpos($value, '//') !== 0;
}
public function isAllowedTag($name)
{
return isset($this->allowed_tags[$name]);
}
public function isAllowedAttribute($tag, $attribute)
{
return in_array($attribute, $this->allowed_tags[$tag]);
}
public function isResource($attribute)
{
return in_array($attribute, $this->protocol_attributes);
}
public function isAllowedProtocol($value)
{
foreach ($this->allowed_protocols as $protocol) {
if (strpos($value, $protocol) === 0) {
return true;
}
}
return false;
}
public function isBlacklistMedia($resource)
{
foreach ($this->blacklist_media as $name) {
if (strpos($resource, $name) !== false) {
return true;
}
}
return false;
}
public function isPixelTracker($tag, array $attributes)
{
return $tag === 'img' &&
isset($attributes['height']) && isset($attributes['width']) &&
$attributes['height'] == 1 && $attributes['width'] == 1;
}
}

61
src/vendor/PicoFeed/Import.php vendored Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace PicoFeed;
class Import
{
private $content = '';
private $items = array();
public function __construct($content)
{
$this->content = $content;
}
public function execute()
{
try {
\libxml_use_internal_errors(true);
$xml = new \SimpleXMLElement(trim($this->content));
if ($xml->getName() !== 'opml') {
return false;
}
$this->parseEntries($xml->body);
}
catch (\Exception $e) {
return false;
}
return $this->items;
}
public function parseEntries($tree)
{
foreach ($tree->outline as $item) {
if (isset($item['type']) && strtolower($item['type']) === 'folder' && isset($item->outline)) {
$this->parseEntries($item);
}
else if (isset($item['type']) && strtolower($item['type']) === 'rss') {
$entry = new \StdClass;
$entry->title = (string) $item['text'];
$entry->site_url = (string) $item['htmlUrl'];
$entry->feed_url = (string) $item['xmlUrl'];
$entry->type = isset($item['version']) ? (string) $item['version'] : (string) $item['type'];
$entry->description = (string) $item['description'];
$this->items[] = $entry;
}
}
}
}

182
src/vendor/PicoFeed/Parser.php vendored Normal file
View File

@ -0,0 +1,182 @@
<?php
namespace PicoFeed;
require_once __DIR__.'/Filter.php';
abstract class Parser
{
protected $content = '';
public $id = '';
public $url = '';
public $title = '';
public $updated = '';
public $items = array();
abstract public function execute();
public function __construct($content)
{
$this->content = $content;
}
public function filterHtml($str, $item_url)
{
$content = '';
if ($str) {
$filter = new Filter($str, $item_url);
$content = $filter->execute();
}
return $content;
}
}
class Atom extends Parser
{
public function execute()
{
try {
\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;
}
}
catch (\Exception $e) {
}
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') {
return (string) $link['href'];
}
}
return (string) $xml->link['href'];
}
}
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;
$this->updated = strtotime($this->updated);
foreach ($xml->channel->item as $entry) {
$author = '';
$content = '';
if (isset($ns['dc'])) {
$ns_dc = $entry->children($ns['dc']);
$author = (string) $ns_dc->creator;
}
if (isset($ns['content'])) {
$ns_content = $entry->children($ns['content']);
if (! empty($entry->content)) {
$content = (string) $ns_content->encoded;
}
}
if (! $content) {
$content = (string) $entry->description;
}
$item = new \StdClass;
$item->id = (string) $entry->guid;
$item->title = (string) $entry->title;
$item->url = (string) $entry->link;
$item->updated = strtotime((string) $entry->pubDate);
$item->content = $this->filterHtml($content, $item->url);
$item->author = $author ?: (string) $xml->channel->webMaster;
$this->items[] = $item;
}
}
catch (\Exception $e) {
}
return $this;
}
}

117
src/vendor/PicoFeed/Reader.php vendored Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace PicoFeed;
class Reader
{
private $url = '';
private $content = '';
public function __construct($content = '')
{
$this->content = $content;
return $this;
}
public function download($url)
{
if (strpos($url, 'http') !== 0) {
$url = 'http://'.$url;
}
$this->url = $url;
$this->content = @file_get_contents($this->url);
return $this;
}
public function getContent()
{
return $this->content;
}
public function getUrl()
{
return $this->url;
}
public function getParser()
{
$first_lines = substr($this->content, 0, 512);
if (stripos($first_lines, 'html') !== false) {
if ($this->discover()) {
$first_lines = substr($this->content, 0, 512);
}
else {
return false;
}
}
if (strpos($first_lines, '<feed ') !== false) {
return new Atom($this->content);
}
else if (strpos($first_lines, '<rss ') !== false && strpos($first_lines, 'version="2.0"') !== false) {
return new Rss20($this->content);
}/*
else if (strpos($first_lines, '<rdf') !== false && strpos($first_lines, 'xmlns="http://purl.org/rss/1.0/"') !== false) {
return new Rss10($this->content);
}*/
return false;
}
public function discover()
{
\libxml_use_internal_errors(true);
$dom = new \DOMDocument;
$dom->loadHTML($this->content);
$xpath = new \DOMXPath($dom);
$queries = array(
"//link[@type='application/atom+xml']",
"//link[@type='application/rss+xml']"
);
foreach ($queries as $query) {
$nodes = $xpath->query($query);
if ($nodes->length !== 0) {
$link = $nodes->item(0)->getAttribute('href');
// Relative links
if (strpos($link, 'http') !== 0) {
if ($link{0} === '/') $link = substr($link, 1);
if ($this->url{strlen($this->url) - 1} !== '/') $this->url .= '/';
$link = $this->url.$link;
}
$this->download($link);
return true;
}
}
return false;
}
}

0
src/vendor/PicoFeed/Writer.php vendored Normal file
View File

100
src/vendor/PicoTools/Chrono.php vendored Normal file
View File

@ -0,0 +1,100 @@
<?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;
}
}
}

175
src/vendor/PicoTools/Command.php vendored Normal file
View File

@ -0,0 +1,175 @@
<?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;
/**
* Execute an external command
*
* @author Frédéric Guillot
*/
class Command
{
/**
* Command line
*
* @access private
* @var string
*/
private $cmd_line = '';
/**
* Command stdout
*
* @access private
* @var string
*/
private $cmd_stdout = '';
/**
* Command stderr
*
* @access private
* @var string
*/
private $cmd_sdterr = '';
/**
* Command environements variables
*
* @access private
* @var array
*/
private $cmd_env = array();
/**
* Command working directory
*
* @access private
* @var string
*/
private $cmd_dir = null;
/**
* Command return value
*
* @access private
* @var integer
*/
private $cmd_return = 0;
/**
* Constructor
*
* @access public
* @param string $command Command line
*/
public function __construct($command)
{
$this->cmd_line = $command;
}
/**
* Execute the command
*
* @access public
*/
public function execute()
{
$process = proc_open(
$this->cmd_line,
array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w')
),
$pipes,
$this->cmd_dir,
$this->cmd_env
);
if (is_resource($process)) {
$this->cmd_stdout = stream_get_contents($pipes[1]);
$this->cmd_stderr = stream_get_contents($pipes[2]);
$this->cmd_return = proc_close($process);
}
}
/**
* Set working directory
*
* @access public
* @param string $dir Working directory
*/
public function setDir($dir)
{
$this->cmd_dir = $dir;
}
/**
* Set command env variables
*
* @access public
* @param array $env Environnement variables
*/
public function setEnv(array $env)
{
$this->cmd_env = $env;
}
/**
* Get the return value
*
* @access public
* @return integer Return value
*/
public function getReturnValue()
{
return $this->cmd_return;
}
/**
* Get stdout
*
* @access public
* @return string stdout
*/
public function getStdout()
{
return $this->cmd_stdout;
}
/**
* Get stderr
*
* @access public
* @return string stderr
*/
public function getStderr()
{
return $this->cmd_stderr;
}
}

97
src/vendor/PicoTools/Config.php vendored Normal file
View File

@ -0,0 +1,97 @@
<?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;
/**
* Handle configuration parameters
*
* @author Frédéric Guillot
*/
class Config
{
/**
* Container
*
* @access private
* @static
* @var array
*/
private static $container = array();
/**
* Set a new configuration parameter
*
* @access public
* @static
* @param string $name Parameter name
* @param string $value Parameter value
*/
public static function set($name, $value)
{
self::$container[$name] = $value;
}
/**
* Fetch a parameter value
*
* @access public
* @static
* @param string $name Parameter name
* @param string $defaultValue Default parameter value
*/
public static function get($name, $defaultValue = null)
{
return isset(self::$container[$name]) ? self::$container[$name] : $defaultValue;
}
/**
* Load a PHP config file
*
* @access public
* @static
*/
public static function load($env = null)
{
if ($env !== null) {
$filename = 'config/'.$env.'.php';
if (file_exists($filename)) {
require $filename;
}
else {
throw new \RuntimeException('Unable to load the config file: '.$filename);
}
}
else {
if (file_exists('config/prod.php')) {
require 'config/prod.php';
}
else if (file_exists('config/dev.php')) {
require 'config/dev.php';
}
else {
throw new \RuntimeException('No config file loaded.');
}
}
}
}

150
src/vendor/PicoTools/Crypto.php vendored Normal file
View File

@ -0,0 +1,150 @@
<?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);
}
// Import of the PHP 5.5 password hashing method
// https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
function password_verify($password, $hash)
{
$ret = crypt($password, $hash);
if (! is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
return false;
}
$status = 0;
for ($i = 0; $i < strlen($ret); $i++) {
$status |= (ord($ret[$i]) ^ ord($hash[$i]));
}
return $status === 0;
}
function password_hash($password)
{
$cost = 10;
$required_salt_len = 22;
$hash_format = sprintf("$2y$%02d$", $cost);
$hash = $hash_format.password_salt($required_salt_len);
$ret = crypt($password, $hash);
if (! is_string($ret) || strlen($ret) <= 13) {
return false;
}
return $ret;
}
function password_salt($required_salt_len)
{
$buffer = '';
$raw_length = (int) ($required_salt_len * 3 / 4 + 1);
$buffer_valid = false;
if (function_exists('mcrypt_create_iv')) {
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
if ($buffer) {
$buffer_valid = true;
}
}
if (! $buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
$buffer = openssl_random_pseudo_bytes($raw_length);
if ($buffer) {
$buffer_valid = true;
}
}
if (! $buffer_valid && file_exists('/dev/urandom')) {
$f = @fopen('/dev/urandom', 'r');
if ($f) {
$read = strlen($buffer);
while ($read < $raw_length) {
$buffer .= fread($f, $raw_length - $read);
$read = strlen($buffer);
}
fclose($f);
if ($read >= $raw_length) {
$buffer_valid = true;
}
}
}
if (! $buffer_valid || strlen($buffer) < $raw_length) {
$bl = strlen($buffer);
for ($i = 0; $i < $raw_length; $i++) {
if ($i < $bl) {
$buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
}
else {
$buffer .= chr(mt_rand(0, 255));
}
}
}
$salt = str_replace('+', '.', base64_encode($buffer));
return substr($salt, 0, $required_salt_len);
}

View File

@ -0,0 +1,49 @@
<?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;
function singleton($name)
{
static $instance = array();
if (! isset($instance[$name])) {
$callback = container($name);
if (! is_callable($callback)) {
return null;
}
$instance[$name] = $callback();
}
return $instance[$name];
}
function container($name, $value = null)
{
static $container = array();
if (null !== $value) {
$container[$name] = $value;
}
else if (isset($container[$name])) {
return $container[$name];
}
return null;
}

176
src/vendor/PicoTools/Helper.php vendored Normal file
View File

@ -0,0 +1,176 @@
<?php
namespace Helper;
function escape($value)
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false);
}
/**
* Get the flash message if there is something
*
* @param string $html HTML tags of the flash message parsed with sprintf
* return string HTML tags with the message or empty string if nothing
*/
function flash($html)
{
$data = '';
if (isset($_SESSION['flash_message'])) {
$data = sprintf($html, escape($_SESSION['flash_message']));
unset($_SESSION['flash_message']);
}
return $data;
}
/**
* Get the flash error message if there is something
*
* @param string $html HTML tags of the flash message parsed with sprintf
* return string HTML tags with the message or empty string if nothing
*/
function flash_error($html)
{
$data = '';
if (isset($_SESSION['flash_error_message'])) {
$data = sprintf($html, escape($_SESSION['flash_error_message']));
unset($_SESSION['flash_error_message']);
}
return $data;
}
function in_list($id, array $listing)
{
if (isset($listing[$id])) {
return escape($listing[$id]);
}
return '?';
}
function error_class(array $errors, $name)
{
return ! isset($errors[$name]) ? '' : ' form-error';
}
function error_list(array $errors, $name)
{
$html = '';
if (isset($errors[$name])) {
$html .= '<ul class="form-errors">';
foreach ($errors[$name] as $error) {
$html .= '<li>'.escape($error).'</li>';
}
$html .= '</ul>';
}
return $html;
}
function form_value($values, $name)
{
if (isset($values->$name)) {
return 'value="'.escape($values->$name).'"';
}
return isset($values[$name]) ? 'value="'.escape($values[$name]).'"' : '';
}
function form_hidden($name, $values = array())
{
return '<input type="hidden" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).'/>';
}
function form_default_select($name, array $options, $values = array(), array $errors = array(), $class = '')
{
$options = array('' => '?') + $options;
return form_select($name, $options, $values, $errors, $class);
}
function form_select($name, array $options, $values = array(), array $errors = array(), $class = '')
{
$html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'">';
foreach ($options as $id => $value) {
$html .= '<option value="'.escape($id).'"';
if (isset($values->$name) && $id == $values->$name) $html .= ' selected="selected"';
if (isset($values[$name]) && $id == $values[$name]) $html .= ' selected="selected"';
$html .= '>'.escape($value).'</option>';
}
$html .= '</select>';
$html .= error_list($errors, $name);
return $html;
}
function form_label($label, $name, $class = '')
{
return '<label for="form-'.$name.'" class="'.$class.'">'.escape($label).'</label>';
}
function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="text" name="'.$name.'" id="form-'.$name.'" '.form_value($values, $name).' class="'.$class.'" ';
$html .= implode(' ', $attributes).'/>';
$html .= error_list($errors, $name);
return $html;
}
function form_password($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '')
{
$class .= error_class($errors, $name);
$html = '<input type="password" 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);
$html = '<textarea name="'.$name.'" id="form-'.$name.'" class="'.$class.'" ';
$html .= implode(' ', $attributes).'>';
$html .= isset($values->$name) ? escape($values->$name) : isset($values[$name]) ? $values[$name] : '';
$html .= '</textarea>';
$html .= error_list($errors, $name);
return $html;
}

243
src/vendor/PicoTools/Pixtag.php vendored Normal file
View File

@ -0,0 +1,243 @@
<?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)]);
}
}

55
src/vendor/PicoTools/Template.php vendored Normal file
View File

@ -0,0 +1,55 @@
<?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\Template;
const PATH = 'templates/';
// Template\load('template_name', ['bla' => 'value']);
function load()
{
if (func_num_args() < 1 || func_num_args() > 2) {
die('Invalid template arguments');
}
if (! file_exists(PATH.func_get_arg(0).'.php')) {
die('Unable to load the template: "'.func_get_arg(0).'"');
}
if (func_num_args() === 2) {
if (! is_array(func_get_arg(1))) {
die('Template variables must be an array');
}
extract(func_get_arg(1));
}
ob_start();
include PATH.func_get_arg(0).'.php';
return ob_get_clean();
}
function layout($template_name, array $template_args = array())
{
$output = load('app_header', $template_args);
$output .= load($template_name, $template_args);
$output .= load('app_footer', $template_args);
return $output;
}

121
src/vendor/PicoTools/Translator.php vendored Normal file
View File

@ -0,0 +1,121 @@
<?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\Translator {
const PATH = 'locales/';
function translate($identifier)
{
$args = \func_get_args();
\array_shift($args);
\array_unshift($args, get($identifier));
return \call_user_func_array(
'sprintf',
$args
);
}
function number($number)
{
return number_format(
$number,
get('number.decimals', 2),
get('number.decimals_separator', '.'),
get('number.thousands_separator', ',')
);
}
function currency($amount)
{
$position = get('currency.position', 'before');
$symbol = get('currency.symbol', '$');
$str = '';
if ($position === 'before') {
$str .= $symbol;
}
$str .= number($amount);
if ($position === 'after') {
$str .= ' '.$symbol;
}
return $str;
}
function get($identifier, $default = '')
{
$locales = container();
if (isset($locales[$identifier])) {
return $locales[$identifier];
}
else {
return $default;
}
}
function load($language)
{
$path = PATH.$language;
$locales = array();
if (is_dir($path)) {
$dir = new \DirectoryIterator($path);
foreach ($dir as $fileinfo) {
if (strpos($fileinfo->getFilename(), '.php') !== false) {
$locales = array_merge($locales, include $fileinfo->getPathname());
}
}
}
container($locales);
}
function container($locales = null)
{
static $values = array();
if ($locales) {
$values = $locales;
}
return $values;
}
}
namespace {
function t() {
return call_user_func_array('\PicoTools\Translator\translate', func_get_args());
}
}

44
src/vendor/SimpleValidator/Base.php vendored Normal file
View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
abstract class Base
{
protected $field = '';
protected $error_message = '';
protected $data = array();
abstract public function execute(array $data);
public function __construct($field, $error_message)
{
$this->field = $field;
$this->error_message = $error_message;
}
public function getErrorMessage()
{
return $this->error_message;
}
public function getField()
{
return $this->field;
}
}

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Validator
{
private $data = array();
private $errors = array();
private $validators = array();
public function __construct(array $data, array $validators)
{
$this->data = $data;
$this->validators = $validators;
}
public function execute()
{
$result = true;
foreach ($this->validators as $validator) {
if (! $validator->execute($this->data)) {
$this->addError(
$validator->getField(),
$validator->getErrorMessage()
);
$result = false;
}
}
return $result;
}
public function addError($field, $message)
{
if (! isset($this->errors[$field])) {
$this->errors[$field] = array();
}
$this->errors[$field][] = $message;
}
public function getErrors()
{
return $this->errors;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Alpha extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! ctype_alpha($data[$this->field])) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class AlphaNumeric extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! ctype_alnum($data[$this->field])) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Email extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
// I use the same validation method as Firefox
// http://hg.mozilla.org/mozilla-central/file/cf5da681d577/content/html/content/src/nsHTMLInputElement.cpp#l3967
$value = $data[$this->field];
$length = strlen($value);
// If the email address begins with a '@' or ends with a '.',
// we know it's invalid.
if ($value[0] === '@' || $value[$length - 1] === '.') {
return false;
}
// Check the username
for ($i = 0; $i < $length && $value[$i] !== '@'; ++$i) {
$c = $value[$i];
if (! (ctype_alnum($c) || $c === '.' || $c === '!' || $c === '#' || $c === '$' ||
$c === '%' || $c === '&' || $c === '\'' || $c === '*' || $c === '+' ||
$c === '-' || $c === '/' || $c === '=' || $c === '?' || $c === '^' ||
$c === '_' || $c === '`' || $c === '{' || $c === '|' || $c === '}' ||
$c === '~')) {
return false;
}
}
// There is no domain name (or it's one-character long),
// that's not a valid email address.
if (++$i >= $length) return false;
if (($i + 1) === $length) return false;
// The domain name can't begin with a dot.
if ($value[$i] === '.') return false;
// Parsing the domain name.
for (; $i < $length; ++$i) {
$c = $value[$i];
if ($c === '.') {
// A dot can't follow a dot.
if ($value[$i - 1] === '.') return false;
}
elseif (! (ctype_alnum($c) || $c === '-')) {
// The domain characters have to be in this list to be valid.
return false;
}
}
}
return true;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Equals extends Base
{
private $field2;
public function __construct($field1, $field2, $error_message)
{
parent::__construct($field1, $error_message);
$this->field2 = $field2;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! isset($data[$this->field2])) return false;
return $data[$this->field] === $data[$this->field2];
}
return true;
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Integer extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (is_string($data[$this->field])) {
if ($data[$this->field][0] === '-') {
return ctype_digit(substr($data[$this->field], 1));
}
return ctype_digit($data[$this->field]);
}
else {
return is_int($data[$this->field]);
}
}
return true;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Ip extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! filter_var($data[$this->field], FILTER_VALIDATE_IP)) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Length extends Base
{
private $min;
private $max;
public function __construct($field, $error_message, $min, $max)
{
parent::__construct($field, $error_message);
$this->min = $min;
$this->max = $max;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
$length = mb_strlen($data[$this->field], 'UTF-8');
if ($length < $this->min || $length > $this->max) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class MacAddress extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
$groups = explode(':', $data[$this->field]);
if (count($groups) !== 6) return false;
foreach ($groups as $group) {
if (! ctype_xdigit($group)) return false;
}
}
return true;
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class MaxLength extends Base
{
private $max;
public function __construct($field, $error_message, $max)
{
parent::__construct($field, $error_message);
$this->max = $max;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
$length = mb_strlen($data[$this->field], 'UTF-8');
if ($length > $this->max) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class MinLength extends Base
{
private $min;
public function __construct($field, $error_message, $min)
{
parent::__construct($field, $error_message);
$this->min = $min;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
$length = mb_strlen($data[$this->field], 'UTF-8');
if ($length < $this->min) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Numeric extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! is_numeric($data[$this->field])) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Range extends Base
{
private $min;
private $max;
public function __construct($field, $error_message, $min, $max)
{
parent::__construct($field, $error_message);
$this->min = $min;
$this->max = $max;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! is_numeric($data[$this->field])) {
return false;
}
if ($data[$this->field] < $this->min || $data[$this->field] > $this->max) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Required extends Base
{
public function execute(array $data)
{
if (! isset($data[$this->field]) || $data[$this->field] === '') {
return false;
}
return true;
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
*/
class Unique extends Base
{
private $pdo;
private $primary_key;
private $table;
public function __construct($field, $error_message, \PDO $pdo, $table, $primary_key = 'id')
{
parent::__construct($field, $error_message);
$this->pdo = $pdo;
$this->primary_key = $primary_key;
$this->table = $table;
}
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
if (! isset($data[$this->primary_key])) {
$rq = $this->pdo->prepare('SELECT COUNT(*) FROM '.$this->table.' WHERE '.$this->field.'=?');
$rq->execute(array(
$data[$this->field]
));
$result = $rq->fetch(\PDO::FETCH_NUM);
if (isset($result[0]) && $result[0] === '1') {
return false;
}
}
else {
$rq = $this->pdo->prepare(
'SELECT COUNT(*) FROM '.$this->table.'
WHERE '.$this->field.'=? AND '.$this->primary_key.' != ?'
);
$rq->execute(array(
$data[$this->field],
$data[$this->primary_key]
));
$result = $rq->fetch(\PDO::FETCH_NUM);
if (isset($result[0]) && $result[0] === '1') {
return false;
}
}
}
return true;
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of Simple Validator.
*
* (c) Frédéric Guillot <contact@fredericguillot.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace SimpleValidator\Validators;
use SimpleValidator\Base;
/**
* @author Frédéric Guillot <contact@fredericguillot.com>
* @link http://semver.org/
*/
class Version extends Base
{
public function execute(array $data)
{
if (isset($data[$this->field]) && $data[$this->field] !== '') {
$pattern = '/^[0-9]+\.[0-9]+\.[0-9]+([+-][^+-][0-9A-Za-z-.]*)?$/';
return (bool) preg_match($pattern, $data[$this->field]);
}
return true;
}
}