commit e3cf8fe8025972b6c648931f405fe39291344ffe Author: Frederic Guillot Date: Sun Feb 17 21:48:21 2013 -0500 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..646d24c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src/data/ diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..f3f2d45 --- /dev/null +++ b/README.markdown @@ -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... diff --git a/screenshots/feeds.png b/screenshots/feeds.png new file mode 100644 index 0000000..1efccd7 Binary files /dev/null and b/screenshots/feeds.png differ diff --git a/screenshots/item.png b/screenshots/item.png new file mode 100644 index 0000000..7597166 Binary files /dev/null and b/screenshots/item.png differ diff --git a/screenshots/items.png b/screenshots/items.png new file mode 100644 index 0000000..3e46c65 Binary files /dev/null and b/screenshots/items.png differ diff --git a/src/assets/css/app.css b/src/assets/css/app.css new file mode 100644 index 0000000..64e33c6 --- /dev/null +++ b/src/assets/css/app.css @@ -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; +} \ No newline at end of file diff --git a/src/common.php b/src/common.php new file mode 100644 index 0000000..a41bff6 --- /dev/null +++ b/src/common.php @@ -0,0 +1,26 @@ + 'sqlite', + 'filename' => 'data/db.sqlite' + )); + + if ($db->schema()->check(1)) { + + return $db; + } + else { + + die('Unable to migrate database schema.'); + } +}); \ No newline at end of file diff --git a/src/cronjob.php b/src/cronjob.php new file mode 100644 index 0000000..796240e --- /dev/null +++ b/src/cronjob.php @@ -0,0 +1,5 @@ + '*' + )); + + 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' + ))); +}); \ No newline at end of file diff --git a/src/model.php b/src/model.php new file mode 100644 index 0000000..2fd8fcd --- /dev/null +++ b/src/model.php @@ -0,0 +1,331 @@ +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); +} diff --git a/src/schema.php b/src/schema.php new file mode 100644 index 0000000..a52e615 --- /dev/null +++ b/src/schema.php @@ -0,0 +1,43 @@ +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 + ) + '); +} \ No newline at end of file diff --git a/src/templates/add.php b/src/templates/add.php new file mode 100644 index 0000000..e3193e1 --- /dev/null +++ b/src/templates/add.php @@ -0,0 +1,12 @@ + + +
+ + +
+ +
+
\ No newline at end of file diff --git a/src/templates/app_footer.php b/src/templates/app_footer.php new file mode 100644 index 0000000..6805ce9 --- /dev/null +++ b/src/templates/app_footer.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/templates/app_header.php b/src/templates/app_header.php new file mode 100644 index 0000000..c3e88a0 --- /dev/null +++ b/src/templates/app_header.php @@ -0,0 +1,24 @@ + + + + + + miniflux + + + +
+ +
+
+ %s') ?> + %s') ?> diff --git a/src/templates/config.php b/src/templates/config.php new file mode 100644 index 0000000..59ddf95 --- /dev/null +++ b/src/templates/config.php @@ -0,0 +1,19 @@ + + +
+ + +
+ + +
+ + +
+ +
+ +
+
\ No newline at end of file diff --git a/src/templates/feed_menu.php b/src/templates/feed_menu.php new file mode 100644 index 0000000..b6ba014 --- /dev/null +++ b/src/templates/feed_menu.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/templates/feeds.php b/src/templates/feeds.php new file mode 100644 index 0000000..6a4319d --- /dev/null +++ b/src/templates/feeds.php @@ -0,0 +1,25 @@ + + + + +

No subscriptions.

+ + + +
+ + + +
+ + \ No newline at end of file diff --git a/src/templates/import.php b/src/templates/import.php new file mode 100644 index 0000000..9972621 --- /dev/null +++ b/src/templates/import.php @@ -0,0 +1,12 @@ + + +
+ + +
+ +
+
\ No newline at end of file diff --git a/src/templates/login.php b/src/templates/login.php new file mode 100644 index 0000000..b34a92f --- /dev/null +++ b/src/templates/login.php @@ -0,0 +1,34 @@ + + + + + + miniflux + + + + + + + + +

+ + + +
+ + +
+ + + + +
+ +
+
+ + \ No newline at end of file diff --git a/src/templates/read_item.php b/src/templates/read_item.php new file mode 100644 index 0000000..9afe580 --- /dev/null +++ b/src/templates/read_item.php @@ -0,0 +1,20 @@ + + +

Article not found.

+ + + +
+

+ +

+ +

+ | + +

+ + +
+ + \ No newline at end of file diff --git a/src/templates/read_items.php b/src/templates/read_items.php new file mode 100644 index 0000000..b99631d --- /dev/null +++ b/src/templates/read_items.php @@ -0,0 +1,26 @@ + + +

No history.

+ + + + + +
+ +
+

+

+ | + +

+
+ +
+ + \ No newline at end of file diff --git a/src/templates/unread_items.php b/src/templates/unread_items.php new file mode 100644 index 0000000..6b803bf --- /dev/null +++ b/src/templates/unread_items.php @@ -0,0 +1,25 @@ + + +

No unread items.

+ + + + + +
+ +
+

+

+ +

+
+ +
+ + \ No newline at end of file diff --git a/src/vendor/PicoDb/Database.php b/src/vendor/PicoDb/Database.php new file mode 100644 index 0000000..ffc37bb --- /dev/null +++ b/src/vendor/PicoDb/Database.php @@ -0,0 +1,114 @@ +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); + } +} \ No newline at end of file diff --git a/src/vendor/PicoDb/Drivers/Sqlite.php b/src/vendor/PicoDb/Drivers/Sqlite.php new file mode 100644 index 0000000..7e276c0 --- /dev/null +++ b/src/vendor/PicoDb/Drivers/Sqlite.php @@ -0,0 +1,47 @@ +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.'"'; + } +} \ No newline at end of file diff --git a/src/vendor/PicoDb/Schema.php b/src/vendor/PicoDb/Schema.php new file mode 100644 index 0000000..2f52b84 --- /dev/null +++ b/src/vendor/PicoDb/Schema.php @@ -0,0 +1,60 @@ +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; + } +} \ No newline at end of file diff --git a/src/vendor/PicoDb/Table.php b/src/vendor/PicoDb/Table.php new file mode 100644 index 0000000..f543fd8 --- /dev/null +++ b/src/vendor/PicoDb/Table.php @@ -0,0 +1,343 @@ +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; + } +} \ No newline at end of file diff --git a/src/vendor/PicoFarad/Request.php b/src/vendor/PicoFarad/Request.php new file mode 100644 index 0000000..b78f0c5 --- /dev/null +++ b/src/vendor/PicoFarad/Request.php @@ -0,0 +1,50 @@ + $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)); +} \ No newline at end of file diff --git a/src/vendor/PicoFarad/Router.php b/src/vendor/PicoFarad/Router.php new file mode 100644 index 0000000..4f0bfff --- /dev/null +++ b/src/vendor/PicoFarad/Router.php @@ -0,0 +1,138 @@ +content = $content; + } + + + public function execute() + { + $xml = new \SimpleXMLElement(''); + + $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(); + } +} \ No newline at end of file diff --git a/src/vendor/PicoFeed/Filter.php b/src/vendor/PicoFeed/Filter.php new file mode 100644 index 0000000..ae5d8bc --- /dev/null +++ b/src/vendor/PicoFeed/Filter.php @@ -0,0 +1,271 @@ + 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' ? '' : '/>'; + } + } + + + 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; + } +} diff --git a/src/vendor/PicoFeed/Import.php b/src/vendor/PicoFeed/Import.php new file mode 100644 index 0000000..40aac90 --- /dev/null +++ b/src/vendor/PicoFeed/Import.php @@ -0,0 +1,61 @@ +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; + } + } + } +} \ No newline at end of file diff --git a/src/vendor/PicoFeed/Parser.php b/src/vendor/PicoFeed/Parser.php new file mode 100644 index 0000000..0420741 --- /dev/null +++ b/src/vendor/PicoFeed/Parser.php @@ -0,0 +1,182 @@ +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; + } +} diff --git a/src/vendor/PicoFeed/Reader.php b/src/vendor/PicoFeed/Reader.php new file mode 100644 index 0000000..a29f93a --- /dev/null +++ b/src/vendor/PicoFeed/Reader.php @@ -0,0 +1,117 @@ +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, 'content); + } + else if (strpos($first_lines, 'content); + }/* + else if (strpos($first_lines, '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; + } +} diff --git a/src/vendor/PicoFeed/Writer.php b/src/vendor/PicoFeed/Writer.php new file mode 100644 index 0000000..e69de29 diff --git a/src/vendor/PicoTools/Chrono.php b/src/vendor/PicoTools/Chrono.php new file mode 100644 index 0000000..8a65505 --- /dev/null +++ b/src/vendor/PicoTools/Chrono.php @@ -0,0 +1,100 @@ + 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; + } + } +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Command.php b/src/vendor/PicoTools/Command.php new file mode 100644 index 0000000..9f8597c --- /dev/null +++ b/src/vendor/PicoTools/Command.php @@ -0,0 +1,175 @@ +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; + } +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Config.php b/src/vendor/PicoTools/Config.php new file mode 100644 index 0000000..0750829 --- /dev/null +++ b/src/vendor/PicoTools/Config.php @@ -0,0 +1,97 @@ += $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); +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Dependency_Injection.php b/src/vendor/PicoTools/Dependency_Injection.php new file mode 100644 index 0000000..30fa26f --- /dev/null +++ b/src/vendor/PicoTools/Dependency_Injection.php @@ -0,0 +1,49 @@ +'; + + foreach ($errors[$name] as $error) { + + $html .= '
  • '.escape($error).'
  • '; + } + + $html .= ''; + } + + 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 ''; +} + + +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 = ''; + $html .= error_list($errors, $name); + + return $html; +} + + +function form_label($label, $name, $class = '') +{ + return ''; +} + + +function form_text($name, $values = array(), array $errors = array(), array $attributes = array(), $class = '') +{ + $class .= error_class($errors, $name); + + $html = ''; + $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 = ''; + $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 = ''; + $html .= error_list($errors, $name); + + return $html; +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Pixtag.php b/src/vendor/PicoTools/Pixtag.php new file mode 100644 index 0000000..a5e59d8 --- /dev/null +++ b/src/vendor/PicoTools/Pixtag.php @@ -0,0 +1,243 @@ +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)]); + } +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Template.php b/src/vendor/PicoTools/Template.php new file mode 100644 index 0000000..3e5effa --- /dev/null +++ b/src/vendor/PicoTools/Template.php @@ -0,0 +1,55 @@ + '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; +} \ No newline at end of file diff --git a/src/vendor/PicoTools/Translator.php b/src/vendor/PicoTools/Translator.php new file mode 100644 index 0000000..40bd653 --- /dev/null +++ b/src/vendor/PicoTools/Translator.php @@ -0,0 +1,121 @@ +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()); + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Base.php b/src/vendor/SimpleValidator/Base.php new file mode 100644 index 0000000..45c01a6 --- /dev/null +++ b/src/vendor/SimpleValidator/Base.php @@ -0,0 +1,44 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validator.php b/src/vendor/SimpleValidator/Validator.php new file mode 100644 index 0000000..8bb4d62 --- /dev/null +++ b/src/vendor/SimpleValidator/Validator.php @@ -0,0 +1,67 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Alpha.php b/src/vendor/SimpleValidator/Validators/Alpha.php new file mode 100644 index 0000000..b00b819 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Alpha.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/AlphaNumeric.php b/src/vendor/SimpleValidator/Validators/AlphaNumeric.php new file mode 100644 index 0000000..e1762d6 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/AlphaNumeric.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Email.php b/src/vendor/SimpleValidator/Validators/Email.php new file mode 100644 index 0000000..e4e3d5d --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Email.php @@ -0,0 +1,81 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Equals.php b/src/vendor/SimpleValidator/Validators/Equals.php new file mode 100644 index 0000000..91f34e4 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Equals.php @@ -0,0 +1,43 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Integer.php b/src/vendor/SimpleValidator/Validators/Integer.php new file mode 100644 index 0000000..150558a --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Integer.php @@ -0,0 +1,42 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Ip.php b/src/vendor/SimpleValidator/Validators/Ip.php new file mode 100644 index 0000000..48afe56 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Ip.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Length.php b/src/vendor/SimpleValidator/Validators/Length.php new file mode 100644 index 0000000..36e50b3 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Length.php @@ -0,0 +1,48 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/MacAddress.php b/src/vendor/SimpleValidator/Validators/MacAddress.php new file mode 100644 index 0000000..d934841 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/MacAddress.php @@ -0,0 +1,37 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/MaxLength.php b/src/vendor/SimpleValidator/Validators/MaxLength.php new file mode 100644 index 0000000..d8e032b --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/MaxLength.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/MinLength.php b/src/vendor/SimpleValidator/Validators/MinLength.php new file mode 100644 index 0000000..4b7f7d2 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/MinLength.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Numeric.php b/src/vendor/SimpleValidator/Validators/Numeric.php new file mode 100644 index 0000000..a958df1 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Numeric.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Range.php b/src/vendor/SimpleValidator/Validators/Range.php new file mode 100644 index 0000000..1d71b92 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Range.php @@ -0,0 +1,51 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Required.php b/src/vendor/SimpleValidator/Validators/Required.php new file mode 100644 index 0000000..e7ef271 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Required.php @@ -0,0 +1,30 @@ + + * + * 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 + */ +class Required extends Base +{ + public function execute(array $data) + { + if (! isset($data[$this->field]) || $data[$this->field] === '') { + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Unique.php b/src/vendor/SimpleValidator/Validators/Unique.php new file mode 100644 index 0000000..c20dbe1 --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Unique.php @@ -0,0 +1,78 @@ + + * + * 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 + */ +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; + } +} \ No newline at end of file diff --git a/src/vendor/SimpleValidator/Validators/Version.php b/src/vendor/SimpleValidator/Validators/Version.php new file mode 100644 index 0000000..273a28a --- /dev/null +++ b/src/vendor/SimpleValidator/Validators/Version.php @@ -0,0 +1,32 @@ + + * + * 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 + * @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; + } +} \ No newline at end of file