From 2cfe210dbbcc812be95a66404d87553af8078520 Mon Sep 17 00:00:00 2001
From: mattab <matthieu.aubry@gmail.com>
Date: Wed, 17 Jul 2013 11:02:25 +1200
Subject: [PATCH] Refs #546 Adding core "Uninstall" feature for Plugins and
 Themes + Cleanups + fix build by moving constant to PluginsManager Todo  *
 ask for confirmation: "Do you want to uninstall X?" // warn that
 plugin-specific data may be deleted as part of calling the plugin's uninstall
 method, and may not be recoverable.  * call uninstall() method on the plugin
 class  * verify core plugins cant be uninstalled

---
 core/Config.php                               | 30 ++++------
 core/PluginsManager.php                       | 60 +++++++++++++++++--
 core/Twig.php                                 |  9 +--
 core/Updates.php                              |  4 --
 core/Updates/1.9.3-b3.php                     |  4 +-
 core/testMinimumPhpVersion.php                | 15 +++--
 plugins/CorePluginsAdmin/Controller.php       | 40 ++++++++++---
 .../CorePluginsAdmin/templates/macros.twig    |  6 +-
 .../stylesheets/simple_structure.css          |  2 +-
 9 files changed, 118 insertions(+), 52 deletions(-)

diff --git a/core/Config.php b/core/Config.php
index 3f8960fe16..354f9e4e37 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -133,24 +133,11 @@ class Piwik_Config
      *
      * @return string
      */
-    public static function getGlobalConfigPath()
+    protected static function getGlobalConfigPath()
     {
         return PIWIK_USER_PATH . '/config/global.ini.php';
     }
 
-    /**
-     * Backward compatibility stub
-     *
-     * @todo remove in 2.0
-     * @since 1.7
-     * @deprecated 1.7
-     * @return string
-     */
-    public static function getDefaultDefaultConfigPath()
-    {
-        return self::getGlobalConfigPath();
-    }
-
     /**
      * Returns absolute path to the local configuration file
      *
@@ -272,12 +259,7 @@ class Piwik_Config
             return $tmp;
         }
 
-        $section = null;
-
-        // merge corresponding sections from global and local settings
-        if (isset($this->configGlobal[$name])) {
-            $section = $this->configGlobal[$name];
-        }
+        $section = $this->getFromDefaultConfig($name);
 
         if (isset($this->configLocal[$name])) {
             // local settings override the global defaults
@@ -297,6 +279,14 @@ class Piwik_Config
         return $tmp;
     }
 
+    public function getFromDefaultConfig($name)
+    {
+        if (isset($this->configGlobal[$name])) {
+            return $this->configGlobal[$name];
+        }
+        return null;
+    }
+
     /**
      * Set value
      *
diff --git a/core/PluginsManager.php b/core/PluginsManager.php
index 7b49d9ff30..7cbd3309b7 100644
--- a/core/PluginsManager.php
+++ b/core/PluginsManager.php
@@ -38,6 +38,10 @@ class Piwik_PluginsManager
 
     protected $doLoadPlugins = true;
     protected $loadedPlugins = array();
+    /**
+     * Default theme used in Piwik.
+     */
+    const DEFAULT_THEME="Zeitgeist";
 
     protected $doLoadAlwaysActivatedPlugins = true;
     protected $pluginToAlwaysActivate = array(
@@ -53,7 +57,7 @@ class Piwik_PluginsManager
         'LanguagesManager',
 
         // default Piwik theme, always enabled
-        Piwik_Twig::DEFAULT_THEME,
+        self::DEFAULT_THEME,
     );
 
     // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker
@@ -122,6 +126,21 @@ class Piwik_PluginsManager
         return in_array($name, $this->pluginToAlwaysActivate);
     }
 
+    /**
+     * Returns true if the plugin can be uninstalled. Any non-core plugin can be uninstalled.
+     *
+     * @param $name
+     * @return bool
+     */
+    public function isPluginUninstallable($name)
+    {
+        // Reading the plugins from the global.ini.php config file
+        $pluginsBundledWithPiwik = Piwik_Config::getInstance()->getFromDefaultConfig('Plugins');
+        $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
+
+        return !in_array($name, $pluginsBundledWithPiwik);
+    }
+
     /**
      * Returns true if plugin has been activated
      *
@@ -188,6 +207,32 @@ class Piwik_PluginsManager
         return PIWIK_INCLUDE_PATH . '/plugins/';
     }
 
+    /**
+     * Uninstalls a Plugin (deletes plugin files from the disk)
+     * Only deactivated plugins can be uninstalled
+     *
+     * @param $pluginName
+     */
+    public function uninstallPlugin($pluginName)
+    {
+        if($this->isPluginActivated($pluginName)) {
+            throw new Exception("To uninstall the plugin $pluginName, first disable it in Piwik > Settings > Plugins");
+        }
+        if(!$this->isPluginInFilesystem($pluginName)) {
+            throw new Exception("You are trying to uninstall the plugin $pluginName but it was not found in the directory piwik/plugins/");
+        }
+        self::deletePluginFromFilesystem($pluginName);
+        if($this->isPluginInFilesystem($pluginName)) {
+            return false;
+        }
+        return true;
+    }
+
+    public static function deletePluginFromFilesystem($plugin)
+    {
+        Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
+    }
+
     /**
      * Deactivate plugin
      *
@@ -253,8 +298,7 @@ class Piwik_PluginsManager
             throw new Exception("Plugin '$pluginName' already activated.");
         }
 
-        $existingPlugins = $this->readPluginsDirectory();
-        if (array_search($pluginName, $existingPlugins) === false) {
+        if (!$this->isPluginInFilesystem($pluginName)) {
             // ToDo: This fails in tracker-mode. We should log this however.
             //Piwik::log(sprintf("Unable to find the plugin '%s' in activatePlugin.", $pluginName));
             return;
@@ -289,6 +333,14 @@ class Piwik_PluginsManager
         Piwik::deleteAllCacheOnUpdate();
     }
 
+    protected function isPluginInFilesystem($pluginName)
+    {
+        $existingPlugins = $this->readPluginsDirectory();
+        $isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false;
+        return Piwik_Common::isValidFilename($pluginName)
+                && $isPluginInFilesystem;
+    }
+
     /**
      * Returns the name of the non default theme currently enabled.
      * If Zeitgeist is enabled, returns false (nb: Zeitgeist cannot be disabled)
@@ -301,7 +353,7 @@ class Piwik_PluginsManager
         foreach($plugins as $plugin) {
             /* @var $plugin Piwik_Plugin */
             if($plugin->isTheme()
-                && $plugin->getPluginName() != Piwik_Twig::DEFAULT_THEME) {
+                && $plugin->getPluginName() != self::DEFAULT_THEME) {
                 return $plugin->getPluginName();
             }
         }
diff --git a/core/Twig.php b/core/Twig.php
index 9022c31ba7..a3f69dbc7e 100644
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -23,12 +23,7 @@ class Piwik_Twig
      */
     private $twig;
 
-    /**
-     * Default theme used in Piwik.
-     */
-    const DEFAULT_THEME="Zeitgeist";
-
-    public function __construct($theme = self::DEFAULT_THEME)
+    public function __construct()
     {
         $loader = $this->getDefaultThemeLoader();
 
@@ -139,7 +134,7 @@ class Piwik_Twig
     private function getDefaultThemeLoader()
     {
         $themeLoader = new Twig_Loader_Filesystem(array(
-            sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, self::DEFAULT_THEME)
+            sprintf("%s/plugins/%s/templates/", PIWIK_INCLUDE_PATH, Piwik_PluginsManager::DEFAULT_THEME)
         ));
 
         return $themeLoader;
diff --git a/core/Updates.php b/core/Updates.php
index 91bdca17e7..b9409de5ea 100644
--- a/core/Updates.php
+++ b/core/Updates.php
@@ -108,9 +108,5 @@ abstract class Piwik_Updates
         }
     }
 
-    public static function deletePluginFromFilesystem($plugin)
-    {
-        Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
-    }
 
 }
diff --git a/core/Updates/1.9.3-b3.php b/core/Updates/1.9.3-b3.php
index 65338a44eb..75c6c165c0 100644
--- a/core/Updates/1.9.3-b3.php
+++ b/core/Updates/1.9.3-b3.php
@@ -19,9 +19,9 @@ class Piwik_Updates_1_9_3_b3 extends Piwik_Updates
         // Insight was a temporary code name for Overlay
         $pluginToDelete = 'Insight';
         self::deletePluginFromConfigFile($pluginToDelete);
-        self::deletePluginFromFilesystem($pluginToDelete);
+        Piwik_PluginsManager::getInstance()->deletePluginFromFilesystem($pluginToDelete);
 
         // We also clean up 1.9.1 and delete Feedburner plugin
-        self::deletePluginFromFilesystem('Feedburner');
+        Piwik_PluginsManager::getInstance()->deletePluginFromFilesystem('Feedburner');
     }
 }
diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php
index 53e34374a4..909208a355 100644
--- a/core/testMinimumPhpVersion.php
+++ b/core/testMinimumPhpVersion.php
@@ -71,8 +71,9 @@ if (!function_exists('Piwik_ExitWithMessage')) {
      * @param string $message Main message, must be html encoded before calling
      * @param bool|string $optionalTrace Backtrace; will be displayed in lighter color
      * @param bool $optionalLinks If true, will show links to the Piwik website for help
+     * @param bool $goBack if true, displays a link to go back
      */
-    function Piwik_ExitWithMessage($message, $optionalTrace = false, $optionalLinks = false)
+    function Piwik_ExitWithMessage($message, $optionalTrace = false, $optionalLinks = false, $optionalLinkBack = false)
     {
         @header('Content-Type: text/html; charset=utf-8');
         if ($optionalTrace) {
@@ -87,14 +88,20 @@ if (!function_exists('Piwik_ExitWithMessage')) {
                             <li><a target="_blank" href="http://demo.piwik.org">Piwik Online Demo</a></li>
                             </ul>';
         }
+        if($optionalLinkBack) {
+            $optionalLinkBack = '<a href="javascript:window.back();">Go Back</a><br/>';
+        }
         $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Zeitgeist/templates/simpleLayoutHeader.tpl');
         $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Zeitgeist/templates/simpleLayoutFooter.tpl');
 
         $headerPage = str_replace('{$HTML_TITLE}', 'Piwik &rsaquo; Error', $headerPage);
         $content = '<p>' . $message . '</p>
-                    <p><a href="index.php">Go to Piwik</a><br/>
-                    <a href="index.php?module=Login">Login</a></p>
-                    ' . $optionalTrace . ' ' . $optionalLinks;
+                    <p>'
+                    . $optionalLinkBack
+                    . '<a href="index.php">Go to Piwik</a><br/>
+                       <a href="index.php?module=Login">Login</a>'
+                    . '</p>'
+                    . ' ' . $optionalTrace . ' ' . $optionalLinks;
 
         echo $headerPage . $content . $footerPage;
         exit;
diff --git a/plugins/CorePluginsAdmin/Controller.php b/plugins/CorePluginsAdmin/Controller.php
index baa04f6da4..d9e6f0fca9 100644
--- a/plugins/CorePluginsAdmin/Controller.php
+++ b/plugins/CorePluginsAdmin/Controller.php
@@ -44,7 +44,7 @@ class Piwik_CorePluginsAdmin_Controller extends Piwik_Controller_Admin
         return $view;
     }
 
-    protected function getPluginsInfo( $themesOnly = false )
+    protected function getPluginsInfo($themesOnly = false)
     {
         $plugins = array();
 
@@ -58,6 +58,7 @@ class Piwik_CorePluginsAdmin_Controller extends Piwik_Controller_Admin
             $plugins[$pluginName] = array(
                 'activated'       => Piwik_PluginsManager::getInstance()->isPluginActivated($pluginName),
                 'alwaysActivated' => Piwik_PluginsManager::getInstance()->isPluginAlwaysActivated($pluginName),
+                'uninstallable'   => Piwik_PluginsManager::getInstance()->isPluginUninstallable($pluginName),
             );
         }
         Piwik_PluginsManager::getInstance()->loadPluginTranslations();
@@ -74,7 +75,7 @@ class Piwik_CorePluginsAdmin_Controller extends Piwik_Controller_Admin
                     'description' => '<strong><em>' . Piwik_Translate('CorePluginsAdmin_PluginCannotBeFound')
                         . '</strong></em>',
                     'version'     => Piwik_Translate('General_Unknown'),
-                    'theme' => false,
+                    'theme'       => false,
                 );
             }
         }
@@ -103,23 +104,46 @@ class Piwik_CorePluginsAdmin_Controller extends Piwik_Controller_Admin
 
     public function deactivate($redirectAfter = true)
     {
-        Piwik::checkUserIsSuperUser();
-        $this->checkTokenInUrl();
-        $pluginName = Piwik_Common::getRequestVar('pluginName', null, 'string');
+        $pluginName = $this->initPluginModification();
         Piwik_PluginsManager::getInstance()->deactivatePlugin($pluginName);
+        $this->redirectAfterModification($redirectAfter);
+    }
+
+    protected function redirectAfterModification($redirectAfter)
+    {
         if ($redirectAfter) {
             Piwik_Url::redirectToReferer();
         }
     }
 
-    public function activate($redirectAfter = true)
+    protected function initPluginModification()
     {
         Piwik::checkUserIsSuperUser();
         $this->checkTokenInUrl();
         $pluginName = Piwik_Common::getRequestVar('pluginName', null, 'string');
+        return $pluginName;
+    }
+
+    public function activate($redirectAfter = true)
+    {
+        $pluginName = $this->initPluginModification();
         Piwik_PluginsManager::getInstance()->activatePlugin($pluginName);
-        if ($redirectAfter) {
-            Piwik_Url::redirectToReferer();
+        $this->redirectAfterModification($redirectAfter);
+    }
+
+    public function uninstall($redirectAfter = true)
+    {
+        $pluginName = $this->initPluginModification();
+        $uninstalled = Piwik_PluginsManager::getInstance()->uninstallPlugin($pluginName);
+        if(!$uninstalled) {
+            $path = Piwik_Common::getPathToPiwikRoot() . '/plugins/' . $pluginName . '/';
+            $messagePermissions = Piwik::getErrorMessageMissingPermissions($path);
+
+            $messageIntro = Piwik_Translate("Warning: \"%s\" could not be uninstalled. Piwik did not have enough permission to delete the files in $path. ",
+                $pluginName);
+            $exitMessage = $messageIntro . "<br/><br/>" . $messagePermissions;
+            Piwik_ExitWithMessage($exitMessage, $optionalTrace = false, $optionalLinks = false, $optionalLinkBack = true);
         }
+        $this->redirectAfterModification($redirectAfter);
     }
 }
diff --git a/plugins/CorePluginsAdmin/templates/macros.twig b/plugins/CorePluginsAdmin/templates/macros.twig
index 7da709fb30..1d3a592159 100644
--- a/plugins/CorePluginsAdmin/templates/macros.twig
+++ b/plugins/CorePluginsAdmin/templates/macros.twig
@@ -32,7 +32,7 @@
                             &nbsp; <cite>By
                             {% if plugin.info.author_homepage is defined %}
                             <a title="{{ 'CorePluginsAdmin_AuthorHomepage'|translate }}" href="{{ plugin.info.author_homepage }}" target="_blank">
-                                {% endif %}{{ plugin.info.author }}{% if plugin.info.author_homepage is defined %}</a>{% endif %}
+                                {% endif %}{{ plugin.info.author }}{% if plugin.info.author_homepage is defined -%}</a>{% endif -%}
                             .</cite>
                         {% endif %}
                     </td>
@@ -40,7 +40,9 @@
                         {% if plugin.activated %}
                             {{ 'CorePluginsAdmin_Active'|translate }}
                         {% else %}
-                            {{ 'CorePluginsAdmin_Inactive'|translate }}
+                            {{ 'CorePluginsAdmin_Inactive'|translate }} <br/>
+                            - {% if plugin.uninstallable %}<a href='index.php?module=CorePluginsAdmin&action=uninstall&pluginName={{ name }}&token_auth={{
+                        token_auth }}'>uninstall</a>{% endif %}
                         {% endif %}
                     </td>
 
diff --git a/plugins/Zeitgeist/stylesheets/simple_structure.css b/plugins/Zeitgeist/stylesheets/simple_structure.css
index c62143a1bb..1582148c9a 100644
--- a/plugins/Zeitgeist/stylesheets/simple_structure.css
+++ b/plugins/Zeitgeist/stylesheets/simple_structure.css
@@ -36,7 +36,7 @@ body#simple  {
 	vertical-align:bottom; 
 }
 #title {
-	padding-bottom:5px;
+	padding-bottom:15px;
 	border-bottom:1px solid #F0F0F0;
 	font:42px Georgia, serif;
 }
-- 
GitLab