experimental new plugin system

This commit is contained in:
Andrew Dolgov 2012-12-23 14:52:18 +04:00
parent 83e6e313be
commit 19c7350770
29 changed files with 204 additions and 164 deletions

View File

@ -126,6 +126,10 @@
if ($handler->before($method)) { if ($handler->before($method)) {
if ($method && method_exists($handler, $method)) { if ($method && method_exists($handler, $method)) {
$handler->$method(); $handler->$method();
} else {
if (method_exists($handler, "catchall")) {
$handler->catchall($method);
}
} }
$handler->after(); $handler->after();
return; return;

View File

@ -1,11 +0,0 @@
<?php
class Button {
protected $link;
function __construct($link) {
$this->link = $link;
}
}
?>

View File

@ -1,31 +0,0 @@
<?php
class Button_Tweet extends Button {
function render($article_id) {
$rv = "<img src=\"".theme_image($this->link, 'images/art-tweet.png')."\"
class='tagsPic' style=\"cursor : pointer\"
onclick=\"tweetArticle($article_id)\"
title='".__('Share on Twitter')."'>";
return $rv;
}
function getTweetInfo() {
$id = db_escape_string($_REQUEST['id']);
$result = db_query($this->link, "SELECT title, link
FROM ttrss_entries, ttrss_user_entries
WHERE id = '$id' AND ref_id = id AND owner_uid = " .$_SESSION['uid']);
if (db_num_rows($result) != 0) {
$title = truncate_string(strip_tags(db_fetch_result($result, 0, 'title')),
100, '...');
$article_link = db_fetch_result($result, 0, 'link');
}
print json_encode(array("title" => $title, "link" => $article_link,
"id" => $id));
}
}
?>

View File

@ -249,7 +249,7 @@ class Feeds extends Handler_Protected {
$headlines_count = db_num_rows($result); $headlines_count = db_num_rows($result);
if (get_pref($this->link, 'COMBINED_DISPLAY_MODE')) { /* if (get_pref($this->link, 'COMBINED_DISPLAY_MODE')) {
$button_plugins = array(); $button_plugins = array();
foreach (explode(",", ARTICLE_BUTTON_PLUGINS) as $p) { foreach (explode(",", ARTICLE_BUTTON_PLUGINS) as $p) {
$pclass = "button_" . trim($p); $pclass = "button_" . trim($p);
@ -259,7 +259,9 @@ class Feeds extends Handler_Protected {
array_push($button_plugins, $plugin); array_push($button_plugins, $plugin);
} }
} }
} } */
global $pluginhost;
if (db_num_rows($result) > 0) { if (db_num_rows($result) > 0) {
@ -706,8 +708,8 @@ class Feeds extends Handler_Protected {
//$note_escaped = htmlspecialchars($line['note'], ENT_QUOTES); //$note_escaped = htmlspecialchars($line['note'], ENT_QUOTES);
foreach ($button_plugins as $p) { foreach ($pluginhost->get_hooks($pluginhost::HOOK_ARTICLE_BUTTON) as $p) {
$reply['content'] .= $p->render($id, $line); $reply['content'] .= $p->hook_article_button($line);
} }
$reply['content'] .= "<img src=\"images/digest_checkbox.png\" $reply['content'] .= "<img src=\"images/digest_checkbox.png\"

View File

@ -1,14 +0,0 @@
<?php
class Filter {
protected $link;
function __construct($link) {
$this->link = $link;
}
function filter_article($article) {
return $article;
}
}
?>

18
classes/pluginhandler.php Normal file
View File

@ -0,0 +1,18 @@
<?php
class PluginHandler extends Handler_Protected {
function csrf_ignore($method) {
return true;
}
function catchall($method) {
global $pluginhost;
$plugin = $pluginhost->get_plugin($_REQUEST["plugin"]);
if (method_exists($plugin, $method)) {
$plugin->$method();
}
}
}
?>

71
classes/pluginhost.php Normal file
View File

@ -0,0 +1,71 @@
<?php
class PluginHost {
private $link;
private $hooks = array();
private $plugins = array();
const HOOK_ARTICLE_BUTTON = 1;
const HOOK_ARTICLE_FILTER = 2;
function __construct($link) {
$this->link = $link;
}
private function register_plugin($name, $plugin) {
//array_push($this->plugins, $plugin);
$this->plugins[$name] = $plugin;
}
function get_link() {
return $this->link;
}
function get_plugins() {
return $this->plugins;
}
function get_plugin($name) {
return $this->plugins[$name];
}
function add_hook($type, $sender) {
if (!is_array($this->hooks[$type])) {
$this->hooks[$type] = array();
}
array_push($this->hooks[$type], $sender);
}
function del_hook($type, $sender) {
if (is_array($this->hooks[$type])) {
$key = array_Search($this->hooks[$type], $sender);
if ($key !== FALSE) {
unset($this->hooks[$type][$key]);
}
}
}
function get_hooks($type) {
return $this->hooks[$type];
}
function load($classlist) {
$plugins = explode(",", $classlist);
foreach ($plugins as $class) {
$class = trim($class);
$class_file = str_replace("_", "/", strtolower(basename($class)));
$file = dirname(__FILE__)."/../plugins/$class_file/$class_file.php";
if (file_exists($file)) require_once $file;
if (class_exists($class)) {
$plugin = new $class($this);
$this->register_plugin($class, $plugin);
}
}
}
}
?>

View File

@ -706,7 +706,7 @@ class RPC extends Handler_Protected {
print json_encode(array("status" => $status)); print json_encode(array("status" => $status));
} }
function buttonPlugin() { /* function buttonPlugin() {
$pclass = "button_" . basename($_REQUEST['plugin']); $pclass = "button_" . basename($_REQUEST['plugin']);
$method = $_REQUEST['plugin_method']; $method = $_REQUEST['plugin_method'];
@ -716,7 +716,7 @@ class RPC extends Handler_Protected {
return $plugin->$method(); return $plugin->$method();
} }
} }
} } */
function genHash() { function genHash() {
$hash = sha1(uniqid(rand(), true)); $hash = sha1(uniqid(rand(), true));

View File

@ -176,16 +176,13 @@
// if you experience weird errors and tt-rss failing to start, blank pages // if you experience weird errors and tt-rss failing to start, blank pages
// after login, or content encoding errors, disable it. // after login, or content encoding errors, disable it.
define('PLUGINS', '');
// Plugins to load. Check plugins/ directory for additional information.
define('FEEDBACK_URL', ''); define('FEEDBACK_URL', '');
// Displays an URL for users to provide feedback or comments regarding // Displays an URL for users to provide feedback or comments regarding
// this instance of tt-rss. Can lead to a forum, contact email, etc. // this instance of tt-rss. Can lead to a forum, contact email, etc.
define('ARTICLE_BUTTON_PLUGINS', 'note,tweet,share,mail');
// Comma-separated list of additional article action button plugins
// to enable, like tweet button, etc.
// The following plugins are available: note, tweet, share, mail
// More plugins: http://tt-rss.org/wiki/Plugins
define('CONFIG_VERSION', 26); define('CONFIG_VERSION', 26);
// Expected config version. Please update this option in config.php // Expected config version. Please update this option in config.php
// if necessary (after migrating all new options from this file). // if necessary (after migrating all new options from this file).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

View File

@ -7,13 +7,6 @@
function __autoload($class) { function __autoload($class) {
$class_file = str_replace("_", "/", strtolower(basename($class))); $class_file = str_replace("_", "/", strtolower(basename($class)));
$file = dirname(__FILE__)."/../plugins/$class_file.php";
if (file_exists($file)) {
require $file;
return;
}
$file = dirname(__FILE__)."/../classes/$class_file.php"; $file = dirname(__FILE__)."/../classes/$class_file.php";
if (file_exists($file)) { if (file_exists($file)) {
@ -3265,15 +3258,10 @@
onclick=\"postOpenInNewTab(event, $id)\" onclick=\"postOpenInNewTab(event, $id)\"
alt='Zoom' title='".__('Open article in new tab')."'>"; alt='Zoom' title='".__('Open article in new tab')."'>";
$button_plugins = explode(",", ARTICLE_BUTTON_PLUGINS); global $pluginhost;
foreach ($button_plugins as $p) { foreach ($pluginhost->get_hooks($pluginhost::HOOK_ARTICLE_BUTTON) as $p) {
$pclass = "button_" . trim($p); $rv['content'] .= $p->hook_article_button($line);
if (class_exists($pclass)) {
$plugin = new $pclass($link);
$rv['content'] .= $plugin->render($id, $line);
}
} }
$rv['content'] .= "<img src=\"".theme_image($link, 'images/digest_checkbox.png')."\" $rv['content'] .= "<img src=\"".theme_image($link, 'images/digest_checkbox.png')."\"
@ -3568,6 +3556,12 @@
db_query($link, "SET NAMES " . MYSQL_CHARSET); db_query($link, "SET NAMES " . MYSQL_CHARSET);
} }
} }
global $pluginhost;
$pluginhost = new PluginHost($link);
$pluginhost->load(PLUGINS);
return true; return true;
} else { } else {
print "Unable to connect to database:" . db_last_error(); print "Unable to connect to database:" . db_last_error();

View File

@ -399,23 +399,6 @@
_debug("update_rss_feed: " . count($filters) . " filters loaded."); _debug("update_rss_feed: " . count($filters) . " filters loaded.");
} }
$filter_plugins = array();
if (defined('_ARTICLE_FILTER_PLUGINS')) {
foreach (explode(",", _ARTICLE_FILTER_PLUGINS) as $p) {
$pclass = "filter_" . trim($p);
if (class_exists($pclass)) {
$plugin = new $pclass($link);
array_push($filter_plugins, $plugin);
}
}
}
if ($debug_enabled) {
_debug("update_rss_feed: " . count($filter_plugins) . " filter plugins loaded.");
}
if ($use_simplepie) { if ($use_simplepie) {
$iterator = $rss->get_items(); $iterator = $rss->get_items();
} else { } else {
@ -782,7 +765,9 @@
} }
// TODO: less memory-hungry implementation // TODO: less memory-hungry implementation
if (count($filter_plugins) > 0) { global $pluginhost;
foreach ($pluginhost->get_hooks($pluginhost::HOOK_ARTICLE_FILTER) as $p) {
if ($debug_enabled) { if ($debug_enabled) {
_debug("update_rss_feed: applying plugin filters..."); _debug("update_rss_feed: applying plugin filters...");
} }
@ -795,7 +780,7 @@
"author" => $entry_author); "author" => $entry_author);
foreach ($filter_plugins as $plugin) { foreach ($filter_plugins as $plugin) {
$article = $plugin->filter_article($article); $article = $plugin->hook_article_filter($article);
} }
$entry_title = $article["title"]; $entry_title = $article["title"];

View File

@ -1,3 +1,3 @@
<?php # This file has been generated at: Fri Sep 7 10:20:51 MSK 2012 <?php # This file has been generated at: Sun Dec 23 13:56:09 MSK 2012
define('GENERATED_CONFIG_CHECK', 26); define('GENERATED_CONFIG_CHECK', 26);
$requred_defines = array( 'DB_TYPE', 'DB_HOST', 'DB_USER', 'DB_NAME', 'DB_PASS', 'MYSQL_CHARSET', 'SELF_URL_PATH', 'SINGLE_USER_MODE', 'PHP_EXECUTABLE', 'LOCK_DIRECTORY', 'CACHE_DIR', 'ICONS_DIR', 'ICONS_URL', 'AUTH_MODULES', 'AUTH_AUTO_CREATE', 'AUTH_AUTO_LOGIN', 'DEFAULT_UPDATE_METHOD', 'FORCE_ARTICLE_PURGE', 'PUBSUBHUBBUB_HUB', 'PUBSUBHUBBUB_ENABLED', 'SPHINX_ENABLED', 'SPHINX_INDEX', 'ENABLE_REGISTRATION', 'REG_NOTIFY_ADDRESS', 'REG_MAX_USERS', 'SESSION_COOKIE_LIFETIME', 'SESSION_EXPIRE_TIME', 'SESSION_CHECK_ADDRESS', 'SMTP_FROM_NAME', 'SMTP_FROM_ADDRESS', 'DIGEST_SUBJECT', 'SMTP_HOST', 'SMTP_LOGIN', 'SMTP_PASSWORD', 'CHECK_FOR_NEW_VERSION', 'ENABLE_GZIP_OUTPUT', 'FEEDBACK_URL', 'ARTICLE_BUTTON_PLUGINS', 'CONFIG_VERSION'); ?> $requred_defines = array( 'DB_TYPE', 'DB_HOST', 'DB_USER', 'DB_NAME', 'DB_PASS', 'MYSQL_CHARSET', 'SELF_URL_PATH', 'SINGLE_USER_MODE', 'PHP_EXECUTABLE', 'LOCK_DIRECTORY', 'CACHE_DIR', 'ICONS_DIR', 'ICONS_URL', 'AUTH_MODULES', 'AUTH_AUTO_CREATE', 'AUTH_AUTO_LOGIN', 'DEFAULT_UPDATE_METHOD', 'FORCE_ARTICLE_PURGE', 'PUBSUBHUBBUB_HUB', 'PUBSUBHUBBUB_ENABLED', 'SPHINX_ENABLED', 'SPHINX_INDEX', 'ENABLE_REGISTRATION', 'REG_NOTIFY_ADDRESS', 'REG_MAX_USERS', 'SESSION_COOKIE_LIFETIME', 'SESSION_EXPIRE_TIME', 'SESSION_CHECK_ADDRESS', 'SMTP_FROM_NAME', 'SMTP_FROM_ADDRESS', 'DIGEST_SUBJECT', 'SMTP_HOST', 'SMTP_LOGIN', 'SMTP_PASSWORD', 'CHECK_FOR_NEW_VERSION', 'ENABLE_GZIP_OUTPUT', 'FEEDBACK_URL', 'CONFIG_VERSION'); ?>

View File

@ -71,10 +71,11 @@
<?php <?php
require 'lib/jsmin.php'; require 'lib/jsmin.php';
foreach (explode(",", ARTICLE_BUTTON_PLUGINS) as $p) { global $pluginhost;
$jsf = "js/".trim($p)."_button.js";
if (file_exists($jsf)) { foreach ($pluginhost->get_plugins() as $n => $p) {
echo JSMin::minify(file_get_contents($jsf)); if (method_exists($p, "get_js")) {
echo JSMin::minify($p->get_js());
} }
} }

View File

@ -1,31 +0,0 @@
function tweetArticle(id) {
try {
var query = "?op=rpc&method=buttonPlugin&plugin=tweet&plugin_method=getTweetInfo&id=" + param_escape(id);
console.log(query);
var d = new Date();
var ts = d.getTime();
var w = window.open('backend.php?op=backend&method=loading', 'ttrss_tweet',
"status=0,toolbar=0,location=0,width=500,height=400,scrollbars=1,menubar=0");
new Ajax.Request("backend.php", {
parameters: query,
onComplete: function(transport) {
var ti = JSON.parse(transport.responseText);
var share_url = "http://twitter.com/share?_=" + ts +
"&text=" + param_escape(ti.title) +
"&url=" + param_escape(ti.link);
w.location.href = share_url;
} });
} catch (e) {
exception_error("tweetArticle", e);
}
}

1
plugins/mail/README.txt Normal file
View File

@ -0,0 +1 @@
Shares article by email

View File

@ -14,7 +14,7 @@ function emailArticle(id) {
if (dijit.byId("emailArticleDlg")) if (dijit.byId("emailArticleDlg"))
dijit.byId("emailArticleDlg").destroyRecursive(); dijit.byId("emailArticleDlg").destroyRecursive();
var query = "backend.php?op=rpc&method=buttonPlugin&plugin=mail&plugin_method=emailArticle&param=" + param_escape(id); var query = "backend.php?op=pluginhandler&plugin=mail&method=emailArticle&param=" + param_escape(id);
dialog = new dijit.Dialog({ dialog = new dijit.Dialog({
id: "emailArticleDlg", id: "emailArticleDlg",
@ -47,7 +47,7 @@ function emailArticle(id) {
dojo.disconnect(tmph); dojo.disconnect(tmph);
new Ajax.Autocompleter('emailArticleDlg_destination', 'emailArticleDlg_dst_choices', new Ajax.Autocompleter('emailArticleDlg_destination', 'emailArticleDlg_dst_choices',
"backend.php?op=rpc&method=buttonPlugin&plugin=mail&plugin_method=completeEmails", "backend.php?op=pluginhandler&plugin=mail&method=completeEmails",
{ tokens: '', paramName: "search" }); { tokens: '', paramName: "search" });
}); });

View File

@ -1,9 +1,24 @@
<?php <?php
class Button_Mail extends Button { class Mail {
function render($article_id) {
return "<img src=\"".theme_image($link, 'images/art-email.png')."\" private $link;
private $host;
function __construct($host) {
$this->link = $host->get_link();
$this->host = $host;
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
}
function get_js() {
return file_get_contents(dirname(__FILE__) . "/mail.js");
}
function hook_article_button($line) {
return "<img src=\"".theme_image($link, 'plugins/mail/mail.png')."\"
class='tagsPic' style=\"cursor : pointer\" class='tagsPic' style=\"cursor : pointer\"
onclick=\"emailArticle($article_id)\" onclick=\"emailArticle(".$line["id"].")\"
alt='Zoom' title='".__('Forward by email')."'>"; alt='Zoom' title='".__('Forward by email')."'>";
} }
@ -16,10 +31,9 @@ class Button_Mail extends Button {
$_SESSION['email_secretkey'] = $secretkey; $_SESSION['email_secretkey'] = $secretkey;
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"secretkey\" value=\"$secretkey\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"secretkey\" value=\"$secretkey\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"rpc\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"pluginhandler\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"buttonPlugin\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin\" value=\"mail\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin\" value=\"mail\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin_method\" value=\"sendEmail\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"sendEmail\">";
$result = db_query($this->link, "SELECT email, full_name FROM ttrss_users WHERE $result = db_query($this->link, "SELECT email, full_name FROM ttrss_users WHERE
id = " . $_SESSION["uid"]); id = " . $_SESSION["uid"]);

View File

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 192 B

1
plugins/note/README.txt Normal file
View File

@ -0,0 +1 @@
Support for article notes

View File

@ -1,7 +1,7 @@
function editArticleNote(id) { function editArticleNote(id) {
try { try {
var query = "backend.php?op=rpc&method=buttonPlugin&plugin=note&plugin_method=edit&param=" + param_escape(id); var query = "backend.php?op=pluginhandler&plugin=note&method=edit&param=" + param_escape(id);
if (dijit.byId("editNoteDlg")) if (dijit.byId("editNoteDlg"))
dijit.byId("editNoteDlg").destroyRecursive(); dijit.byId("editNoteDlg").destroyRecursive();

View File

@ -1,9 +1,24 @@
<?php <?php
class Button_Note extends Button { class Note {
function render($article_id) { private $link;
return "<img src=\"".theme_image($this->link, "images/art-pub-note.png")."\" private $host;
function __construct($host) {
$this->link = $host->get_link();
$this->host = $host;
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
}
function get_js() {
return file_get_contents(dirname(__FILE__) . "/note.js");
}
function hook_article_button($line) {
return "<img src=\"".theme_image($this->link, "plugins/note/note.png")."\"
style=\"cursor : pointer\" style=\"cursor : pointer\" style=\"cursor : pointer\" style=\"cursor : pointer\"
onclick=\"editArticleNote($article_id)\" onclick=\"editArticleNote(".$line["id"].")\"
class='tagsPic' title='".__('Edit article note')."'>"; class='tagsPic' title='".__('Edit article note')."'>";
} }
@ -16,10 +31,9 @@ class Button_Note extends Button {
$note = db_fetch_result($result, 0, "note"); $note = db_fetch_result($result, 0, "note");
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"id\" value=\"$param\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"id\" value=\"$param\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"rpc\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"op\" value=\"pluginhandler\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"buttonPlugin\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"method\" value=\"setNote\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin\" value=\"note\">"; print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin\" value=\"note\">";
print "<input dojoType=\"dijit.form.TextBox\" style=\"display : none\" name=\"plugin_method\" value=\"setNote\">";
print "<table width='100%'><tr><td>"; print "<table width='100%'><tr><td>";
print "<textarea dojoType=\"dijit.form.SimpleTextarea\" print "<textarea dojoType=\"dijit.form.SimpleTextarea\"
@ -50,6 +64,5 @@ class Button_Note extends Button {
"raw_length" => mb_strlen($note))); "raw_length" => mb_strlen($note)));
} }
} }
?> ?>

View File

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 159 B

View File

@ -0,0 +1 @@
Inline image links in Reddit RSS

View File

@ -1,7 +1,17 @@
<?php <?php
class Filter_RedditImgur { class RedditImgur {
function filter_article($article) { private $link;
private $host;
function __construct($host) {
$this->link = $host->get_link();
$this->host = $host;
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
}
function hook_article_filter($article) {
if (strpos($article["link"], "reddit.com/r/") !== FALSE) { if (strpos($article["link"], "reddit.com/r/") !== FALSE) {
if (strpos($article["content"], "i.imgur.com") !== FALSE) { if (strpos($article["content"], "i.imgur.com") !== FALSE) {

1
plugins/share/README.txt Normal file
View File

@ -0,0 +1 @@
Support for sharing articles by URL

View File

@ -3,7 +3,7 @@ function shareArticle(id) {
if (dijit.byId("shareArticleDlg")) if (dijit.byId("shareArticleDlg"))
dijit.byId("shareArticleDlg").destroyRecursive(); dijit.byId("shareArticleDlg").destroyRecursive();
var query = "backend.php?op=rpc&method=buttonPlugin&plugin=share&plugin_method=shareArticle&param=" + param_escape(id); var query = "backend.php?op=pluginhandler&plugin=share&method=shareArticle&param=" + param_escape(id);
dialog = new dijit.Dialog({ dialog = new dijit.Dialog({
id: "shareArticleDlg", id: "shareArticleDlg",

View File

@ -1,6 +1,20 @@
<?php <?php
class Button_Share extends Button { class Share {
function render($article_id, $line) { private $link;
private $host;
function __construct($host) {
$this->link = $host->get_link();
$this->host = $host;
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
}
function get_js() {
return file_get_contents(dirname(__FILE__) . "/share.js");
}
function hook_article_button($line) {
return "<img src=\"".theme_image($this->link, 'images/art-share.png')."\" return "<img src=\"".theme_image($this->link, 'images/art-share.png')."\"
class='tagsPic' style=\"cursor : pointer\" class='tagsPic' style=\"cursor : pointer\"
onclick=\"shareArticle(".$line['int_id'].")\" onclick=\"shareArticle(".$line['int_id'].")\"

View File

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B