Skip to content

Commit

Permalink
Added install support for plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
jaxwilko committed Jan 8, 2025
1 parent 22aaef0 commit 60a7089
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</button>
</div>
</div>
<div class="products row m-t-md">
<div class="products row m-t-sm">
<Product v-for="plugin in activePlugins" :product="plugin" type="plugin"></Product>
</div>
</div>
Expand Down
110 changes: 106 additions & 4 deletions modules/system/controllers/updates/assets/src/components/Product.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
</div>
</div>
<div class="absolute">
<button v-if="!product.installed"
<button v-if="!product.installed && !installing"
class="btn btn-info"
data-control="popup"
data-handler="onInstallPlugin"
:data-request-data="`package: '${product.package}'`"
@click="install()"
>Install</button>
<div v-if="installing" class="installing"></div>
<p v-if="product.installed" class="text-muted">This {{type}} is installed.</p>
</div>
</div>
Expand Down Expand Up @@ -47,6 +46,76 @@
<script>
export default {
props: ['product', 'type'],
data: () => {
return {
installing: false
}
},
methods: {
async install() {
this.installing = true;
this.$request('onInstallPlugin', {
data: {
package: this.product.package
},
success: (response) => {
$.popup({
size: 'installer-popup',
content: `
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Installing ${this.product.name}</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Blue Pill</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Red Pill</button>
</div>
`
});
const popup = document.querySelector('.size-installer-popup .modal-body');
const prepareMessage = (str) => {
return `<div class="install-message">${
str.split("\n").filter((line) => line.indexOf('FINISHED:') === 0 ? false : !!line).map((line) => {
['INFO', 'ERROR'].forEach((status) => {
if (line.indexOf(status) === 0) {
line = `
<div class="message-line">
<span class="message-${status.toLowerCase()}">${status}</span> <pre>${line.substring(status.length + 1)}</pre>
</div>
`;
}
});
return line;
}).join("\n")
}</div>`;
};
const checkStatus = () => {
this.$request('onInstallProductStatus', {
data: {
install_key: response.install_key
},
success: (statusResponse) => {
popup.innerHTML = prepareMessage(statusResponse.data);
if (!statusResponse.done) {
return setTimeout(checkStatus, 500);
}
this.installing = false;
}
})
};
checkStatus();
}
});
}
}
};
</script>
<style>
Expand Down Expand Up @@ -146,4 +215,37 @@ export default {
.product-footer .stars, .product-footer .github {
margin-right: 7px;
}
.installing:after {
content: ' ';
display: block;
background-size: 50px 50px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-image: url(/modules/system/assets/ui/images/loader-transparent.svg);
animation: spin 1s linear infinite;
width: 50px;
height: 50px;
margin: 0;
}
.install-message span, .install-message pre {
display: inline;
text-wrap: wrap;
}
.install-message .message-info {
color: #0EA804;
}
.install-message .message-error {
color: #c23c3c;
}
.install-message {
padding: 15px;
margin-bottom: 15px;
border-radius: 6px;
background: #121f2c;
color: #f5f5f5
}
.message-line {
margin-bottom: 5px;
}
</style>
73 changes: 56 additions & 17 deletions modules/system/controllers/updates/traits/ManagesPlugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
use Exception;
use Illuminate\Console\OutputStyle;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
Expand All @@ -28,6 +31,8 @@

trait ManagesPlugins
{
public string $cachePrefix = 'winter-x-install-';

/**
* Plugin manage controller
*/
Expand Down Expand Up @@ -248,30 +253,64 @@ public function onInstallUploadedPlugin(): string
*
* @throws ApplicationException If validation fails or the plugin cannot be installed
*/
public function onInstallPlugin(): StreamedResponse
public function onInstallPlugin(): array
{
if (!$code = trim(post('package'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_plugin_name'));
}

return Response::stream(function () use ($code) {
PluginManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), new class extends BufferedOutput {
protected function doWrite(string $message, bool $newline)
$key = base64_encode($this->cachePrefix . Session::getId() . md5(time() . $code));

App::terminating(function () use ($code, $key) {
$output = new class extends BufferedOutput {
protected string $key;

protected function doWrite(string $message, bool $newline): void
{
Cache::put($this->key, Cache::get($this->key, '') . trim($message) . ($newline ? "\n" : ''));
}

public function setKey(string $key): void
{
echo 'event: message' . "\n";
echo 'data: ' . json_encode(['content' => trim($message)]) . "\n\n";
flush();
ob_flush();
$this->key = $key;
}
}));

(new ComposerSource(ExtensionSource::TYPE_PLUGIN, composerPackage: $code))
->install();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no'
]);
};

$output->setKey($key);

PluginManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));

try {
$response = (new ComposerSource(ExtensionSource::TYPE_PLUGIN, composerPackage: $code))
->install();
} catch (\Throwable $e) {
$response = null;
} finally {
Cache::put($key, Cache::get($key, '') . 'FINISHED:' . ($response ? 'SUCCESS' : 'FAILED'));
}
});

return [
'install_key' => $key
];
}

public function onInstallProductStatus(): array
{
if (!$key = trim(post('install_key'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_plugin_name'));
}

if (!str_starts_with(base64_decode($key), $this->cachePrefix . Session::getId())) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}

$data = Cache::get($key, '');

return [
'done' => !$data || str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
'data' => $data
];
}

/**
Expand Down

0 comments on commit 60a7089

Please sign in to comment.