Overhaul the plugin installation process in the back-end

Themes can now be installed via the back-end
This commit is contained in:
Samuel Georges 2015-04-04 17:28:51 +11:00
parent 9e9aa97101
commit c54821f175
24 changed files with 1552 additions and 98 deletions

View File

@ -0,0 +1,126 @@
<?php namespace Cms\Classes;
use File;
use ApplicationException;
use System\Models\Parameters;
use Cms\Classes\Theme as CmsTheme;
/**
* Theme manager
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class ThemeManager
{
use \October\Rain\Support\Traits\Singleton;
//
// Gateway spawned
//
/**
* Returns a collection of themes installed via the update gateway
* @return array
*/
public function getInstalled()
{
return Parameters::get('system::theme.history', []);
}
/**
* Checks if a theme has ever been installed before.
* @param string $name Theme code
* @return boolean
*/
public function isInstalled($name)
{
return array_key_exists($name, Parameters::get('system::theme.history', []));
}
/**
* Flags a theme as being installed, so it is not downloaded twice.
* @param string $name Theme code
*/
public function setInstalled($code, $dirName = null)
{
if (!$dirName) {
$dirName = strtolower(str_replace('.', '-', $code));
}
$history = Parameters::get('system::theme.history', []);
$history[$code] = $dirName;
Parameters::set('system::theme.history', $history);
}
/**
* Flags a theme as being uninstalled.
* @param string $name Theme code
*/
public function setUninstalled($code)
{
$history = Parameters::get('system::theme.history', []);
if (array_key_exists($code, $history)) {
unset($history[$code]);
}
Parameters::set('system::theme.history', $history);
}
/**
* Returns an installed theme's code from it's dirname.
* @return string
*/
public function findByDirName($dirName)
{
$installed = $this->getInstalled();
foreach ($installed as $code => $name) {
if ($dirName == $name) {
return $code;
}
}
return null;
}
//
// Management
//
/**
* Completely delete a theme from the system.
* @param string $id Theme code/namespace
* @return void
*/
public function deleteTheme($theme)
{
if (!$theme) {
return false;
}
if (is_string($theme)) {
$theme = CmsTheme::load($theme);
}
if ($theme->isActiveTheme()) {
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
}
/*
* Delete from file system
*/
$themePath = $theme->getPath();
if (File::isDirectory($themePath)) {
File::deleteDirectory($themePath);
}
/*
* Set uninstalled
*/
if ($themeCode = $this->findByDirName($theme->getDirName())) {
$this->setUninstalled($themeCode);
}
}
}

View File

@ -13,6 +13,7 @@ use Cms\Models\ThemeData;
use Cms\Models\ThemeExport;
use Cms\Models\ThemeImport;
use Cms\Classes\Theme as CmsTheme;
use Cms\Classes\ThemeManager;
use System\Classes\SettingsManager;
use Backend\Classes\Controller;
use Exception;
@ -71,16 +72,7 @@ class Themes extends Controller
public function index_onDelete()
{
$theme = $this->findThemeObject();
if ($theme->isActiveTheme()) {
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
}
$themePath = $theme->getPath();
if (File::isDirectory($themePath)) {
File::deleteDirectory($themePath);
}
ThemeManager::instance()->deleteTheme(post('theme'));
Flash::success(trans('cms::lang.theme.delete_theme_success'));
return Redirect::refresh();

View File

@ -19,14 +19,12 @@
data-control="popup"
data-handler="onLoadCreateForm"
data-size="huge"
href="javascript:;"
target="_blank">
href="javascript:;">
<?= e(trans('cms::lang.theme.create_new_blank_theme')) ?>
</a>
<a
class="find-more-themes"
href="http://octobercms.com/themes"
target="_blank">
href="<?= Backend::url('system/updates/install/themes') ?>">
<?= e(trans('cms::lang.theme.find_more_themes')) ?>
</a>
</div>

View File

@ -74,7 +74,7 @@ return [
'new_directory_name_comment' => 'Provide a new directory name for the duplicated theme.',
'dir_name_invalid' => 'Name can contain only digits, Latin letters and the following symbols: _-',
'dir_name_taken' => 'Desired theme directory already exists.',
'find_more_themes' => 'Find more themes on OctoberCMS Theme Marketplace',
'find_more_themes' => 'Find more themes',
'return' => 'Return to themes list',
],
'maintenance' => [

View File

@ -0,0 +1,276 @@
.product-list-empty {
padding: 5px 0;
font-size: 16px;
color: #999;
}
.product-list {
margin: 0;
padding: 10px 0;
overflow: hidden;
/* clearfix */
}
.product-list li button,
.product-list li .image,
.product-list li .details {
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
transition: opacity .2s linear;
}
.product-list li button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
opacity: 0;
outline: none;
}
.product-list li:hover button {
opacity: .3;
}
.product-list li:hover button:hover {
opacity: .8;
}
.plugin-list li {
list-style: none;
position: relative;
border-bottom: 1px solid #E6E9E9;
margin-bottom: 10px;
padding-bottom: 10px;
overflow: hidden;
}
.plugin-list li:last-child {
border-bottom: none;
}
.plugin-list li .image {
float: left;
margin-right: 15px;
margin-left: 5px;
}
.plugin-list li .image img {
width: 50px;
height: 50px;
}
.plugin-list li .details p {
padding: 0;
margin: 3px 0 0 0;
color: #808C8D;
}
.plugin-list li h4 {
padding: 5px 0 0;
margin: 0;
color: #C03F31;
font-weight: 400;
}
.theme-list li {
float: left;
padding: 0;
margin: 0 10px 10px 0;
list-style: none;
border: 1px solid #E6E9E9;
background: #fff;
position: relative;
border-radius: 3px;
}
.theme-list li:hover {
border-color: transparent;
}
.theme-list li {
-webkit-transition: border .2s linear;
-moz-transition: border .2s linear;
transition: border .2s linear;
}
.theme-list li .image {
padding: 5px;
}
.theme-list li .image img {
width: 210px;
height: 140px;
}
.theme-list li:hover .image {
opacity: 0;
}
.theme-list li .details {
position: absolute;
bottom: 0;
left: 0;
opacity: 0;
padding: 10px;
overflow: hidden;
}
.theme-list li:hover .details {
opacity: 1;
}
.theme-list li h4 {
padding: 15px 0 0;
margin: 0;
}
.theme-list li p {
padding: 0;
margin: 0;
color: #999;
text-transform: uppercase;
font-size: 12px;
}
.suggested-products {
padding: 0;
}
.suggested-products .product {
padding: 0;
}
.suggested-products .image img {
width: 40px;
height: 40px;
margin-top: 10px;
}
.suggested-themes .image img {
width: 60px;
height: 40px;
}
.suggested-products .image {
float: left;
position: relative;
}
.suggested-products .details {
margin-left: 50px;
padding: 10px 0;
}
.suggested-themes .details {
margin-left: 70px;
}
.suggested-products .details h5 {
margin: 0 0 3px;
font-size: 14px;
color: #C03F31;
font-weight: 400;
}
.suggested-products .details p {
font-size: 12px;
}
.suggested-products a {
color: #777;
background: #fff;
padding: 5px;
text-decoration: none;
display: block;
overflow: hidden;
border-bottom: 1px solid #E6E9E9;
}
.suggested-products a:hover {
color: #333;
background: #f9f9f9;
}
.suggested-products a:hover .image:after {
content: "+";
color: #999;
font-size: 32px;
display: block;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
position: absolute;
top: 7px;
left: 0;
}
.suggested-products a:hover .image img {
opacity: .5;
}
/*!
* Typeahead
*/
.product-search {
position: relative;
width: 100%;
margin: 0 auto 0 auto;
text-align: left;
padding-bottom: 15px;
}
.twitter-typeahead {
width: 100%;
}
.typeahead,
.tt-hint {
width: 100%;
height: 46px;
padding: 8px 12px;
font-size: 24px;
line-height: 30px;
border: 1px solid #024e6a;
border-radius: 3px;
outline: none;
}
.typeahead {
background-color: #fff;
border-color: #e0e0e0;
}
.tt-input {
font-weight: 200;
}
.tt-input:focus {
border-color: #E6E9E9;
}
.tt-hint {
color: #999;
font-weight: 200;
}
.tt-dropdown-menu {
width: 100%;
margin-top: 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
.tt-suggestion {
font-size: 14px;
line-height: 18px;
}
.tt-suggestion + .tt-suggestion {
font-size: 14px;
border-top: 1px solid #ccc;
}
.tt-suggestions .product-details {
padding: 5px;
overflow: hidden;
position: relative;
}
.tt-suggestions .product-image {
float: left;
margin-right: 10px;
}
.tt-suggestions .product-image img {
height: 45px;
width: 45px;
}
.tt-suggestions .product-name {
font-size: 20px;
padding-top: 5px;
}
.tt-suggestion.tt-cursor {
cursor: pointer;
}
.tt-suggestion.tt-cursor .product-details {
color: #333;
background: #f9f9f9;
border-color: #f0f0f0;
}
.tt-suggestion.tt-cursor .product-details .product-image:after {
content: "+";
color: #999;
font-size: 38px;
display: block;
width: 45px;
height: 45px;
text-align: center;
line-height: 45px;
position: absolute;
top: 5px;
left: 5px;
}
.tt-suggestion.tt-cursor .product-details .product-image img {
opacity: .5;
}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
@import "../../../backend/assets/less/core/boot.less";
@import "../../../../backend/assets/less/core/boot.less";
.control-settings {

View File

@ -0,0 +1,293 @@
@import "../../../../backend/assets/less/core/boot.less";
.product-list-empty {
padding: 5px 0;
font-size: 16px;
color: #999;
}
.product-list {
margin: 0;
padding: 10px 0;
overflow: hidden; /* clearfix */
}
.product-list li button,
.product-list li .image,
.product-list li .details {
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
transition: opacity .2s linear;
}
.product-list li button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
opacity: 0;
outline: none;
}
.product-list li:hover button {
opacity: .3;
}
.product-list li:hover button:hover {
opacity: .8;
}
.plugin-list {
}
.plugin-list li {
list-style: none;
position: relative;
border-bottom: 1px solid #E6E9E9;
margin-bottom: 10px;
padding-bottom: 10px;
overflow: hidden;
}
.plugin-list li:last-child {
border-bottom: none;
}
.plugin-list li .image {
float: left;
margin-right: 15px;
margin-left: 5px;
}
.plugin-list li .image img {
width: 50px;
height: 50px;
}
.plugin-list li .details p {
padding: 0;
margin: 3px 0 0 0;
color: #808C8D;
}
.plugin-list li h4 {
padding: 5px 0 0;
margin: 0;
color: #C03F31;
font-weight: 400;
}
.theme-list li {
float: left;
padding: 0;
margin: 0 10px 10px 0;
list-style: none;
border: 1px solid #E6E9E9;
background: #fff;
position: relative;
border-radius: 3px;
}
.theme-list li:hover {
border-color: transparent;
}
.theme-list li {
-webkit-transition: border .2s linear;
-moz-transition: border .2s linear;
transition: border .2s linear;
}
.theme-list li .image {
padding: 5px;
}
.theme-list li .image img {
width: 210px;
height: 140px;
}
.theme-list li:hover .image {
opacity: 0;
}
.theme-list li .details {
position: absolute;
bottom: 0;
left: 0;
opacity: 0;
padding: 10px;
overflow: hidden;
}
.theme-list li:hover .details {
opacity: 1;
}
.theme-list li h4 {
padding: 15px 0 0;
margin: 0;
}
.theme-list li p {
padding: 0;
margin: 0;
color: #999;
text-transform: uppercase;
font-size: 12px;
}
.suggested-products-container {
}
.suggested-products {
padding: 0;
}
.suggested-products .product {
padding: 0;
}
.suggested-products .image img {
width: 40px;
height: 40px;
margin-top: 10px;
}
.suggested-themes .image img {
width: 60px;
height: 40px;
}
.suggested-products .image {
float: left;
position: relative;
}
.suggested-products .details {
margin-left: 50px;
padding: 10px 0;
}
.suggested-themes .details {
margin-left: 70px;
}
.suggested-products .details h5 {
margin: 0 0 3px;
font-size: 14px;
color: #C03F31;
font-weight: 400;
}
.suggested-products .details p {
font-size: 12px;
}
.suggested-products a {
color: #777;
background: #fff;
padding: 5px;
text-decoration: none;
display: block;
overflow: hidden;
border-bottom: 1px solid #E6E9E9;
}
.suggested-products a:hover {
color: #333;
background: #f9f9f9;
}
.suggested-products a:hover .image {
}
.suggested-products a:hover .image:after {
content: "+";
color: #999;
font-size: 32px;
display: block;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
position: absolute;
top: 7px;
left: 0;
}
.suggested-products a:hover .image img {
opacity: .5;
}
/*!
* Typeahead
*/
.product-search {
position: relative;
width: 100%;
margin: 0 auto 0 auto;
text-align: left;
padding-bottom: 15px;
}
.twitter-typeahead {
width: 100%;
}
.typeahead,
.tt-hint {
width: 100%;
height: 46px;
padding: 8px 12px;
font-size: 24px;
line-height: 30px;
border: 1px solid #024e6a;
border-radius: 3px;
outline: none;
}
.typeahead {
background-color: #fff;
border-color: #e0e0e0;
}
.tt-input {
font-weight: 200;
}
.tt-input:focus {
border-color: #E6E9E9;
}
.tt-hint {
color: #999;
font-weight: 200;
}
.tt-dropdown-menu {
width: 100%;
margin-top: 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 3px;
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}
.tt-suggestion {
font-size: 14px;
line-height: 18px;
}
.tt-suggestion + .tt-suggestion {
font-size: 14px;
border-top: 1px solid #ccc;
}
.tt-suggestions .product-details {
padding: 5px;
overflow: hidden;
position: relative;
}
.tt-suggestions .product-image {
float: left;
margin-right: 10px;
}
.tt-suggestions .product-image img {
height: 45px;
width: 45px;
}
.tt-suggestions .product-name {
font-size: 20px;
padding-top: 5px;
}
.tt-suggestions .product-description {
}
.tt-suggestion.tt-cursor {
cursor: pointer;
}
.tt-suggestion.tt-cursor .product-details {
color: #333;
background: #f9f9f9;
border-color: #f0f0f0;
}
.tt-suggestion.tt-cursor .product-details .product-image:after {
content: "+";
color: #999;
font-size: 38px;
display: block;
width: 45px;
height: 45px;
text-align: center;
line-height: 45px;
position: absolute;
top: 5px;
left: 5px;
}
.tt-suggestion.tt-cursor .product-details .product-image img {
opacity: .5;
}

View File

@ -1,4 +1,4 @@
@import "../../../backend/assets/less/core/boot.less";
@import "../../../../backend/assets/less/core/boot.less";
.control-updatelist {

View File

@ -645,4 +645,40 @@ class PluginManager
return $result;
}
//
// Management
//
/**
* Completely roll back and delete a plugin from the system.
* @param string $id Plugin code/namespace
* @return void
*/
public function deletePlugin($id)
{
/*
* Rollback plugin
*/
UpdateManager::instance()->rollbackPlugin($id);
/*
* Delete from file system
*/
if ($pluginPath = PluginManager::instance()->getPluginPath($id)) {
File::deleteDirectory($pluginPath);
}
}
/**
* Tears down a plugin's database tables and rebuilds them.
* @param string $id Plugin code/namespace
* @return void
*/
public function refreshPlugin($id)
{
$manager = UpdateManager::instance();
$manager->rollbackPlugin($id);
$manager->updatePlugin($id);
}
}

View File

@ -5,14 +5,16 @@ use URL;
use File;
use Lang;
use Http;
use Cache;
use Schema;
use Config;
use Carbon\Carbon;
use ApplicationException;
use Cms\Classes\ThemeManager;
use System\Models\Parameters;
use System\Models\PluginVersion;
use ApplicationException;
use System\Helpers\Cache as CacheHelper;
use October\Rain\Filesystem\Zip;
use Carbon\Carbon;
use Exception;
/**
@ -48,6 +50,11 @@ class UpdateManager
*/
protected $pluginManager;
/**
* @var Cms\Classes\ThemeManager
*/
protected $themeManager;
/**
* @var System\Classes\VersionManager
*/
@ -68,12 +75,18 @@ class UpdateManager
*/
protected $disableCoreUpdates = false;
/**
* @var array Cache of gateway products
*/
protected $productCache;
/**
* Initialize this singleton.
*/
protected function init()
{
$this->pluginManager = PluginManager::instance();
$this->themeManager = ThemeManager::instance();
$this->versionManager = VersionManager::instance();
$this->tempDirectory = temp_path();
$this->baseDirectory = base_path();
@ -235,7 +248,7 @@ class UpdateManager
*/
$themes = [];
foreach (array_get($result, 'themes', []) as $code => $info) {
if (!$this->isThemeInstalled($code)) {
if (!$this->themeManager->isInstalled($code)) {
$themes[$code] = $info;
}
}
@ -492,6 +505,17 @@ class UpdateManager
// Themes
//
/**
* Looks up a theme from the update server.
* @param string $name Theme name.
* @return array Details about the theme.
*/
public function requestThemeDetails($name)
{
$result = $this->requestServerData('theme/detail', ['name' => $name]);
return $result;
}
/**
* Downloads a theme from the update server.
* @param string $name Theme name.
@ -516,29 +540,118 @@ class UpdateManager
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
}
$this->setThemeInstalled($name);
$this->themeManager->setInstalled($name);
@unlink($filePath);
}
/**
* Checks if a theme has ever been installed before.
* @param string $name Theme code
* @return boolean
*/
public function isThemeInstalled($name)
//
// Products
//
public function requestProductDetails($codes, $type = null)
{
return array_key_exists($name, Parameters::get('system::theme.history', []));
if ($type != 'plugin' && $type != 'theme')
$type = 'plugin';
$codes = (array) $codes;
$this->loadProductDetailCache();
/*
* New products requested
*/
$newCodes = array_diff($codes, array_keys($this->productCache[$type]));
if (count($newCodes)) {
$dataCodes = [];
$data = $this->requestServerData($type.'/details', ['names' => $newCodes]);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
$dataCodes[] = $code;
}
/*
* Cache unknown products
*/
$unknownCodes = array_diff($newCodes, $dataCodes);
foreach ($unknownCodes as $code) {
$this->cacheProductDetail($type, $code, -1);
}
$this->saveProductDetailCache();
}
/*
* Build details from cache
*/
$result = [];
$requestedDetails = array_intersect_key($this->productCache[$type], array_flip($codes));
foreach ($requestedDetails as $detail) {
if ($detail === -1) continue;
$result[] = $detail;
}
return $result;
}
/**
* Flags a theme as being installed, so it is not downloaded twice.
* @param string $name Theme code
* Returns popular themes found on the marketplace.
*/
public function setThemeInstalled($name)
public function requestPopularProducts($type = null)
{
$history = Parameters::get('system::theme.history', []);
$history[$name] = Carbon::now()->timestamp;
Parameters::set('system::theme.history', $history);
if ($type != 'plugin' && $type != 'theme')
$type = 'plugin';
$cacheKey = 'system-updates-popular-'.$type;
if (Cache::has($cacheKey)) {
return @unserialize(Cache::get($cacheKey)) ?: [];
}
$data = $this->requestServerData($type.'/popular');
Cache::put($cacheKey, serialize($data), 60);
foreach ($data as $product) {
$code = array_get($product, 'code', -1);
$this->cacheProductDetail($type, $code, $product);
}
$this->saveProductDetailCache();
return $data;
}
protected function loadProductDetailCache()
{
$defaultCache = ['theme' => [], 'plugin' => []];
$cacheKey = 'system-updates-product-details';
if (Cache::has($cacheKey)) {
$this->productCache = @unserialize(Cache::get($cacheKey)) ?: $defaultCache;
}
else {
$this->productCache = $defaultCache;
}
}
protected function saveProductDetailCache()
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$cacheKey = 'system-updates-product-details';
$expiresAt = Carbon::now()->addDays(2);
Cache::put($cacheKey, serialize($this->productCache), $expiresAt);
}
protected function cacheProductDetail($type, $code, $data)
{
if ($this->productCache === null) {
$this->loadProductDetailCache();
}
$this->productCache[$type][$code] = $data;
}
//

View File

@ -34,7 +34,7 @@ class Settings extends Controller
$this->requiredPermissions = null;
}
$this->addCss('/modules/system/assets/css/settings.css', 'core');
$this->addCss('/modules/system/assets/css/settings/settings.css', 'core');
BackendMenu::setContext('October.System', 'system', 'settings');
}

View File

@ -7,7 +7,9 @@ use Flash;
use Config;
use Backend;
use Redirect;
use Response;
use BackendMenu;
use Cms\Classes\ThemeManager;
use Backend\Classes\Controller;
use System\Models\Parameters;
use System\Models\PluginVersion;
@ -37,7 +39,7 @@ class Updates extends Controller
parent::__construct();
$this->addJs('/modules/system/assets/js/updates/updates.js', 'core');
$this->addCss('/modules/system/assets/css/updates.css', 'core');
$this->addCss('/modules/system/assets/css/updates/updates.css', 'core');
BackendMenu::setContext('October.System', 'system', 'updates');
SettingsManager::setContext('October.System', 'updates');
@ -67,6 +69,31 @@ class Updates extends Controller
return $this->asExtension('ListController')->index();
}
/**
* Install new plugins / themes
*/
public function install($tab = null)
{
if (get('search')) {
return Response::make($this->onSearchProducts());
}
try {
$this->bodyClass = 'compact-container breadcrumb-flush';
$this->pageTitle = 'Install products';
$this->addJs('/modules/system/assets/js/updates/install.js', 'core');
$this->addCss('/modules/system/assets/css/updates/install.css', 'core');
$this->vars['activeTab'] = $tab ?: 'plugins';
$this->vars['installedPlugins'] = $this->getInstalledPlugins();
$this->vars['installedThemes'] = $this->getInstalledThemes();
}
catch (Exception $ex) {
$this->handleError($ex);
}
}
/**
* {@inheritDoc}
*/
@ -132,12 +159,12 @@ class Updates extends Controller
case 'completeUpdate':
$manager->update();
Flash::success(Lang::get('system::lang.updates.update_success'));
return Backend::redirect('system/updates');
return Redirect::refresh();
case 'completeInstall':
$manager->update();
Flash::success(Lang::get('system::lang.install.install_success'));
return Backend::redirect('system/updates');
return Redirect::refresh();
}
}
@ -442,7 +469,7 @@ class Updates extends Controller
}
/**
* Removes or purges plugins from the system.
* Rollback and remove plugins from the system.
* @return void
*/
public function onRemovePlugins()
@ -454,18 +481,7 @@ class Updates extends Controller
continue;
}
/*
* Rollback plugin
*/
$pluginCode = $object->code;
UpdateManager::instance()->rollbackPlugin($pluginCode);
/*
* Delete from file system
*/
if ($pluginPath = PluginManager::instance()->getPluginPath($pluginCode)) {
File::deleteDirectory($pluginPath);
}
PluginManager::instance()->deletePlugin($object->code);
}
Flash::success(Lang::get('system::lang.plugins.remove_success'));
@ -474,6 +490,22 @@ class Updates extends Controller
return $this->listRefresh('manage');
}
/**
* Rollback and remove a single plugin from the system.
* @return void
*/
public function onRemovePlugin()
{
if ($pluginCode = post('code')) {
PluginManager::instance()->deletePlugin($pluginCode);
Flash::success(Lang::get('system::lang.plugins.remove_success'));
}
return Redirect::refresh();
}
/**
* Rebuilds plugin database migrations.
* @return void
@ -482,19 +514,12 @@ class Updates extends Controller
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
$manager = UpdateManager::instance();
foreach ($checkedIds as $objectId) {
if (!$object = PluginVersion::find($objectId)) {
continue;
}
/*
* Refresh plugin
*/
$pluginCode = $object->code;
$manager->rollbackPlugin($pluginCode);
$manager->updatePlugin($pluginCode);
PluginManager::instance()->refreshPlugin($object->code);
}
Flash::success(Lang::get('system::lang.plugins.refresh_success'));
@ -548,4 +573,145 @@ class Updates extends Controller
return Backend::redirect('system/updates/manage');
}
//
// Theme management
//
/**
* Validate the theme code and execute the theme installation
*/
public function onInstallTheme()
{
try {
if (!$code = trim(post('code'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_theme_name'));
}
$manager = UpdateManager::instance();
$result = $manager->requestThemeDetails($code);
if (!isset($result['code']) || !isset($result['hash'])) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
$name = $result['code'];
$hash = $result['hash'];
$themes = [$name => $hash];
$plugins = [];
/*
* Update steps
*/
$updateSteps = $this->buildUpdateSteps(null, $plugins, $themes);
/*
* Finish up
*/
$updateSteps[] = [
'code' => 'completeInstall',
'label' => Lang::get('system::lang.install.install_completing'),
];
$this->vars['updateSteps'] = $updateSteps;
return $this->makePartial('execute');
}
catch (Exception $ex) {
$this->handleError($ex);
return $this->makePartial('theme_form');
}
}
/**
* Deletes a single theme from the system.
* @return void
*/
public function onRemoveTheme()
{
if ($themeCode = post('code')) {
ThemeManager::instance()->deleteTheme($themeCode);
Flash::success(trans('cms::lang.theme.delete_theme_success'));
}
return Redirect::refresh();
}
//
// Product install
//
public function install_onSearchProducts()
{
$searchType = get('search', 'plugins');
$serverUri = $searchType == 'plugins' ? 'plugin/search' : 'theme/search';
$manager = UpdateManager::instance();
return $manager->requestServerData($serverUri, ['query' => get('query')]);
}
public function install_onGetPopularPlugins()
{
$installed = $this->getInstalledPlugins();
$popular = UpdateManager::instance()->requestPopularProducts('plugin');
$popular = $this->filterPopularProducts($popular, $installed);
return ['result' => $popular];
}
public function install_onGetPopularThemes()
{
$installed = $this->getInstalledThemes();
$popular = UpdateManager::instance()->requestPopularProducts('theme');
$popular = $this->filterPopularProducts($popular, $installed);
return ['result' => $popular];
}
protected function getInstalledPlugins()
{
$installed = PluginVersion::lists('code');
$manager = UpdateManager::instance();
return $manager->requestProductDetails($installed, 'plugin');
}
protected function getInstalledThemes()
{
$history = Parameters::get('system::theme.history', []);
$manager = UpdateManager::instance();
$installed = $manager->requestProductDetails(array_keys($history), 'theme');
/*
* Splice in the directory names
*/
foreach ($installed as $key => $data) {
$code = array_get($data, 'code');
$installed[$key]['dirName'] = array_get($history, $code, $code);
}
return $installed;
}
/*
* Remove installed products from the collection
*/
protected function filterPopularProducts($popular, $installed)
{
$installedArray = [];
foreach ($installed as $product) {
$installedArray[] = array_get($product, 'code', -1);
}
foreach ($popular as $key => $product) {
$code = array_get($product, 'code');
if (in_array($code, $installedArray)) {
unset($popular[$key]);
}
}
return $popular;
}
}

View File

@ -0,0 +1,102 @@
<div>
<!-- Search -->
<form
id="installPluginsForm"
data-handler="onInstallPlugin"
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
<div class="product-search">
<input
name="code"
id="pluginSearchInput"
class="product-search-input search-input-lg typeahead"
placeholder="search plugins to install..."
data-search-type="plugins"
/>
</div>
</form>
<div class="row">
<div class="col-md-7">
<!-- Installed plugins -->
<div id="pluginList"
class="product-list-manager">
<h4 class="section-header">
<a href="<?= Backend::url('system/updates') ?>">Installed plugins</a>
<small>(<span class="product-counter"><?= count($installedPlugins) ?></span>)</small>
</h4>
<?php if (!count($installedPlugins)): ?>
<div class="product-list-empty">
<p>There are no plugins installed from the marketplace.</p>
</div>
<?php else: ?>
<ul class="product-list plugin-list">
<?php foreach ($installedPlugins as $plugin): ?>
<li data-code="<?= $plugin['code'] ?>">
<div class="image">
<img src="<?= $plugin['image'] ?>" alt="">
</div>
<div class="details">
<h4><?= $plugin['name'] ?></h4>
<p>by <?= $plugin['author'] ?></p>
</div>
<button
type="button"
class="close"
aria-hidden="true"
data-request="onRemovePlugin"
data-request-data="code: '<?= $plugin['code'] ?>'"
data-request-confirm="Are you sure you want to remove this?"
data-stripe-load-indicator>
&times;
</button>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
</div>
<div class="col-md-5">
<!-- Recommended extras -->
<div class="suggested-products-container">
<h4 class="section-header">Recommended</h4>
<div class="scroll-panel">
<div
id="suggestedPlugins"
class="suggested-products suggested-plugins"
data-handler="onGetPopularPlugins"
data-view="plugin/suggestion"></div>
</div>
</div>
</div>
</div>
</div>
<script type="text/template" data-partial="plugin/suggestion">
<div class="product">
<a
data-control="popup"
data-handler="onInstallPlugin"
data-request-data="code: '{{code}}'"
href="javascript:;">
<div class="image"><img src="{{image}}" alt=""></div>
<div class="details">
<h5 class="text-overflow">{{code}}</h5>
<p>{{description}}</p>
</div>
</a>
</div>
</script>

View File

@ -0,0 +1,104 @@
<div>
<!-- Search -->
<form
id="installThemesForm"
data-handler="onInstallTheme"
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
<div class="product-search">
<input
name="code"
id="themeSearchInput"
class="product-search-input search-input-lg typeahead"
placeholder="search themes to install..."
data-search-type="themes"
/>
</div>
</form>
<div class="row">
<div class="col-md-7">
<!-- Installed themes -->
<div id="themeList"
class="product-list-manager"
data-handler="onGetInstalledThemes"
data-view="product/theme">
<h4 class="section-header">
<a href="<?= Backend::url('cms/themes') ?>">Installed themes</a>
<small>(<span class="product-counter">0</span>)</small>
</h4>
<?php if (!count($installedThemes)): ?>
<div class="product-list-empty">
<p>There are no themes installed from the marketplace.</p>
</div>
<?php else: ?>
<ul class="product-list theme-list">
<?php foreach ($installedThemes as $theme): ?>
<li data-code="<?= $theme['code'] ?>">
<div class="image">
<img src="<?= $theme['image'] ?>" alt="">
</div>
<div class="details">
<h4><?= $theme['name'] ?></h4>
<p>by <?= $theme['author'] ?></p>
</div>
<button
type="button"
class="close"
aria-hidden="true"
data-request="onRemoveTheme"
data-request-data="code: '<?= $theme['dirName'] ?>'"
data-request-confirm="Are you sure you want to remove this?"
data-stripe-load-indicator>
&times;
</button>
</li>
<?php endforeach ?>
</ul>
<?php endif ?>
</div>
</div>
<div class="col-md-5">
<!-- Recommended extras -->
<div class="suggested-products-container">
<h4 class="section-header">Recommended</h4>
<div class="scroll-panel">
<div
id="suggestedThemes"
class="suggested-products suggested-themes"
data-handler="onGetPopularThemes"
data-view="theme/suggestion"></div>
</div>
</div>
</div>
</div>
</div>
<script type="text/template" data-partial="theme/suggestion">
<div class="product">
<a
data-control="popup"
data-handler="onInstallTheme"
data-request-data="code: '{{code}}'"
href="javascript:;">
<div class="image"><img src="{{image}}" alt=""></div>
<div class="details">
<h5 class="text-overflow">{{code}}</h5>
<p>{{description}}</p>
</div>
</a>
</div>
</script>

View File

@ -1,24 +0,0 @@
<?= Form::open() ?>
<div class="input-group" style="width: 207px">
<input
placeholder="<?= e(trans('backend::lang.list.search_prompt')) ?>"
type="text"
name="code"
value=""
class="form-control icon plus growable pull-right"
autocomplete="off" />
<span class="input-group-btn">
<button
type="submit"
class="btn btn-success"
id="installPluginButton"
data-control="popup"
data-handler="onInstallPlugin">
<?= e(trans('system::lang.install.plugin_label')) ?>
</button>
</span>
</div>
<?= Form::close() ?>

View File

@ -11,4 +11,9 @@
class="btn btn-default oc-icon-puzzle-piece">
<?= e(trans('system::lang.plugins.manage')) ?>
</a>
<a
href="<?= Backend::url('system/updates/install') ?>"
class="btn btn-success oc-icon-plus">
Install plugins
</a>
</div>

View File

@ -0,0 +1,47 @@
<?= Form::open(['id' => 'themeForm']) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('system::lang.install.theme_label')) ?></h4>
</div>
<div class="modal-body">
<?php if ($this->fatalError): ?>
<p class="flash-message static error"><?= $fatalError ?></p>
<?php endif ?>
<div class="form-group">
<label for="themeCode"><?= e(trans('system::lang.theme.name.label')) ?></label>
<input
name="code"
type="text"
class="form-control"
id="themeCode"
value="<?= post('code') ?>" />
<p class="help-block"><?= e(trans('system::lang.theme.name.help')) ?></p>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
data-dismiss="popup"
data-control="popup"
data-handler="onInstallTheme">
<?= e(trans('system::lang.install.theme_label')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<script>
setTimeout(
function(){ $('#themeCode').select() },
310
)
</script>
<?= Form::close() ?>

View File

@ -9,4 +9,3 @@ noRecordsMessage: backend::lang.list.no_records
toolbar:
buttons: list_toolbar
search: list_search

View File

@ -0,0 +1,49 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('system/updates') ?>"><?= e(trans('system::lang.updates.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="control-tabs content-tabs tabs-flush" data-control="tab">
<ul class="nav nav-tabs">
<li class="<?= $activeTab == 'plugins' ? 'active' : '' ?>">
<a
href="#tabPlugins"
data-tab-url="<?= Backend::url('system/updates/install/plugins') ?>">
Plugins
</a>
</li>
<li class="<?= $activeTab == 'themes' ? 'active' : '' ?>">
<a
href="#tabThemes"
data-tab-url="<?= Backend::url('system/updates/install/themes') ?>">
Themes
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane <?= $activeTab == 'plugins' ? 'active' : '' ?>">
<div class="padded-container">
<?= $this->makePartial('install_plugins') ?>
</div>
</div>
<div class="tab-pane <?= $activeTab == 'themes' ? 'active' : '' ?>">
<div class="padded-container">
<?= $this->makePartial('install_themes') ?>
</div>
</div>
</div>
</div>
<?php else: ?>
<div class="padded-container">
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<p><a href="<?= Backend::url('system/updates') ?>" class="btn btn-default"><?= e(trans('system::lang.settings.return')) ?></a></p>
<p><a href="<?= Backend::url('cms/themes') ?>" class="btn btn-default"><?= e(trans('cms::lang.theme.return')) ?></a></p>
</div>
<?php endif ?>

View File

@ -52,6 +52,12 @@ return [
'my_settings' => 'My Settings'
]
],
'theme' => [
'name' => [
'label' => 'Theme Name',
'help' => 'Name the theme by its unique code. For example, RainLab.Vanilla'
]
],
'plugin' => [
'unnamed' => 'Unnamed plugin',
'name' => [
@ -156,7 +162,9 @@ return [
'install' => [
'project_label' => 'Attach to Project',
'plugin_label' => 'Install Plugin',
'theme_label' => 'Install Theme',
'missing_plugin_name' => 'Please specify a Plugin name to install.',
'missing_theme_name' => 'Please specify a Theme name to install.',
'install_completing' => 'Finishing installation process',
'install_success' => 'The plugin has been installed successfully.'
],

View File

@ -4,20 +4,20 @@
columns:
name:
label: system::lang.updates.plugin_name
sortable: false
name:
label: system::lang.updates.plugin_name
sortable: false
description:
label: system::lang.updates.plugin_description
sortable: false
description:
label: system::lang.updates.plugin_description
sortable: false
version:
label: system::lang.updates.plugin_version
sortable: false
version:
label: system::lang.updates.plugin_version
sortable: false
author:
label: system::lang.updates.plugin_author
sortable: false
type: partial
path: column_author
author:
label: system::lang.updates.plugin_author
sortable: false
type: partial
path: column_author