summaryrefslogtreecommitdiff
path: root/lib/plugins/extension
diff options
context:
space:
mode:
authorChristopher Smith <chris@jalakai.co.uk>2014-03-12 18:39:11 +0000
committerChristopher Smith <chris@jalakai.co.uk>2014-03-12 18:39:11 +0000
commit4e60057c8ccbee18b94a64208311f9bbb338eec6 (patch)
tree08d79159aa78693c27f54ecebc3105034dfc5933 /lib/plugins/extension
parent57a6f99d09d3662a8a2ad72e312aa6f53bcc2d01 (diff)
parent069942acdaa5ba825bc3f92c7093b5071789f1ca (diff)
downloadrpg-4e60057c8ccbee18b94a64208311f9bbb338eec6.tar.gz
rpg-4e60057c8ccbee18b94a64208311f9bbb338eec6.tar.bz2
Merge branch 'master' into tablethead
Diffstat (limited to 'lib/plugins/extension')
-rw-r--r--lib/plugins/extension/_test/extension.test.php293
-rw-r--r--lib/plugins/extension/_test/testdata/either1/script.js0
-rw-r--r--lib/plugins/extension/_test/testdata/eithersub2/either2/script.js0
-rw-r--r--lib/plugins/extension/_test/testdata/plgfoo5/plugin.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/plgsub3/plugin3/syntax.php0
-rw-r--r--lib/plugins/extension/_test/testdata/plgsub4/plugin4/plugin.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/plgsub6/plgfoo6/plugin.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/plugin1/syntax.php0
-rw-r--r--lib/plugins/extension/_test/testdata/plugin2/plugin.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/template1/main.php0
-rw-r--r--lib/plugins/extension/_test/testdata/template1/style.ini0
-rw-r--r--lib/plugins/extension/_test/testdata/template2/template.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/tplfoo5/template.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/tplsub3/template3/main.php0
-rw-r--r--lib/plugins/extension/_test/testdata/tplsub3/template3/style.ini0
-rw-r--r--lib/plugins/extension/_test/testdata/tplsub4/template4/template.info.txt7
-rw-r--r--lib/plugins/extension/_test/testdata/tplsub6/tplfoo6/template.info.txt7
-rw-r--r--lib/plugins/extension/action.php85
-rw-r--r--lib/plugins/extension/admin.php155
-rw-r--r--lib/plugins/extension/all.less37
-rw-r--r--lib/plugins/extension/helper/extension.php1093
-rw-r--r--lib/plugins/extension/helper/gui.php193
-rw-r--r--lib/plugins/extension/helper/list.php568
-rw-r--r--lib/plugins/extension/helper/repository.php191
-rw-r--r--lib/plugins/extension/images/disabled.pngbin0 -> 1486 bytes
-rw-r--r--lib/plugins/extension/images/donate.pngbin0 -> 724 bytes
-rw-r--r--lib/plugins/extension/images/down.pngbin0 -> 280 bytes
-rw-r--r--lib/plugins/extension/images/enabled.pngbin0 -> 1231 bytes
-rw-r--r--lib/plugins/extension/images/icons.xcfbin0 -> 67195 bytes
-rw-r--r--lib/plugins/extension/images/license.txt4
-rw-r--r--lib/plugins/extension/images/overlay.pngbin0 -> 109 bytes
-rw-r--r--lib/plugins/extension/images/plugin.pngbin0 -> 6824 bytes
-rw-r--r--lib/plugins/extension/images/tag.pngbin0 -> 753 bytes
-rw-r--r--lib/plugins/extension/images/template.pngbin0 -> 7547 bytes
-rw-r--r--lib/plugins/extension/images/up.pngbin0 -> 281 bytes
-rw-r--r--lib/plugins/extension/images/warning.pngbin0 -> 613 bytes
-rw-r--r--lib/plugins/extension/lang/de/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/de/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/de/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/de/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/de/lang.php83
-rw-r--r--lib/plugins/extension/lang/en/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/en/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/en/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/en/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/en/lang.php99
-rw-r--r--lib/plugins/extension/lang/eo/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/eo/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/eo/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/eo/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/eo/lang.php87
-rw-r--r--lib/plugins/extension/lang/fr/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/fr/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/fr/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/fr/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/fr/lang.php87
-rw-r--r--lib/plugins/extension/lang/ja/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/ja/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/ja/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/ja/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/ja/lang.php54
-rw-r--r--lib/plugins/extension/lang/nl/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/nl/intro_plugins.txt1
-rw-r--r--lib/plugins/extension/lang/nl/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/nl/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/nl/lang.php87
-rw-r--r--lib/plugins/extension/lang/ru/lang.php41
-rw-r--r--lib/plugins/extension/lang/sk/lang.php58
-rw-r--r--lib/plugins/extension/lang/zh/intro_install.txt1
-rw-r--r--lib/plugins/extension/lang/zh/intro_search.txt1
-rw-r--r--lib/plugins/extension/lang/zh/intro_templates.txt1
-rw-r--r--lib/plugins/extension/lang/zh/lang.php50
-rw-r--r--lib/plugins/extension/plugin.info.txt7
-rw-r--r--lib/plugins/extension/script.js113
-rw-r--r--lib/plugins/extension/style.less363
75 files changed, 3831 insertions, 0 deletions
diff --git a/lib/plugins/extension/_test/extension.test.php b/lib/plugins/extension/_test/extension.test.php
new file mode 100644
index 000000000..453b95e79
--- /dev/null
+++ b/lib/plugins/extension/_test/extension.test.php
@@ -0,0 +1,293 @@
+<?php
+
+/**
+ * Class mock_helper_plugin_extension_extension
+ *
+ * makes protected methods accessible
+ */
+class mock_helper_plugin_extension_extension extends helper_plugin_extension_extension {
+ public function find_folders(&$result, $base, $default_type = 'plugin', $dir = '') {
+ return parent::find_folders($result, $base, $default_type, $dir);
+ }
+
+}
+
+/**
+ * @group plugin_extension
+ * @group plugins
+ */
+class helper_plugin_extension_extension_test extends DokuWikiTest {
+
+ protected $pluginsEnabled = array('extension');
+
+ /**
+ * FIXME should we test this without internet first?
+ *
+ * @group internet
+ */
+ public function testExtensionParameters() {
+ $extension = new helper_plugin_extension_extension();
+
+ $extension->setExtension('extension');
+ $this->assertEquals('extension', $extension->getID());
+ $this->assertEquals('extension', $extension->getBase());
+ $this->assertEquals('Extension Manager', $extension->getDisplayName());
+ $this->assertEquals('Michael Hamann', $extension->getAuthor());
+ $this->assertEquals('michael@content-space.de', $extension->getEmail());
+ $this->assertEquals(md5('michael@content-space.de'), $extension->getEmailID());
+ $this->assertEquals('https://www.dokuwiki.org/plugin:extension', $extension->getURL());
+ $this->assertEquals('Allows managing and installing plugins and templates', $extension->getDescription());
+ $this->assertFalse($extension->isTemplate());
+ $this->assertTrue($extension->isEnabled());
+ $this->assertTrue($extension->isInstalled());
+ $this->assertTrue($extension->isBundled());
+
+ $extension->setExtension('testing');
+ $this->assertEquals('testing', $extension->getID());
+ $this->assertEquals('testing', $extension->getBase());
+ $this->assertEquals('Testing Plugin', $extension->getDisplayName());
+ $this->assertEquals('Tobias Sarnowski', $extension->getAuthor());
+ $this->assertEquals('tobias@trustedco.de', $extension->getEmail());
+ $this->assertEquals(md5('tobias@trustedco.de'), $extension->getEmailID());
+ $this->assertEquals('http://www.dokuwiki.org/plugin:testing', $extension->getURL());
+ $this->assertEquals('Used to test the test framework. Should always be disabled.', $extension->getDescription());
+ $this->assertFalse($extension->isTemplate());
+ $this->assertFalse($extension->isEnabled());
+ $this->assertTrue($extension->isInstalled());
+ $this->assertTrue($extension->isBundled());
+
+ $extension->setExtension('template:dokuwiki');
+ $this->assertEquals('template:dokuwiki', $extension->getID());
+ $this->assertEquals('dokuwiki', $extension->getBase());
+ $this->assertEquals('DokuWiki Template', $extension->getDisplayName());
+ $this->assertEquals('Anika Henke', $extension->getAuthor());
+ $this->assertEquals('anika@selfthinker.org', $extension->getEmail());
+ $this->assertEquals(md5('anika@selfthinker.org'), $extension->getEmailID());
+ $this->assertEquals('http://www.dokuwiki.org/template:dokuwiki', $extension->getURL());
+ $this->assertEquals('DokuWiki\'s default template since 2012', $extension->getDescription());
+ $this->assertTrue($extension->isTemplate());
+ $this->assertTrue($extension->isEnabled());
+ $this->assertTrue($extension->isInstalled());
+ $this->assertTrue($extension->isBundled());
+ }
+
+ public function testFindFoldersPlugins() {
+ $extension = new mock_helper_plugin_extension_extension();
+ $tdir = dirname(__FILE__).'/testdata';
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plugin1", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('plugin1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plugin2", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('plugin', $result['new'][0]['type']);
+ $this->assertEquals('plugin2', $result['new'][0]['base']);
+ $this->assertEquals('plugin2', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plgsub3", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('plgsub3/plugin3', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plgsub4", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('plugin', $result['new'][0]['type']);
+ $this->assertEquals('plugin4', $result['new'][0]['base']);
+ $this->assertEquals('plgsub4/plugin4', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plgfoo5", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('plugin', $result['new'][0]['type']);
+ $this->assertEquals('plugin5', $result['new'][0]['base']);
+ $this->assertEquals('plgfoo5', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/plgsub6/plgfoo6", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('plugin', $result['new'][0]['type']);
+ $this->assertEquals('plugin6', $result['new'][0]['base']);
+ $this->assertEquals('plgsub6/plgfoo6', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/either1", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('either1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/eithersub2/either2", 'plugin');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('eithersub2/either2', $this->extdir($result['old'][0]['tmp']));
+ }
+
+ public function testFindFoldersTemplates() {
+ $extension = new mock_helper_plugin_extension_extension();
+ $tdir = dirname(__FILE__).'/testdata';
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/template1", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('template1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/template2", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template2', $result['new'][0]['base']);
+ $this->assertEquals('template2', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub3", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('tplsub3/template3', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub4", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template4', $result['new'][0]['base']);
+ $this->assertEquals('tplsub4/template4', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplfoo5", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template5', $result['new'][0]['base']);
+ $this->assertEquals('tplfoo5', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub6/tplfoo6", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template6', $result['new'][0]['base']);
+ $this->assertEquals('tplsub6/tplfoo6', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/either1", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('either1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/eithersub2/either2", 'template');
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('eithersub2/either2', $this->extdir($result['old'][0]['tmp']));
+ }
+
+ public function testFindFoldersTemplatesAutodetect() {
+ $extension = new mock_helper_plugin_extension_extension();
+ $tdir = dirname(__FILE__).'/testdata';
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/template1");
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('template1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/template2");
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template2', $result['new'][0]['base']);
+ $this->assertEquals('template2', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub3");
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('template', $result['old'][0]['type']);
+ $this->assertEquals('tplsub3/template3', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub4");
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template4', $result['new'][0]['base']);
+ $this->assertEquals('tplsub4/template4', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplfoo5");
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template5', $result['new'][0]['base']);
+ $this->assertEquals('tplfoo5', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/tplsub6/tplfoo6");
+ $this->assertTrue($ok);
+ $this->assertEquals(1, count($result['new']));
+ $this->assertEquals('template', $result['new'][0]['type']);
+ $this->assertEquals('template6', $result['new'][0]['base']);
+ $this->assertEquals('tplsub6/tplfoo6', $this->extdir($result['new'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/either1");
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('either1', $this->extdir($result['old'][0]['tmp']));
+
+ $result = array('old' => array(), 'new' => array());
+ $ok = $extension->find_folders($result, "$tdir/eithersub2/either2");
+ $this->assertTrue($ok);
+ $this->assertEquals(0, count($result['new']));
+ $this->assertEquals(1, count($result['old']));
+ $this->assertEquals('plugin', $result['old'][0]['type']);
+ $this->assertEquals('eithersub2/either2', $this->extdir($result['old'][0]['tmp']));
+ }
+
+ /**
+ * remove the test data directory from a dir name for cross install comparison
+ *
+ * @param string $dir
+ * @return string
+ */
+ protected function extdir($dir) {
+ $tdir = dirname(__FILE__).'/testdata';
+ $len = strlen($tdir);
+ $dir = trim(substr($dir, $len), '/');
+ return $dir;
+ }
+} \ No newline at end of file
diff --git a/lib/plugins/extension/_test/testdata/either1/script.js b/lib/plugins/extension/_test/testdata/either1/script.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/either1/script.js
diff --git a/lib/plugins/extension/_test/testdata/eithersub2/either2/script.js b/lib/plugins/extension/_test/testdata/eithersub2/either2/script.js
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/eithersub2/either2/script.js
diff --git a/lib/plugins/extension/_test/testdata/plgfoo5/plugin.info.txt b/lib/plugins/extension/_test/testdata/plgfoo5/plugin.info.txt
new file mode 100644
index 000000000..cc4532d29
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plgfoo5/plugin.info.txt
@@ -0,0 +1,7 @@
+base plugin5
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Plugin
+desc Dummy plugin data
+url http://example.com/plugin:plugin5
diff --git a/lib/plugins/extension/_test/testdata/plgsub3/plugin3/syntax.php b/lib/plugins/extension/_test/testdata/plgsub3/plugin3/syntax.php
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plgsub3/plugin3/syntax.php
diff --git a/lib/plugins/extension/_test/testdata/plgsub4/plugin4/plugin.info.txt b/lib/plugins/extension/_test/testdata/plgsub4/plugin4/plugin.info.txt
new file mode 100644
index 000000000..374b6bf24
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plgsub4/plugin4/plugin.info.txt
@@ -0,0 +1,7 @@
+base plugin4
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Plugin
+desc Dummy plugin data
+url http://example.com/plugin:plugin4
diff --git a/lib/plugins/extension/_test/testdata/plgsub6/plgfoo6/plugin.info.txt b/lib/plugins/extension/_test/testdata/plgsub6/plgfoo6/plugin.info.txt
new file mode 100644
index 000000000..461ff8735
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plgsub6/plgfoo6/plugin.info.txt
@@ -0,0 +1,7 @@
+base plugin6
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Plugin
+desc Dummy plugin data
+url http://example.com/plugin:plugin6
diff --git a/lib/plugins/extension/_test/testdata/plugin1/syntax.php b/lib/plugins/extension/_test/testdata/plugin1/syntax.php
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plugin1/syntax.php
diff --git a/lib/plugins/extension/_test/testdata/plugin2/plugin.info.txt b/lib/plugins/extension/_test/testdata/plugin2/plugin.info.txt
new file mode 100644
index 000000000..d56758fe9
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/plugin2/plugin.info.txt
@@ -0,0 +1,7 @@
+base plugin2
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Plugin
+desc Dummy Plugin data
+url http://example.com/plugin:plugin2
diff --git a/lib/plugins/extension/_test/testdata/template1/main.php b/lib/plugins/extension/_test/testdata/template1/main.php
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/template1/main.php
diff --git a/lib/plugins/extension/_test/testdata/template1/style.ini b/lib/plugins/extension/_test/testdata/template1/style.ini
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/template1/style.ini
diff --git a/lib/plugins/extension/_test/testdata/template2/template.info.txt b/lib/plugins/extension/_test/testdata/template2/template.info.txt
new file mode 100644
index 000000000..882a7b914
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/template2/template.info.txt
@@ -0,0 +1,7 @@
+base template2
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Template
+desc Dummy template data
+url http://example.com/template:template2
diff --git a/lib/plugins/extension/_test/testdata/tplfoo5/template.info.txt b/lib/plugins/extension/_test/testdata/tplfoo5/template.info.txt
new file mode 100644
index 000000000..4d7ecb8ef
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/tplfoo5/template.info.txt
@@ -0,0 +1,7 @@
+base template5
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Template
+desc Dummy template data
+url http://example.com/template:template5
diff --git a/lib/plugins/extension/_test/testdata/tplsub3/template3/main.php b/lib/plugins/extension/_test/testdata/tplsub3/template3/main.php
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/tplsub3/template3/main.php
diff --git a/lib/plugins/extension/_test/testdata/tplsub3/template3/style.ini b/lib/plugins/extension/_test/testdata/tplsub3/template3/style.ini
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/tplsub3/template3/style.ini
diff --git a/lib/plugins/extension/_test/testdata/tplsub4/template4/template.info.txt b/lib/plugins/extension/_test/testdata/tplsub4/template4/template.info.txt
new file mode 100644
index 000000000..f050555e5
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/tplsub4/template4/template.info.txt
@@ -0,0 +1,7 @@
+base template4
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Template
+desc Dummy template data
+url http://example.com/template:template4
diff --git a/lib/plugins/extension/_test/testdata/tplsub6/tplfoo6/template.info.txt b/lib/plugins/extension/_test/testdata/tplsub6/tplfoo6/template.info.txt
new file mode 100644
index 000000000..ea4dc230d
--- /dev/null
+++ b/lib/plugins/extension/_test/testdata/tplsub6/tplfoo6/template.info.txt
@@ -0,0 +1,7 @@
+base template6
+author Andreas Gohr
+email andi@splitbrain.org
+date 2013-05-02
+name Dummy Template
+desc Dummy template data
+url http://example.com/template:template6
diff --git a/lib/plugins/extension/action.php b/lib/plugins/extension/action.php
new file mode 100644
index 000000000..9e48f134b
--- /dev/null
+++ b/lib/plugins/extension/action.php
@@ -0,0 +1,85 @@
+<?php
+/** DokuWiki Plugin extension (Action Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+class action_plugin_extension extends DokuWiki_Action_Plugin {
+
+ /**
+ * Registers a callback function for a given event
+ *
+ * @param Doku_Event_Handler $controller DokuWiki's event controller object
+ * @return void
+ */
+ public function register(Doku_Event_Handler $controller) {
+
+ $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'info');
+
+ }
+
+ /**
+ * Create the detail info for a single plugin
+ *
+ * @param Doku_Event $event
+ * @param $param
+ */
+ public function info(Doku_Event &$event, $param) {
+ global $USERINFO;
+ global $INPUT;
+
+ if($event->data != 'plugin_extension') return;
+ $event->preventDefault();
+ $event->stopPropagation();
+
+ if(empty($_SERVER['REMOTE_USER']) || !auth_isadmin($_SERVER['REMOTE_USER'], $USERINFO['grps'])) {
+ http_status(403);
+ echo 'Forbidden';
+ exit;
+ }
+
+ $ext = $INPUT->str('ext');
+ if(!$ext) {
+ http_status(400);
+ echo 'no extension given';
+ return;
+ }
+
+ /** @var helper_plugin_extension_extension $extension */
+ $extension = plugin_load('helper', 'extension_extension');
+ $extension->setExtension($ext);
+
+ $act = $INPUT->str('act');
+ switch($act) {
+ case 'enable':
+ case 'disable':
+ $json = new JSON();
+ $extension->$act(); //enables/disables
+
+ $reverse = ($act == 'disable') ? 'enable' : 'disable';
+
+ $return = array(
+ 'state' => $act.'d', // isn't English wonderful? :-)
+ 'reverse' => $reverse,
+ 'label' => $extension->getLang('btn_'.$reverse)
+ );
+
+ header('Content-Type: application/json');
+ echo $json->encode($return);
+ break;
+
+ case 'info':
+ default:
+ /** @var helper_plugin_extension_list $list */
+ $list = plugin_load('helper', 'extension_list');
+ header('Content-Type: text/html; charset=utf-8');
+ echo $list->make_info($extension);
+ }
+ }
+
+}
+
diff --git a/lib/plugins/extension/admin.php b/lib/plugins/extension/admin.php
new file mode 100644
index 000000000..99c74848b
--- /dev/null
+++ b/lib/plugins/extension/admin.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Admin Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Admin part of the extension manager
+ */
+class admin_plugin_extension extends DokuWiki_Admin_Plugin {
+ protected $infoFor = null;
+ /** @var helper_plugin_extension_gui */
+ protected $gui;
+
+ /**
+ * Constructor
+ *
+ * loads additional helpers
+ */
+ public function __construct() {
+ $this->gui = plugin_load('helper', 'extension_gui');
+ }
+
+ /**
+ * @return int sort number in admin menu
+ */
+ public function getMenuSort() {
+ return 0;
+ }
+
+ /**
+ * @return bool true if only access for superuser, false is for superusers and moderators
+ */
+ public function forAdminOnly() {
+ return true;
+ }
+
+ /**
+ * Execute the requested action(s) and initialize the plugin repository
+ */
+ public function handle() {
+ global $INPUT;
+ // initialize the remote repository
+ /* @var helper_plugin_extension_repository $repository */
+ $repository = $this->loadHelper('extension_repository');
+
+ if(!$repository->hasAccess()) {
+ $url = $this->gui->tabURL('', array('purge' => 1));
+ msg($this->getLang('repo_error').' [<a href="'.$url.'">'.$this->getLang('repo_retry').'</a>]', -1);
+ }
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+
+ try {
+ if($INPUT->post->has('fn') && checkSecurityToken()) {
+ $actions = $INPUT->post->arr('fn');
+ foreach($actions as $action => $extensions) {
+ foreach($extensions as $extname => $label) {
+ switch($action) {
+ case 'install':
+ case 'reinstall':
+ case 'update':
+ $extension->setExtension($extname);
+ $installed = $extension->installOrUpdate();
+ foreach($installed as $ext => $info) {
+ msg(sprintf($this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'), $info['base']), 1);
+ }
+ break;
+ case 'uninstall':
+ $extension->setExtension($extname);
+ $status = $extension->uninstall();
+ if($status !== true) {
+ msg($status, -1);
+ } else {
+ msg(sprintf($this->getLang('msg_delete_success'), hsc($extension->getDisplayName())), 1);
+ }
+ break;
+ case 'enable';
+ $extension->setExtension($extname);
+ $status = $extension->enable();
+ if($status !== true) {
+ msg($status, -1);
+ } else {
+ msg(sprintf($this->getLang('msg_enabled'), hsc($extension->getDisplayName())), 1);
+ }
+ break;
+ case 'disable';
+ $extension->setExtension($extname);
+ $status = $extension->disable();
+ if($status !== true) {
+ msg($status, -1);
+ } else {
+ msg(sprintf($this->getLang('msg_disabled'), hsc($extension->getDisplayName())), 1);
+ }
+ break;
+ }
+ }
+ }
+ send_redirect($this->gui->tabURL('', array(), '&', true));
+ } elseif($INPUT->post->str('installurl') && checkSecurityToken()) {
+ $installed = $extension->installFromURL($INPUT->post->str('installurl'));
+ foreach($installed as $ext => $info) {
+ msg(sprintf($this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'), $info['base']), 1);
+ }
+ send_redirect($this->gui->tabURL('', array(), '&', true));
+ } elseif(isset($_FILES['installfile']) && checkSecurityToken()) {
+ $installed = $extension->installFromUpload('installfile');
+ foreach($installed as $ext => $info) {
+ msg(sprintf($this->getLang('msg_'.$info['type'].'_'.$info['action'].'_success'), $info['base']), 1);
+ }
+ send_redirect($this->gui->tabURL('', array(), '&', true));
+ }
+
+ } catch(Exception $e) {
+ msg($e->getMessage(), -1);
+ send_redirect($this->gui->tabURL('', array(), '&', true));
+ }
+
+ }
+
+ /**
+ * Render HTML output
+ */
+ public function html() {
+ ptln('<h1>'.$this->getLang('menu').'</h1>');
+ ptln('<div id="extension__manager">');
+
+ $this->gui->tabNavigation();
+
+ switch($this->gui->currentTab()) {
+ case 'search':
+ $this->gui->tabSearch();
+ break;
+ case 'templates':
+ $this->gui->tabTemplates();
+ break;
+ case 'install':
+ $this->gui->tabInstall();
+ break;
+ case 'plugins':
+ default:
+ $this->gui->tabPlugins();
+ }
+
+ ptln('</div>');
+ }
+}
+
+// vim:ts=4:sw=4:et: \ No newline at end of file
diff --git a/lib/plugins/extension/all.less b/lib/plugins/extension/all.less
new file mode 100644
index 000000000..3d9688e14
--- /dev/null
+++ b/lib/plugins/extension/all.less
@@ -0,0 +1,37 @@
+
+@media only screen and (max-width: 600px) {
+
+#extension__list .legend {
+ > div {
+ padding-left: 0;
+ }
+
+ div.screenshot {
+ margin: 0 .5em .5em 0;
+ }
+
+ h2 {
+ width: auto;
+ float: none;
+ }
+
+ div.linkbar {
+ clear: left;
+ }
+}
+
+[dir=rtl] #extension__list .legend {
+ > div {
+ padding-right: 0;
+ }
+
+ div.screenshot {
+ margin: 0 0 .5em .5em;
+ }
+
+ div.linkbar {
+ clear: right;
+ }
+}
+
+} /* /@media */
diff --git a/lib/plugins/extension/helper/extension.php b/lib/plugins/extension/helper/extension.php
new file mode 100644
index 000000000..7958cd2da
--- /dev/null
+++ b/lib/plugins/extension/helper/extension.php
@@ -0,0 +1,1093 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+if(!defined('DOKU_TPLLIB')) define('DOKU_TPLLIB', DOKU_INC.'lib/tpl/');
+
+/**
+ * Class helper_plugin_extension_extension represents a single extension (plugin or template)
+ */
+class helper_plugin_extension_extension extends DokuWiki_Plugin {
+ private $id;
+ private $base;
+ private $is_template = false;
+ private $localInfo;
+ private $remoteInfo;
+ private $managerData;
+ /** @var helper_plugin_extension_repository $repository */
+ private $repository = null;
+
+ /** @var array list of temporary directories */
+ private $temporary = array();
+
+ /**
+ * Destructor
+ *
+ * deletes any dangling temporary directories
+ */
+ public function __destruct() {
+ foreach($this->temporary as $dir){
+ io_rmdir($dir, true);
+ }
+ }
+
+ /**
+ * @return bool false, this component is not a singleton
+ */
+ public function isSingleton() {
+ return false;
+ }
+
+ /**
+ * Set the name of the extension this instance shall represents, triggers loading the local and remote data
+ *
+ * @param string $id The id of the extension (prefixed with template: for templates)
+ * @return bool If some (local or remote) data was found
+ */
+ public function setExtension($id) {
+ $this->id = $id;
+ $this->base = $id;
+
+ if(substr($id, 0 , 9) == 'template:'){
+ $this->base = substr($id, 9);
+ $this->is_template = true;
+ }
+
+ $this->localInfo = array();
+ $this->managerData = array();
+ $this->remoteInfo = array();
+
+ if ($this->isInstalled()) {
+ $this->readLocalData();
+ $this->readManagerData();
+ }
+
+ if ($this->repository == null) {
+ $this->repository = $this->loadHelper('extension_repository');
+ }
+
+ $this->remoteInfo = $this->repository->getData($this->getID());
+
+ return ($this->localInfo || $this->remoteInfo);
+ }
+
+ /**
+ * If the extension is installed locally
+ *
+ * @return bool If the extension is installed locally
+ */
+ public function isInstalled() {
+ return is_dir($this->getInstallDir());
+ }
+
+ /**
+ * If the extension is under git control
+ *
+ * @return bool
+ */
+ public function isGitControlled() {
+ if(!$this->isInstalled()) return false;
+ return is_dir($this->getInstallDir().'/.git');
+ }
+
+ /**
+ * If the extension is bundled
+ *
+ * @return bool If the extension is bundled
+ */
+ public function isBundled() {
+ if (!empty($this->remoteInfo['bundled'])) return $this->remoteInfo['bundled'];
+ return in_array($this->base,
+ array(
+ 'authad', 'authldap', 'authmysql', 'authpgsql', 'authplain', 'acl', 'info', 'extension',
+ 'revert', 'popularity', 'config', 'safefnrecode', 'testing', 'template:dokuwiki'
+ )
+ );
+ }
+
+ /**
+ * If the extension is protected against any modification (disable/uninstall)
+ *
+ * @return bool if the extension is protected
+ */
+ public function isProtected() {
+ // never allow deinstalling the current auth plugin:
+ global $conf;
+ if ($this->id == $conf['authtype']) return true;
+
+ /** @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ $cascade = $plugin_controller->getCascade();
+ return (isset($cascade['protected'][$this->id]) && $cascade['protected'][$this->id]);
+ }
+
+ /**
+ * If the extension is installed in the correct directory
+ *
+ * @return bool If the extension is installed in the correct directory
+ */
+ public function isInWrongFolder() {
+ return $this->base != $this->getBase();
+ }
+
+ /**
+ * If the extension is enabled
+ *
+ * @return bool If the extension is enabled
+ */
+ public function isEnabled() {
+ global $conf;
+ if($this->isTemplate()){
+ return ($conf['template'] == $this->getBase());
+ }
+
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ return !$plugin_controller->isdisabled($this->base);
+ }
+
+ /**
+ * If the extension should be updated, i.e. if an updated version is available
+ *
+ * @return bool If an update is available
+ */
+ public function updateAvailable() {
+ if(!$this->isInstalled()) return false;
+ if($this->isBundled()) return false;
+ $lastupdate = $this->getLastUpdate();
+ if ($lastupdate === false) return false;
+ $installed = $this->getInstalledVersion();
+ if ($installed === false || $installed === $this->getLang('unknownversion')) return true;
+ return $this->getInstalledVersion() < $this->getLastUpdate();
+ }
+
+ /**
+ * If the extension is a template
+ *
+ * @return bool If this extension is a template
+ */
+ public function isTemplate() {
+ return $this->is_template;
+ }
+
+ /**
+ * Get the ID of the extension
+ *
+ * This is the same as getName() for plugins, for templates it's getName() prefixed with 'template:'
+ *
+ * @return string
+ */
+ public function getID() {
+ return $this->id;
+ }
+
+ /**
+ * Get the name of the installation directory
+ *
+ * @return string The name of the installation directory
+ */
+ public function getInstallName() {
+ return $this->base;
+ }
+
+ // Data from plugin.info.txt/template.info.txt or the repo when not available locally
+ /**
+ * Get the basename of the extension
+ *
+ * @return string The basename
+ */
+ public function getBase() {
+ if (!empty($this->localInfo['base'])) return $this->localInfo['base'];
+ return $this->base;
+ }
+
+ /**
+ * Get the display name of the extension
+ *
+ * @return string The display name
+ */
+ public function getDisplayName() {
+ if (!empty($this->localInfo['name'])) return $this->localInfo['name'];
+ if (!empty($this->remoteInfo['name'])) return $this->remoteInfo['name'];
+ return $this->base;
+ }
+
+ /**
+ * Get the author name of the extension
+ *
+ * @return string|bool The name of the author or false if there is none
+ */
+ public function getAuthor() {
+ if (!empty($this->localInfo['author'])) return $this->localInfo['author'];
+ if (!empty($this->remoteInfo['author'])) return $this->remoteInfo['author'];
+ return false;
+ }
+
+ /**
+ * Get the email of the author of the extension if there is any
+ *
+ * @return string|bool The email address or false if there is none
+ */
+ public function getEmail() {
+ // email is only in the local data
+ if (!empty($this->localInfo['email'])) return $this->localInfo['email'];
+ return false;
+ }
+
+ /**
+ * Get the email id, i.e. the md5sum of the email
+ *
+ * @return string|bool The md5sum of the email if there is any, false otherwise
+ */
+ public function getEmailID() {
+ if (!empty($this->remoteInfo['emailid'])) return $this->remoteInfo['emailid'];
+ if (!empty($this->localInfo['email'])) return md5($this->localInfo['email']);
+ return false;
+ }
+
+ /**
+ * Get the description of the extension
+ *
+ * @return string The description
+ */
+ public function getDescription() {
+ if (!empty($this->localInfo['desc'])) return $this->localInfo['desc'];
+ if (!empty($this->remoteInfo['description'])) return $this->remoteInfo['description'];
+ return '';
+ }
+
+ /**
+ * Get the URL of the extension, usually a page on dokuwiki.org
+ *
+ * @return string The URL
+ */
+ public function getURL() {
+ if (!empty($this->localInfo['url'])) return $this->localInfo['url'];
+ return 'https://www.dokuwiki.org/'.($this->isTemplate() ? 'template' : 'plugin').':'.$this->getBase();
+ }
+
+ /**
+ * Get the installed version of the extension
+ *
+ * @return string|bool The version, usually in the form yyyy-mm-dd if there is any
+ */
+ public function getInstalledVersion() {
+ if (!empty($this->localInfo['date'])) return $this->localInfo['date'];
+ if ($this->isInstalled()) return $this->getLang('unknownversion');
+ return false;
+ }
+
+ /**
+ * Get the install date of the current version
+ *
+ * @return string|bool The date of the last update or false if not available
+ */
+ public function getUpdateDate() {
+ if (!empty($this->managerData['updated'])) return $this->managerData['updated'];
+ return false;
+ }
+
+ /**
+ * Get the date of the installation of the plugin
+ *
+ * @return string|bool The date of the installation or false if not available
+ */
+ public function getInstallDate() {
+ if (!empty($this->managerData['installed'])) return $this->managerData['installed'];
+ return false;
+ }
+
+ /**
+ * Get the names of the dependencies of this extension
+ *
+ * @return array The base names of the dependencies
+ */
+ public function getDependencies() {
+ if (!empty($this->remoteInfo['dependencies'])) return $this->remoteInfo['dependencies'];
+ return array();
+ }
+
+ /**
+ * Get the names of the missing dependencies
+ *
+ * @return array The base names of the missing dependencies
+ */
+ public function getMissingDependencies() {
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ $dependencies = $this->getDependencies();
+ $missing_dependencies = array();
+ foreach ($dependencies as $dependency) {
+ if ($plugin_controller->isdisabled($dependency)) {
+ $missing_dependencies[] = $dependency;
+ }
+ }
+ return $missing_dependencies;
+ }
+
+ /**
+ * Get the names of all conflicting extensions
+ *
+ * @return array The names of the conflicting extensions
+ */
+ public function getConflicts() {
+ if (!empty($this->remoteInfo['conflicts'])) return $this->remoteInfo['dependencies'];
+ return array();
+ }
+
+ /**
+ * Get the names of similar extensions
+ *
+ * @return array The names of similar extensions
+ */
+ public function getSimilarExtensions() {
+ if (!empty($this->remoteInfo['similar'])) return $this->remoteInfo['similar'];
+ return array();
+ }
+
+ /**
+ * Get the names of the tags of the extension
+ *
+ * @return array The names of the tags of the extension
+ */
+ public function getTags() {
+ if (!empty($this->remoteInfo['tags'])) return $this->remoteInfo['tags'];
+ return array();
+ }
+
+ /**
+ * Get the popularity information as floating point number [0,1]
+ *
+ * @return float|bool The popularity information or false if it isn't available
+ */
+ public function getPopularity() {
+ if (!empty($this->remoteInfo['popularity'])) return $this->remoteInfo['popularity'];
+ return false;
+ }
+
+
+ /**
+ * Get the text of the security warning if there is any
+ *
+ * @return string|bool The security warning if there is any, false otherwise
+ */
+ public function getSecurityWarning() {
+ if (!empty($this->remoteInfo['securitywarning'])) return $this->remoteInfo['securitywarning'];
+ return false;
+ }
+
+ /**
+ * Get the text of the security issue if there is any
+ *
+ * @return string|bool The security issue if there is any, false otherwise
+ */
+ public function getSecurityIssue() {
+ if (!empty($this->remoteInfo['securityissue'])) return $this->remoteInfo['securityissue'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the screenshot of the extension if there is any
+ *
+ * @return string|bool The screenshot URL if there is any, false otherwise
+ */
+ public function getScreenshotURL() {
+ if (!empty($this->remoteInfo['screenshoturl'])) return $this->remoteInfo['screenshoturl'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the thumbnail of the extension if there is any
+ *
+ * @return string|bool The thumbnail URL if there is any, false otherwise
+ */
+ public function getThumbnailURL() {
+ if (!empty($this->remoteInfo['thumbnailurl'])) return $this->remoteInfo['thumbnailurl'];
+ return false;
+ }
+ /**
+ * Get the last used download URL of the extension if there is any
+ *
+ * @return string|bool The previously used download URL, false if the extension has been installed manually
+ */
+ public function getLastDownloadURL() {
+ if (!empty($this->managerData['downloadurl'])) return $this->managerData['downloadurl'];
+ return false;
+ }
+
+ /**
+ * Get the download URL of the extension if there is any
+ *
+ * @return string|bool The download URL if there is any, false otherwise
+ */
+ public function getDownloadURL() {
+ if (!empty($this->remoteInfo['downloadurl'])) return $this->remoteInfo['downloadurl'];
+ return false;
+ }
+
+ /**
+ * If the download URL has changed since the last download
+ *
+ * @return bool If the download URL has changed
+ */
+ public function hasDownloadURLChanged() {
+ $lasturl = $this->getLastDownloadURL();
+ $currenturl = $this->getDownloadURL();
+ return ($lasturl && $currenturl && $lasturl != $currenturl);
+ }
+
+ /**
+ * Get the bug tracker URL of the extension if there is any
+ *
+ * @return string|bool The bug tracker URL if there is any, false otherwise
+ */
+ public function getBugtrackerURL() {
+ if (!empty($this->remoteInfo['bugtracker'])) return $this->remoteInfo['bugtracker'];
+ return false;
+ }
+
+ /**
+ * Get the URL of the source repository if there is any
+ *
+ * @return string|bool The URL of the source repository if there is any, false otherwise
+ */
+ public function getSourcerepoURL() {
+ if (!empty($this->remoteInfo['sourcerepo'])) return $this->remoteInfo['sourcerepo'];
+ return false;
+ }
+
+ /**
+ * Get the donation URL of the extension if there is any
+ *
+ * @return string|bool The donation URL if there is any, false otherwise
+ */
+ public function getDonationURL() {
+ if (!empty($this->remoteInfo['donationurl'])) return $this->remoteInfo['donationurl'];
+ return false;
+ }
+
+ /**
+ * Get the extension type(s)
+ *
+ * @return array The type(s) as array of strings
+ */
+ public function getTypes() {
+ if (!empty($this->remoteInfo['types'])) return $this->remoteInfo['types'];
+ if ($this->isTemplate()) return array(32 => 'template');
+ return array();
+ }
+
+ /**
+ * Get a list of all DokuWiki versions this extension is compatible with
+ *
+ * @return array The versions in the form yyyy-mm-dd => ('label' => label, 'implicit' => implicit)
+ */
+ public function getCompatibleVersions() {
+ if (!empty($this->remoteInfo['compatible'])) return $this->remoteInfo['compatible'];
+ return array();
+ }
+
+ /**
+ * Get the date of the last available update
+ *
+ * @return string|bool The last available update in the form yyyy-mm-dd if there is any, false otherwise
+ */
+ public function getLastUpdate() {
+ if (!empty($this->remoteInfo['lastupdate'])) return $this->remoteInfo['lastupdate'];
+ return false;
+ }
+
+ /**
+ * Get the base path of the extension
+ *
+ * @return string The base path of the extension
+ */
+ public function getInstallDir() {
+ if ($this->isTemplate()) {
+ return DOKU_TPLLIB.$this->base;
+ } else {
+ return DOKU_PLUGIN.$this->base;
+ }
+ }
+
+ /**
+ * The type of extension installation
+ *
+ * @return string One of "none", "manual", "git" or "automatic"
+ */
+ public function getInstallType() {
+ if (!$this->isInstalled()) return 'none';
+ if (!empty($this->managerData)) return 'automatic';
+ if (is_dir($this->getInstallDir().'/.git')) return 'git';
+ return 'manual';
+ }
+
+ /**
+ * If the extension can probably be installed/updated or uninstalled
+ *
+ * @return bool|string True or error string
+ */
+ public function canModify() {
+ if($this->isInstalled()) {
+ if(!is_writable($this->getInstallDir())) {
+ return 'noperms';
+ }
+ }
+
+ if($this->isTemplate() && !is_writable(DOKU_TPLLIB)) {
+ return 'notplperms';
+
+ } elseif(!is_writable(DOKU_PLUGIN)) {
+ return 'nopluginperms';
+ }
+ return true;
+ }
+
+ /**
+ * Install an extension from a user upload
+ *
+ * @param string $field name of the upload file
+ * @throws Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installFromUpload($field){
+ if($_FILES[$field]['error']){
+ throw new Exception($this->getLang('msg_upload_failed').' ('.$_FILES[$field]['error'].')');
+ }
+
+ $tmp = $this->mkTmpDir();
+ if(!$tmp) throw new Exception($this->getLang('error_dircreate'));
+
+ // filename may contain the plugin name for old style plugins...
+ $basename = basename($_FILES[$field]['name']);
+ $basename = preg_replace('/\.(tar\.gz|tar\.bz|tar\.bz2|tar|tgz|tbz|zip)$/', '', $basename);
+ $basename = preg_replace('/[\W]+/', '', $basename);
+
+ if(!move_uploaded_file($_FILES[$field]['tmp_name'], "$tmp/upload.archive")){
+ throw new Exception($this->getLang('msg_upload_failed'));
+ }
+
+ try {
+ $installed = $this->installArchive("$tmp/upload.archive", true, $basename);
+ // purge cache
+ $this->purgeCache();
+ }catch (Exception $e){
+ throw $e;
+ }
+ return $installed;
+ }
+
+ /**
+ * Install an extension from a remote URL
+ *
+ * @param string $url
+ * @throws Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installFromURL($url){
+ try {
+ $path = $this->download($url);
+ $installed = $this->installArchive($path, true);
+
+ // purge caches
+ foreach($installed as $ext => $info){
+ $this->setExtension($ext);
+ $this->purgeCache();
+ }
+ }catch (Exception $e){
+ throw $e;
+ }
+ return $installed;
+ }
+
+ /**
+ * Install or update the extension
+ *
+ * @throws \Exception when something goes wrong
+ * @return array The list of installed extensions
+ */
+ public function installOrUpdate() {
+ $path = $this->download($this->getDownloadURL());
+ $installed = $this->installArchive($path, $this->isInstalled(), $this->getBase());
+
+ // refresh extension information
+ if (!isset($installed[$this->getID()])) {
+ throw new Exception('Error, the requested extension hasn\'t been installed or updated');
+ }
+ $this->setExtension($this->getID());
+ $this->purgeCache();
+ return $installed;
+ }
+
+ /**
+ * Uninstall the extension
+ *
+ * @return bool If the plugin was sucessfully uninstalled
+ */
+ public function uninstall() {
+ $this->purgeCache();
+ return io_rmdir($this->getInstallDir(), true);
+ }
+
+ /**
+ * Enable the extension
+ *
+ * @return bool|string True or an error message
+ */
+ public function enable() {
+ if ($this->isTemplate()) return $this->getLang('notimplemented');
+ if (!$this->isInstalled()) return $this->getLang('notinstalled');
+ if ($this->isEnabled()) return $this->getLang('alreadyenabled');
+
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ if ($plugin_controller->enable($this->base)) {
+ $this->purgeCache();
+ return true;
+ } else {
+ return $this->getLang('pluginlistsaveerror');
+ }
+ }
+
+ /**
+ * Disable the extension
+ *
+ * @return bool|string True or an error message
+ */
+ public function disable() {
+ if ($this->isTemplate()) return $this->getLang('notimplemented');
+
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ if (!$this->isInstalled()) return $this->getLang('notinstalled');
+ if (!$this->isEnabled()) return $this->getLang('alreadydisabled');
+ if ($plugin_controller->disable($this->base)) {
+ $this->purgeCache();
+ return true;
+ } else {
+ return $this->getLang('pluginlistsaveerror');
+ }
+ }
+
+ /**
+ * Purge the cache by touching the main configuration file
+ */
+ protected function purgeCache() {
+ global $config_cascade;
+
+ // expire dokuwiki caches
+ // touching local.php expires wiki page, JS and CSS caches
+ @touch(reset($config_cascade['main']['local']));
+ }
+
+ /**
+ * Read local extension data either from info.txt or getInfo()
+ */
+ protected function readLocalData() {
+ if ($this->isTemplate()) {
+ $infopath = $this->getInstallDir().'/template.info.txt';
+ } else {
+ $infopath = $this->getInstallDir().'/plugin.info.txt';
+ }
+
+ if (is_readable($infopath)) {
+ $this->localInfo = confToHash($infopath);
+ } elseif (!$this->isTemplate() && $this->isEnabled()) {
+ global $plugin_types;
+ $path = $this->getInstallDir().'/';
+ $plugin = null;
+
+ foreach($plugin_types as $type) {
+ if(@file_exists($path.$type.'.php')) {
+ $plugin = plugin_load($type, $this->base);
+ if ($plugin) break;
+ }
+
+ if($dh = @opendir($path.$type.'/')) {
+ while(false !== ($cp = readdir($dh))) {
+ if($cp == '.' || $cp == '..' || strtolower(substr($cp, -4)) != '.php') continue;
+
+ $plugin = plugin_load($type, $this->base.'_'.substr($cp, 0, -4));
+ if ($plugin) break;
+ }
+ if ($plugin) break;
+ closedir($dh);
+ }
+ }
+
+ if ($plugin) {
+ /* @var DokuWiki_Plugin $plugin */
+ $this->localInfo = $plugin->getInfo();
+ }
+ }
+ }
+
+ /**
+ * Read the manager.dat file
+ */
+ protected function readManagerData() {
+ $managerpath = $this->getInstallDir().'/manager.dat';
+ if (is_readable($managerpath)) {
+ $file = @file($managerpath);
+ if(!empty($file)) {
+ foreach($file as $line) {
+ list($key, $value) = explode('=', trim($line, DOKU_LF), 2);
+ $key = trim($key);
+ $value = trim($value);
+ // backwards compatible with old plugin manager
+ if($key == 'url') $key = 'downloadurl';
+ $this->managerData[$key] = $value;
+ }
+ }
+ }
+ }
+
+ /**
+ * Write the manager.data file
+ */
+ protected function writeManagerData() {
+ $managerpath = $this->getInstallDir().'/manager.dat';
+ $data = '';
+ foreach ($this->managerData as $k => $v) {
+ $data .= $k.'='.$v.DOKU_LF;
+ }
+ io_saveFile($managerpath, $data);
+ }
+
+ /**
+ * Returns a temporary directory
+ *
+ * The directory is registered for cleanup when the class is destroyed
+ *
+ * @return bool|string
+ */
+ protected function mkTmpDir(){
+ $dir = io_mktmpdir();
+ if(!$dir) return false;
+ $this->temporary[] = $dir;
+ return $dir;
+ }
+
+ /**
+ * Download an archive to a protected path
+ *
+ * @param string $url The url to get the archive from
+ * @throws Exception when something goes wrong
+ * @return string The path where the archive was saved
+ */
+ public function download($url) {
+ // check the url
+ if(!preg_match('/https?:\/\//i', $url)){
+ throw new Exception($this->getLang('error_badurl'));
+ }
+
+ // try to get the file from the path (used as plugin name fallback)
+ $file = parse_url($url, PHP_URL_PATH);
+ if(is_null($file)){
+ $file = md5($url);
+ }else{
+ $file = utf8_basename($file);
+ }
+
+ // create tmp directory for download
+ if(!($tmp = $this->mkTmpDir())) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // download
+ if(!$file = io_download($url, $tmp.'/', true, $file, 0)) {
+ io_rmdir($tmp, true);
+ throw new Exception(sprintf($this->getLang('error_download'), '<bdi>'.hsc($url).'</bdi>'));
+ }
+
+ return $tmp.'/'.$file;
+ }
+
+ /**
+ * @param string $file The path to the archive that shall be installed
+ * @param bool $overwrite If an already installed plugin should be overwritten
+ * @param string $base The basename of the plugin if it's known
+ * @throws Exception when something went wrong
+ * @return array list of installed extensions
+ */
+ public function installArchive($file, $overwrite=false, $base = '') {
+ $installed_extensions = array();
+
+ // create tmp directory for decompression
+ if(!($tmp = $this->mkTmpDir())) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // add default base folder if specified to handle case where zip doesn't contain this
+ if($base && !@mkdir($tmp.'/'.$base)) {
+ throw new Exception($this->getLang('error_dircreate'));
+ }
+
+ // decompress
+ $this->decompress($file, "$tmp/".$base);
+
+ // search $tmp/$base for the folder(s) that has been created
+ // move the folder(s) to lib/..
+ $result = array('old'=>array(), 'new'=>array());
+ $default = ($this->isTemplate() ? 'template' : 'plugin');
+ if(!$this->find_folders($result, $tmp.'/'.$base, $default)) {
+ throw new Exception($this->getLang('error_findfolder'));
+ }
+
+ // choose correct result array
+ if(count($result['new'])) {
+ $install = $result['new'];
+ }else{
+ $install = $result['old'];
+ }
+
+ if(!count($install)){
+ throw new Exception($this->getLang('error_findfolder'));
+ }
+
+ // now install all found items
+ foreach($install as $item) {
+ // where to install?
+ if($item['type'] == 'template') {
+ $target_base_dir = DOKU_TPLLIB;
+ }else{
+ $target_base_dir = DOKU_PLUGIN;
+ }
+
+ if(!empty($item['base'])) {
+ // use base set in info.txt
+ } elseif($base && count($install) == 1) {
+ $item['base'] = $base;
+ } else {
+ // default - use directory as found in zip
+ // plugins from github/master without *.info.txt will install in wrong folder
+ // but using $info->id will make 'code3' fail (which should install in lib/code/..)
+ $item['base'] = basename($item['tmp']);
+ }
+
+ // check to make sure we aren't overwriting anything
+ $target = $target_base_dir.$item['base'];
+ if(!$overwrite && @file_exists($target)) {
+ // TODO remember our settings, ask the user to confirm overwrite
+ continue;
+ }
+
+ $action = @file_exists($target) ? 'update' : 'install';
+
+ // copy action
+ if($this->dircopy($item['tmp'], $target)) {
+ // return info
+ $id = $item['base'];
+ if($item['type'] == 'template') $id = 'template:'.$id;
+ $installed_extensions[$id] = array(
+ 'base' => $item['base'],
+ 'type' => $item['type'],
+ 'action' => $action
+ );
+ } else {
+ throw new Exception(sprintf($this->getLang('error_copy').DOKU_LF, '<bdi>'.$item['base'].'</bdi>'));
+ }
+ }
+
+ // cleanup
+ if($tmp) io_rmdir($tmp, true);
+
+ return $installed_extensions;
+ }
+
+ /**
+ * Find out what was in the extracted directory
+ *
+ * Correct folders are searched recursively using the "*.info.txt" configs
+ * as indicator for a root folder. When such a file is found, it's base
+ * setting is used (when set). All folders found by this method are stored
+ * in the 'new' key of the $result array.
+ *
+ * For backwards compatibility all found top level folders are stored as
+ * in the 'old' key of the $result array.
+ *
+ * When no items are found in 'new' the copy mechanism should fall back
+ * the 'old' list.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param array $result - results are stored here
+ * @param string $directory - the temp directory where the package was unpacked to
+ * @param string $default_type - type used if no info.txt available
+ * @param string $subdir - a subdirectory. do not set. used by recursion
+ * @return bool - false on error
+ */
+ protected function find_folders(&$result, $directory, $default_type='plugin', $subdir='') {
+ $this_dir = "$directory$subdir";
+ $dh = @opendir($this_dir);
+ if(!$dh) return false;
+
+ $found_dirs = array();
+ $found_files = 0;
+ $found_template_parts = 0;
+ while (false !== ($f = readdir($dh))) {
+ if($f == '.' || $f == '..') continue;
+
+ if(is_dir("$this_dir/$f")) {
+ $found_dirs[] = "$subdir/$f";
+
+ } else {
+ // it's a file -> check for config
+ $found_files++;
+ switch ($f) {
+ case 'plugin.info.txt':
+ case 'template.info.txt':
+ // we have found a clear marker, save and return
+ $info = array();
+ $type = explode('.', $f, 2);
+ $info['type'] = $type[0];
+ $info['tmp'] = $this_dir;
+ $conf = confToHash("$this_dir/$f");
+ $info['base'] = basename($conf['base']);
+ $result['new'][] = $info;
+ return true;
+
+ case 'main.php':
+ case 'details.php':
+ case 'mediamanager.php':
+ case 'style.ini':
+ $found_template_parts++;
+ break;
+ }
+ }
+ }
+ closedir($dh);
+
+ // files where found but no info.txt - use old method
+ if($found_files){
+ $info = array();
+ $info['tmp'] = $this_dir;
+ // does this look like a template or should we use the default type?
+ if($found_template_parts >= 2) {
+ $info['type'] = 'template';
+ } else {
+ $info['type'] = $default_type;
+ }
+
+ $result['old'][] = $info;
+ return true;
+ }
+
+ // we have no files yet -> recurse
+ foreach ($found_dirs as $found_dir) {
+ $this->find_folders($result, $directory, $default_type, "$found_dir");
+ }
+ return true;
+ }
+
+ /**
+ * Decompress a given file to the given target directory
+ *
+ * Determines the compression type from the file extension
+ *
+ * @param string $file archive to extract
+ * @param string $target directory to extract to
+ * @throws Exception
+ * @return bool
+ */
+ private function decompress($file, $target) {
+ // decompression library doesn't like target folders ending in "/"
+ if(substr($target, -1) == "/") $target = substr($target, 0, -1);
+
+ $ext = $this->guess_archive($file);
+ if(in_array($ext, array('tar', 'bz', 'gz'))) {
+ switch($ext) {
+ case 'bz':
+ $compress_type = Tar::COMPRESS_BZIP;
+ break;
+ case 'gz':
+ $compress_type = Tar::COMPRESS_GZIP;
+ break;
+ default:
+ $compress_type = Tar::COMPRESS_NONE;
+ }
+
+ $tar = new Tar();
+ try {
+ $tar->open($file, $compress_type);
+ $tar->extract($target);
+ } catch (Exception $e) {
+ throw new Exception($this->getLang('error_decompress').' '.$e->getMessage());
+ }
+
+ return true;
+ } elseif($ext == 'zip') {
+
+ $zip = new ZipLib();
+ $ok = $zip->Extract($file, $target);
+
+ if($ok == -1){
+ throw new Exception($this->getLang('error_decompress').' Error extracting the zip archive');
+ }
+
+ return true;
+ }
+
+ // the only case when we don't get one of the recognized archive types is when the archive file can't be read
+ throw new Exception($this->getLang('error_decompress').' Couldn\'t read archive file');
+ }
+
+ /**
+ * Determine the archive type of the given file
+ *
+ * Reads the first magic bytes of the given file for content type guessing,
+ * if neither bz, gz or zip are recognized, tar is assumed.
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ * @param string $file The file to analyze
+ * @return string|bool false if the file can't be read, otherwise an "extension"
+ */
+ private function guess_archive($file) {
+ $fh = fopen($file, 'rb');
+ if(!$fh) return false;
+ $magic = fread($fh, 5);
+ fclose($fh);
+
+ if(strpos($magic, "\x42\x5a") === 0) return 'bz';
+ if(strpos($magic, "\x1f\x8b") === 0) return 'gz';
+ if(strpos($magic, "\x50\x4b\x03\x04") === 0) return 'zip';
+ return 'tar';
+ }
+
+ /**
+ * Copy with recursive sub-directory support
+ */
+ private function dircopy($src, $dst) {
+ global $conf;
+
+ if(is_dir($src)) {
+ if(!$dh = @opendir($src)) return false;
+
+ if($ok = io_mkdir_p($dst)) {
+ while ($ok && (false !== ($f = readdir($dh)))) {
+ if($f == '..' || $f == '.') continue;
+ $ok = $this->dircopy("$src/$f", "$dst/$f");
+ }
+ }
+
+ closedir($dh);
+ return $ok;
+
+ } else {
+ $exists = @file_exists($dst);
+
+ if(!@copy($src, $dst)) return false;
+ if(!$exists && !empty($conf['fperm'])) chmod($dst, $conf['fperm']);
+ @touch($dst, filemtime($src));
+ }
+
+ return true;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/extension/helper/gui.php b/lib/plugins/extension/helper/gui.php
new file mode 100644
index 000000000..3a0f0c589
--- /dev/null
+++ b/lib/plugins/extension/helper/gui.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class helper_plugin_extension_list takes care of the overall GUI
+ */
+class helper_plugin_extension_gui extends DokuWiki_Plugin {
+
+ protected $tabs = array('plugins', 'templates', 'search', 'install');
+
+ /** @var string the extension that should have an open info window FIXME currently broken */
+ protected $infoFor = '';
+
+ /**
+ * Constructor
+ *
+ * initializes requested info window
+ */
+ public function __construct() {
+ global $INPUT;
+ $this->infoFor = $INPUT->str('info');
+ }
+
+ /**
+ * display the plugin tab
+ */
+ public function tabPlugins() {
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_plugins');
+ echo '</div>';
+
+ $pluginlist = $plugin_controller->getList('', true);
+ sort($pluginlist);
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+ $list->start_form();
+ foreach($pluginlist as $name) {
+ $extension->setExtension($name);
+ $list->add_row($extension, $extension->getID() == $this->infoFor);
+ }
+ $list->end_form();
+ $list->render();
+ }
+
+ /**
+ * Display the template tab
+ */
+ public function tabTemplates() {
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_templates');
+ echo '</div>';
+
+ // FIXME do we have a real way?
+ $tpllist = glob(DOKU_INC.'lib/tpl/*', GLOB_ONLYDIR);
+ $tpllist = array_map('basename', $tpllist);
+ sort($tpllist);
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+ $list->start_form();
+ foreach($tpllist as $name) {
+ $extension->setExtension("template:$name");
+ $list->add_row($extension, $extension->getID() == $this->infoFor);
+ }
+ $list->end_form();
+ $list->render();
+ }
+
+ /**
+ * Display the search tab
+ */
+ public function tabSearch() {
+ global $INPUT;
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_search');
+ echo '</div>';
+
+ $form = new Doku_Form(array('action' => $this->tabURL('', array(), '&'), 'class' => 'search'));
+ $form->addElement(form_makeTextField('q', $INPUT->str('q'), $this->getLang('search_for')));
+ $form->addElement(form_makeButton('submit', '', $this->getLang('search')));
+ $form->printForm();
+
+ if(!$INPUT->bool('q')) return;
+
+ /* @var helper_plugin_extension_repository $repository FIXME should we use some gloabl instance? */
+ $repository = $this->loadHelper('extension_repository');
+ $result = $repository->search($INPUT->str('q'));
+
+ /* @var helper_plugin_extension_extension $extension */
+ $extension = $this->loadHelper('extension_extension');
+ /* @var helper_plugin_extension_list $list */
+ $list = $this->loadHelper('extension_list');
+ $list->start_form();
+ if($result){
+ foreach($result as $name) {
+ $extension->setExtension($name);
+ $list->add_row($extension, $extension->getID() == $this->infoFor);
+ }
+ } else {
+ $list->nothing_found();
+ }
+ $list->end_form();
+ $list->render();
+
+ }
+
+ /**
+ * Display the template tab
+ */
+ public function tabInstall() {
+ echo '<div class="panelHeader">';
+ echo $this->locale_xhtml('intro_install');
+ echo '</div>';
+
+ $form = new Doku_Form(array('action' => $this->tabURL('', array(), '&'), 'enctype' => 'multipart/form-data', 'class' => 'install'));
+ $form->addElement(form_makeTextField('installurl', '', $this->getLang('install_url'), '', 'block'));
+ $form->addElement(form_makeFileField('installfile', $this->getLang('install_upload'), '', 'block'));
+ $form->addElement(form_makeButton('submit', '', $this->getLang('btn_install')));
+ $form->printForm();
+ }
+
+ /**
+ * Print the tab navigation
+ *
+ * @fixme style active one
+ */
+ public function tabNavigation() {
+ echo '<ul class="tabs">';
+ foreach($this->tabs as $tab) {
+ $url = $this->tabURL($tab);
+ if($this->currentTab() == $tab) {
+ $class = 'class="active"';
+ } else {
+ $class = '';
+ }
+ echo '<li '.$class.'><a href="'.$url.'">'.$this->getLang('tab_'.$tab).'</a></li>';
+ }
+ echo '</ul>';
+ }
+
+ /**
+ * Return the currently selected tab
+ *
+ * @return string
+ */
+ public function currentTab() {
+ global $INPUT;
+
+ $tab = $INPUT->str('tab', 'plugins', true);
+ if(!in_array($tab, $this->tabs)) $tab = 'plugins';
+ return $tab;
+ }
+
+ /**
+ * Create an URL inside the extension manager
+ *
+ * @param string $tab tab to load, empty for current tab
+ * @param array $params associative array of parameter to set
+ * @param string $sep seperator to build the URL
+ * @param bool $absolute create absolute URLs?
+ * @return string
+ */
+ public function tabURL($tab = '', $params = array(), $sep = '&amp;', $absolute = false) {
+ global $ID;
+ global $INPUT;
+
+ if(!$tab) $tab = $this->currentTab();
+ $defaults = array(
+ 'do' => 'admin',
+ 'page' => 'extension',
+ 'tab' => $tab,
+ );
+ if($tab == 'search') $defaults['q'] = $INPUT->str('q');
+
+ return wl($ID, array_merge($defaults, $params), $absolute, $sep);
+ }
+
+}
diff --git a/lib/plugins/extension/helper/list.php b/lib/plugins/extension/helper/list.php
new file mode 100644
index 000000000..47edca8c1
--- /dev/null
+++ b/lib/plugins/extension/helper/list.php
@@ -0,0 +1,568 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class helper_plugin_extension_list takes care of creating a HTML list of extensions
+ */
+class helper_plugin_extension_list extends DokuWiki_Plugin {
+ protected $form = '';
+ /** @var helper_plugin_extension_gui */
+ protected $gui;
+
+ /**
+ * Constructor
+ *
+ * loads additional helpers
+ */
+ public function __construct(){
+ $this->gui = plugin_load('helper', 'extension_gui');
+ }
+
+ function start_form() {
+ $this->form .= '<form id="extension__list" accept-charset="utf-8" method="post" action="">';
+ $hidden = array(
+ 'do'=>'admin',
+ 'page'=>'extension',
+ 'sectok'=>getSecurityToken()
+ );
+ $this->add_hidden($hidden);
+ $this->form .= '<ul class="extensionList">';
+ }
+ /**
+ * Build single row of extension table
+ * @param helper_plugin_extension_extension $extension The extension that shall be added
+ * @param bool $showinfo Show the info area
+ */
+ function add_row(helper_plugin_extension_extension $extension, $showinfo = false) {
+ $this->start_row($extension);
+ $this->populate_column('legend', $this->make_legend($extension, $showinfo));
+ $this->populate_column('actions', $this->make_actions($extension));
+ $this->end_row();
+ }
+
+ /**
+ * Adds a header to the form
+ *
+ * @param string $id The id of the header
+ * @param string $header The content of the header
+ * @param int $level The level of the header
+ */
+ function add_header($id, $header, $level = 2) {
+ $this->form .='<h'.$level.' id="'.$id.'">'.hsc($header).'</h'.$level.'>'.DOKU_LF;
+ }
+
+ /**
+ * Adds a paragraph to the form
+ *
+ * @param string $data The content
+ */
+ function add_p($data) {
+ $this->form .= '<p>'.hsc($data).'</p>'.DOKU_LF;
+ }
+
+ /**
+ * Add hidden fields to the form with the given data
+ * @param array $array
+ */
+ function add_hidden(array $array) {
+ $this->form .= '<div class="no">';
+ foreach ($array as $key => $value) {
+ $this->form .= '<input type="hidden" name="'.hsc($key).'" value="'.hsc($value).'" />';
+ }
+ $this->form .= '</div>'.DOKU_LF;
+ }
+
+ /**
+ * Add closing tags
+ */
+ function end_form() {
+ $this->form .= '</ul>';
+ $this->form .= '</form>'.DOKU_LF;
+ }
+
+ /**
+ * Show message when no results are found
+ */
+ function nothing_found() {
+ global $lang;
+ $this->form .= '<li class="notfound">'.$lang['nothingfound'].'</li>';
+ }
+
+ /**
+ * Print the form
+ */
+ function render() {
+ echo $this->form;
+ }
+
+ /**
+ * Start the HTML for the row for the extension
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ */
+ private function start_row(helper_plugin_extension_extension $extension) {
+ $this->form .= '<li id="extensionplugin__'.hsc($extension->getID()).'" class="'.$this->make_class($extension).'">';
+ }
+
+ /**
+ * Add a column with the given class and content
+ * @param string $class The class name
+ * @param string $html The content
+ */
+ private function populate_column($class, $html) {
+ $this->form .= '<div class="'.$class.' col">'.$html.'</div>'.DOKU_LF;
+ }
+
+ /**
+ * End the row
+ */
+ private function end_row() {
+ $this->form .= '</li>'.DOKU_LF;
+ }
+
+ /**
+ * Generate the link to the plugin homepage
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ function make_homepagelink(helper_plugin_extension_extension $extension) {
+ $text = $this->getLang('homepage_link');
+ $url = hsc($extension->getURL());
+ return '<a href="'.$url.'" title="'.$url.'" class ="urlextern">'.$text.'</a> ';
+ }
+
+ /**
+ * Generate the class name for the row of the extensio
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The class name
+ */
+ function make_class(helper_plugin_extension_extension $extension) {
+ $class = ($extension->isTemplate()) ? 'template' : 'plugin';
+ if($extension->isInstalled()) {
+ $class.=' installed';
+ $class.= ($extension->isEnabled()) ? ' enabled':' disabled';
+ }
+ if(!$extension->canModify()) $class.= ' notselect';
+ if($extension->isProtected()) $class.= ' protected';
+ //if($this->showinfo) $class.= ' showinfo';
+ return $class;
+ }
+
+ /**
+ * Generate a link to the author of the extension
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The HTML code of the link
+ */
+ function make_author(helper_plugin_extension_extension $extension) {
+ global $ID;
+
+ if($extension->getAuthor()) {
+
+ $mailid = $extension->getEmailID();
+ if($mailid){
+ $url = $this->gui->tabURL('search', array('q' => 'authorid:'.$mailid));
+ return '<bdi><a href="'.$url.'" class="author" title="'.$this->getLang('author_hint').'" ><img src="//www.gravatar.com/avatar/'.$mailid.'?s=20&amp;d=mm" width="20" height="20" alt="" /> '.hsc($extension->getAuthor()).'</a></bdi>';
+
+ }else{
+ return '<bdi><span class="author">'.hsc($extension->getAuthor()).'</span></bdi>';
+ }
+ }
+ return "<em class=\"author\">".$this->getLang('unknown_author')."</em>".DOKU_LF;
+ }
+
+ /**
+ * Get the link and image tag for the screenshot/thumbnail
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @return string The HTML code
+ */
+ function make_screenshot(helper_plugin_extension_extension $extension) {
+ $screen = $extension->getScreenshotURL();
+ $thumb = $extension->getThumbnailURL();
+
+ if($screen) {
+ // use protocol independent URLs for images coming from us #595
+ $screen = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $screen);
+ $thumb = str_replace('http://www.dokuwiki.org', '//www.dokuwiki.org', $thumb);
+
+ $title = sprintf($this->getLang('screenshot'), hsc($extension->getDisplayName()));
+ $img = '<a href="'.hsc($screen).'" target="_blank" class="extension_screenshot">'.
+ '<img alt="'.$title.'" width="120" height="70" src="'.hsc($thumb).'" />'.
+ '</a>';
+ } elseif($extension->isTemplate()) {
+ $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE.'lib/plugins/extension/images/template.png" />';
+
+ } else {
+ $img = '<img alt="" width="120" height="70" src="'.DOKU_BASE.'lib/plugins/extension/images/plugin.png" />';
+ }
+ return '<div class="screenshot" >'.$img.'<span></span></div>'.DOKU_LF;
+ }
+
+ /**
+ * Extension main description
+ *
+ * @param helper_plugin_extension_extension $extension The extension object
+ * @param bool $showinfo Show the info section
+ * @return string The HTML code
+ */
+ function make_legend(helper_plugin_extension_extension $extension, $showinfo = false) {
+ $return = '<div>';
+ $return .= '<h2>';
+ $return .= sprintf($this->getLang('extensionby'), '<bdi>'.hsc($extension->getDisplayName()).'</bdi>', $this->make_author($extension));
+ $return .= '</h2>'.DOKU_LF;
+
+ $return .= $this->make_screenshot($extension);
+
+ $popularity = $extension->getPopularity();
+ if ($popularity !== false && !$extension->isBundled()) {
+ $popularityText = sprintf($this->getLang('popularity'), round($popularity*100, 2));
+ $return .= '<div class="popularity" title="'.$popularityText.'"><div style="width: '.($popularity * 100).'%;"><span class="a11y">'.$popularityText.'</span></div></div>'.DOKU_LF;
+ }
+
+ if($extension->getDescription()) {
+ $return .= '<p><bdi>';
+ $return .= hsc($extension->getDescription()).' ';
+ $return .= '</bdi></p>'.DOKU_LF;
+ }
+
+ $return .= $this->make_linkbar($extension);
+
+ if($showinfo){
+ $url = $this->gui->tabURL('');
+ $class = 'close';
+ }else{
+ $url = $this->gui->tabURL('', array('info' => $extension->getID()));
+ $class = '';
+ }
+ $return .= ' <a href="'.$url.'#extensionplugin__'.$extension->getID().'" class="info '.$class.'" title="'.$this->getLang('btn_info').'" data-extid="'.$extension->getID().'">'.$this->getLang('btn_info').'</a>';
+
+ if ($showinfo) {
+ $return .= $this->make_info($extension);
+ }
+ $return .= $this->make_noticearea($extension);
+ $return .= '</div>'.DOKU_LF;
+ return $return;
+ }
+
+ /**
+ * Generate the link bar HTML code
+ *
+ * @param helper_plugin_extension_extension $extension The extension instance
+ * @return string The HTML code
+ */
+ function make_linkbar(helper_plugin_extension_extension $extension) {
+ $return = '<div class="linkbar">';
+ $return .= $this->make_homepagelink($extension);
+ if ($extension->getBugtrackerURL()) {
+ $return .= ' <a href="'.hsc($extension->getBugtrackerURL()).'" title="'.hsc($extension->getBugtrackerURL()).'" class ="interwiki iw_dokubug">'.$this->getLang('bugs_features').'</a> ';
+ }
+ if ($extension->getTags()){
+ $first = true;
+ $return .= '<span class="tags">'.$this->getLang('tags').' ';
+ foreach ($extension->getTags() as $tag) {
+ if (!$first){
+ $return .= ', ';
+ } else {
+ $first = false;
+ }
+ $url = $this->gui->tabURL('search', array('q' => 'tag:'.$tag));
+ $return .= '<bdi><a href="'.$url.'">'.hsc($tag).'</a></bdi>';
+ }
+ $return .= '</span>';
+ }
+ $return .= '</div>'.DOKU_LF;
+ return $return;
+ }
+
+ /**
+ * Notice area
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ function make_noticearea(helper_plugin_extension_extension $extension) {
+ $return = '';
+ $missing_dependencies = $extension->getMissingDependencies();
+ if(!empty($missing_dependencies)) {
+ $return .= '<div class="msg error">'.
+ sprintf($this->getLang('missing_dependency'), '<bdi>'.implode(', ', /*array_map(array($this->helper, 'make_extensionsearchlink'),*/ $missing_dependencies).'</bdi>').
+ '</div>';
+ }
+ if($extension->isInWrongFolder()) {
+ $return .= '<div class="msg error">'.
+ sprintf($this->getLang('wrong_folder'), '<bdi>'.hsc($extension->getInstallName()).'</bdi>', '<bdi>'.hsc($extension->getBase()).'</bdi>').
+ '</div>';
+ }
+ if(($securityissue = $extension->getSecurityIssue()) !== false) {
+ $return .= '<div class="msg error">'.
+ sprintf($this->getLang('security_issue'), '<bdi>'.hsc($securityissue).'</bdi>').
+ '</div>';
+ }
+ if(($securitywarning = $extension->getSecurityWarning()) !== false) {
+ $return .= '<div class="msg notify">'.
+ sprintf($this->getLang('security_warning'), '<bdi>'.hsc($securitywarning).'</bdi>').
+ '</div>';
+ }
+ if($extension->updateAvailable()) {
+ $return .= '<div class="msg notify">'.
+ sprintf($this->getLang('update_available'), hsc($extension->getLastUpdate())).
+ '</div>';
+ }
+ if($extension->hasDownloadURLChanged()) {
+ $return .= '<div class="msg notify">'.
+ sprintf($this->getLang('url_change'), '<bdi>'.hsc($extension->getDownloadURL()).'</bdi>', '<bdi>'.hsc($extension->getLastDownloadURL()).'</bdi>').
+ '</div>';
+ }
+ return $return.DOKU_LF;
+ }
+
+ /**
+ * Create a link from the given URL
+ *
+ * Shortens the URL for display
+ *
+ * @param string $url
+ *
+ * @return string HTML link
+ */
+ function shortlink($url){
+ $link = parse_url($url);
+
+ $base = $link['host'];
+ if($link['port']) $base .= $base.':'.$link['port'];
+ $long = $link['path'];
+ if($link['query']) $long .= $link['query'];
+
+ $name = shorten($base, $long, 55);
+
+ return '<a href="'.hsc($url).'" class="urlextern">'.hsc($name).'</a>';
+ }
+
+ /**
+ * Plugin/template details
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ function make_info(helper_plugin_extension_extension $extension) {
+ $default = $this->getLang('unknown');
+ $return = '<dl class="details">';
+
+ $return .= '<dt>'.$this->getLang('status').'</dt>';
+ $return .= '<dd>'.$this->make_status($extension).'</dd>';
+
+ if ($extension->getDonationURL()) {
+ $return .= '<dt>'.$this->getLang('donate').'</dt>';
+ $return .= '<dd>';
+ $return .= '<a href="'.$extension->getDonationURL().'" class="donate">'.$this->getLang('donate_action').'</a>';
+ $return .= '</dd>';
+ }
+
+ if (!$extension->isBundled()) {
+ $return .= '<dt>'.$this->getLang('downloadurl').'</dt>';
+ $return .= '<dd><bdi>';
+ $return .= ($extension->getDownloadURL() ? $this->shortlink($extension->getDownloadURL()) : $default);
+ $return .= '</bdi></dd>';
+
+ $return .= '<dt>'.$this->getLang('repository').'</dt>';
+ $return .= '<dd><bdi>';
+ $return .= ($extension->getSourcerepoURL() ? $this->shortlink($extension->getSourcerepoURL()) : $default);
+ $return .= '</bdi></dd>';
+ }
+
+ if ($extension->isInstalled()) {
+ if ($extension->getInstalledVersion()) {
+ $return .= '<dt>'.$this->getLang('installed_version').'</dt>';
+ $return .= '<dd>';
+ $return .= hsc($extension->getInstalledVersion());
+ $return .= '</dd>';
+ } else {
+ $return .= '<dt>'.$this->getLang('install_date').'</dt>';
+ $return .= '<dd>';
+ $return .= ($extension->getUpdateDate() ? hsc($extension->getUpdateDate()) : $this->getLang('unknown'));
+ $return .= '</dd>';
+ }
+ }
+ if (!$extension->isInstalled() || $extension->updateAvailable()) {
+ $return .= '<dt>'.$this->getLang('available_version').'</dt>';
+ $return .= '<dd>';
+ $return .= ($extension->getLastUpdate() ? hsc($extension->getLastUpdate()) : $this->getLang('unknown'));
+ $return .= '</dd>';
+ }
+
+ if($extension->getInstallDate()) {
+ $return .= '<dt>'.$this->getLang('installed').'</dt>';
+ $return .= '<dd>';
+ $return .= hsc($extension->getInstallDate());
+ $return .= '</dd>';
+ }
+
+ $return .= '<dt>'.$this->getLang('provides').'</dt>';
+ $return .= '<dd><bdi>';
+ $return .= ($extension->getTypes() ? hsc(implode(', ', $extension->getTypes())) : $default);
+ $return .= '</bdi></dd>';
+
+ if(!$extension->isBundled() && $extension->getCompatibleVersions()) {
+ $return .= '<dt>'.$this->getLang('compatible').'</dt>';
+ $return .= '<dd>';
+ foreach ($extension->getCompatibleVersions() as $date => $version) {
+ $return .= '<bdi>'.$version['label'].' ('.$date.')</bdi>, ';
+ }
+ $return = rtrim($return, ', ');
+ $return .= '</dd>';
+ }
+ if($extension->getDependencies()) {
+ $return .= '<dt>'.$this->getLang('depends').'</dt>';
+ $return .= '<dd>';
+ $return .= $this->make_linklist($extension->getDependencies());
+ $return .= '</dd>';
+ }
+
+ if($extension->getSimilarExtensions()) {
+ $return .= '<dt>'.$this->getLang('similar').'</dt>';
+ $return .= '<dd>';
+ $return .= $this->make_linklist($extension->getSimilarExtensions());
+ $return .= '</dd>';
+ }
+
+ if($extension->getConflicts()) {
+ $return .= '<dt>'.$this->getLang('conflicts').'</dt>';
+ $return .= '<dd>';
+ $return .= $this->make_linklist($extension->getConflicts());
+ $return .= '</dd>';
+ }
+ $return .= '</dl>'.DOKU_LF;
+ return $return;
+ }
+
+ /**
+ * Generate a list of links for extensions
+ *
+ * @param array $ext The extensions
+ * @return string The HTML code
+ */
+ function make_linklist($ext) {
+ $return = '';
+ foreach ($ext as $link) {
+ $return .= '<bdi><a href="'.$this->gui->tabURL('search', array('q'=>'ext:'.$link)).'">'.hsc($link).'</a></bdi>, ';
+ }
+ return rtrim($return, ', ');
+ }
+
+ /**
+ * Display the action buttons if they are possible
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ function make_actions(helper_plugin_extension_extension $extension) {
+ $return = '';
+ $errors = '';
+
+ if ($extension->isInstalled()) {
+ if (($canmod = $extension->canModify()) === true) {
+ if (!$extension->isProtected()) {
+ $return .= $this->make_action('uninstall', $extension);
+ }
+ if ($extension->getDownloadURL()) {
+ if ($extension->updateAvailable()) {
+ $return .= $this->make_action('update', $extension);
+ } else {
+ $return .= $this->make_action('reinstall', $extension);
+ }
+ }
+ }else{
+ $errors .= '<p class="permerror">'.$this->getLang($canmod).'</p>';
+ }
+
+ if (!$extension->isProtected() && !$extension->isTemplate()) { // no enable/disable for templates
+ if ($extension->isEnabled()) {
+ $return .= $this->make_action('disable', $extension);
+ } else {
+ $return .= $this->make_action('enable', $extension);
+ }
+ }
+
+ if ($extension->isGitControlled()){
+ $errors .= '<p class="permerror">'.$this->getLang('git').'</p>';
+ }
+
+ }else{
+ if (($canmod = $extension->canModify()) === true) {
+ if ($extension->getDownloadURL()) {
+ $return .= $this->make_action('install', $extension);
+ }
+ }else{
+ $errors .= '<div class="permerror">'.$this->getLang($canmod).'</div>';
+ }
+ }
+
+ if (!$extension->isInstalled() && $extension->getDownloadURL()) {
+ $return .= ' <span class="version">'.$this->getLang('available_version').' ';
+ $return .= ($extension->getLastUpdate() ? hsc($extension->getLastUpdate()) : $this->getLang('unknown')).'</span>';
+ }
+
+ return $return.' '.$errors.DOKU_LF;
+ }
+
+ /**
+ * Display an action button for an extension
+ *
+ * @param string $action The action
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The HTML code
+ */
+ function make_action($action, $extension) {
+ $title = '';
+
+ switch ($action) {
+ case 'install':
+ case 'reinstall':
+ $title = 'title="'.hsc($extension->getDownloadURL()).'"';
+ break;
+ }
+
+ $classes = 'button '.$action;
+ $name = 'fn['.$action.']['.hsc($extension->getID()).']';
+
+ return '<input class="'.$classes.'" name="'.$name.'" type="submit" value="'.$this->getLang('btn_'.$action).'" '.$title.' />';
+ }
+
+ /**
+ * Plugin/template status
+ *
+ * @param helper_plugin_extension_extension $extension The extension
+ * @return string The description of all relevant statusses
+ */
+ function make_status(helper_plugin_extension_extension $extension) {
+ $status = array();
+
+
+ if ($extension->isInstalled()) {
+ $status[] = $this->getLang('status_installed');
+ if ($extension->isProtected()) {
+ $status[] = $this->getLang('status_protected');
+ } else {
+ $status[] = $extension->isEnabled() ? $this->getLang('status_enabled') : $this->getLang('status_disabled');
+ }
+ } else {
+ $status[] = $this->getLang('status_not_installed');
+ }
+ if(!$extension->canModify()) $status[] = $this->getLang('status_unmodifiable');
+ if($extension->isBundled()) $status[] = $this->getLang('status_bundled');
+ $status[] = $extension->isTemplate() ? $this->getLang('status_template') : $this->getLang('status_plugin');
+ return join(', ', $status);
+ }
+
+}
diff --git a/lib/plugins/extension/helper/repository.php b/lib/plugins/extension/helper/repository.php
new file mode 100644
index 000000000..6ffe89eb7
--- /dev/null
+++ b/lib/plugins/extension/helper/repository.php
@@ -0,0 +1,191 @@
+<?php
+/**
+ * DokuWiki Plugin extension (Helper Component)
+ *
+ * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
+ * @author Michael Hamann <michael@content-space.de>
+ */
+
+#define('EXTENSION_REPOSITORY_API', 'http://localhost/dokuwiki/lib/plugins/pluginrepo/api.php');
+
+if (!defined('EXTENSION_REPOSITORY_API_ENDPOINT'))
+ define('EXTENSION_REPOSITORY_API', 'http://www.dokuwiki.org/lib/plugins/pluginrepo/api.php');
+
+// must be run within Dokuwiki
+if(!defined('DOKU_INC')) die();
+
+/**
+ * Class helper_plugin_extension_repository provides access to the extension repository on dokuwiki.org
+ */
+class helper_plugin_extension_repository extends DokuWiki_Plugin {
+ private $loaded_extensions = array();
+ private $has_access = null;
+ /**
+ * Initialize the repository (cache), fetches data for all installed plugins
+ */
+ public function init() {
+ /* @var Doku_Plugin_Controller $plugin_controller */
+ global $plugin_controller;
+ if ($this->hasAccess()) {
+ $list = $plugin_controller->getList('', true);
+ $request_data = array('fmt' => 'php');
+ $request_needed = false;
+ foreach ($list as $name) {
+ $cache = new cache('##extension_manager##'.$name, '.repo');
+ $result = null;
+ if (!isset($this->loaded_extensions[$name]) && $this->hasAccess() && !$cache->useCache(array('age' => 3600 * 24))) {
+ $this->loaded_extensions[$name] = true;
+ $request_data['ext'][] = $name;
+ $request_needed = true;
+ }
+ }
+
+ if ($request_needed) {
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->post(EXTENSION_REPOSITORY_API, $request_data);
+ if ($data !== false) {
+ $extensions = unserialize($data);
+ foreach ($extensions as $extension) {
+ $cache = new cache('##extension_manager##'.$extension['plugin'], '.repo');
+ $cache->storeCache(serialize($extension));
+ }
+ } else {
+ $this->has_access = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * If repository access is available
+ *
+ * @return bool If repository access is available
+ */
+ public function hasAccess() {
+ if ($this->has_access === null) {
+ $cache = new cache('##extension_manager###hasAccess', '.repo');
+ $result = null;
+ if (!$cache->useCache(array('age' => 3600 * 24, 'purge'=>1))) {
+ $httpclient = new DokuHTTPClient();
+ $httpclient->timeout = 5;
+ $data = $httpclient->get(EXTENSION_REPOSITORY_API.'?cmd=ping');
+ if ($data !== false) {
+ $this->has_access = true;
+ $cache->storeCache(1);
+ } else {
+ $this->has_access = false;
+ $cache->storeCache(0);
+ }
+ } else {
+ $this->has_access = ($cache->retrieveCache(false) == 1);
+ }
+ }
+ return $this->has_access;
+ }
+
+ /**
+ * Get the remote data of an individual plugin or template
+ *
+ * @param string $name The plugin name to get the data for, template names need to be prefix by 'template:'
+ * @return array The data or null if nothing was found (possibly no repository access)
+ */
+ public function getData($name) {
+ $cache = new cache('##extension_manager##'.$name, '.repo');
+ $result = null;
+ if (!isset($this->loaded_extensions[$name]) && $this->hasAccess() && !$cache->useCache(array('age' => 3600 * 24))) {
+ $this->loaded_extensions[$name] = true;
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->get(EXTENSION_REPOSITORY_API.'?fmt=php&ext[]='.urlencode($name));
+ if ($data !== false) {
+ $result = unserialize($data);
+ $cache->storeCache(serialize($result[0]));
+ return $result[0];
+ } else {
+ $this->has_access = false;
+ }
+ }
+ if (file_exists($cache->cache)) {
+ return unserialize($cache->retrieveCache(false));
+ }
+ return array();
+ }
+
+ /**
+ * Search for plugins or templates using the given query string
+ *
+ * @param string $q the query string
+ * @return array a list of matching extensions
+ */
+ public function search($q){
+ $query = $this->parse_query($q);
+ $query['fmt'] = 'php';
+
+ $httpclient = new DokuHTTPClient();
+ $data = $httpclient->post(EXTENSION_REPOSITORY_API, $query);
+ if ($data === false) return array();
+ $result = unserialize($data);
+
+ $ids = array();
+
+ // store cache info for each extension
+ foreach($result as $ext){
+ $name = $ext['plugin'];
+ $cache = new cache('##extension_manager##'.$name, '.repo');
+ $cache->storeCache(serialize($ext));
+ $ids[] = $name;
+ }
+
+ return $ids;
+ }
+
+ /**
+ * Parses special queries from the query string
+ *
+ * @param string $q
+ * @return array
+ */
+ protected function parse_query($q){
+ $parameters = array(
+ 'tag' => array(),
+ 'mail' => array(),
+ 'type' => array(),
+ 'ext' => array()
+ );
+
+ // extract tags
+ if(preg_match_all('/(^|\s)(tag:([\S]+))/', $q, $matches, PREG_SET_ORDER)){
+ foreach($matches as $m){
+ $q = str_replace($m[2], '', $q);
+ $parameters['tag'][] = $m[3];
+ }
+ }
+ // extract author ids
+ if(preg_match_all('/(^|\s)(authorid:([\S]+))/', $q, $matches, PREG_SET_ORDER)){
+ foreach($matches as $m){
+ $q = str_replace($m[2], '', $q);
+ $parameters['mail'][] = $m[3];
+ }
+ }
+ // extract extensions
+ if(preg_match_all('/(^|\s)(ext:([\S]+))/', $q, $matches, PREG_SET_ORDER)){
+ foreach($matches as $m){
+ $q = str_replace($m[2], '', $q);
+ $parameters['ext'][] = $m[3];
+ }
+ }
+ // extract types
+ if(preg_match_all('/(^|\s)(type:([\S]+))/', $q, $matches, PREG_SET_ORDER)){
+ foreach($matches as $m){
+ $q = str_replace($m[2], '', $q);
+ $parameters['type'][] = $m[3];
+ }
+ }
+
+ // FIXME make integer from type value
+
+ $parameters['q'] = trim($q);
+ return $parameters;
+ }
+}
+
+// vim:ts=4:sw=4:et:
diff --git a/lib/plugins/extension/images/disabled.png b/lib/plugins/extension/images/disabled.png
new file mode 100644
index 000000000..7a0dbb3b5
--- /dev/null
+++ b/lib/plugins/extension/images/disabled.png
Binary files differ
diff --git a/lib/plugins/extension/images/donate.png b/lib/plugins/extension/images/donate.png
new file mode 100644
index 000000000..9e234da1c
--- /dev/null
+++ b/lib/plugins/extension/images/donate.png
Binary files differ
diff --git a/lib/plugins/extension/images/down.png b/lib/plugins/extension/images/down.png
new file mode 100644
index 000000000..df7beda4e
--- /dev/null
+++ b/lib/plugins/extension/images/down.png
Binary files differ
diff --git a/lib/plugins/extension/images/enabled.png b/lib/plugins/extension/images/enabled.png
new file mode 100644
index 000000000..7c051cda1
--- /dev/null
+++ b/lib/plugins/extension/images/enabled.png
Binary files differ
diff --git a/lib/plugins/extension/images/icons.xcf b/lib/plugins/extension/images/icons.xcf
new file mode 100644
index 000000000..ab69b3099
--- /dev/null
+++ b/lib/plugins/extension/images/icons.xcf
Binary files differ
diff --git a/lib/plugins/extension/images/license.txt b/lib/plugins/extension/images/license.txt
new file mode 100644
index 000000000..254b9cdf6
--- /dev/null
+++ b/lib/plugins/extension/images/license.txt
@@ -0,0 +1,4 @@
+enabled.png - CC-BY-ND, (c) Emey87 http://www.iconfinder.com/icondetails/65590/48/lightbulb_icon
+disabled.png - CC-BY-ND, (c) Emey87 http://www.iconfinder.com/icondetails/65589/48/idea_lightbulb_off_icon
+plugin.png - public domain, (c) nicubunu, http://openclipart.org/detail/15093/blue-jigsaw-piece-07-by-nicubunu
+template.png - public domain, (c) mathec, http://openclipart.org/detail/166596/palette-by-mathec
diff --git a/lib/plugins/extension/images/overlay.png b/lib/plugins/extension/images/overlay.png
new file mode 100644
index 000000000..8f92c2fe7
--- /dev/null
+++ b/lib/plugins/extension/images/overlay.png
Binary files differ
diff --git a/lib/plugins/extension/images/plugin.png b/lib/plugins/extension/images/plugin.png
new file mode 100644
index 000000000..e4a2d3be6
--- /dev/null
+++ b/lib/plugins/extension/images/plugin.png
Binary files differ
diff --git a/lib/plugins/extension/images/tag.png b/lib/plugins/extension/images/tag.png
new file mode 100644
index 000000000..155dbb3dd
--- /dev/null
+++ b/lib/plugins/extension/images/tag.png
Binary files differ
diff --git a/lib/plugins/extension/images/template.png b/lib/plugins/extension/images/template.png
new file mode 100644
index 000000000..ee74bc1d5
--- /dev/null
+++ b/lib/plugins/extension/images/template.png
Binary files differ
diff --git a/lib/plugins/extension/images/up.png b/lib/plugins/extension/images/up.png
new file mode 100644
index 000000000..ec9337715
--- /dev/null
+++ b/lib/plugins/extension/images/up.png
Binary files differ
diff --git a/lib/plugins/extension/images/warning.png b/lib/plugins/extension/images/warning.png
new file mode 100644
index 000000000..c5e482f84
--- /dev/null
+++ b/lib/plugins/extension/images/warning.png
Binary files differ
diff --git a/lib/plugins/extension/lang/de/intro_install.txt b/lib/plugins/extension/lang/de/intro_install.txt
new file mode 100644
index 000000000..4ecebe959
--- /dev/null
+++ b/lib/plugins/extension/lang/de/intro_install.txt
@@ -0,0 +1 @@
+Hier können Sie Plugins und Templates von Hand installieren indem Sie sie hochladen oder eine Download-URL angeben. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/de/intro_plugins.txt b/lib/plugins/extension/lang/de/intro_plugins.txt
new file mode 100644
index 000000000..1a1521050
--- /dev/null
+++ b/lib/plugins/extension/lang/de/intro_plugins.txt
@@ -0,0 +1 @@
+Dies sind die Plugins, die bereits installiert sind. Sie können sie hier an- oder abschalten oder sie komplett deinstallieren. Außerdem werden hier Updates zu den installiereten Plugins angezeigt. Bitte lesen Sie vor einem Update die zugehörige Dokumentation. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/de/intro_search.txt b/lib/plugins/extension/lang/de/intro_search.txt
new file mode 100644
index 000000000..7df8de185
--- /dev/null
+++ b/lib/plugins/extension/lang/de/intro_search.txt
@@ -0,0 +1 @@
+Dieser Tab gibt Ihnen Zugriff auf alle vorhandenen Plugins und Templates für DokuWiki. Bitte bedenken sie das jede installierte Erweiterung ein Sicherheitsrisiko darstellen kann. Sie sollten vor einer Installation die [[doku>security#plugin_security|Plugin Security]] Informationen lesen. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/de/intro_templates.txt b/lib/plugins/extension/lang/de/intro_templates.txt
new file mode 100644
index 000000000..d71ce6237
--- /dev/null
+++ b/lib/plugins/extension/lang/de/intro_templates.txt
@@ -0,0 +1 @@
+Dies sind die in Ihrem Dokuwiki installierten Templates. Sie können das gewünschte Template im [[?do=admin&page=config|Konfigurations Manager]] aktivieren. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/de/lang.php b/lib/plugins/extension/lang/de/lang.php
new file mode 100644
index 000000000..f2333cc25
--- /dev/null
+++ b/lib/plugins/extension/lang/de/lang.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author H. Richard <wanderer379@t-online.de>
+ * @author Joerg <scooter22@gmx.de>
+ */
+$lang['menu'] = 'Erweiterungen verwalten';
+$lang['tab_plugins'] = 'Installierte Plugins';
+$lang['tab_templates'] = 'Installierte Templates';
+$lang['tab_search'] = 'Suchen und Installieren';
+$lang['tab_install'] = 'Händisch installieren';
+$lang['notimplemented'] = 'Dieses Fähigkeit/Eigenschaft wurde noch nicht implementiert';
+$lang['notinstalled'] = 'Diese Erweiterung ist nicht installiert';
+$lang['alreadyenabled'] = 'Diese Erweiterung ist bereits aktiviert';
+$lang['alreadydisabled'] = 'Diese Erweiterung ist bereits deaktiviert';
+$lang['pluginlistsaveerror'] = 'Es gab einen Fehler beim Speichern der Plugin-Liste';
+$lang['unknownauthor'] = 'Unbekannter Autor';
+$lang['unknownversion'] = 'Unbekannte Version';
+$lang['btn_info'] = 'Zeige weitere Info';
+$lang['btn_update'] = 'Update';
+$lang['btn_uninstall'] = 'Deinstallation';
+$lang['btn_enable'] = 'Aktivieren';
+$lang['btn_disable'] = 'Deaktivieren';
+$lang['btn_install'] = 'Installieren';
+$lang['btn_reinstall'] = 'Neu installieren';
+$lang['js']['reallydel'] = 'Wollen Sie diese Erweiterung wirklich löschen?';
+$lang['search_for'] = 'Erweiterung suchen:';
+$lang['search'] = 'Suchen';
+$lang['extensionby'] = '<strong>%s</strong> von %s';
+$lang['screenshot'] = 'Bildschirmfoto von %s';
+$lang['popularity'] = 'Popularität: %s%%';
+$lang['homepage_link'] = 'Doku';
+$lang['bugs_features'] = 'Bugs';
+$lang['tags'] = 'Schlagworte';
+$lang['author_hint'] = 'Suche weitere Erweiterungen dieses Autors';
+$lang['installed'] = 'Installiert:';
+$lang['downloadurl'] = 'URL zum Herunterladen';
+$lang['repository'] = 'Quelle:';
+$lang['unknown'] = '<em>unbekannt</em>';
+$lang['installed_version'] = 'Installierte Version';
+$lang['install_date'] = 'Ihr letztes Update:';
+$lang['available_version'] = 'Verfügbare Version: ';
+$lang['compatible'] = 'Kompatibel mit:';
+$lang['depends'] = 'Benötigt:';
+$lang['similar'] = 'Ist ähnlich zu:';
+$lang['conflicts'] = 'Nicht kompatibel mit:';
+$lang['donate'] = 'Nützlich?';
+$lang['donate_action'] = 'Spendieren Sie dem Autor einen Kaffee!';
+$lang['repo_retry'] = 'Neu versuchen';
+$lang['provides'] = 'Enthält';
+$lang['status'] = 'Status';
+$lang['status_installed'] = 'installiert';
+$lang['status_not_installed'] = 'nicht installiert';
+$lang['status_protected'] = 'geschützt';
+$lang['status_enabled'] = 'aktiviert';
+$lang['status_disabled'] = 'deaktiviert';
+$lang['status_unmodifiable'] = 'unveränderlich';
+$lang['status_plugin'] = 'Plugin';
+$lang['status_template'] = 'Template';
+$lang['status_bundled'] = 'gebündelt';
+$lang['msg_enabled'] = 'Plugin %s ist aktiviert';
+$lang['msg_disabled'] = 'Erweiterung %s ist deaktiviert';
+$lang['msg_delete_success'] = 'Erweiterung wurde entfernt';
+$lang['msg_template_install_success'] = 'Das Template %s wurde erfolgreich installiert';
+$lang['msg_template_update_success'] = 'Das Update des Templates %s war erfolgreich ';
+$lang['msg_plugin_install_success'] = 'Das Plugin %s wurde erfolgreich installiert';
+$lang['msg_plugin_update_success'] = 'Das Update des Plugins %s war erfolgreich';
+$lang['msg_upload_failed'] = 'Fehler beim Hochladen der Datei';
+$lang['missing_dependency'] = '<strong>fehlende oder deaktivierte Abhängigkeit:<strong>%s';
+$lang['error_badurl'] = 'URLs sollten mit http oder https beginnen';
+$lang['error_dircreate'] = 'Temporären Ordner konnte nicht erstellt werden, um Download zu empfangen';
+$lang['error_download'] = 'Download der Datei: %s nicht möglich.';
+$lang['error_decompress'] = 'Die heruntergeladene Datei konnte nicht entpackt werden. Dies kann die Folge eines fehlerhaften Downloads sein. In diesem Fall sollten Sie versuchen den Vorgang zu wiederholen. Es kann auch die Folge eines unbekannten Kompressionsformates sein, in diesem ​​Fall müssen Sie die Datei selber herunterladen und manuell installieren.';
+$lang['error_findfolder'] = 'Das Erweiterungs-Verzeichnis konnte nicht identifiziert werden, laden und installieren sie die Datei manuell.';
+$lang['error_copy'] = 'Beim Versuch Dateien in den Ordner <em>%s</em>: zu installieren trat ein Kopierfehler auf. Die Dateizugriffsberechtigungen könnten falsch sein. Dies kann an einem unvollständig installierten Plugin liegen und beeinträchtigt somit die Stabilität Ihre Wiki-Installation.';
+$lang['noperms'] = 'Das Erweiterungs-Verzeichnis ist schreibgeschützt';
+$lang['notplperms'] = 'Das Template-Verzeichnis ist schreibgeschützt';
+$lang['nopluginperms'] = 'Das Plugin-Verzeichnis ist schreibgeschützt';
+$lang['git'] = 'Diese Erweiterung wurde über git installiert, daher kann diese nicht hier aktualisiert werden.';
+$lang['install_url'] = 'Von Webadresse (URL) installieren';
+$lang['install_upload'] = 'Erweiterung hochladen:';
diff --git a/lib/plugins/extension/lang/en/intro_install.txt b/lib/plugins/extension/lang/en/intro_install.txt
new file mode 100644
index 000000000..a5d5ab008
--- /dev/null
+++ b/lib/plugins/extension/lang/en/intro_install.txt
@@ -0,0 +1 @@
+Here you can manually install plugins and templates by either uploading them or providing a direct download URL.
diff --git a/lib/plugins/extension/lang/en/intro_plugins.txt b/lib/plugins/extension/lang/en/intro_plugins.txt
new file mode 100644
index 000000000..4e42efee1
--- /dev/null
+++ b/lib/plugins/extension/lang/en/intro_plugins.txt
@@ -0,0 +1 @@
+These are the plugins currently installed in your DokuWiki. You can enable or disable or even completely uninstall them here. Plugin updates are shown here as well, be sure to read the plugin's documentation before updating. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/en/intro_search.txt b/lib/plugins/extension/lang/en/intro_search.txt
new file mode 100644
index 000000000..244cd6812
--- /dev/null
+++ b/lib/plugins/extension/lang/en/intro_search.txt
@@ -0,0 +1 @@
+This tab gives you access to all available 3rd party plugins and templates for DokuWiki. Please be aware that installing 3rd party code may pose a **security risk**, you may want to read about [[doku>security#plugin_security|plugin security]] first. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/en/intro_templates.txt b/lib/plugins/extension/lang/en/intro_templates.txt
new file mode 100644
index 000000000..012a74995
--- /dev/null
+++ b/lib/plugins/extension/lang/en/intro_templates.txt
@@ -0,0 +1 @@
+These are the templates currently installed in your DokuWiki. You can select the template to be used in the [[?do=admin&page=config|Configuration Manager]].
diff --git a/lib/plugins/extension/lang/en/lang.php b/lib/plugins/extension/lang/en/lang.php
new file mode 100644
index 000000000..5224f694a
--- /dev/null
+++ b/lib/plugins/extension/lang/en/lang.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * English language file for extension plugin
+ *
+ * @author Michael Hamann <michael@content-space.de>
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ */
+
+$lang['menu'] = 'Extension Manager';
+
+$lang['tab_plugins'] = 'Installed Plugins';
+$lang['tab_templates'] = 'Installed Templates';
+$lang['tab_search'] = 'Search and Install';
+$lang['tab_install'] = 'Manual Install';
+
+$lang['notimplemented'] = 'This feature hasn\'t been implemented yet';
+$lang['notinstalled'] = 'This extension is not installed';
+$lang['alreadyenabled'] = 'This extension has already been enabled';
+$lang['alreadydisabled'] = 'This extension has already been disabled';
+$lang['pluginlistsaveerror'] = 'There was an error saving the plugin list';
+$lang['unknownauthor'] = 'Unknown author';
+$lang['unknownversion'] = 'Unknown version';
+
+$lang['btn_info'] = 'Show more info';
+$lang['btn_update'] = 'Update';
+$lang['btn_uninstall'] = 'Uninstall';
+$lang['btn_enable'] = 'Enable';
+$lang['btn_disable'] = 'Disable';
+$lang['btn_install'] = 'Install';
+$lang['btn_reinstall'] = 'Re-install';
+
+$lang['js']['reallydel'] = 'Really uninstall this extension?';
+
+$lang['search_for'] = 'Search Extension:';
+$lang['search'] = 'Search';
+
+$lang['extensionby'] = '<strong>%s</strong> by %s';
+$lang['screenshot'] = 'Screenshot of %s';
+$lang['popularity'] = 'Popularity: %s%%';
+$lang['homepage_link'] = 'Docs';
+$lang['bugs_features'] = 'Bugs';
+$lang['tags'] = 'Tags:';
+$lang['author_hint'] = 'Search extensions by this author';
+$lang['installed'] = 'Installed:';
+$lang['downloadurl'] = 'Download URL:';
+$lang['repository'] = 'Repository:';
+$lang['unknown'] = '<em>unknown</em>';
+$lang['installed_version'] = 'Installed version:';
+$lang['install_date'] = 'Your last update:';
+$lang['available_version'] = 'Available version:';
+$lang['compatible'] = 'Compatible with:';
+$lang['depends'] = 'Depends on:';
+$lang['similar'] = 'Similar to:';
+$lang['conflicts'] = 'Conflicts with:';
+$lang['donate'] = 'Like this?';
+$lang['donate_action'] = 'Buy the author a coffee!';
+$lang['repo_retry'] = 'Retry';
+$lang['provides'] = 'Provides:';
+$lang['status'] = 'Status:';
+$lang['status_installed'] = 'installed';
+$lang['status_not_installed'] = 'not installed';
+$lang['status_protected'] = 'protected';
+$lang['status_enabled'] = 'enabled';
+$lang['status_disabled'] = 'disabled';
+$lang['status_unmodifiable'] = 'unmodifiable';
+$lang['status_plugin'] = 'plugin';
+$lang['status_template'] = 'template';
+$lang['status_bundled'] = 'bundled';
+
+$lang['msg_enabled'] = 'Plugin %s enabled';
+$lang['msg_disabled'] = 'Plugin %s disabled';
+$lang['msg_delete_success'] = 'Extension uninstalled';
+$lang['msg_template_install_success'] = 'Template %s installed successfully';
+$lang['msg_template_update_success'] = 'Template %s updated successfully';
+$lang['msg_plugin_install_success'] = 'Plugin %s installed successfully';
+$lang['msg_plugin_update_success'] = 'Plugin %s updated successfully';
+$lang['msg_upload_failed'] = 'Uploading the file failed';
+
+$lang['missing_dependency'] = '<strong>Missing or disabled dependency:</strong> %s';
+$lang['security_issue'] = '<strong>Security Issue:</strong> %s';
+$lang['security_warning'] = '<strong>Security Warning:</strong> %s';
+$lang['update_available'] = '<strong>Update:</strong> New version %s is available.';
+$lang['wrong_folder'] = '<strong>Plugin installed incorrectly:</strong> Rename plugin directory "%s" to "%s".';
+$lang['url_change'] = '<strong>URL changed:</strong> Download URL has changed since last download. Check if the new URL is valid before updating the extension.<br />New: %s<br />Old: %s';
+
+$lang['error_badurl'] = 'URLs should start with http or https';
+$lang['error_dircreate'] = 'Unable to create temporary folder to receive download';
+$lang['error_download'] = 'Unable to download the file: %s';
+$lang['error_decompress'] = 'Unable to decompress the downloaded file. This maybe as a result of a bad download, in which case you should try again; or the compression format may be unknown, in which case you will need to download and install manually.';
+$lang['error_findfolder'] = 'Unable to identify extension directory, you need to download and install manually';
+$lang['error_copy'] = 'There was a file copy error while attempting to install files for directory <em>%s</em>: the disk could be full or file access permissions may be incorrect. This may have resulted in a partially installed plugin and leave your wiki installation unstable';
+
+$lang['noperms'] = 'Extension directory is not writable';
+$lang['notplperms'] = 'Template directory is not writable';
+$lang['nopluginperms'] = 'Plugin directory is not writable';
+$lang['git'] = 'This extension was installed via git, you may not want to update it here.';
+
+$lang['install_url'] = 'Install from URL:';
+$lang['install_upload'] = 'Upload Extension:'; \ No newline at end of file
diff --git a/lib/plugins/extension/lang/eo/intro_install.txt b/lib/plugins/extension/lang/eo/intro_install.txt
new file mode 100644
index 000000000..d9c63da1d
--- /dev/null
+++ b/lib/plugins/extension/lang/eo/intro_install.txt
@@ -0,0 +1 @@
+Tie vi povas permane instali kromaĵojn kaj ŝablonojn tra alŝuto aŭ indiko de URL por rekta elŝuto. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/eo/intro_plugins.txt b/lib/plugins/extension/lang/eo/intro_plugins.txt
new file mode 100644
index 000000000..cc7ae6628
--- /dev/null
+++ b/lib/plugins/extension/lang/eo/intro_plugins.txt
@@ -0,0 +1 @@
+Jenaj kromaĵoj momente estas instalitaj en via DokuWiki. Vi povas ebligi, malebligi aŭ eĉ tute malinstali ilin tie. Ankaŭ montriĝos aktualigoj de kromaĵoj -- certiĝu, ke vi legis la dokumentadon de la kromaĵo antaŭ aktualigo. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/eo/intro_search.txt b/lib/plugins/extension/lang/eo/intro_search.txt
new file mode 100644
index 000000000..5d194948c
--- /dev/null
+++ b/lib/plugins/extension/lang/eo/intro_search.txt
@@ -0,0 +1 @@
+Tiu tabelo donas aliron al ĉiuj haveblaj eksteraj kromaĵoj kaj ŝablonoj por DokuWiki. Bonvolu konscii, ke instali eksteran kodaĵon povas enkonduki **sekurecriskon**, prefere legu antaŭe pri [[doku>security#plugin_security|sekureco de kromaĵo]]. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/eo/intro_templates.txt b/lib/plugins/extension/lang/eo/intro_templates.txt
new file mode 100644
index 000000000..6dc0ef671
--- /dev/null
+++ b/lib/plugins/extension/lang/eo/intro_templates.txt
@@ -0,0 +1 @@
+Jenaj ŝablonoj momente instaliĝis en via DokuWiki. Elektu la ŝablonon por uzi en la [[?do=admin&page=config|Opcia administrilo]]. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/eo/lang.php b/lib/plugins/extension/lang/eo/lang.php
new file mode 100644
index 000000000..6ce840be8
--- /dev/null
+++ b/lib/plugins/extension/lang/eo/lang.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Robert Bogenschneider <bogi@uea.org>
+ */
+$lang['menu'] = 'Aldonaĵa administrado';
+$lang['tab_plugins'] = 'Instalitaj kromaĵoj';
+$lang['tab_templates'] = 'Instalitaj ŝablonoj';
+$lang['tab_search'] = 'Serĉi kaj instali';
+$lang['tab_install'] = 'Permana instalado';
+$lang['notimplemented'] = 'Tiu funkcio ankoraŭ ne realiĝis';
+$lang['notinstalled'] = 'Tiu aldonaĵo ne estas instalita';
+$lang['alreadyenabled'] = 'Tiu aldonaĵo jam ebliĝis';
+$lang['alreadydisabled'] = 'Tiu aldonaĵo jam malebliĝis';
+$lang['pluginlistsaveerror'] = 'Okazis eraro dum la kromaĵlisto konserviĝis';
+$lang['unknownauthor'] = 'Nekonata aŭtoro';
+$lang['unknownversion'] = 'Nekonata versio';
+$lang['btn_info'] = 'Montri pliajn informojn';
+$lang['btn_update'] = 'Aktualigi';
+$lang['btn_uninstall'] = 'Malinstali';
+$lang['btn_enable'] = 'Ebligi';
+$lang['btn_disable'] = 'Malebligi';
+$lang['btn_install'] = 'Instali';
+$lang['btn_reinstall'] = 'Re-instali';
+$lang['js']['reallydel'] = 'Ĉu vere malinstali la aldonaĵon?';
+$lang['search_for'] = 'Serĉi la aldonaĵon:';
+$lang['search'] = 'Serĉi';
+$lang['extensionby'] = '<strong>%s</strong> fare de %s';
+$lang['screenshot'] = 'Ekrankopio de %s';
+$lang['popularity'] = 'Populareco: %s%%';
+$lang['homepage_link'] = 'Dokumentoj';
+$lang['bugs_features'] = 'Cimoj';
+$lang['tags'] = 'Etikedoj:';
+$lang['author_hint'] = 'Serĉi aldonaĵojn laŭ tiu aŭtoro:';
+$lang['installed'] = 'Instalitaj:';
+$lang['downloadurl'] = 'URL por elŝuti:';
+$lang['repository'] = 'Kodbranĉo:';
+$lang['unknown'] = '<em>nekonata</em>';
+$lang['installed_version'] = 'Instalita versio:';
+$lang['install_date'] = 'Via lasta aktualigo:';
+$lang['available_version'] = 'Havebla versio:';
+$lang['compatible'] = 'Kompatibla kun:';
+$lang['depends'] = 'Dependas de:';
+$lang['similar'] = 'Simila al:';
+$lang['conflicts'] = 'Konfliktas kun:';
+$lang['donate'] = 'Ĉu vi ŝatas tion?';
+$lang['donate_action'] = 'Aĉetu kafon al la aŭtoro!';
+$lang['repo_retry'] = 'Reprovi';
+$lang['provides'] = 'Provizas per:';
+$lang['status'] = 'Statuso:';
+$lang['status_installed'] = 'instalita';
+$lang['status_not_installed'] = 'ne instalita';
+$lang['status_protected'] = 'protektita';
+$lang['status_enabled'] = 'ebligita';
+$lang['status_disabled'] = 'malebligita';
+$lang['status_unmodifiable'] = 'neŝanĝebla';
+$lang['status_plugin'] = 'kromaĵo';
+$lang['status_template'] = 'ŝablono';
+$lang['status_bundled'] = 'kunliverita';
+$lang['msg_enabled'] = 'Kromaĵo %s ebligita';
+$lang['msg_disabled'] = 'Kromaĵo %s malebligita';
+$lang['msg_delete_success'] = 'Aldonaĵo malinstaliĝis';
+$lang['msg_template_install_success'] = 'Ŝablono %s sukcese instaliĝis';
+$lang['msg_template_update_success'] = 'Ŝablono %s sukcese aktualiĝis';
+$lang['msg_plugin_install_success'] = 'Kromaĵo %s sukcese instaliĝis';
+$lang['msg_plugin_update_success'] = 'Kromaĵo %s sukcese aktualiĝis';
+$lang['msg_upload_failed'] = 'Ne eblis alŝuti la dosieron';
+$lang['missing_dependency'] = '<strong>Mankanta aŭ malebligita dependeco:</strong> %s';
+$lang['security_issue'] = '<strong>Sekureca problemo:</strong> %s';
+$lang['security_warning'] = '<strong>Sekureca averto:</strong> %s';
+$lang['update_available'] = '<strong>Aktualigo:</strong> Nova versio %s haveblas.';
+$lang['wrong_folder'] = '<strong>Kromaĵo instalita malĝuste:</strong> Renomu la kromaĵdosierujon "%s" al "%s".';
+$lang['url_change'] = '<strong>URL ŝanĝita:</strong> La elŝuta URL ŝanĝiĝis ekde la lasta elŝuto. Kontrolu, ĉu la nova URL validas antaŭ aktualigi aldonaĵon.<br />Nova: %s<br />Malnova: %s';
+$lang['error_badurl'] = 'URLoj komenciĝu per http aŭ https';
+$lang['error_dircreate'] = 'Ne eblis krei portempan dosierujon por akcepti la elŝuton';
+$lang['error_download'] = 'Ne eblis elŝuti la dosieron: %s';
+$lang['error_decompress'] = 'Ne eblis malpaki la elŝutitan dosieron. Kialo povus esti fuŝa elŝuto, kaj vi reprovu; aŭ la pakiga formato estas nekonata, kaj vi devas elŝuti kaj instali permane.';
+$lang['error_findfolder'] = 'Ne eblis rekoni la aldonaĵ-dosierujon, vi devas elŝuti kaj instali permane';
+$lang['error_copy'] = 'Okazis kopiad-eraro dum la provo instali dosierojn por la dosierujo <em>%s</em>: la disko povus esti plena aŭ la alirpermesoj por dosieroj malĝustaj. Rezulto eble estas nur parte instalita kromaĵo, kiu malstabiligas vian vikion';
+$lang['noperms'] = 'La aldonaĵ-dosierujo ne estas skribebla';
+$lang['notplperms'] = 'La ŝablon-dosierujo ne estas skribebla';
+$lang['nopluginperms'] = 'La kromaĵ-dosierujo ne estas skribebla';
+$lang['git'] = 'Tiu aldonaĵo estis instalita pere de git, eble vi ne aktualigu ĝin ĉi tie.';
+$lang['install_url'] = 'Instali de URL:';
+$lang['install_upload'] = 'Alŝuti aldonaĵon:';
diff --git a/lib/plugins/extension/lang/fr/intro_install.txt b/lib/plugins/extension/lang/fr/intro_install.txt
new file mode 100644
index 000000000..6f68a2606
--- /dev/null
+++ b/lib/plugins/extension/lang/fr/intro_install.txt
@@ -0,0 +1 @@
+Ici, vous pouvez installer des extensions, greffons et modèles. Soit en les téléversant, soit en indiquant un URL de téléchargement. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/fr/intro_plugins.txt b/lib/plugins/extension/lang/fr/intro_plugins.txt
new file mode 100644
index 000000000..a40b863d2
--- /dev/null
+++ b/lib/plugins/extension/lang/fr/intro_plugins.txt
@@ -0,0 +1 @@
+Voilà la liste des extensions actuellement installées. À partir d'ici, vous pouvez les activer, les désactiver ou même les désinstaller complètement. Cette page affiche également les mises à jour. Assurez vous de lire la documentation avant de faire la mise à jour. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/fr/intro_search.txt b/lib/plugins/extension/lang/fr/intro_search.txt
new file mode 100644
index 000000000..418e35972
--- /dev/null
+++ b/lib/plugins/extension/lang/fr/intro_search.txt
@@ -0,0 +1 @@
+Cet onglet vous donne accès à toutes les extensions de tierces parties. Restez conscients qu'installer du code de tierce partie peut poser un problème de **sécurité**. Vous voudrez peut-être au préalable lire l'article sur la [[doku>fr:security##securite_des_plugins|sécurité des plugins]]. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/fr/intro_templates.txt b/lib/plugins/extension/lang/fr/intro_templates.txt
new file mode 100644
index 000000000..fefdb5538
--- /dev/null
+++ b/lib/plugins/extension/lang/fr/intro_templates.txt
@@ -0,0 +1 @@
+Voici la liste des modèles actuellement installés. Le [[?do=admin&page=config|gestionnaire de configuration]] vous permet de choisir le modèle à utiliser. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/fr/lang.php b/lib/plugins/extension/lang/fr/lang.php
new file mode 100644
index 000000000..c2dae0fc9
--- /dev/null
+++ b/lib/plugins/extension/lang/fr/lang.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Schplurtz le Déboulonné <schplurtz@laposte.net>
+ */
+$lang['menu'] = 'Gestionnaire d\'extension';
+$lang['tab_plugins'] = 'Greffons installés';
+$lang['tab_templates'] = 'Modèles installés';
+$lang['tab_search'] = 'Rechercher et installer';
+$lang['tab_install'] = 'Installation manuelle';
+$lang['notimplemented'] = 'Cette fonctionnalité n\'est pas encore installée';
+$lang['notinstalled'] = 'Cette extension n\'est pas installée';
+$lang['alreadyenabled'] = 'Cette extension a déjà été installée';
+$lang['alreadydisabled'] = 'Cette extension a déjà été désactivée';
+$lang['pluginlistsaveerror'] = 'Une erreur s\'est produite lors de l\'enregistrement de la liste des greffons.';
+$lang['unknownauthor'] = 'Auteur inconnu';
+$lang['unknownversion'] = 'Version inconnue';
+$lang['btn_info'] = 'Montrer plus d\'informations';
+$lang['btn_update'] = 'Mettre à jour';
+$lang['btn_uninstall'] = 'Désinstaller';
+$lang['btn_enable'] = 'Activer';
+$lang['btn_disable'] = 'Désactiver';
+$lang['btn_install'] = 'Installer';
+$lang['btn_reinstall'] = 'Réinstaller';
+$lang['js']['reallydel'] = 'Vraiment désinstaller cette extension';
+$lang['search_for'] = 'Rechercher l\'extension :';
+$lang['search'] = 'Chercher';
+$lang['extensionby'] = '<strong>%s</strong> de %s';
+$lang['screenshot'] = 'Aperçu de %s';
+$lang['popularity'] = 'Popularité : %s%%';
+$lang['homepage_link'] = 'Documents';
+$lang['bugs_features'] = 'Bugs';
+$lang['tags'] = 'Étiquettes :';
+$lang['author_hint'] = 'Chercher les extensions de cet auteur';
+$lang['installed'] = 'Installés :';
+$lang['downloadurl'] = 'URL de téléchargement :';
+$lang['repository'] = 'Entrepôt : ';
+$lang['unknown'] = '<em>inconnu</em>';
+$lang['installed_version'] = 'Version installée :';
+$lang['install_date'] = 'Votre dernière mise à jour :';
+$lang['available_version'] = 'Version disponible :';
+$lang['compatible'] = 'Compatible avec :';
+$lang['depends'] = 'Dépend de :';
+$lang['similar'] = 'Similaire à :';
+$lang['conflicts'] = 'En conflit avec :';
+$lang['donate'] = 'Vous aimez ?';
+$lang['donate_action'] = 'Payer un café à l\'auteur !';
+$lang['repo_retry'] = 'Réessayer';
+$lang['provides'] = 'Fournit :';
+$lang['status'] = 'État :';
+$lang['status_installed'] = 'installé';
+$lang['status_not_installed'] = 'non installé';
+$lang['status_protected'] = 'protégé';
+$lang['status_enabled'] = 'activé';
+$lang['status_disabled'] = 'désactivé';
+$lang['status_unmodifiable'] = 'non modifiable';
+$lang['status_plugin'] = 'greffon';
+$lang['status_template'] = 'modèle';
+$lang['status_bundled'] = 'fourni';
+$lang['msg_enabled'] = 'Greffon %s activé';
+$lang['msg_disabled'] = 'Greffon %s désactivé';
+$lang['msg_delete_success'] = 'Extension désinstallée';
+$lang['msg_template_install_success'] = 'Modèle %s installée avec succès';
+$lang['msg_template_update_success'] = 'Modèle %s mis à jour avec succès';
+$lang['msg_plugin_install_success'] = 'Greffon %s installé avec succès';
+$lang['msg_plugin_update_success'] = 'Greffon %s mis à jour avec succès';
+$lang['msg_upload_failed'] = 'Téléversement échoué';
+$lang['missing_dependency'] = '<strong>Dépendance absente ou désactivée :</strong> %s';
+$lang['security_issue'] = '<strong>Problème de sécurité :</strong> %s';
+$lang['security_warning'] = '<strong>Avertissement deSécurité :</strong> %s';
+$lang['update_available'] = '<strong>Mise à jour :</strong> La version %s est disponible.';
+$lang['wrong_folder'] = '<strong>Greffon installé incorrectement :</strong> Renomer le dossier du greffon "%s" en "%s".';
+$lang['url_change'] = '<strong>URL modifié :</strong> L\'URL de téléchargement a changé depuis le dernier téléchargement. Vérifiez si l\'URL est valide avant de mettre à jour l\'extension.<br />Nouvel URL : %s<br />Ancien : %s';
+$lang['error_badurl'] = 'Les URL doivent commencer par http ou https';
+$lang['error_dircreate'] = 'Impossible de créer le dossier temporaire pour le téléchargement.';
+$lang['error_download'] = 'Impossible de télécharger le fichier : %s';
+$lang['error_decompress'] = 'Impossible de décompresser le fichier téléchargé. C\'est peut être le résultat d\'une erreur de téléchargement, auquel cas vous devriez réessayer. Le format de compression est peut-être inconnu. Dans ce cas il vous faudra procéder à une installation manuelle.';
+$lang['error_findfolder'] = 'Impossible d\'idnetifier le dossier de l\'extension. vous devez procéder à une installation manuelle.';
+$lang['error_copy'] = 'Une erreur de copie de fichier s\'est produite lors de l\'installation des fichiers dans le dossier <em>%s</em>. Il se peut que le disque soit plein, ou que les permissions d\'accès aux fichiers soient incorrectes. Il est possible que le greffon soit partiellement installé et que cela laisse votre installation de DoluWiki instable.';
+$lang['noperms'] = 'Impossible d\'écrire dans le dossier des extensions.';
+$lang['notplperms'] = 'Impossible d\'écrire dans le dossier des modèles.';
+$lang['nopluginperms'] = 'Impossible d\'écrire dans le dossier des greffons.';
+$lang['git'] = 'Cette extension a été installé via git, vous voudrez peut-être ne pas la mettre à jour ici.';
+$lang['install_url'] = 'Installez depuis l\'URL :';
+$lang['install_upload'] = 'Téléversez l\'extension :';
diff --git a/lib/plugins/extension/lang/ja/intro_install.txt b/lib/plugins/extension/lang/ja/intro_install.txt
new file mode 100644
index 000000000..889ed6879
--- /dev/null
+++ b/lib/plugins/extension/lang/ja/intro_install.txt
@@ -0,0 +1 @@
+ここでは、アップロードするかダウンロードURLを指定して、手動でプラグインやテンプレートをインストールできます。
diff --git a/lib/plugins/extension/lang/ja/intro_plugins.txt b/lib/plugins/extension/lang/ja/intro_plugins.txt
new file mode 100644
index 000000000..9bfc68431
--- /dev/null
+++ b/lib/plugins/extension/lang/ja/intro_plugins.txt
@@ -0,0 +1 @@
+このDokuWikiに現在インストールされているプラグインです。ここでは、これらプラグインを有効化、無効化、アンインストールすることができます。同様にプラグインのアップデートも表示されます。アップデート前に、プラグインのマニュアルをお読みください。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/ja/intro_search.txt b/lib/plugins/extension/lang/ja/intro_search.txt
new file mode 100644
index 000000000..66d977b1b
--- /dev/null
+++ b/lib/plugins/extension/lang/ja/intro_search.txt
@@ -0,0 +1 @@
+このタブでは、DokuWiki用の利用可能なすべてのサードパーティのプラグインとテンプレートにアクセスできます。サードパーティ製のコードには、**セキュリティ上のリスク**の可能性があることに注意してください、最初に[[doku>ja:security#プラグインのセキュリティ|プラグインのセキュリティ]]を読むことをお勧めします。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/ja/intro_templates.txt b/lib/plugins/extension/lang/ja/intro_templates.txt
new file mode 100644
index 000000000..f97694aaa
--- /dev/null
+++ b/lib/plugins/extension/lang/ja/intro_templates.txt
@@ -0,0 +1 @@
+このDokuWikiに現在インストールされているテンプレートです。[[?do=admin&page=config|設定管理]]で使用するテンプレートを選択できます。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/ja/lang.php b/lib/plugins/extension/lang/ja/lang.php
new file mode 100644
index 000000000..0401d7630
--- /dev/null
+++ b/lib/plugins/extension/lang/ja/lang.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Hideaki SAWADA <chuno@live.jp>
+ */
+$lang['menu'] = '拡張機能管理';
+$lang['tab_plugins'] = 'インストール済プラグイン';
+$lang['tab_templates'] = 'インストール済テンプレート';
+$lang['tab_install'] = '手動インストール';
+$lang['notimplemented'] = 'この機能は未実装です。';
+$lang['notinstalled'] = 'この拡張機能はインストールされていません。';
+$lang['alreadyenabled'] = 'この拡張機能は有効です。';
+$lang['alreadydisabled'] = 'この拡張機能は無効です。';
+$lang['pluginlistsaveerror'] = 'プラグイン一覧の保存中にエラーが発生しました。';
+$lang['unknownauthor'] = '作者不明';
+$lang['unknownversion'] = 'バージョン不明';
+$lang['btn_info'] = '詳細情報を表示する。';
+$lang['btn_update'] = 'アップデート';
+$lang['btn_uninstall'] = 'アンインストール';
+$lang['btn_enable'] = '有効化';
+$lang['btn_disable'] = '無効化';
+$lang['btn_install'] = 'インストール';
+$lang['btn_reinstall'] = '再インストール';
+$lang['js']['reallydel'] = 'この拡張機能を本当にアンインストールしますか?';
+$lang['downloadurl'] = 'ダウンロード URL:';
+$lang['repository'] = 'リポジトリ:';
+$lang['depends'] = '依存:';
+$lang['similar'] = '類似:';
+$lang['status_installed'] = 'インストール済';
+$lang['status_not_installed'] = '未インストール';
+$lang['status_enabled'] = '有効';
+$lang['status_disabled'] = '無効';
+$lang['status_plugin'] = 'プラグイン';
+$lang['status_template'] = 'テンプレート';
+$lang['status_bundled'] = '同梱';
+$lang['msg_enabled'] = '%s プラグインを有効化しました。';
+$lang['msg_disabled'] = '%s プラグインを無効化しました。';
+$lang['msg_delete_success'] = '拡張機能をアンインストールしました。';
+$lang['msg_template_install_success'] = '%s テンプレートをインストールできました。';
+$lang['msg_template_update_success'] = '%s テンプレートをアップデートできました。';
+$lang['msg_plugin_install_success'] = '%s プラグインをインストールできました。';
+$lang['msg_plugin_update_success'] = '%s プラグインをアップデートできました。';
+$lang['msg_upload_failed'] = 'ファイルのアップロードに失敗しました。';
+$lang['security_issue'] = '<strong>セキュリティ問題:</strong> %s';
+$lang['security_warning'] = '<strong>セキュリティ警告:</strong> %s';
+$lang['update_available'] = '<strong>アップデート:</strong>%sの新バージョンが利用可能です。 ';
+$lang['error_badurl'] = 'URLはhttpかhttpsで始まる必要があります。';
+$lang['error_dircreate'] = 'ダウンロード用の一時フォルダが作成できません。';
+$lang['error_download'] = 'ファイルをダウンロードできません:%s';
+$lang['noperms'] = '拡張機能ディレクトリが書き込み不可です。';
+$lang['notplperms'] = 'テンプレートディレクトリが書き込み不可です。';
+$lang['nopluginperms'] = 'プラグインディレクトリが書き込み不可です。';
diff --git a/lib/plugins/extension/lang/nl/intro_install.txt b/lib/plugins/extension/lang/nl/intro_install.txt
new file mode 100644
index 000000000..6a0b41055
--- /dev/null
+++ b/lib/plugins/extension/lang/nl/intro_install.txt
@@ -0,0 +1 @@
+Hier kunt u handmatig plugins en templates installeren door deze te uploaden of door een directe download URL op te geven. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/nl/intro_plugins.txt b/lib/plugins/extension/lang/nl/intro_plugins.txt
new file mode 100644
index 000000000..0077aca30
--- /dev/null
+++ b/lib/plugins/extension/lang/nl/intro_plugins.txt
@@ -0,0 +1 @@
+Dit zijn de momenteel in uw Dokuwiki geïnstalleerde plugins. U kunt deze hier aan of uitschakelen danwel geheel deïnstalleren. Plugin updates zijn hier ook opgenomen, lees de pluin documentatie voordat u update. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/nl/intro_search.txt b/lib/plugins/extension/lang/nl/intro_search.txt
new file mode 100644
index 000000000..8fc3900ad
--- /dev/null
+++ b/lib/plugins/extension/lang/nl/intro_search.txt
@@ -0,0 +1 @@
+Deze tab verschaft u toegang tot alle plugins en templates vervaardigd door derden en bestemd voor Dokuwiki. Houdt er rekening meel dat indien u Plugins van derden installeerd deze een **veiligheids risico ** kunnen bevatten, geadviseerd wordt om eerst te lezen [[doku>security#plugin_security|plugin security]]. \ No newline at end of file
diff --git a/lib/plugins/extension/lang/nl/intro_templates.txt b/lib/plugins/extension/lang/nl/intro_templates.txt
new file mode 100644
index 000000000..5ef23dadf
--- /dev/null
+++ b/lib/plugins/extension/lang/nl/intro_templates.txt
@@ -0,0 +1 @@
+Deze templates zijn thans in DokuWiki geïnstalleerd. U kent een template selecteren middels [[?do=admin&page=config|Configuration Manager]] . \ No newline at end of file
diff --git a/lib/plugins/extension/lang/nl/lang.php b/lib/plugins/extension/lang/nl/lang.php
new file mode 100644
index 000000000..783168c2e
--- /dev/null
+++ b/lib/plugins/extension/lang/nl/lang.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Rene <wllywlnt@yahoo.com>
+ */
+$lang['menu'] = 'Extension Manager (Uitbreidings Beheerder)';
+$lang['tab_plugins'] = 'Geïnstalleerde Plugins';
+$lang['tab_templates'] = 'Geïnstalleerde Templates';
+$lang['tab_search'] = 'Zoek en installeer';
+$lang['tab_install'] = 'Handmatige installatie';
+$lang['notimplemented'] = 'Deze toepassing is nog niet geïnstalleerd';
+$lang['notinstalled'] = 'Deze uitbreiding is nog niet geïnstalleerd';
+$lang['alreadyenabled'] = 'Deze uitbreiding is reeds ingeschakeld';
+$lang['alreadydisabled'] = 'Deze uitbreiding is reeds uitgeschakeld';
+$lang['pluginlistsaveerror'] = 'Fout bij het opslaan van de plugin lijst';
+$lang['unknownauthor'] = 'Onbekende auteur';
+$lang['unknownversion'] = 'Onbekende versie';
+$lang['btn_info'] = 'Toon meer informatie';
+$lang['btn_update'] = 'Update';
+$lang['btn_uninstall'] = 'Deinstalleer';
+$lang['btn_enable'] = 'Schakel aan';
+$lang['btn_disable'] = 'Schakel uit';
+$lang['btn_install'] = 'Installeer';
+$lang['btn_reinstall'] = 'Her-installeer';
+$lang['js']['reallydel'] = 'Wilt u deze uitbreiding deinstalleren ?';
+$lang['search_for'] = 'Zoek Uitbreiding:';
+$lang['search'] = 'Zoek';
+$lang['extensionby'] = '<strong>%s</strong> by %s';
+$lang['screenshot'] = 'Schermafdruk bij %s';
+$lang['popularity'] = 'Populariteit:%s%%';
+$lang['homepage_link'] = 'Dokumenten';
+$lang['msg_delete_success'] = 'Uitbreiding gedeinstalleerd';
+$lang['msg_template_install_success'] = 'Template %s werd succesvol geïnstalleerd';
+$lang['msg_template_update_success'] = 'Template %s werd succesvol ge-update';
+$lang['msg_plugin_install_success'] = 'Plugin %s werd succesvol geïnstalleerd';
+$lang['msg_plugin_update_success'] = 'Plugin %s werd succesvol ge-update';
+$lang['msg_upload_failed'] = 'Uploaden van het bestand is mislukt';
+$lang['missing_dependency'] = '<strong>niet aanwezige of uitgeschakelde afhankelijkheid</strong> %s';
+$lang['security_issue'] = '<strong>Veiligheids kwestie:</strong> %s';
+$lang['security_warning'] = '<strong>Veiligheids Waarschuwing</strong> %s';
+$lang['update_available'] = '<strong>Update:</strong> Nieuwe versie %s is beschikbaar.';
+$lang['wrong_folder'] = '<strong>Plugin onjuist geïnstalleerd:</strong> Hernoem de plugin directory van "%s" naar"%s"';
+$lang['url_change'] = '<strong>URL gewijzigd:</strong> Download URL is gewijzigd sinds de laatste download. Controleer of de nieuwe URL juist is voordat u de uitbreiding update. <br />Nieuw:%s<Br /> Vorig: %s';
+$lang['error_badurl'] = 'URLs moeten beginnen met http of https';
+$lang['error_dircreate'] = 'De tijdelijke map kon niet worden gemaakt om de download te ontvangen';
+$lang['error_download'] = 'Het is niet mogelijk het bestand te downloaden: %s';
+$lang['error_decompress'] = 'Onmogelijk om het gedownloade bestand uit te pakken. Dit is wellicht het gevolg van een onvolledige/onjuiste download, in welk geval u het nog eens moet proberen; of het compressie formaat is onbekend in welk geval u het bestand handmatig moet downloaden en installeren.';
+$lang['error_findfolder'] = 'Onmogelijk om de uitbreidings directory te vinden, u moet het zelf downloaden en installeren';
+$lang['error_copy'] = 'Er was een bestand kopieer fout tijdens het installeren van bestanden in directory <em>%s</em>: de schijf kan vol zijn of de bestand toegangs rechten kunnen onjuist zijn. Dit kan tot gevolg hebben dat de plugin slechts gedeeltelijk werd geïnstalleerd waardoor uw wiki installatie onstabiel is ';
+$lang['noperms'] = 'Uitbreidings directory is niet schrijfbaar';
+$lang['notplperms'] = 'Template directory is niet schrijfbaar';
+$lang['nopluginperms'] = 'Plugin directory is niet schrijfbaar';
+$lang['git'] = 'De uitbreiding werd geïnstalleerd via git, u wilt deze hier wellicht niet aanpassen.';
+$lang['install_url'] = 'Installeer vanaf URL:';
+$lang['install_upload'] = 'Upload Uitbreiding:';
+$lang['bugs_features'] = 'Bugs';
+$lang['tags'] = 'Tags:';
+$lang['author_hint'] = 'Zoek uitbreidingen van deze auteur:';
+$lang['installed'] = 'Geinstalleerd:';
+$lang['downloadurl'] = 'Download URL:';
+$lang['repository'] = 'Repository ( centrale opslag)';
+$lang['unknown'] = '<em>onbekend</em>';
+$lang['installed_version'] = 'Geïnstalleerde versie';
+$lang['install_date'] = 'Uw laatste update :';
+$lang['available_version'] = 'Beschikbare versie:';
+$lang['compatible'] = 'Compatible met :';
+$lang['depends'] = 'Afhankelijk van :';
+$lang['similar'] = 'Soortgelijk :';
+$lang['conflicts'] = 'Conflicteerd met :';
+$lang['donate'] = 'Vindt u dit leuk ?';
+$lang['donate_action'] = 'Koop een kop koffie voor de auteur!';
+$lang['repo_retry'] = 'Herhaal';
+$lang['provides'] = 'Zorgt voor:';
+$lang['status'] = 'Status:';
+$lang['status_installed'] = 'Geïnstalleerd';
+$lang['status_not_installed'] = 'niet geïnstalleerd ';
+$lang['status_protected'] = 'beschermd';
+$lang['status_enabled'] = 'ingeschakeld';
+$lang['status_disabled'] = 'uitgeschakeld';
+$lang['status_unmodifiable'] = 'Niet wijzigbaar';
+$lang['status_plugin'] = 'plugin';
+$lang['status_template'] = 'template';
+$lang['status_bundled'] = 'Gebundeld';
+$lang['msg_enabled'] = 'Plugin %s ingeschakeld';
+$lang['msg_disabled'] = 'Plugin %s uitgeschakeld';
diff --git a/lib/plugins/extension/lang/ru/lang.php b/lib/plugins/extension/lang/ru/lang.php
new file mode 100644
index 000000000..4a36cb85d
--- /dev/null
+++ b/lib/plugins/extension/lang/ru/lang.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Aleksandr Selivanov <alexgearbox@yandex.ru>
+ */
+$lang['menu'] = 'Управление дополнениями';
+$lang['tab_plugins'] = 'Установленные плагины';
+$lang['tab_templates'] = 'Установленные шаблоны';
+$lang['tab_search'] = 'Поиск и установка';
+$lang['tab_install'] = 'Ручная установка';
+$lang['notinstalled'] = 'Это дополнение не установлено';
+$lang['unknownauthor'] = 'Автор неизвестен';
+$lang['unknownversion'] = 'Версия неизвестна';
+$lang['btn_info'] = 'Отобразить доп. информацию';
+$lang['btn_update'] = 'Обновить';
+$lang['btn_uninstall'] = 'Удалить';
+$lang['btn_enable'] = 'Включить';
+$lang['btn_disable'] = 'Отключить';
+$lang['btn_install'] = 'Установить';
+$lang['btn_reinstall'] = 'Переустановить';
+$lang['js']['reallydel'] = 'Действительно удалить это дополнение?';
+$lang['search_for'] = 'Поиск дополнения:';
+$lang['search'] = 'Найти';
+$lang['extensionby'] = '<strong>%s</strong> — %s';
+$lang['popularity'] = 'Попоулярность: %s%%';
+$lang['bugs_features'] = 'Ошибки';
+$lang['tags'] = 'Метки:';
+$lang['repository'] = 'Репозиторий:';
+$lang['unknown'] = '<em>неизвестно</em>';
+$lang['repo_retry'] = 'Повторить';
+$lang['status_enabled'] = 'включен';
+$lang['status_disabled'] = 'отключено';
+$lang['status_unmodifiable'] = 'неизменяемо';
+$lang['status_plugin'] = 'плагин';
+$lang['status_template'] = 'шаблон';
+$lang['status_bundled'] = 'в комплекте';
+$lang['msg_enabled'] = 'Плагин %s включен';
+$lang['msg_disabled'] = 'Плагин %s отключен';
+$lang['msg_delete_success'] = 'Дополнение удалено';
diff --git a/lib/plugins/extension/lang/sk/lang.php b/lib/plugins/extension/lang/sk/lang.php
new file mode 100644
index 000000000..d00c2e32b
--- /dev/null
+++ b/lib/plugins/extension/lang/sk/lang.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Martin Michalek <michalek.dev@gmail.com>
+ */
+$lang['tab_plugins'] = 'Inštalované pluginy';
+$lang['tab_templates'] = 'Inštalované šablóny';
+$lang['tab_search'] = 'Hľadanie e inštalácia';
+$lang['tab_install'] = 'Manuálna inštalácia';
+$lang['notimplemented'] = 'Táto vlastnosť ešte nebola implementovaná';
+$lang['unknownauthor'] = 'Neznámy autor';
+$lang['unknownversion'] = 'Neznáma verzia';
+$lang['btn_info'] = 'Viac informácií';
+$lang['btn_update'] = 'Aktualizácia';
+$lang['btn_uninstall'] = 'Odinštalovanie';
+$lang['btn_enable'] = 'Povolenie';
+$lang['btn_disable'] = 'Zablokovanie';
+$lang['btn_install'] = 'Inštalácia';
+$lang['btn_reinstall'] = 'Re-Inštalácia';
+$lang['search'] = 'Vyhľadávanie';
+$lang['extensionby'] = '<strong>%s</strong> od %s';
+$lang['screenshot'] = 'Obrázok od %s';
+$lang['popularity'] = 'Popularita: %s%%';
+$lang['homepage_link'] = 'Dokumentácia';
+$lang['bugs_features'] = 'Chyby:';
+$lang['tags'] = 'Kľúčové slová:';
+$lang['unknown'] = '<em>neznámy</em>';
+$lang['installed_version'] = 'Inštalovaná verzia:';
+$lang['install_date'] = 'Posledná aktualizácia:';
+$lang['available_version'] = 'Dostupné verzie:';
+$lang['compatible'] = 'Kompaktibilita:';
+$lang['similar'] = 'Podobné:';
+$lang['conflicts'] = 'V konflikte:';
+$lang['status_installed'] = 'inštalovaný';
+$lang['status_not_installed'] = 'neinštalovaný';
+$lang['status_protected'] = 'chránený';
+$lang['status_enabled'] = 'povolený';
+$lang['status_disabled'] = 'nepovolený';
+$lang['status_plugin'] = 'plugin';
+$lang['status_template'] = 'šablóna';
+$lang['msg_enabled'] = 'Plugin %s povolený';
+$lang['msg_disabled'] = 'Plugin %s nepovolený';
+$lang['msg_template_install_success'] = 'Šablóna %s úspešne nainštalovaná';
+$lang['msg_template_update_success'] = 'Šablóna %s úspešne aktualizovaná';
+$lang['msg_plugin_install_success'] = 'Plugin %s úspešne nainštalovaný';
+$lang['msg_plugin_update_success'] = 'Plugin %s úspešne aktualizovaný';
+$lang['msg_upload_failed'] = 'Nahrávanie súboru zlyhalo';
+$lang['update_available'] = '<strong>Aktualizácia:</strong> Nová verzia %s.';
+$lang['wrong_folder'] = '<strong>Plugin nesprávne nainštalovaný:</strong> Premenujte adresár s pluginom "%s" na "%s".';
+$lang['error_badurl'] = 'URL by mali mať na začiatku http alebo https';
+$lang['error_dircreate'] = 'Nie je možné vytvoriť dočasný adresár pre uloženie sťahovaného súboru';
+$lang['error_download'] = 'Nie je možné stiahnuť súbor: %s';
+$lang['error_decompress'] = 'Nie je možné dekomprimovať stiahnutý súbor. Môže to byť dôvodom chyby sťahovania (v tom prípade to skúste znova) alebo neznámym kompresným formátom (v tom prípade musíte stiahnuť a inštalovať manuálne).';
+$lang['error_copy'] = 'Chyba kopírovania pri inštalácii do adresára <em>%s</em>: disk môže byť plný alebo nemáte potrebné prístupové oprávnenie. Dôsledkom može byť čiastočne inštalovaný plugin a nestabilná wiki inštalácia.';
+$lang['nopluginperms'] = 'Adresár s pluginom nie je zapisovateľný.';
+$lang['install_url'] = 'Inštalácia z URL:';
diff --git a/lib/plugins/extension/lang/zh/intro_install.txt b/lib/plugins/extension/lang/zh/intro_install.txt
new file mode 100644
index 000000000..640839319
--- /dev/null
+++ b/lib/plugins/extension/lang/zh/intro_install.txt
@@ -0,0 +1 @@
+你可以通过上传或直接提供下载链接来安装插件和模板。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/zh/intro_search.txt b/lib/plugins/extension/lang/zh/intro_search.txt
new file mode 100644
index 000000000..0059075c0
--- /dev/null
+++ b/lib/plugins/extension/lang/zh/intro_search.txt
@@ -0,0 +1 @@
+这个标签会为你展示所有DokuWiki的第三方插件和模板。但你需要知道这些由第三方提供的代码可能会给你带来**安全方面的风险**,你最好先读一下[[doku>security#plugin_security|插件安全性]]。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/zh/intro_templates.txt b/lib/plugins/extension/lang/zh/intro_templates.txt
new file mode 100644
index 000000000..20575d381
--- /dev/null
+++ b/lib/plugins/extension/lang/zh/intro_templates.txt
@@ -0,0 +1 @@
+DokuWiki当前所使用的模板已经安装了,你可以在[[?do=admin&page=config|配置管理器]]里选择你要的模板。 \ No newline at end of file
diff --git a/lib/plugins/extension/lang/zh/lang.php b/lib/plugins/extension/lang/zh/lang.php
new file mode 100644
index 000000000..b9db01540
--- /dev/null
+++ b/lib/plugins/extension/lang/zh/lang.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
+ *
+ * @author Cupen <Cupenoruler@foxmail.com>
+ * @author xiqingongzi <Xiqingongzi@Gmail.com>
+ */
+$lang['menu'] = '扩展管理器';
+$lang['tab_plugins'] = '安装插件';
+$lang['tab_templates'] = '安装模板';
+$lang['tab_search'] = '搜索和安装';
+$lang['tab_install'] = '手动安装';
+$lang['notimplemented'] = '未实现的特性';
+$lang['notinstalled'] = '该扩展未安装';
+$lang['alreadyenabled'] = '该扩展已激活';
+$lang['alreadydisabled'] = '该扩展已关闭';
+$lang['pluginlistsaveerror'] = '保存插件列表时碰到个错误';
+$lang['unknownauthor'] = '未知作者';
+$lang['unknownversion'] = '未知版本';
+$lang['btn_info'] = '查看更多信息';
+$lang['btn_update'] = '更新';
+$lang['btn_uninstall'] = '卸载';
+$lang['btn_enable'] = '激活';
+$lang['btn_disable'] = '关闭';
+$lang['btn_install'] = '安装';
+$lang['btn_reinstall'] = '重新安装';
+$lang['js']['reallydel'] = '确定卸载这个扩展么?';
+$lang['search_for'] = '搜索扩展';
+$lang['search'] = '搜索';
+$lang['extensionby'] = '<strong>%s</strong> by %s';
+$lang['screenshot'] = '%s 的截图';
+$lang['popularity'] = '人气: %s%%';
+$lang['homepage_link'] = '文档';
+$lang['bugs_features'] = '错误';
+$lang['tags'] = '标签:';
+$lang['author_hint'] = '搜索这个作者的插件';
+$lang['installed'] = '已安装的:';
+$lang['downloadurl'] = '下载地址:';
+$lang['repository'] = '版本库:';
+$lang['unknown'] = '<em>未知的</em>';
+$lang['installed_version'] = '已安装版本:';
+$lang['install_date'] = '您的最后一次升级:';
+$lang['donate'] = '喜欢?';
+$lang['donate_action'] = '捐给作者一杯咖啡钱!';
+$lang['repo_retry'] = '重试';
+$lang['status'] = '现状:';
+$lang['status_installed'] = '已安装的';
+$lang['status_plugin'] = '插件';
+$lang['status_template'] = '模板';
diff --git a/lib/plugins/extension/plugin.info.txt b/lib/plugins/extension/plugin.info.txt
new file mode 100644
index 000000000..ef16d78a1
--- /dev/null
+++ b/lib/plugins/extension/plugin.info.txt
@@ -0,0 +1,7 @@
+base extension
+author Michael Hamann
+email michael@content-space.de
+date 2013-08-01
+name Extension Manager
+desc Allows managing and installing plugins and templates
+url https://www.dokuwiki.org/plugin:extension
diff --git a/lib/plugins/extension/script.js b/lib/plugins/extension/script.js
new file mode 100644
index 000000000..fab88162d
--- /dev/null
+++ b/lib/plugins/extension/script.js
@@ -0,0 +1,113 @@
+jQuery(function(){
+
+ var $extmgr = jQuery('#extension__manager');
+
+ /**
+ * Confirm uninstalling
+ */
+ $extmgr.find('input.uninstall').click(function(e){
+ if(!window.confirm(LANG.plugins.extension.reallydel)){
+ e.preventDefault();
+ return false;
+ }
+ return true;
+ });
+
+ /**
+ * very simple lightbox
+ * @link http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/super-simple-lightbox-with-css-and-jquery/
+ */
+ $extmgr.find('a.extension_screenshot').click(function(e) {
+ e.preventDefault();
+
+ //Get clicked link href
+ var image_href = jQuery(this).attr("href");
+
+ // create lightbox if needed
+ var $lightbox = jQuery('#plugin__extensionlightbox');
+ if(!$lightbox.length){
+ $lightbox = jQuery('<div id="plugin__extensionlightbox"><p>Click to close</p><div></div></div>')
+ .appendTo(jQuery('body'))
+ .hide()
+ .click(function(){
+ $lightbox.hide();
+ });
+ }
+
+ // fill and show it
+ $lightbox
+ .show()
+ .find('div').html('<img src="' + image_href + '" />');
+
+
+ return false;
+ });
+
+ /**
+ * Enable/Disable extension via AJAX
+ */
+ $extmgr.find('input.disable, input.enable').click(function (e) {
+ e.preventDefault();
+ var $btn = jQuery(this);
+
+ // get current state
+ var extension = $btn.attr('name').split('[')[2];
+ extension = extension.substr(0, extension.length - 1);
+ var act = ($btn.hasClass('disable')) ? 'disable' : 'enable';
+
+ // disable while we wait
+ $btn.attr('disabled', 'disabled');
+ $btn.css('cursor', 'wait');
+
+ // execute
+ jQuery.get(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_extension',
+ ext: extension,
+ act: act
+ },
+ function (data) {
+ $btn.css('cursor', '')
+ .removeAttr('disabled')
+ .removeClass('disable')
+ .removeClass('enable')
+ .val(data.label)
+ .addClass(data.reverse)
+ .parents('li')
+ .removeClass('disabled')
+ .removeClass('enabled')
+ .addClass(data.state);
+ }
+ );
+ });
+
+ /**
+ * AJAX detail infos
+ */
+ $extmgr.find('a.info').click(function(e){
+ e.preventDefault();
+
+ var $link = jQuery(this);
+ var $details = $link.parent().find('dl.details');
+ if($details.length){
+ $link.toggleClass('close');
+ $details.toggle();
+ return;
+ }
+
+ $link.addClass('close');
+ jQuery.get(
+ DOKU_BASE + 'lib/exe/ajax.php',
+ {
+ call: 'plugin_extension',
+ ext: $link.data('extid'),
+ act: 'info'
+ },
+ function(data){
+ $link.parent().append(data);
+ }
+ );
+ });
+
+}); \ No newline at end of file
diff --git a/lib/plugins/extension/style.less b/lib/plugins/extension/style.less
new file mode 100644
index 000000000..d20689099
--- /dev/null
+++ b/lib/plugins/extension/style.less
@@ -0,0 +1,363 @@
+/*
+ * Extension plugin styles
+ *
+ * @author Christopher Smith <chris@jalakai.co.uk>
+ * @author Piyush Mishra <me@piyushmishra.com>
+ * @author Håkan Sandell <sandell.hakan@gmail.com>
+ * @author Anika Henke <anika@selfthinker.org>
+ */
+
+/**
+ * very simple lightbox
+ * @link http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/super-simple-lightbox-with-css-and-jquery/
+ */
+#plugin__extensionlightbox {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: url(images/overlay.png) repeat;
+ text-align: center;
+ cursor: pointer;
+ z-index: 9999;
+
+ p {
+ text-align: right;
+ color: #fff;
+ margin-right: 20px;
+ font-size: 12px;
+ }
+
+ img {
+ box-shadow: 0 0 25px #111;
+ -webkit-box-shadow: 0 0 25px #111;
+ -moz-box-shadow: 0 0 25px #111;
+ max-width: 90%;
+ max-height: 90%;
+ }
+}
+
+/**
+ * general styles
+ */
+#extension__manager {
+ // tab layout - most of it is in the main template
+ ul.tabs li.active a {
+ background-color: @ini_background_alt;
+ border-bottom: solid 1px @ini_background_alt;
+ z-index: 2;
+ }
+ .panelHeader {
+ background-color: @ini_background_alt;
+ margin: 0 0 10px 0;
+ padding: 10px 10px 8px;
+ overflow: hidden;
+ }
+
+ // message spacing
+ div.msg {
+ margin: 0.4em 0 0 0;
+ }
+}
+
+/*
+ * extensions table
+ */
+#extension__list {
+ ul.extensionList {
+ margin-left: 0;
+ margin-right: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ ul.extensionList li {
+ margin: 0 0 .5em;
+ padding: 0 0 .5em;
+ color: @ini_text;
+ border-bottom: 1px solid @ini_border;
+ overflow: hidden;
+ }
+
+ input.button {
+ margin: 0 .3em .3em 0;
+ }
+}
+
+/**
+ * extension table left column
+ */
+#extension__list .legend {
+ position: relative;
+ width: 75%;
+ float: left;
+
+ // padding
+ > div {
+ padding: 0 .5em 0 132px;
+ border-right: 1px solid @ini_background_alt;
+ overflow: hidden;
+ }
+
+ // screenshot
+ div.screenshot {
+ margin-top: 4px;
+ margin-left: -132px;
+ max-width: 120px;
+ float: left;
+ position: relative;
+
+ img {
+ width: 120px;
+ height: 70px;
+ border-radius: 5px;
+ box-shadow: 2px 2px 2px #666;
+ }
+
+ span {
+ min-height: 24px;
+ min-width: 24px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ }
+
+ // plugin headline
+ h2 {
+ width: 100%;
+ float: right;
+ margin: 0.2em 0 0.5em;
+ font-size: 100%;
+ font-weight: normal;
+ border: none;
+
+ strong {
+ font-size: 120%;
+ font-weight: bold;
+ vertical-align: baseline;
+ }
+ }
+
+ // description
+ p {
+ margin: 0 0 0.6em 0;
+ }
+
+ // popularity bar
+ div.popularity {
+ background-color: @ini_background;
+ border: 1px solid silver;
+ height: .4em;
+ margin: 0 auto;
+ padding: 1px;
+ width: 5.5em;
+ position: absolute;
+ right: .5em;
+ top: 0.2em;
+
+ div {
+ background-color: @ini_border;
+ height: 100%;
+ }
+ }
+
+ // Docs, Bugs, Tags
+ div.linkbar {
+ font-size: 85%;
+
+ span.tags {
+ padding-left: 18px;
+ background: transparent url(images/tag.png) no-repeat 0 0;
+ }
+ }
+
+ // more info button
+ a.info {
+ background: transparent url(images/down.png) no-repeat 0 0;
+ border-width: 0;
+ height: 13px;
+ width: 13px;
+ text-indent: -9999px;
+ float: right;
+ margin: .5em 0 0;
+ overflow: hidden;
+
+ &.close {
+ background: transparent url(images/up.png) no-repeat 0 0;
+ }
+ }
+
+ // detailed info box
+ dl.details {
+ margin: 0.4em 0 0 0;
+ font-size: 85%;
+ border-top: 1px solid @ini_background_alt;
+ clear: both;
+
+ dt {
+ clear: left;
+ float: left;
+ width: 25%;
+ margin: 0;
+ text-align: right;
+ font-weight: normal;
+ padding: 0.2em 5px 0 0;
+ font-weight: bold;
+ }
+
+ dd {
+ margin-left: 25%;
+ padding: 0.2em 0 0 5px;
+
+ a.donate {
+ padding-left: 18px;
+ background: transparent url(images/donate.png) left center no-repeat;
+ }
+ }
+ }
+}
+
+[dir=rtl] #extension__list .legend {
+ float: right;
+
+ > div {
+ padding: 0 132px 0 .5em;
+ border-left: 1px solid @ini_background_alt;
+ border-right-width: 0;
+ }
+
+ div.screenshot {
+ margin-left: 0;
+ margin-right: -132px;
+ float: right;
+
+ span {
+ left: auto;
+ right: 0;
+ }
+ }
+
+ h2 {
+ float: left;
+ }
+
+ div.popularity {
+ right: auto;
+ left: .5em;
+ }
+
+ div.linkbar span.tags,
+ dl.details dd a.donate {
+ padding-left: 0;
+ padding-right: 18px;
+ background-position: top right;
+ }
+
+ a.info {
+ float: left;
+ }
+
+ dl.details {
+ dt {
+ clear: right;
+ float: right;
+ text-align: left;
+ padding-left: 5px;
+ padding-right: 0;
+ }
+
+ dd {
+ margin-left: 0;
+ margin-right: 25%;
+ padding-left: 0;
+ padding-right: 5px;
+ }
+ }
+}
+
+/*
+ * Enabled/Disabled overrides
+ */
+#extension__list {
+ .enabled div.screenshot span {
+ background: transparent url(images/enabled.png) no-repeat 2px 2px;
+ }
+
+ .disabled div.screenshot span {
+ background: transparent url(images/disabled.png) no-repeat 2px 2px;
+ }
+
+ .disabled .legend {
+ opacity: 0.7;
+ }
+}
+
+/**
+ * extension table right column
+ */
+#extension__manager .actions {
+ padding: 0;
+ font-size: 95%;
+ width: 25%;
+ float: right;
+ text-align: right;
+
+ .version {
+ display: block;
+ }
+
+ p {
+ margin: 0.2em 0;
+ text-align: center;
+ }
+
+ p.permerror {
+ margin-left: 0.4em;
+ text-align: left;
+ padding-left: 19px;
+ background: transparent url(images/warning.png) center left no-repeat;
+ line-height: 18px;
+ font-size: 12px;
+ }
+}
+
+[dir=rtl] #extension__manager .actions {
+ float: left;
+ text-align: left;
+
+ p.permerror {
+ margin-left: 0;
+ margin-right: 0.4em;
+ text-align: right;
+ padding-left: 0;
+ padding-right: 19px;
+ background-position: center right;
+ }
+}
+
+/**
+ * Search form
+ */
+#extension__manager form.search {
+ display: block;
+ margin-bottom: 2em;
+
+ span {
+ font-weight: bold;
+ }
+
+ input.edit {
+ width: 25em;
+ }
+}
+
+/**
+ * Install form
+ */
+#extension__manager form.install {
+ text-align: center;
+ display: block;
+ width: 60%;
+}