diff options
author | Thomas Steur <thomas.steur@gmail.com> | 2013-10-01 05:05:54 +0400 |
---|---|---|
committer | Thomas Steur <thomas.steur@gmail.com> | 2013-10-01 05:05:54 +0400 |
commit | e58b0d1efc28225f5599946a40fefbee20f5a74f (patch) | |
tree | 708eb2fa7340543d7874899ee4baa89752a0dd29 | |
parent | 58233e38185173cfd7a44656c9560e2a95ede3aa (diff) |
refs #607 added possibility to install a plugin or theme by uploading a ZIP file
-rw-r--r-- | plugins/CorePluginsAdmin/Controller.php | 43 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/CorePluginsAdmin.php | 1 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/PluginInstaller.php | 98 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/javascripts/pluginExtend.js | 20 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/stylesheets/marketplace.less | 12 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/templates/extend.twig | 28 | ||||
-rw-r--r-- | plugins/CorePluginsAdmin/templates/uploadPlugin.twig | 44 |
7 files changed, 241 insertions, 5 deletions
diff --git a/plugins/CorePluginsAdmin/Controller.php b/plugins/CorePluginsAdmin/Controller.php index cf83b013e1..a5ca8c74f4 100644 --- a/plugins/CorePluginsAdmin/Controller.php +++ b/plugins/CorePluginsAdmin/Controller.php @@ -73,6 +73,47 @@ class Controller extends \Piwik\Controller\Admin echo $view->render(); } + public function uploadPlugin() + { + Piwik::checkUserIsSuperUser(); + + $nonce = Common::getRequestVar('nonce', null, 'string'); + + if (!Nonce::verifyNonce(static::INSTALL_NONCE, $nonce)) { + throw new \Exception(Piwik_Translate('General_ExceptionNonceMismatch')); + } + + Nonce::discardNonce(static::INSTALL_NONCE); + + if (empty($_FILES['pluginZip'])) { + throw new \Exception('You did not specify a ZIP file.'); + } + + if (!empty($_FILES['pluginZip']['error'])) { + throw new \Exception('Something went wrong during the plugin file upload. Please try again.'); + } + + $file = $_FILES['pluginZip']['tmp_name']; + if (!file_exists($file)) { + throw new \Exception('Something went wrong during the plugin file upload. Please try again.'); + } + + $view = $this->configureView('@CorePluginsAdmin/uploadPlugin'); + + $pluginInstaller = new PluginInstaller('uploaded'); + $pluginMetadata = $pluginInstaller->installOrUpdatePluginFromFile($file); + + $view->nonce = Nonce::getNonce(static::ACTIVATE_NONCE); + $view->plugin = array( + 'name' => $pluginMetadata->name, + 'version' => $pluginMetadata->version, + 'isTheme' => !empty($pluginMetadata->theme), + 'isActivated' => PluginsManager::getInstance()->isPluginActivated($pluginMetadata->name) + ); + + echo $view->render(); + } + public function pluginDetails() { $pluginName = Common::getRequestVar('pluginName', null, 'string'); @@ -127,6 +168,8 @@ class Controller extends \Piwik\Controller\Admin function extend() { $view = $this->configureView('@CorePluginsAdmin/extend'); + $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE); + echo $view->render(); } diff --git a/plugins/CorePluginsAdmin/CorePluginsAdmin.php b/plugins/CorePluginsAdmin/CorePluginsAdmin.php index 53bbdf0a76..6fb999a1ce 100644 --- a/plugins/CorePluginsAdmin/CorePluginsAdmin.php +++ b/plugins/CorePluginsAdmin/CorePluginsAdmin.php @@ -90,6 +90,7 @@ class CorePluginsAdmin extends \Piwik\Plugin $jsFiles[] = "plugins/CoreHome/javascripts/popover.js"; $jsFiles[] = "plugins/CorePluginsAdmin/javascripts/pluginDetail.js"; $jsFiles[] = "plugins/CorePluginsAdmin/javascripts/pluginOverview.js"; + $jsFiles[] = "plugins/CorePluginsAdmin/javascripts/pluginExtend.js"; } } diff --git a/plugins/CorePluginsAdmin/PluginInstaller.php b/plugins/CorePluginsAdmin/PluginInstaller.php index ff58b45d9c..5f634f048e 100644 --- a/plugins/CorePluginsAdmin/PluginInstaller.php +++ b/plugins/CorePluginsAdmin/PluginInstaller.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\CorePluginsAdmin; use Piwik\Filechecks; use Piwik\Filesystem; +use Piwik\Log; use Piwik\SettingsPiwik; use Piwik\Unzip; @@ -49,6 +50,27 @@ class PluginInstaller $this->removeFolderIfExists($tmpPluginFolder); } + public function installOrUpdatePluginFromFile($pathToZip) + { + $tmpPluginFolder = PIWIK_USER_PATH . self::PATH_TO_DOWNLOAD . $this->pluginName; + $tmpPluginFolder = SettingsPiwik::rewriteTmpPathWithHostname($tmpPluginFolder); + + $this->makeSureFoldersAreWritable(); + $this->extractPluginFiles($pathToZip, $tmpPluginFolder); + + $this->makeSurePluginJsonExists($tmpPluginFolder); + $metadata = $this->getPluginMetadataIfValid($tmpPluginFolder); + + $this->pluginName = $metadata->name; + + $this->fixPluginFolderIfNeeded($tmpPluginFolder); + $this->copyPluginToDestination($tmpPluginFolder); + $this->removeFileIfExists($pathToZip); + $this->removeFolderIfExists($tmpPluginFolder); + + return $metadata; + } + private function makeSureFoldersAreWritable() { Filechecks::dieIfDirectoriesNotWritable(array(self::PATH_TO_DOWNLOAD, self::PATH_TO_EXTRACT)); @@ -98,11 +120,85 @@ class PluginInstaller private function makeSurePluginJsonExists($tmpPluginFolder) { - if (!file_exists($tmpPluginFolder . DIRECTORY_SEPARATOR . $this->pluginName . DIRECTORY_SEPARATOR . 'plugin.json')) { + $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder); + + if (!file_exists($pluginJsonPath)) { throw new PluginInstallerException('Plugin is not valid, it is missing the plugin.json file.'); } } + private function getPluginMetadataIfValid($tmpPluginFolder) + { + $pluginJsonPath = $this->getPathToPluginJson($tmpPluginFolder); + + $metadata = file_get_contents($pluginJsonPath); + $metadata = json_decode($metadata); + + if (empty($metadata)) { + throw new PluginInstallerException('Plugin is not valid, plugin.json is empty or does not contain valid JSON.'); + } + + if (empty($metadata->name)) { + throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin name.'); + } + + if (empty($metadata->version)) { + throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify the plugin version.'); + } + + if (empty($metadata->description)) { + throw new PluginInstallerException('Plugin is not valid, the plugin.json file does not specify a description.'); + } + + return $metadata; + } + + private function getPathToPluginJson($tmpPluginFolder) + { + $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder); + $path = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder . DIRECTORY_SEPARATOR . 'plugin.json'; + + return $path; + } + + /** + * @param $pluginDir + * @throws PluginInstallerException + * @return string + */ + private function getNameOfFirstSubfolder($pluginDir) + { + if ($dir = opendir($pluginDir)) { + $firstSubFolder = ''; + + while ($file = readdir($dir)) { + if ($file[0] != '.' && is_dir($pluginDir . DIRECTORY_SEPARATOR . $file)) { + $firstSubFolder = $file; + break; + } + } + + if (empty($firstSubFolder)) { + throw new PluginInstallerException('The plugin ZIP file does not contain a subfolder.'); + } + + return $firstSubFolder; + } + } + + private function fixPluginFolderIfNeeded($tmpPluginFolder) + { + $firstSubFolder = $this->getNameOfFirstSubfolder($tmpPluginFolder); + + if ($firstSubFolder === $this->pluginName) { + return; + } + + $from = $tmpPluginFolder . DIRECTORY_SEPARATOR . $firstSubFolder; + $to = $tmpPluginFolder . DIRECTORY_SEPARATOR . $this->pluginName; + rename($from, $to); + } + private function copyPluginToDestination($tmpPluginFolder) { $pluginTargetPath = PIWIK_USER_PATH . self::PATH_TO_EXTRACT . $this->pluginName; diff --git a/plugins/CorePluginsAdmin/javascripts/pluginExtend.js b/plugins/CorePluginsAdmin/javascripts/pluginExtend.js new file mode 100644 index 0000000000..1063a179f8 --- /dev/null +++ b/plugins/CorePluginsAdmin/javascripts/pluginExtend.js @@ -0,0 +1,20 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +$(document).ready(function () { + + $('.extendPlatform .uploadPlugin').click(function (event) { + event.preventDefault(); + + piwikHelper.modalConfirm('#installPluginByUpload', { + yes: function () { + window.location = link; + } + }); + }); + +});
\ No newline at end of file diff --git a/plugins/CorePluginsAdmin/stylesheets/marketplace.less b/plugins/CorePluginsAdmin/stylesheets/marketplace.less index 7c6ba22bb7..653d255051 100644 --- a/plugins/CorePluginsAdmin/stylesheets/marketplace.less +++ b/plugins/CorePluginsAdmin/stylesheets/marketplace.less @@ -9,6 +9,18 @@ .callToAction { font-size: 1.1em;line-height: 2em; } } +#installPluginByUpload { + .description { + margin-top: 30px; + margin-bottom: 20px; + } + + .startUpload { + margin-top: 20px; + margin-bottom: 20px; + } +} + .pluginslist { margin-top: 20px; max-width: 980px; diff --git a/plugins/CorePluginsAdmin/templates/extend.twig b/plugins/CorePluginsAdmin/templates/extend.twig index 0b04efb308..a45ec1de2a 100644 --- a/plugins/CorePluginsAdmin/templates/extend.twig +++ b/plugins/CorePluginsAdmin/templates/extend.twig @@ -5,6 +5,19 @@ {% block content %} <div class="extendPlatform"> + <div class="ui-confirm" id="installPluginByUpload"> + <h2>Extend Piwik by uploading a ZIP file</h2> + + <p class="description">You may upload here a ZIP downloaded from the Piwik Marketplace or your own plugin or theme.</p> + + <form enctype="multipart/form-data" + method="post" + action="{{ linkTo({'action':'uploadPlugin', 'nonce': installNonce}) }}"> + <input type="file" name="pluginZip"><br /> + <input class="startUpload" type="submit" value="Upload ZIP file"> + </form> + </div> + <div class="introduction"> <h2>Extend Piwik with Plugins and Themes</h2> @@ -21,24 +34,31 @@ <div> <div class="byPlugins"> <h3 class="header">Get new functionality</h3> - <span class="callToAction">by <a href="{{ linkTo({'action':'browsePlugins', 'sort': ''}) }}">installing a new plugin</a></span> + <span class="callToAction">by <a href="{{ linkTo({'action':'browsePlugins', 'sort': ''}) }}">installing a new plugin from the Marketplace</a></span> <p> <a href="{{ linkTo({'action':'browsePlugins', 'sort': ''}) }}"><img class="teaserImage" alt="Install a new plugin" src=""/></a> </p> - <span class="callToAction">or <a href="#">write your own plugin</a></span> + <span class="callToAction"> + by <a href="#">writing your own plugin</a> + <br/> or by <a href="#" class="uploadPlugin">uploading a plugin</a> + </span> + </div> <div class="byThemes"> <h3 class="header">Enjoy another look & feel</h3> - <span class="callToAction">by <a href="{{ linkTo({'action':'browseThemes', 'sort': ''}) }}">installing a new theme</a></span> + <span class="callToAction">by <a href="{{ linkTo({'action':'browseThemes', 'sort': ''}) }}">installing a new theme from the Marketplace</a></span> <p> <a href="{{ linkTo({'action':'browseThemes', 'sort': ''}) }}"><img class="teaserImage" alt="Install a new theme" src=""/></a> </p> - <span class="callToAction">or <a href="#">design your own theme</a></span> + <span class="callToAction"> + by <a href="#">design your own theme</a><br /> + or by <a href="#" class="uploadPlugin">uploading a theme</a> + </span> </div> </div> </div> diff --git a/plugins/CorePluginsAdmin/templates/uploadPlugin.twig b/plugins/CorePluginsAdmin/templates/uploadPlugin.twig new file mode 100644 index 0000000000..ae4a611a38 --- /dev/null +++ b/plugins/CorePluginsAdmin/templates/uploadPlugin.twig @@ -0,0 +1,44 @@ +{% extends 'admin.twig' %} + +{% block content %} + + <div style="max-width:980px;"> + + <div> + <h2>Installing {{ plugin.name }}</h2> + + {% if plugin.isTheme %} + + <p>Unzipping theme</p> + + <p>You have successfully installed the theme {{ plugin.name }} {{ plugin.version }}.</p> + + <p> + {% if not plugin.isActivated %} + <strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">Activate Theme</a></strong> + + | + {% endif %} + <a href="{{ linkTo({'action': 'extend'}) }}">Back to Extend Piwik</a> + </p> + + {% else %} + + <p>Unzipping plugin</p> + + <p>You have successfully installed the Plugin {{ plugin.name }} {{ plugin.version }}.</p> + + <p> + {% if not plugin.isActivated %} + <strong><a href="{{ linkTo({'action': 'activate', 'pluginName': plugin.name, 'nonce': nonce}) }}">Activate Plugin</a></strong> + + | + {% endif %} + <a href="{{ linkTo({'action': 'extend'}) }}">Back to Extend Piwik</a> + </p> + + {% endif %} + </div> + </div> + +{% endblock %} |