diff --git a/NEWS.md b/NEWS.md index d99cf1c3f5..571fea5f50 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,9 @@ ### Bug fixes - Reddit spout allows wider range of URLs, including absolute URLs and searches ([#1033](https://github.com/SSilence/selfoss/pull/1033)) +### API changes +- `tags` attribute is now consistently array of strings, numbers are numbers and booleans are booleans. + ### Other changes - Removed broken instapaper scraping from Reddit spout ([#1033](https://github.com/SSilence/selfoss/pull/1033)) diff --git a/controllers/Index.php b/controllers/Index.php index 2d9d7a348e..fc9643d34f 100644 --- a/controllers/Index.php +++ b/controllers/Index.php @@ -217,8 +217,7 @@ private function loadItems($options, $tags) { $tagsController = new \controllers\Tags(); foreach ($itemDao->get($options) as $item) { // parse tags and assign tag colors - $itemTags = explode(',', $item['tags']); - $item['tags'] = $tagsController->tagsAddColors($itemTags, $tags); + $item['tags'] = $tagsController->tagsAddColors($item['tags'], $tags); $this->view->item = $item; $itemsHtml .= $this->view->render('templates/item.phtml'); diff --git a/controllers/Opml.php b/controllers/Opml.php index 38d190dcc9..e3c82623a4 100644 --- a/controllers/Opml.php +++ b/controllers/Opml.php @@ -301,7 +301,7 @@ public function export() { $sources = ['tagged' => [], 'untagged' => []]; foreach ($this->sourcesDao->get() as $source) { if ($source['tags']) { - foreach (explode(',', $source['tags']) as $tag) { + foreach ($source['tags'] as $tag) { $sources['tagged'][$tag][] = $source; } } else { diff --git a/controllers/Sources.php b/controllers/Sources.php index 37bcbcb605..db1090550e 100644 --- a/controllers/Sources.php +++ b/controllers/Sources.php @@ -153,7 +153,7 @@ public function write() { // clean up title and tag data to prevent XSS $title = htmlspecialchars($data['title']); - $tags = htmlspecialchars($data['tags']); + $tags = array_map('htmlspecialchars', $data['tags']); $spout = $data['spout']; $filter = $data['filter']; $isAjax = isset($data['ajax']); @@ -199,7 +199,6 @@ public function write() { // autocolor tags $tagsDao = new \daos\Tags(); - $tags = explode(',', $tags); foreach ($tags as $tag) { $tagsDao->autocolorTag(trim($tag)); } @@ -209,7 +208,7 @@ public function write() { $return = [ 'success' => true, - 'id' => $id, + 'id' => (int) $id, 'title' => $title ]; diff --git a/daos/Database.php b/daos/Database.php index ffd166010e..b1aefd64ef 100644 --- a/daos/Database.php +++ b/daos/Database.php @@ -2,6 +2,10 @@ namespace daos; +const PARAM_INT = 1; +const PARAM_BOOL = 2; +const PARAM_CSV = 3; + /** * Base class for database access * diff --git a/daos/Items.php b/daos/Items.php index a278de7168..9ae60febb1 100644 --- a/daos/Items.php +++ b/daos/Items.php @@ -80,8 +80,7 @@ public function get($options = []) { // remove private posts with private tags if (!\F3::get('auth')->showPrivateTags()) { foreach ($items as $idx => $item) { - $tags = explode(',', $item['tags']); - foreach ($tags as $tag) { + foreach ($item['tags'] as $tag) { if (strpos(trim($tag), '@') === 0) { unset($items[$idx]); break; @@ -94,8 +93,7 @@ public function get($options = []) { // remove posts with hidden tags if (!isset($options['tag']) || strlen($options['tag']) === 0) { foreach ($items as $idx => $item) { - $tags = explode(',', $item['tags']); - foreach ($tags as $tag) { + foreach ($item['tags'] as $tag) { if (strpos(trim($tag), '#') === 0) { unset($items[$idx]); break; diff --git a/daos/Sources.php b/daos/Sources.php index 79986f73f6..adb8edfa8a 100644 --- a/daos/Sources.php +++ b/daos/Sources.php @@ -48,8 +48,7 @@ public function get($id = null) { // remove items with private tags if (!\F3::get('auth')->showPrivateTags()) { foreach ($sources as $idx => $source) { - $tags = explode(',', $source['tags']); - foreach ($tags as $tag) { + foreach ($source['tags'] as $tag) { if (strpos(trim($tag), '@') === 0) { unset($sources[$idx]); break; diff --git a/daos/mysql/Items.php b/daos/mysql/Items.php index 59b5e4f2c1..e914029099 100644 --- a/daos/mysql/Items.php +++ b/daos/mysql/Items.php @@ -344,7 +344,13 @@ public function get($options = []) { $query = "$select $where_sql $order_sql LIMIT " . $options['items'] . ' OFFSET ' . $options['offset']; } - return \F3::get('db')->exec($query, $params); + return $this->stmt->ensureRowTypes(\F3::get('db')->exec($query, $params), [ + 'id' => \daos\PARAM_INT, + 'unread' => \daos\PARAM_BOOL, + 'starred' => \daos\PARAM_BOOL, + 'source' => \daos\PARAM_INT, + 'tags' => \daos\PARAM_CSV + ]); } /** @@ -503,9 +509,9 @@ public function stats() { ' . $this->stmt->sumBool('starred') . ' AS starred FROM ' . \F3::get('db_prefix') . 'items;'); $res = $this->stmt->ensureRowTypes($res, [ - 'total' => \PDO::PARAM_INT, - 'unread' => \PDO::PARAM_INT, - 'starred' => \PDO::PARAM_INT + 'total' => \daos\PARAM_INT, + 'unread' => \daos\PARAM_INT, + 'starred' => \daos\PARAM_INT ]); return $res[0]; @@ -537,9 +543,9 @@ public function statuses($since) { WHERE ' . \F3::get('db_prefix') . 'items.updatetime > :since;', [':since' => [$since, \PDO::PARAM_STR]]); $res = $this->stmt->ensureRowTypes($res, [ - 'id' => \PDO::PARAM_INT, - 'unread' => \PDO::PARAM_BOOL, - 'starred' => \PDO::PARAM_BOOL + 'id' => \daos\PARAM_INT, + 'unread' => \daos\PARAM_BOOL, + 'starred' => \daos\PARAM_BOOL ]); return $res; diff --git a/daos/mysql/Sources.php b/daos/mysql/Sources.php index 337445c18b..d44b974e17 100644 --- a/daos/mysql/Sources.php +++ b/daos/mysql/Sources.php @@ -14,19 +14,16 @@ class Sources extends Database { * add new source * * @param string $title - * @param string $tags + * @param string[] $tags * @param string $spout the source type * @param mixed $params depends from spout * * @return int new id */ - public function add($title, $tags, $filter, $spout, $params) { - // sanitize tag list - $tags = implode(',', preg_split('/\s*,\s*/', trim($tags), -1, PREG_SPLIT_NO_EMPTY)); - + public function add($title, array $tags, $filter, $spout, $params) { return $this->stmt->insert('INSERT INTO ' . \F3::get('db_prefix') . 'sources (title, tags, filter, spout, params) VALUES (:title, :tags, :filter, :spout, :params)', [ ':title' => trim($title), - ':tags' => $tags, + ':tags' => $this->stmt->csvRow($tags), ':filter' => $filter, ':spout' => $spout, ':params' => htmlentities(json_encode($params)) @@ -38,19 +35,16 @@ public function add($title, $tags, $filter, $spout, $params) { * * @param int $id the source id * @param string $title new title - * @param string $tags new tags + * @param string[] $tags new tags * @param string $spout new spout * @param mixed $params the new params * * @return void */ - public function edit($id, $title, $tags, $filter, $spout, $params) { - // sanitize tag list - $tags = implode(',', preg_split('/\s*,\s*/', trim($tags), -1, PREG_SPLIT_NO_EMPTY)); - + public function edit($id, $title, array $tags, $filter, $spout, $params) { \F3::get('db')->exec('UPDATE ' . \F3::get('db_prefix') . 'sources SET title=:title, tags=:tags, filter=:filter, spout=:spout, params=:params WHERE id=:id', [ ':title' => trim($title), - ':tags' => $tags, + ':tags' => $this->stmt->csvRow($tags), ':filter' => $filter, ':spout' => $spout, ':params' => htmlentities(json_encode($params)), @@ -144,6 +138,7 @@ public function get($id = null) { // select source by id if specified or return all sources if (isset($id)) { $ret = \F3::get('db')->exec('SELECT id, title, tags, spout, params, filter, error FROM ' . \F3::get('db_prefix') . 'sources WHERE id=:id', [':id' => $id]); + $ret = $this->stmt->ensureRowTypes($ret, ['id' => \daos\PARAM_INT]); if (isset($ret[0])) { $ret = $ret[0]; } else { @@ -151,6 +146,10 @@ public function get($id = null) { } } else { $ret = \F3::get('db')->exec('SELECT id, title, tags, spout, params, filter, error FROM ' . \F3::get('db_prefix') . 'sources ORDER BY error DESC, lower(title) ASC'); + $ret = $this->stmt->ensureRowTypes($ret, [ + 'id' => \daos\PARAM_INT, + 'tags' => \daos\PARAM_CSV + ]); } return $ret; @@ -162,13 +161,18 @@ public function get($id = null) { * @return mixed all sources */ public function getWithUnread() { - return \F3::get('db')->exec('SELECT + $ret = \F3::get('db')->exec('SELECT sources.id, sources.title, COUNT(items.id) AS unread FROM ' . \F3::get('db_prefix') . 'sources AS sources LEFT OUTER JOIN ' . \F3::get('db_prefix') . 'items AS items ON (items.source=sources.id AND ' . $this->stmt->isTrue('items.unread') . ') GROUP BY sources.id, sources.title ORDER BY lower(sources.title) ASC'); + + return $this->stmt->ensureRowTypes($ret, [ + 'id' => \daos\PARAM_INT, + 'unread' => \daos\PARAM_INT + ]); } /** @@ -194,7 +198,10 @@ public function getWithIcon() { ON sources.id=sourceicons.source ORDER BY ' . $this->stmt->nullFirst('sources.error', 'DESC') . ', lower(sources.title)'); - return $ret; + return $this->stmt->ensureRowTypes($ret, [ + 'id' => \daos\PARAM_INT, + 'tags' => \daos\PARAM_CSV + ]); } /** diff --git a/daos/mysql/Statements.php b/daos/mysql/Statements.php index e514080a64..aca527c275 100644 --- a/daos/mysql/Statements.php +++ b/daos/mysql/Statements.php @@ -22,7 +22,7 @@ public static function insert($query, array $params) { \F3::get('db')->exec($query, $params); $res = \F3::get('db')->exec('SELECT LAST_INSERT_ID() as lastid'); - return $res[0]['lastid']; + return (int) $res[0]['lastid']; } /** @@ -152,22 +152,48 @@ public function ensureRowTypes(array $rows, array $expectedRowTypes) { foreach ($expectedRowTypes as $columnIndex => $type) { if (array_key_exists($columnIndex, $row)) { switch ($type) { - case \PDO::PARAM_INT: - $value = intval($row[$columnIndex]); + case \daos\PARAM_INT: + $value = (int) $row[$columnIndex]; break; - case \PDO::PARAM_BOOL: + case \daos\PARAM_BOOL: if ($row[$columnIndex] == '1') { $value = true; } else { $value = false; } break; + case \daos\PARAM_CSV: + $value = explode(',', $row[$columnIndex]); + break; + default: + $value = null; + } + if ($value !== null) { + $rows[$rowIndex][$columnIndex] = $value; } - $rows[$rowIndex][$columnIndex] = $value; } } } return $rows; } + + /** + * convert string array to string for storage in table row + * + * @param string[] $a + * + * @return string + */ + public function csvRow(array $a) { + $filtered = []; + foreach ($a as $s) { + $t = trim($s); + if ($t) { + $filtered[] = $t; + } + } + + return implode(',', $filtered); + } } diff --git a/daos/mysql/Tags.php b/daos/mysql/Tags.php index b3ba1155f2..8dbfb3e8f8 100644 --- a/daos/mysql/Tags.php +++ b/daos/mysql/Tags.php @@ -87,7 +87,7 @@ public function getWithUnread() { GROUP BY tags.tag, tags.color ORDER BY LOWER(tags.tag);'; - return \F3::get('db')->exec($select); + return $this->stmt->ensureRowTypes(\F3::get('db')->exec($select), ['unread' => \daos\PARAM_INT]); } /** diff --git a/daos/pgsql/Statements.php b/daos/pgsql/Statements.php index dd25c64ac0..b924fb293f 100644 --- a/daos/pgsql/Statements.php +++ b/daos/pgsql/Statements.php @@ -98,6 +98,24 @@ public static function csvRowMatches($column, $value) { * expected types */ public function ensureRowTypes(array $rows, array $expectedRowTypes) { - return $rows; // pgsql returns correct PHP types + foreach ($rows as $rowIndex => $row) { + foreach ($expectedRowTypes as $columnIndex => $type) { + if (array_key_exists($columnIndex, $row)) { + switch ($type) { + // pgsql returns correct PHP types for INT and BOOL + case \daos\PARAM_CSV: + $value = explode(',', $row[$columnIndex]); + break; + default: + $value = null; + } + if ($value !== null) { + $rows[$rowIndex][$columnIndex] = $value; + } + } + } + } + + return $rows; } } diff --git a/daos/sqlite/Statements.php b/daos/sqlite/Statements.php index e47e29f16c..c33901cbae 100644 --- a/daos/sqlite/Statements.php +++ b/daos/sqlite/Statements.php @@ -22,7 +22,7 @@ public static function insert($query, array $params) { \F3::get('db')->exec($query, $params); $res = \F3::get('db')->exec('SELECT last_insert_rowid() as lastid'); - return $res[0]['lastid']; + return (int) $res[0]['lastid']; } /** diff --git a/docs/api-description.json b/docs/api-description.json index 8d1e80a37c..2c4668926f 100644 --- a/docs/api-description.json +++ b/docs/api-description.json @@ -462,7 +462,7 @@ { "id": "2", "title": "devart", - "tags": "da", + "tags": ["da"], "spout": "spouts\\deviantart\\dailydeviations", "params": [], "error": "", @@ -471,7 +471,7 @@ { "id": "1", "title": "Tobis Blog", - "tags": "blog", + "tags": ["blog"], "spout": "spouts\\rss\\feed", "params": { "url": "http://blog.aditu.de/feed" @@ -925,7 +925,10 @@ }, "tags": { "description": "all tags of the source of this article", - "type": "string" + "type": "array", + "items": { + "type": "string" + } } } }, @@ -942,7 +945,10 @@ }, "tags": { "description": "user given tags", - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "spout": { "description": "the spout type. You can also get all available spout types by using the json api", @@ -980,7 +986,10 @@ }, "tags": { "description": "tags for this source", - "type": "string" + "type": "array", + "items": { + "type": "string" + } }, "username": { "description": "Username in “deviantART - user”, “deviantART - favs of a user”, Tumblr, Reddit anf “Twitter - User timeline” spouts", diff --git a/public/js/selfoss-events-sources.js b/public/js/selfoss-events-sources.js index 68077de67d..a2c2a56ed6 100644 --- a/public/js/selfoss-events-sources.js +++ b/public/js/selfoss-events-sources.js @@ -56,6 +56,7 @@ selfoss.events.sources = function() { // get values and params var values = selfoss.getValues(parent); + values['tags'] = values['tags'].split(','); values['ajax'] = true; $.ajax({ diff --git a/templates/source.phtml b/templates/source.phtml index 260929565e..3606100ed3 100644 --- a/templates/source.phtml +++ b/templates/source.phtml @@ -47,7 +47,7 @@
  • - +