Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[0.11] Add plugin hooks #523

Draft
wants to merge 6 commits into
base: release-0.11
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/AttributeTypes/Units/Implementations/UnitManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use Exception;

class UnitManager extends Singleton {

// Important: This must be redifined, otherwise PHP will use the 'last' child class that was loaded.
protected static $instance = null;

private array $unitSystems = [];

Expand Down
33 changes: 33 additions & 0 deletions app/Http/Middleware/PluginHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Http\Middleware;

use App\Plugin;
use App\Plugin\HookRegister;
use Closure;
use Illuminate\Http\Request;

class PluginHook
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/

public function handle(Request $request, Closure $next) {

// PHP doesn't keep the state in cache between requests, so we need to reinitialize the plugin hooks.
// REFACTOR:: This should be done in a more elegant way.
Plugin::init();

$actionNamespace = $request->route()->getActionName();
$namespaceParts = explode('\\', $actionNamespace);
$name = array_pop($namespaceParts);

$response = $next($request);
$response = HookRegister::get()->call($name, $request, $response);

return $response;
}
}
6 changes: 3 additions & 3 deletions app/Patterns/Singleton.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

abstract class Singleton {

public static ?Singleton $instance = null;
protected static $instance = null; // Must be redefined in the child class.

protected function __construct() {
static::$instance = $this;
}

static function get(): static {
if (static::$instance != null) {
final static function get() {
if(static::$instance != null) {
return static::$instance;
}
return new static();
Expand Down
95 changes: 80 additions & 15 deletions app/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;

class Plugin extends Model
{

static $instantiated = false;

/**
* The attributes that are assignable.
*
Expand Down Expand Up @@ -47,6 +49,10 @@ public function publicName($withPath = true) {
}
return $name;
}

public function getPath(){
return base_path("app/Plugins/$this->name");
}

public static function getInfo($path, $isString = false) {
if(!$isString) {
Expand Down Expand Up @@ -110,11 +116,11 @@ public static function updateOrCreateFromInfo(array $info) : Plugin {
}

public static function updateState() : void {
$pluginPath = base_path('app/Plugins');
$pluginPath = base_path('app' . DIRECTORY_SEPARATOR . 'Plugins');
$availablePlugins = File::directories($pluginPath);

self::discoverPlugins($availablePlugins);
self::cleanupPlugins($availablePlugins);
$activePlugins = self::discoverPlugins($availablePlugins);
self::initializePlugins($activePlugins);
// self::cleanupPlugins($availablePlugins);
}

public static function cleanupPlugins(array $list) : void {
Expand All @@ -131,13 +137,44 @@ public static function cleanupPlugins(array $list) : void {
}
}

public static function discoverPlugins(array $list) : void {
public static function discoverPlugins(array $list) : array {
$activePlugins = [];
foreach($list as $ap) {
$info = self::getInfo($ap);
if($info !== false) {
self::updateOrCreateFromInfo($info);
$plugin = self::updateOrCreateFromInfo($info);

if($plugin->isActive()){
$activePlugins[] = $plugin;
}
}
}
return $activePlugins;
}

public static function init(){
if(!self::$instantiated){
self::updateState();
self::$instantiated = true;
}
}

private static function initializePlugins(array $plugins){
foreach($plugins as $plugin) {
self::initializePluginHooks($plugin);
self::verifyScriptIsPublished($plugin);
}
}

private static function initializePluginHooks(Plugin $plugin) {
$path = $plugin->getPath();
$fullPath = str_replace(DIRECTORY_SEPARATOR, '\\', $path);
$namespacePart = str_replace($path, 'App\\Plugins' , $fullPath);
$pluginHookNamespace = $namespacePart . "\\$plugin->name\\App\\Hooks";

if(class_exists($pluginHookNamespace)) {
return new $pluginHookNamespace();
}
}

public static function discoverPluginByName($name) : Plugin|null {
Expand Down Expand Up @@ -171,6 +208,10 @@ public function updateUpdateState($fromInfoVersion) {
$this->save();
}
}

public function isActive(){
return isset($this->installed_at);
}

public function handleInstallation() {
$this->runMigrations();
Expand Down Expand Up @@ -289,17 +330,41 @@ private function rollbackMigrations() {
call_user_func([$instance, 'rollback']);
}
}

private static function verifyScriptIsPublished($plugin){
$path = $plugin->publicName();
if(!Storage::exists($path)) {
$plugin->publishScript();
}
}

private function publishScript() {
$name = $this->name;
$scriptPath = base_path("app/Plugins/$name/js/script.js");
if(file_exists($scriptPath)) {
$filehandle = fopen($scriptPath, 'r');
Storage::put(
$this->publicName(),
$filehandle,
);
fclose($filehandle);
$scriptPath = null;


$legacyPath = base_path("app/Plugins/$name/js/script.js");
$newPath = base_path("app/Plugins/$name/dist/script.umd.js");
if(file_exists($legacyPath)) {
$scriptPath = $scriptPath;
}else if(file_exists($newPath)) {
$scriptPath = $newPath;
}

if($scriptPath === null) {
info("No script found for plugin $name");
return;
} else {

$localPath = $this->publicName(true);
$storagePath = storage_path("app/public/$localPath");

info("Publishing script for plugin $name to $storagePath");
try{
File::link($scriptPath, $storagePath);
}catch(\Exception $e){
info("Failed to link script for plugin $name");
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions app/Plugin/Hook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php


namespace App\Plugin;


/**
* The Hook class contains all available hooks for the application.
* This should give the user a better overview of the available hooks.
* And a more error-proof way to use them.
*/
enum Hook: string {
case GLOBAL = 'HomeController@getGlobalData';
case ENTITY_TYPE_UPDATE = 'EditorController@setRelationInfo';
case ENTITY_TYPE_GET = 'EditorController@getEntityType';
}
43 changes: 43 additions & 0 deletions app/Plugin/HookRegister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Plugin;

use App\Patterns\Singleton;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class HookRegister extends Singleton {

private array $hooks = [];

// Important: This must be redifined, otherwise PHP will use the 'last' child class that was loaded.
protected static $instance = null;

public function register(string $pluginName, Hook $hook, callable $callback): void {
if(!isset($this->hooks[$hook->value])) {
$this->hooks[$hook->value] = [];
}

$this->hooks[$hook->value][] = ['plugin' => $pluginName, 'callback' => $callback];
}

public function call(string $method, Request $request, JsonResponse $response): JsonResponse {
if(isset($this->hooks[$method])) {
foreach($this->hooks[$method] as $data) {
$pluginName = $data['plugin'];
$callback = $data['callback'];
try {
$callback($request, $response);
} catch(\Exception $e) {
info("Error in plugin $pluginName: ".$e->getMessage());
}
}
}

return $response;
}

public function count(string $name):int {
return isset($this->hooks[$name]) ? count($this->hooks[$name]) : 0;
}
}
2 changes: 1 addition & 1 deletion app/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function sp_get_permission_groups($onlyGroups = false) {
if(!function_exists('sp_get_themes')) {
function sp_get_themes() {
$themeDir = base_path("resources/sass/");
$fileList = glob("${themeDir}app*.scss");
$fileList = glob($themeDir . "app*.scss");
$themes = [];
foreach($fileList as $file) {
$theme = [];
Expand Down
2 changes: 1 addition & 1 deletion resources/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export async function confirmUserPassword(uid, password = null) {
}

export async function updateEntityTypeRelation(etid, values) {
const data = only(values, ['is_root', 'sub_entity_types']);
const data = only(values, ['is_root', 'sub_entity_types', 'plugin_data']);
const apiData = { ...data };
if(data.sub_entity_types) {
apiData.sub_entity_types = data.sub_entity_types.map(t => t.id);
Expand Down
2 changes: 2 additions & 0 deletions resources/js/bootstrap/global-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import VueUploadComponent from 'vue-upload-component';
import DatePicker from 'vue-datepicker-next';
import draggable from 'vuedraggable';
import { Tree, Node, } from 'tree-vue-component';
import EntityDetail from '../components/EntityDetail.vue';


export default function initGlobalComponents(app) {
Expand All @@ -48,6 +49,7 @@ export default function initGlobalComponents(app) {
app.component('MdViewer', MarkdownViewer);
app.component('MdEditor', MarkdownEditor);
app.component('BibtexCode', BibtexCode);
app.component('EntityDetail', EntityDetail);
// Third-Party components
app.component('Multiselect', Multiselect);
app.component('FileUpload', VueUploadComponent);
Expand Down
31 changes: 28 additions & 3 deletions resources/js/bootstrap/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {

import RouteRootDummy from '@/components/plugins/RouteRootDummy.vue';
import * as buffer from 'buffer';
import { validateSubscription } from '../helpers/plugins.js';

const defaultPluginOptions = {
id: null,
Expand All @@ -56,7 +57,10 @@ const defaultSlotOptions = {
key: null,
component: null,
componentTag: null,
href: '', // for 'tools' and 'settings'
href: '', // for 'tools' and 'settings',
vBind: {},
vOn: {},
methods: {}
};
const defaultDatatypeOptions = {
of: null, // id of registered plugin
Expand Down Expand Up @@ -220,7 +224,12 @@ export const SpPS = {
SpPS.data.app.component(mergedOptions.componentTag, mergedOptions.component);
}
}
store.dispatch('registerPluginInSlot', mergedOptions);

store.commit('registerPluginInSlot', mergedOptions);
},
subscribe: (options) => {
validateSubscription(options);
store.commit('registerPluginSubscription', options);
},
registerPreference: (options) => {
if(!options.of || !SpPS.data.plugins[options.of]) {
Expand Down Expand Up @@ -261,6 +270,22 @@ export const SpPS = {
}
store.dispatch('registerPluginPreference', mergedOptions);
},
}
};

SpPS.get = {
component(name) {
if(SpPS.get.components[name]) {
return SpPS.get.components[name];
} else
throw new Error(`Component "${name}" not found`);
},
get components() {
if(SpPS.data?.app?._context?.components) {
return SpPS.data.app._context.components;
} else {
throw new Error('No components found');
}
}
};

export default SpPS;
Loading
Loading