diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c6a907a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,140 @@ +# Contributing to this project + +Please take a moment to review this document in order to make the contribution +process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + +## Using the issue tracker + +The issue tracker is the preferred channel for [bug reports](#bugs), +[features requests](#features) and [submitting pull +requests](#pull-requests), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests. + +* Please **do not** derail or troll issues. Keep the discussion on topic and + respect the opinions of others. + + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. + +3. **Isolate the problem** — create a [reduced test + case](http://css-tricks.com/reduced-test-cases/) and a live example. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? What would you expect to be the outcome? All these +details will help people to fix any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the Yii2 and Yii2-plugins-system versions in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + + + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. + +Please adhere to the coding conventions used throughout a project (indentation, +accurate comments, etc.) and any other requirements (such as test coverage). + +Follow this process if you'd like your work considered for inclusion in the +project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, + and configure the remotes: + + ```bash + # Clone your fork of the repo into the current directory + git clone https://github.com// + # Navigate to the newly cloned directory + cd + # Assign the original repo to a remote called "upstream" + git remote add upstream https://github.com// + ``` + +2. If you cloned a while ago, get the latest changes from upstream: + + ```bash + git checkout + git pull upstream + ``` + +3. Create a new topic branch (off the main project development branch) to + contain your feature, change, or fix: + + ```bash + git checkout -b + ``` + +4. Commit your changes in logical chunks. Please adhere to these [git commit + message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) + or your code is unlikely be merged into the main project. Use Git's + [interactive rebase](https://help.github.com/articles/interactive-rebase) + feature to tidy up your commits before making them public. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + + ```bash + git pull [--rebase] upstream + ``` + +6. Push your topic branch up to your fork: + + ```bash + git push origin + ``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) + with a clear title and description. + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to +license your work under the same license as that used by the project. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..96e283b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 LoveOrigami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..96e283b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 LoveOrigami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Module.php b/Module.php new file mode 100644 index 0000000..594d3d5 --- /dev/null +++ b/Module.php @@ -0,0 +1,36 @@ +i18n->translations['plugin'])) { + \Yii::$app->i18n->translations['plugin'] = [ + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en', + 'basePath' => '@lo/plugins/messages' + ]; + } + + //user did not define the Navbar? + if (!$this->pluginsDir) { + $this->pluginsDir = [ + '@lo/plugins/plugins' + ]; + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d482ddb --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Getting started with Yii2-plugins-system + +Yii2-plugins-system is designed to work out of the box. It means that installation requires +minimal steps. Only one configuration step should be taken and you are ready to +have plugin system on your Yii2 website. + +!["Plugins"](docs/img/tab_plugins.jpg) + +### 1. Download + +Yii2-plugins-system can be installed using composer. Run following command to download and +install Yii2-plugins-system: + +```bash +composer require "loveorigami/yii2-plugins-system": "*" +``` + +### 2. Update database schema + +The last thing you need to do is updating your database schema by applying the +migrations. Make sure that you have properly configured `db` application component +and run the following command: + +```bash +$ php yii migrate/up --migrationPath=@vendor/loveorigami/yii2-plugins-system/migrations +``` + +### 3. Configure application + +Let's start with defining module in `@backend/config/main.php`: + +```php +'modules' => [ + 'plugins' => [ + 'class' => 'lo\plugins\Module', + 'pluginsDir'=>[ + '@lo/plugins/plugins', // default dir with core plugins + // '@common/plugins', // dir with our plugins + ] + ], +], +``` +That's all, now you have module installed and configured in advanced template. + +Next, open `@frontend/config/main.php` and add following: + +``` +'bootstrap' => ['log', 'plugins'], +... +'components' => [ + 'plugins' => [ + 'class' => 'lo\plugins\components\EventBootstrap', + 'appId' => 'frontend' + ], + ... +] +``` + +Also do the same thing with `@backend/config/main.php`: + +``` +'bootstrap' => ['log', 'plugins'], +... +'components' => [ + 'plugins' => [ + 'class' => 'lo\plugins\components\EventBootstrap', + 'appId' => 'backend' + ], + ... +] +``` + +## Core plugins (examples) + +* [Hello world!] (plugins/helloworld) +* [Code Highlighting] (plugins/code) +* [Http Authentication] (plugins/httpauth) + +## Your plugins + +* [Create] (docs/create_plugin.md) +* [Install] (docs/install_plugin.md) + +## Contributing to this project + +Anyone and everyone is welcome to contribute. Please take a moment to +review the [guidelines for contributing](CONTRIBUTING.md). + +## License + +Yii2-plugins-system is released under the MIT License. See the bundled [LICENSE.md](LICENSE.md) +for details. + +## Thanks + +* Bariew for [event manager] (https://github.com/bariew/yii2-event-component) component +* Troxa for [yii2-shortcodes] (https://github.com/tpoxa/yii2-shortcodes) component +* ElisDn for great webinar about [yii2-events] (http://www.elisdn.ru/blog/74/events-on-yii2-and-javascript) (by ru) \ No newline at end of file diff --git a/components/EventBootstrap.php b/components/EventBootstrap.php new file mode 100644 index 0000000..3acd5f3 --- /dev/null +++ b/components/EventBootstrap.php @@ -0,0 +1,84 @@ +, modify Loveorigami + */ +class EventBootstrap implements BootstrapInterface +{ + /** + * Application id for category plugins. + * Support values: frontend, backend, common + * Default: frontend + * @var appId string + */ + public static $appId = 'frontend'; + + /** + * @var EventManager EventManager memory storage for getEventManager method + */ + protected static $_eventManager = []; + + /** + * @inheritdoc + */ + public function bootstrap($app) + { + self::getEventManager($app); + } + + /** + * finds and creates app event manager from its settings + * @param Application $app yii app + * @return EventManager app event manager component + * @throws Exception Define event manager + */ + public static function getEventManager($app) + { + if (self::$_eventManager) { + return self::$_eventManager; + } + + foreach ($app->components as $name => $config) { + $class = is_string($config) ? $config : @$config['class']; + + // if eventManager component in config + if ($class == str_replace('Bootstrap', 'Manager', get_called_class())) { + self::$_eventManager = $app->$name->events; + } + + // this class. set $appId from config + if ($class == str_replace('Manager', 'Bootstrap', get_called_class())) { + if($app->$name->appId){ + self::$appId = $app->$name->appId; + }; + } + } + + $events = ModelEvent::eventList(self::$appId); + + // merge config events with plugins + self::$_eventManager = array_merge_recursive($events, self::$_eventManager); + + $app->setComponents([ + 'eventManager' => [ + 'class' => 'lo\plugins\components\EventManager', + 'events' => self::$_eventManager + ], + ]); + + return self::$_eventManager = $app->eventManager; + } +} diff --git a/components/EventManager.php b/components/EventManager.php new file mode 100644 index 0000000..c186356 --- /dev/null +++ b/components/EventManager.php @@ -0,0 +1,67 @@ + [ + * $eventName => [ + * [$handlerClassName, $handlerMethodName] + * ] + * ] + * ] + * + * @since 1.3.0 handler can also keep additional data and $append boolean as for Event::on() method eg: + * ... [[$handlerClassName, $handlerMethodName], ['myData'], false] + * + * @var array events settings + */ + public $events = []; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + // var_dump($this->events); + $this->attachEvents($this->events); + } + + /** + * attaches all events to all classNames + * @param array $eventConfig commonly $this->events config + */ + public function attachEvents($eventConfig) + { + foreach ($eventConfig as $className => $events) { + foreach ($events as $eventName => $handlers) { + foreach ($handlers as $handler) { + if (is_array($handler) && is_callable($handler[0])) { + $data = isset($handler[1]) ? array_pop($handler) : null; + $append = isset($handler[2]) ? array_pop($handler) : null; + Event::on($className, $eventName, $handler[0], $data, $append); + } else if (is_callable($handler)) { + Event::on($className, $eventName, $handler); + } + } + } + } + } + +} \ No newline at end of file diff --git a/components/Shortcode.php b/components/Shortcode.php new file mode 100644 index 0000000..74cbd10 --- /dev/null +++ b/components/Shortcode.php @@ -0,0 +1,12 @@ +=2.0.6", + "components/highlightjs":"*", + "loveorigami/yii2-jsoneditor": "*", + "tpoxa/shortcodes": "dev-master" + }, + "autoload": { + "psr-4": { + "lo\\plugins\\": "" + } + } +} diff --git a/controllers/EventController.php b/controllers/EventController.php new file mode 100644 index 0000000..fe2bc59 --- /dev/null +++ b/controllers/EventController.php @@ -0,0 +1,121 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['post'], + ], + ], + ]; + } + + /** + * Lists all Event models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new EventSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a single Event model. + * @param integer $id + * @return mixed + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Event model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function _actionCreate() + { + $model = new Event(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing Event model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect('index'); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing Event model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Event model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Event the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Event::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } +} diff --git a/controllers/ItemController.php b/controllers/ItemController.php new file mode 100644 index 0000000..03e49bf --- /dev/null +++ b/controllers/ItemController.php @@ -0,0 +1,511 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['post'], + ], + ], + ]; + } + + /** + * Lists all Item models. + * @return mixed + */ + public function actionIndex() + { + $searchModel = new ItemSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); + } + + /** + * Displays a info page + * @return mixed + */ + public function actionFind() + { + // Find all plugins + $this->findPlugins(); + + // Get all activated plugins + $this->getActivatedPlugins(); + + // Include and update plugins + $this->includePlugins(); + + $data['plugins'] = self::$plugins_pool; + $data['active'] = self::$plugins_active; + + $dataProvider = new ArrayDataProvider([ + 'allModels' => array_diff_key(self::$plugins_pool, self::$plugins_active), + 'pagination' => [ + 'pageSize' => 20, + ], + ]); + + return $this->render('find', compact('data', 'dataProvider')); + } + + /** + * Displays a info page + * @return mixed + */ + public function actionInstall($id) + { + // Find all plugins + $this->findPlugins(); + if (self::$plugins_pool) { + foreach (self::$plugins_pool AS $handlerClass => $value) { + + if (md5($handlerClass) == $id) { + + // The plugin information being added to the database + $data['Item'] = [ + "handler_class" => $handlerClass, + "name" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['name']), + "url" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['url']), + "version" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['version']), + "text" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['text']), + "author" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['author']), + "author_url" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['author_url']), + "status" => Item::STATUS_ACTIVE + ]; + + $model = new Item(); + + if ($model->load($data) && $model->save()) { + // here install events to Event + $this->includeEvents($model->id, $handlerClass); + } else { + Yii::$app->session->setFlash('error', Yii::t('plugin', 'This plugin alredy installed')); + } + } + } + } + return $this->redirect(Yii::$app->request->referrer); + } + + /** + * Displays a info page + * @return mixed + */ + public function actionInfo() + { + return $this->render('info'); + } + + + /** + * Displays a single Item model. + * @param integer $id + * @return mixed + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Item model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return mixed + */ + public function _actionCreate() + { + $model = new Item(); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } else { + return $this->render('create', [ + 'model' => $model, + ]); + } + } + + /** + * Updates an existing Item model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param integer $id + * @return mixed + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } else { + return $this->render('update', [ + 'model' => $model, + ]); + } + } + + /** + * Deletes an existing Item model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param integer $id + * @return mixed + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Item model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param integer $id + * @return Item the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Item::findOne($id)) !== null) { + return $model; + } else { + throw new NotFoundHttpException('The requested page does not exist.'); + } + } + + /** + * Find all plugins from $plugins_dir + * @param var $plugin + * @return $plugins_pool + */ + protected function findPlugins() + { + if (!is_array($this->module->pluginsDir)) { + throw new \yii\base\InvalidConfigException("Plugins directory is not array."); + } + + foreach ($this->module->pluginsDir as $path) { + $dir = Yii::getAlias($path); + if (!file_exists($dir)) { + return $result; + } + $plugins = array_diff(scandir($dir), ['.', '..']); + + foreach ($plugins AS $key => $name) { + + $basePath = $dir . DIRECTORY_SEPARATOR . $name; + if (!is_dir($basePath)) { + continue; + } + + // Make sure a valid plugin file by the same name as the folder exists + $file = $basePath . DIRECTORY_SEPARATOR . ucfirst($name) . ".php"; + + if (file_exists($file)) { + + // Register the plugin + $handlerClass = Crawler::getNamespace($file) . '\\' . ucfirst($name); + + // If the plugin hasn't already been added and isn't a file + if (!isset(self::$plugins_pool[$handlerClass]) AND !stripos($name, ".")) { + + self::$plugins_pool[$handlerClass] = [ + 'plugin' => $name, + 'class' => $handlerClass + ]; + + // else may be plugin make as inactive + if (is_callable([$handlerClass, 'events'])) { + self::$plugins_pool[$handlerClass]['events'] = $handlerClass::events(); + } else { + self::$plugins_pool[$handlerClass]['events'] = []; + } + + // add info to pool + $this->getInfo($handlerClass); + } + + } else { + // self::$errors[$name][] = "Plugin file " . $name . ".php does not exist."; + } + } + } + } + + + /** + * Get Activated Plugins + * Get all activated plugins from the database + * + */ + protected function getActivatedPlugins() + { + // Only plugins in the database are active ones + $plugins = Item::find()->all(); + + if ($plugins) { + // For every plugin, store it + //var_dump($plugins); + foreach ($plugins AS $plugin) { + self::$plugins_active[$plugin->handler_class] = [ + 'plugin' => $plugin->name, + 'class' => $plugin->handler_class + ]; + + if ($plugin->events) { + foreach ($plugin->events AS $event) { + if ($event->data) { + self::$plugins_active[$plugin->handler_class]['events'][$event->trigger_class] = [ + $event->trigger_event => [$event->handler_method, json_decode($event->data, true)] + ]; + } else { + self::$plugins_active[$plugin->handler_class]['events'][$event->trigger_class] = [ + $event->trigger_event => $event->handler_method + ]; + } + } + } + } + } else { + return true; + } + } + + /** + * Include Plugins + * Include all active plugins that are in the database + * + */ + protected function includePlugins() + { + if (self::$plugins_active AND !empty(self::$plugins_active)) { + // Validate and include our found plugins + foreach (self::$plugins_active AS $handlerClass => $value) { + // The plugin information being added to the database + $data['Item'] = [ + "handler_class" => $handlerClass, + "name" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['name']), + "url" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['url']), + "version" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['version']), + "text" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['text']), + "author" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['author']), + "author_url" => trim(self::$plugins_pool[$handlerClass]['plugin_info']['author_url']) + ]; + + $model = Item::findOne(['handler_class' => $handlerClass]); + + if ($model->load($data) && $model->save()) { + // here install events to Event + $this->includeEvents($model->id, $handlerClass); + } + } + } + } + + /** + * Include events from $handlerClass + * @param int $plugin_id + * @param var $handlerClass + * @return bool + */ + protected function includeEvents($plugin_id, $handlerClass) + { + if (isset(self::$plugins_active[$handlerClass]['events']) && isset(self::$plugins_pool[$handlerClass]['events'])) { + // here remove all active_events from db, if is not in $plugins_pool + foreach (self::$plugins_active[$handlerClass]['events'] as $className => $events) { + + foreach ($events as $eventName => $handler) { + + $data1['plugin_id'] = $plugin_id; + $data1['trigger_event'] = $eventName; + + if (isset(self::$plugins_pool[$handlerClass]['events'][$className][$eventName])) { + + $handlerPool = self::$plugins_pool[$handlerClass]['events'][$className][$eventName]; + $handlerMethodPool = is_array($handlerPool) ? $handlerPool[0] : $handlerPool; + $handlerMethodActive = is_array($handler) ? $handler[0] : $handler; + + $data2['plugin_id'] = $plugin_id; + $data2['trigger_event'] = $eventName; + $data2['trigger_class'] = $className; + $data2['handler_method'] = $handlerMethodActive; + + if ($handlerMethodPool == $handlerMethodActive) { + // plugin event is 'in pool' + return true; + } else { + $this->deleteEvent($data2); + } + } else { + $this->deleteEvent($data1); + } + } + } + } + if (isset(self::$plugins_pool[$handlerClass]['events'])) { + // get all events from plugin + foreach (self::$plugins_pool[$handlerClass]['events'] as $className => $events) { + foreach ($events as $eventName => $handler) { + + $handlerActive = self::$plugins_active[$handlerClass]['events'][$className][$eventName]; + $handlerMethodActive = is_array($handlerActive) ? $handlerActive[0] : $handlerActive; + $handlerMethodPool = is_array($handler) ? $handler[0] : $handler; + + $data['app_id'] = (int)$this->checkAppId($handlerClass); + $data['plugin_id'] = (int)$plugin_id; + $data['trigger_class'] = $className; + $data['trigger_event'] = $eventName; + $data['handler_method'] = $handlerMethodPool; + $data['data'] = isset($handler[1]) ? json_encode($handler[1]) : ''; + $data['status'] = Event::STATUS_INACTIVE; + + if ($handlerMethodPool != $handlerMethodActive) { + self::$plugins_active[$handlerClass]['install'][] = $data; + // echo 'install'; + // var_dump($data); + $this->installEvent($data); + } + } + } + } + } + + /** + * Convert var AppId to int app_id + * @param var $handlerClass + * @return int $app_id + */ + protected function checkAppId($handlerClass) + { + if (!isset($handlerClass::$appId)) return App::APP_FRONTEND; + switch ($handlerClass::$appId) { + case 'backend': + return App::APP_BACKEND; + break; + case 'common': + return App::APP_COMMON; + break; + default: + return App::APP_FRONTEND; + } + } + + /** + * Install event from config + * @param var $data + * @return bool + */ + protected function installEvent($event) + { + $data['Event'] = $event; + $model = new Event(); + if ($model->load($data) && $model->save()) { + Yii::$app->session->setFlash('success', Yii::t('plugin', 'New event installed')); + } else { + return ['error' => $model->errors]; + // var_dump($model->errors); + } + } + + /** + * Delete events + * @param array $data + * @return bool + */ + protected function deleteEvent($data) + { + foreach (Event::find()->where($data)->all() as $event) { + $event->delete(); + } + } + + /** + * Get info about plugin from Htdoc + * @param var $plugin + * @return $plugins_pool + */ + protected static function getInfo($handlerClass) + { + + $plugin_data = Crawler::getDoc($handlerClass); + + preg_match('|Plugin Name:(.*)$|mi', $plugin_data, $name); + preg_match('|Plugin URI:(.*)$|mi', $plugin_data, $uri); + preg_match('|Version:(.*)|i', $plugin_data, $version); + preg_match('|Description:(.*)$|mi', $plugin_data, $description); + preg_match('|Author:(.*)$|mi', $plugin_data, $author_name); + preg_match('|Author URI:(.*)$|mi', $plugin_data, $author_uri); + + if (isset($name[1])) { + $arr['name'] = trim($name[1]); + } + + if (isset($uri[1])) { + $arr['url'] = trim($uri[1]); + } + + if (isset($version[1])) { + $arr['version'] = trim($version[1]); + } + + if (isset($description[1])) { + $arr['text'] = trim($description[1]); + } + + if (isset($author_name[1])) { + $arr['author'] = trim($author_name[1]); + } + + if (isset($author_uri[1])) { + $arr['author_url'] = trim($author_uri[1]); + } + + $arr['handler_class'] = trim($handlerClass); + + // For every plugin header item + foreach ($arr AS $k => $v) { + // If the key doesn't exist or the value is not the same, update the array + if (!isset(self::$plugins_pool[$handlerClass]['plugin_info'][$k]) OR self::$plugins_pool[$handlerClass]['plugin_info'][$k] != $v) { + self::$plugins_pool[$handlerClass]['plugin_info'][$k] = trim($v); + } else { + return true; + } + } + + } + +} diff --git a/docs/create_plugin.md b/docs/create_plugin.md new file mode 100644 index 0000000..14b4baf --- /dev/null +++ b/docs/create_plugin.md @@ -0,0 +1,106 @@ +# Create your plugin + +To create your plugin you need to run the following required steps + +### 1. Create in dir with our plugins `@common\plugins` new folder: +* For example: `test` + +### 2. In this folder create: +* `README.md` with usage instruction for this plugin +* New named as folder class `Test`, with information about plugin + +```php + 'Hello, world!', + ]; +``` + +* Then, assign a template events + +```php + public static function events() + { + return [ + $eventSenderClassName => [ + $eventName => [$handlerMethodName, self::$config] + ], + ]; + } +``` + +for example: + +```php + public static function events() + { + return [ + 'yii\base\View' => [ + 'afterRender' => ['foo', self::$config] + ], + ]; + } +``` +more about `$eventSenderClassName` and `$eventName` you can be found on the info tab of this module + +!["Info tab"](img/tab_info.jpg) + +* Create a handler method `foo` with the necessary logic + +```php + /** + * handler method foo + */ + public function foo($event) + { + $term = ($event->data['term']) ? $event->data['term'] : self::$config['term']; + + if (isset($event->output)) { + $content = $event->output; + $event->output = str_replace($term,"

$term

", $content); + } + + return true; + } +``` + +* That's all. Then you have to [install](install_plugin.md) this plugin \ No newline at end of file diff --git a/docs/img/demo_events.jpg b/docs/img/demo_events.jpg new file mode 100644 index 0000000..0f8f1a3 Binary files /dev/null and b/docs/img/demo_events.jpg differ diff --git a/docs/img/event_edit.jpg b/docs/img/event_edit.jpg new file mode 100644 index 0000000..ea03717 Binary files /dev/null and b/docs/img/event_edit.jpg differ diff --git a/docs/img/tab_events.jpg b/docs/img/tab_events.jpg new file mode 100644 index 0000000..e40715a Binary files /dev/null and b/docs/img/tab_events.jpg differ diff --git a/docs/img/tab_info.jpg b/docs/img/tab_info.jpg new file mode 100644 index 0000000..233c7e1 Binary files /dev/null and b/docs/img/tab_info.jpg differ diff --git a/docs/img/tab_install.jpg b/docs/img/tab_install.jpg new file mode 100644 index 0000000..f8b081c Binary files /dev/null and b/docs/img/tab_install.jpg differ diff --git a/docs/img/tab_plugins.jpg b/docs/img/tab_plugins.jpg new file mode 100644 index 0000000..ba0870a Binary files /dev/null and b/docs/img/tab_plugins.jpg differ diff --git a/docs/install_plugin.md b/docs/install_plugin.md new file mode 100644 index 0000000..7924719 --- /dev/null +++ b/docs/install_plugin.md @@ -0,0 +1,31 @@ +# Install plugin + +After [creating](create_plugin.md) your `Test` plugin first, you must make sure that the our directory plugins included in the module configuration + +```php +'modules' => [ + 'plugins' => [ + 'class' => 'lo\plugins\Module', + 'pluginsDir'=>[ + '@lo/plugins/plugins', // default dir with core plugins + '@common/plugins', // dir with our plugins + ] + ], +], +``` + +* Then go to the install tab and press button + +!["Install tab"](img/tab_install.jpg) + +* Go to the events tab for enabled and configure plugin event + +!["Events tab"](img/tab_events.jpg) + +* If you want, change configuration, update... + +!["Event edit"](img/event_edit.jpg) + +* Go to the website to see the result + +!["Result"](img/demo_events.jpg) \ No newline at end of file diff --git a/helpers/Crawler.php b/helpers/Crawler.php new file mode 100644 index 0000000..ae16aa7 --- /dev/null +++ b/helpers/Crawler.php @@ -0,0 +1,174 @@ +getConstants() as $name => $value) { + if (!preg_match('/^EVENT/', $name)) { + continue; + } + $result[$name] = $value; + } + } catch (\Exception $e) { + echo $className; + exit; + } + + + return $result; + } + + public static function getEventHandlerMethodNames($className) + { + $result = []; + if (!$reflection = self::getReflection($className)) { + return $result; + } + foreach ($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) { + if (!$method->isPublic()) { + continue; + } + if ((!$params = $method->getParameters()) || ($params[0]->name != 'event')) { + continue; + } + $result[$method->name] = $method->name; + } + + return $result; + } + + public static function getMethodTriggeredEvents($className, $methodName) + { + $result = []; + if (!$reflection = self::getReflection($className)) { + return $result; + } + if (!$reflection->hasMethod($methodName)) { + return $result; + } + $method = $reflection->getMethod($methodName); + $body = self::getReflectionBody($method); + $events = array_flip(self::extractTriggeredEvents($body)); + foreach ($events as $name => $trash) { + $events[$name] = $reflection->getConstant($name); + } + return $events; + } + + public static function getAllClasses() + { + if (self::$_allClasses !== null) { + return self::$_allClasses; + } + $result = []; + foreach (self::getAllAliases() as $alias) { + $path = Yii::getAlias($alias); + if (!file_exists($path) || is_file($path)) { + continue; + } + $files = BaseFileHelper::findFiles($path, ['except' => ['/yii2-gii/', 'Yii.php']]); + foreach ($files as $filePath) { + if (!preg_match('/.*\/[A-Z]\w+\.php/', $filePath)) { + continue; + } + $className = str_replace([$path, '.php', '/', '@'], [$alias, '', '\\', ''], $filePath); + $result[] = $className; + } + } + + return self::$_allClasses = $result; + } + + public static function getAllAliases() + { + $result = []; + foreach (\Yii::$aliases as $aliases) { + foreach (array_keys((array)$aliases) as $alias) { + if (!$alias) { + continue; + } + $result[] = $alias; + } + } + return $result; + } + + protected static function getReflection($className) + { + try { + if (in_array($className, ['yii\requirements\YiiRequirementChecker', 'yii\helpers\Markdown'])) { + return false; + } + $reflection = new \ReflectionClass($className); + } catch (\Exception $e) { + return false; + } + return $reflection; + } + + protected static function extractTriggeredEvents($string) + { + $string = preg_replace('/\s/', '', $string); + return preg_match_all('/\-\>trigger\(self\:\:(EVENT_[\w\_]+)/', $string, $matches) + ? $matches[1] : []; + } + + public static function getDoc($className) + { + $result = ''; + + try { + if (!$reflection = self::getReflection($className)) { + return $result; + } + $result = $reflection->getDocComment(); + + } catch (\Exception $e) { + echo $className; + exit; + } + return $result; + + } + + + /** + * @param $reflection + * @return string + * @author http://stackoverflow.com/questions/7026690/reconstruct-get-code-of-php-function + */ + protected static function getReflectionBody($reflection) + { + $filename = $reflection->getFileName(); + $start_line = $reflection->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block + $end_line = $reflection->getEndLine(); + $length = $end_line - $start_line; + $source = file($filename); + return implode("", array_slice($source, $start_line, $length)); + } + + // Makes many assumptions on file format: + // namespace is declared on its own line, starting with "namespace" (no spaces). + public static function getNamespace($filename) + { + $lines = file($filename); + $namespaceLine = array_shift(preg_grep('/^namespace /', $lines)); + $match = array(); + preg_match('/^namespace (.*);/', $namespaceLine, $match); + $fullNamespace = trim(array_pop($match)); + return $fullNamespace; + } +} \ No newline at end of file diff --git a/helpers/JsonValidator.php b/helpers/JsonValidator.php new file mode 100644 index 0000000..aa16604 --- /dev/null +++ b/helpers/JsonValidator.php @@ -0,0 +1,51 @@ + + */ +class JsonValidator extends Validator +{ + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('common', '"{attribute}" must be a valid JSON'); + } + } + /** + * @inheritdoc + */ + public function validateValue($value) + { + if(!$value) return true; + + if (!@json_decode($value)) { + return [$this->message, []]; + } + } + + /** + * @inheritdoc + */ + public function clientValidateAttribute($model, $attribute, $view) + { + $message = Yii::$app->getI18n()->format($this->message, [ + 'attribute' => $model->getAttributeLabel($attribute) + ], Yii::$app->language); + return <<<"JS" + try { + if(value) JSON.parse(value); + } catch (e) { + messages.push('{$message}') + } +JS; + } +} diff --git a/messages/en/plugin.php b/messages/en/plugin.php new file mode 100644 index 0000000..2837d67 --- /dev/null +++ b/messages/en/plugin.php @@ -0,0 +1,61 @@ + 'Are you sure to delete this item?', + 'App' => 'App', + 'App Id' => 'App Id', + 'Author' => 'Author', + 'Author Url' => 'Author Url', + + 'Create' => 'Create', + 'Create Item' => 'Create Plugin', + + 'Data' => 'Data', + 'Delete' => 'Delete', + 'Disabled' => 'Disabled', + + 'Enabled' => 'Enabled', + 'Events' => 'Events', + + 'Handler Class' => 'Handler Class', + 'Handler Method' => 'Handler Method', + + 'ID' => 'ID', + 'Info' => 'Info', + 'Install' => 'Install', + 'Items' => 'Plugins', + + 'Name' => 'Name', + 'New event installed' => 'New event installed', + + 'Plugin ID' => 'Plugin ID', + 'Position' => 'Position', + + 'Status' => 'Status', + + 'Text' => 'Text', + 'This plugin alredy installed' => 'This plugin alredy installed', + 'Trigger Class' => 'Trigger Class', + 'Trigger Event' => 'Trigger Event', + + 'Update' => 'Update', + 'Url' => 'Url', + + 'Version' => 'Version', +]; diff --git a/messages/ru/plugin.php b/messages/ru/plugin.php new file mode 100644 index 0000000..138e6df --- /dev/null +++ b/messages/ru/plugin.php @@ -0,0 +1,36 @@ + 'Уверены, что хотите удалить эту запись?', + 'Author' => 'Автор', + 'Author Url' => 'Url автора', + 'Create' => 'Добавить', + 'Create Item' => 'Создать плагин', + 'Delete' => 'Удалить', + 'Events' => 'События', + 'ID' => 'ID', + 'Info' => 'Информация', + 'Install' => 'Установить', + 'Items' => 'Плагины', + 'Name' => 'Название', + 'Status' => 'Статус', + 'Text' => 'Описание', + 'Url' => 'Url', + 'Version' => 'Версия', +]; diff --git a/migrations/m150720_090901_plugin_table.php b/migrations/m150720_090901_plugin_table.php new file mode 100644 index 0000000..f959959 --- /dev/null +++ b/migrations/m150720_090901_plugin_table.php @@ -0,0 +1,45 @@ +db->driverName === 'mysql') { + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable("{{%plugins__item}}", [ + 'id' => Schema::TYPE_PK, + 'handler_class' => Schema::TYPE_STRING, + 'name' => Schema::TYPE_STRING, + 'url' => Schema::TYPE_STRING, + 'version' => Schema::TYPE_STRING, + 'text'=> Schema::TYPE_TEXT, + 'author'=> Schema::TYPE_STRING, + 'author_url'=> Schema::TYPE_STRING, + 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', + ], $tableOptions); + } + + public function down() + { + echo "m150720_090901_plugin_table cannot be reverted.\n"; + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/migrations/m150720_090905_app_table.php b/migrations/m150720_090905_app_table.php new file mode 100644 index 0000000..57b0e73 --- /dev/null +++ b/migrations/m150720_090905_app_table.php @@ -0,0 +1,55 @@ +db->driverName === 'mysql') { + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable("{{%plugins__app}}", [ + 'id' => Schema::TYPE_PK, + 'name' => Schema::TYPE_STRING, + ], $tableOptions); + + $this->insert('{{%plugins__app}}', [ + 'id' => 1, + 'name' => 'frontend', + ]); + + $this->insert('{{%plugins__app}}', [ + 'id' => 2, + 'name' => 'common', + ]); + + $this->insert('{{%plugins__app}}', [ + 'id' => 3, + 'name' => 'backend', + ]); + + + } + + public function down() + { + echo "m150720_090905_app_table cannot be reverted.\n"; + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/migrations/m150720_091726_event_table.php b/migrations/m150720_091726_event_table.php new file mode 100644 index 0000000..d37463b --- /dev/null +++ b/migrations/m150720_091726_event_table.php @@ -0,0 +1,91 @@ +db->driverName === 'mysql') { + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable("{{%plugins__event}}", [ + 'id' => Schema::TYPE_PK, + 'app_id' => Schema::TYPE_INTEGER . ' NOT NULL DEFAULT 1', + 'plugin_id' => Schema::TYPE_INTEGER . ' NOT NULL', + 'trigger_class' => Schema::TYPE_STRING, + 'trigger_event' => Schema::TYPE_STRING, + 'handler_method'=> Schema::TYPE_STRING, + 'data'=> Schema::TYPE_TEXT, + 'pos'=> Schema::TYPE_INTEGER . ' NOT NULL DEFAULT 0', + 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', + ], $tableOptions); + + $this->addForeignKey( + 'fk_plugins_event_plugins_item', + '{{%plugins__event}}', + 'plugin_id', + '{{%plugins__item}}', + 'id', + 'cascade', + 'cascade' + ); + + $this->addForeignKey( + 'fk_plugins_event_plugins_app', + '{{%plugins__event}}', + 'app_id', + '{{%plugins__app}}', + 'id', + 'cascade', + 'cascade' + ); + + $this->insert('{{%plugins__item}}', [ + 'id' => 1, + 'handler_class' => 'lo\plugins\plugins\code\Code', + 'name' => 'Code Highlighting plugin', + 'status' => 1, + ]); + + $this->insert('{{%plugins__event}}', [ + 'id' => 1, + 'app_id' => 1, // frontend + 'plugin_id' => 1, // Code Highlighting + 'trigger_class' => 'yii\base\View', + 'trigger_event' => 'afterRender', + 'handler_method' => 'shortCode', + 'data' => '{"style":"github","lang":"php"}', + 'status' => 1, + ]); + } + + public function down() + { + $this->dropForeignKey( + 'fk_plugins_event_plugins_item', + '{{%plugins__event}}' + ); + + $this->dropForeignKey( + 'fk_plugins_event_plugins_app', + '{{%plugins__event}}' + ); + + $this->dropTable('{{%plugins__event}}'); + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/models/App.php b/models/App.php new file mode 100644 index 0000000..0774171 --- /dev/null +++ b/models/App.php @@ -0,0 +1,56 @@ + 255], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('plugin', 'ID'), + 'name' => Yii::t('plugin', 'Name'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getEvents() + { + return $this->hasMany(Event::className(), ['app_id' => 'id']); + } + +} diff --git a/models/Event.php b/models/Event.php new file mode 100644 index 0000000..3c0a4f4 --- /dev/null +++ b/models/Event.php @@ -0,0 +1,130 @@ + 255], + [['data'], JsonValidator::className()], + //[['plugin_id'], 'exist', 'skipOnError' => true, 'targetClass' => Item::className(), 'targetAttribute' => ['id']], + //[['app_id'], 'exist', 'skipOnError' => true, 'targetClass' => App::className(), 'targetAttribute' => ['id']], + ]; + + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('plugin', 'ID'), + 'app_id' => Yii::t('plugin', 'App ID'), + 'plugin_id' => Yii::t('plugin', 'Plugin ID'), + 'trigger_class' => Yii::t('plugin', 'Trigger Class'), + 'trigger_event' => Yii::t('plugin', 'Trigger Event'), + 'handler_method' => Yii::t('plugin', 'Handler Method'), + 'data' => Yii::t('plugin', 'Data'), + 'pos' => Yii::t('plugin', 'Position'), + 'status' => Yii::t('plugin', 'Status'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getApp() + { + return $this->hasOne(App::className(), ['id' => 'app_id']); + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getPlugin() + { + return $this->hasOne(Item::className(), ['id' => 'plugin_id']); + } + + /** + * @inheritdoc + * @return EventQuery the active query used by this AR class. + */ + public static function find() + { + return new EventQuery(get_called_class()); + } + + /** + * eventList for BootstrapManager + * @return array + */ + public static function eventList($appId = 'frontend') + { + // (frontentd and common) or (backend and common) events + $cond = ($appId == 'backend') ? '>=' : '<='; + $attributes = ['trigger_class', 'trigger_event', 'plugin_id', 'pos', 'handler_method']; // handler_class + $order = array_combine($attributes, array_fill(0, count($attributes), SORT_ASC)); + + $allEvents = self::find() + ->select('t.*') + ->from(self::tableName() . 'AS t') + ->joinWith(['plugin']) + ->where([ + 't.status' => self::STATUS_ACTIVE, + Item::tableName() . '.status' => Item::STATUS_ACTIVE, + ]) + ->andWhere([$cond, 't.app_id', App::APP_COMMON]) + ->orderBy($order) + ->all(); + + $result = []; + + foreach ($allEvents as $data) { + if ($data->data) { + $handler = [[$data->plugin->handler_class, $data->handler_method], json_decode($data->data, true)]; + } else { + $handler = [$data->plugin->handler_class, $data->handler_method]; + } + $result[$data->trigger_class][$data->trigger_event][] = $handler; + } + + return $result; + } +} diff --git a/models/EventQuery.php b/models/EventQuery.php new file mode 100644 index 0000000..7c38530 --- /dev/null +++ b/models/EventQuery.php @@ -0,0 +1,35 @@ +andWhere('[[status]]=1'); + return $this; + }*/ + + /** + * @inheritdoc + * @return Event[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return Event|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} \ No newline at end of file diff --git a/models/EventSearch.php b/models/EventSearch.php new file mode 100644 index 0000000..999e6ca --- /dev/null +++ b/models/EventSearch.php @@ -0,0 +1,71 @@ + $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + $query->andFilterWhere([ + 'id' => $this->id, + 'plugin_id' => $this->plugin_id, + 'app_id' => $this->app_id, + 'status' => $this->status, + ]); + + $query->andFilterWhere(['like', 'trigger_class', $this->trigger_class]) + ->andFilterWhere(['like', 'trigger_event', $this->trigger_event]) + ->andFilterWhere(['like', 'handler_method', $this->handler_method]); + + return $dataProvider; + } +} diff --git a/models/Item.php b/models/Item.php new file mode 100644 index 0000000..06c05ac --- /dev/null +++ b/models/Item.php @@ -0,0 +1,83 @@ + 255], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('plugin', 'ID'), + 'handler_class' => Yii::t('plugin', 'Handler Class'), + 'name' => Yii::t('plugin', 'Name'), + 'url' => Yii::t('plugin', 'Url'), + 'version' => Yii::t('plugin', 'Version'), + 'text' => Yii::t('plugin', 'Text'), + 'author' => Yii::t('plugin', 'Author'), + 'author_url' => Yii::t('plugin', 'Author Url'), + 'status' => Yii::t('plugin', 'Status'), + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getEvents() + { + return $this->hasMany(Event::className(), ['plugin_id' => 'id']); + } + + /** + * @inheritdoc + * @return ItemQuery the active query used by this AR class. + */ + public static function find() + { + return new ItemQuery(get_called_class()); + } +} diff --git a/models/ItemQuery.php b/models/ItemQuery.php new file mode 100644 index 0000000..7de381c --- /dev/null +++ b/models/ItemQuery.php @@ -0,0 +1,35 @@ +andWhere('[[status]]=1'); + return $this; + }*/ + + /** + * @inheritdoc + * @return Item[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return Item|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} \ No newline at end of file diff --git a/models/ItemSearch.php b/models/ItemSearch.php new file mode 100644 index 0000000..a72147b --- /dev/null +++ b/models/ItemSearch.php @@ -0,0 +1,73 @@ + $query, + ]); + + $this->load($params); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + $query->andFilterWhere([ + 'id' => $this->id, + 'status' => $this->status, + ]); + + $query->andFilterWhere(['like', 'name', $this->name]) + ->andFilterWhere(['like', 'handler_class', $this->handler_class]) + ->andFilterWhere(['like', 'url', $this->url]) + ->andFilterWhere(['like', 'version', $this->version]) + ->andFilterWhere(['like', 'text', $this->text]) + ->andFilterWhere(['like', 'author', $this->author]) + ->andFilterWhere(['like', 'author_url', $this->author_url]); + + return $dataProvider; + } +} diff --git a/plugins/code/Code.php b/plugins/code/Code.php new file mode 100644 index 0000000..3ff0e47 --- /dev/null +++ b/plugins/code/Code.php @@ -0,0 +1,69 @@ + 'github', 'lang' => 'php']; + + public static function events() + { + return [ + 'yii\base\View' => [ + 'afterRender' => ['shortCode', self::$config] + ], + ]; + } + + /** + * Parse shortcode [code], more styles you can find in https://highlightjs.org + */ + public function shortCode($event) + { + $view = $event->sender; + + $style = ($event->data['style']) ? $event->data['style'] : self::$config['style']; + $lang = ($event->data['lang']) ? $event->data['lang'] : self::$config['lang']; + + CodeAsset::register($view, ['style' => $style]); + + $view->registerJs("hljs.initHighlightingOnLoad();"); + + if (isset($event->output)) { + + $shortcode = new Shortcode(); + + $shortcode->callbacks = [ + 'code' => function ($attrs, $content, $tag) use ($lang) { + $lg = isset($attrs['lang']) ? $attrs['lang'] : $lang; + return '
' . htmlspecialchars($content) . '
'; + }, + ]; + + $event->output = $shortcode->parse($event->output); + } + + return true; + } +} \ No newline at end of file diff --git a/plugins/code/CodeAsset.php b/plugins/code/CodeAsset.php new file mode 100644 index 0000000..02ce0cf --- /dev/null +++ b/plugins/code/CodeAsset.php @@ -0,0 +1,24 @@ +getAssetManager()->getBundle(__CLASS__); + + $thisBundle->css[] = sprintf('styles/%s.css', $config['style']); + + return parent::register($view); + } + +} \ No newline at end of file diff --git a/plugins/code/README.md b/plugins/code/README.md new file mode 100644 index 0000000..78fc92c --- /dev/null +++ b/plugins/code/README.md @@ -0,0 +1,29 @@ +# Code Highlighting + +### Config + +After installiation in plugins system, you can change default config + +```php + [ + 'style' => 'github', + 'lang' => 'php' + ]; +``` + +### Usage + +All the blocks of text enclosed in the shortcode [code], will be highlighted. +For example: + +```php + [code] ... our text ... [/code] +``` +or, if the language of highlighting is different from the by default `php` + ```php + [code lang="html"] ... our text ... [/code] + ``` + + ### Links + + * More langs and styles you can find in [https://highlightjs.org] (https://highlightjs.org) \ No newline at end of file diff --git a/plugins/helloworld/Helloworld.php b/plugins/helloworld/Helloworld.php new file mode 100644 index 0000000..9c6c4c4 --- /dev/null +++ b/plugins/helloworld/Helloworld.php @@ -0,0 +1,55 @@ + 'Hello, world!', + 'replace' => 'Hello, Yii!', + 'color' => '#FFDB51' + ]; + + public static function events() + { + return [ + 'yii\base\View' => [ + 'afterRender' => ['hello', self::$config] + ] + ]; + } + + /** + * Plugin action for event + */ + public static function hello($event) + { + $search = ($event->data['search']) ? $event->data['search'] : self::$config['search']; + $replace = ($event->data['replace']) ? $event->data['replace'] : self::$config['replace']; + $color = ($event->data['color']) ? $event->data['color'] : self::$config['color']; + + if (isset($event->output)) { + $content = $event->output; + $event->output = str_replace($search,"$replace", $content); + } + return true; + } +} \ No newline at end of file diff --git a/plugins/helloworld/README.md b/plugins/helloworld/README.md new file mode 100644 index 0000000..6ef6a08 --- /dev/null +++ b/plugins/helloworld/README.md @@ -0,0 +1,17 @@ +# Hello World plugin + +### Config + +After installiation in plugins system, you can change default config + +```php + [ + 'search' => 'Hello, world!', + 'replace' => 'Hello, Yii!', + 'color' => '#FFDB51' + ]; +``` + +### Usage + +After enabled this plugin, all phrases `search` will be replaced on `replace`, and highlighted `color` \ No newline at end of file diff --git a/plugins/httpauth/Httpauth.php b/plugins/httpauth/Httpauth.php new file mode 100644 index 0000000..3f21959 --- /dev/null +++ b/plugins/httpauth/Httpauth.php @@ -0,0 +1,100 @@ + ['127.0.0.1', '127.0.0.2'], + 'users' => [ + 'admin' => '123456', + ] + ]; + + public static function events() + { + return [ + 'yii\base\Application' => [ + 'beforeRequest' => ['login', self::$config] + ], + ]; + } + + /** + * @var array Username and password pairs. + */ + private static $_users = []; + + /** + * @var array the list of IPs that are allowed to access this application. + */ + private static $_allowedIps = []; + + /** + * Logining + */ + public static function login($event) + { + self::$_allowedIps = ($event->data['allowedIps']) ? $event->data['allowedIps'] : self::$config['allowedIps']; + self::$_users = ($event->data['users']) ? $event->data['users'] : self::$config['users']; + + if (Yii::$app->request->isConsoleRequest || self::_checkAllowedIps() || self::_checkHttpAuthentication()) { + return; + } + + Yii::$app->response->headers->add('WWW-Authenticate', 'Basic realm="HTTP authentication"'); + throw new UnauthorizedHttpException(Yii::t('yii', 'You are not allowed to perform this action.'), 401); + + return false; + } + + /** + * @return boolean Whether the application can be accessed by the current user. + */ + private function _checkAllowedIps() + { + if (in_array(Yii::$app->request->getUserIP(), self::$_allowedIps)) { + return true; + } + return false; + } + + /** + * @return boolean Whether the application can be accessed by the current user. + */ + private function _checkHttpAuthentication() + { + $username = Yii::$app->request->getAuthUser(); + $password = Yii::$app->request->getAuthPassword(); + if ( + isset(self::$_users[$username]) && + ( + $password == self::$_users[$username] || + md5($password) == self::$_users[$username]) + ) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/plugins/httpauth/README.md b/plugins/httpauth/README.md new file mode 100644 index 0000000..98496ac --- /dev/null +++ b/plugins/httpauth/README.md @@ -0,0 +1,18 @@ +# Http Authentication plugin + +### Config + +After installiation in plugins system, you can change default config + +```php + [ + 'allowedIps' => ['127.0.0.1', '127.0.0.2'], + 'users' => [ + 'admin' => '123456', + ] + ]; +``` + +### Thanks + +* Lajax for idea from [Yii2 Http Authentication] (https://github.com/lajax/yii2-http-auth) extension \ No newline at end of file diff --git a/views/_menu.php b/views/_menu.php new file mode 100644 index 0000000..ddc887a --- /dev/null +++ b/views/_menu.php @@ -0,0 +1,35 @@ + + + [ + 'class' => 'nav-tabs', + 'style' => 'margin-bottom: 15px' + ], + 'items' => [ + [ + 'label' => Yii::t('plugin', 'Items'), + 'url' => ['/plugins/item/index'], + ], + [ + 'label' => Yii::t('plugin', 'Events'), + 'url' => ['/plugins/event/index'], + ], + [ + 'label' => Yii::t('plugin', 'Install'), + 'url' => ['/plugins/item/find'], + ], + [ + 'label' => Yii::t('plugin', 'Info'), + 'url' => ['/plugins/item/info'], + ], + ] +]) +?> diff --git a/views/event/_form.php b/views/event/_form.php new file mode 100644 index 0000000..bc06581 --- /dev/null +++ b/views/event/_form.php @@ -0,0 +1,59 @@ + + +
+ + + +
+
+ field($model, 'data')->widget(Jsoneditor::className(), + [ + 'editorOptions' => [ + 'modes' => ['code', 'form', 'text', 'tree', 'view'], // available modes + 'mode' => 'form', // current mode + ], + 'options' => ['style' => 'height:225px'], // html options + ] + ); ?> +
+ +
+ +
+ field($model, 'trigger_class')->textInput(['disabled' => true, 'maxlength' => true]) ?> + field($model, 'handler_method')->textInput(['disabled' => true, 'maxlength' => true]) ?> + field($model, 'status')->dropDownList([ + $model::STATUS_INACTIVE => Yii::t('plugin', 'Disabled'), + $model::STATUS_ACTIVE => Yii::t('plugin', 'Enabled') + ]) ?> +
+ +
+ field($model, 'trigger_event')->textInput(['disabled' => true, 'maxlength' => true]) ?> + field($model, 'app_id')->dropDownList([ + 1=> Yii::t('plugin', 'Frontend'), + 2 => Yii::t('plugin', 'Common'), + 3 => Yii::t('plugin', 'Backend') + ]) ?> + field($model, 'pos')->textInput() ?> +
+ +
+
+ +
+ isNewRecord ? Yii::t('plugin', 'Create') : Yii::t('plugin', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
+ + + +
diff --git a/views/event/_search.php b/views/event/_search.php new file mode 100644 index 0000000..3cc3186 --- /dev/null +++ b/views/event/_search.php @@ -0,0 +1,39 @@ + + + diff --git a/views/event/create.php b/views/event/create.php new file mode 100644 index 0000000..77a6cee --- /dev/null +++ b/views/event/create.php @@ -0,0 +1,21 @@ +title = Yii::t('plugin', 'Create Event'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Events'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/views/event/index.php b/views/event/index.php new file mode 100644 index 0000000..0541f74 --- /dev/null +++ b/views/event/index.php @@ -0,0 +1,93 @@ +title = Yii::t('plugin', 'Events'); +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ render('/_menu') ?> + + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + [ + 'attribute' => 'app_id', + 'label' => Yii::t('plugin','App'), + 'options' => ['style' => 'width: 25px; align: center;'], + 'value' => function ($model, $key, $index, $column) { + switch ($model->app_id) { + case 1: + return 'F'; + break; + case 2: + return 'C'; + break; + case 3: + return 'B'; + break; + } + }, + 'filter' => [ + 1 => Yii::t('plugin', 'Frontend'), + 2 => Yii::t('plugin', 'Common'), + 3 => Yii::t('plugin', 'Backend') + ], + 'format' => "raw" + ], + 'trigger_class', + 'trigger_event', + 'plugin.handler_class', + 'handler_method', + [ + 'attribute' => 'status', + 'options' => ['style' => 'width: 75px; align: center;'], + 'value' => function ($model, $key, $index, $column) { + return $model->status == $model::STATUS_ACTIVE ? 'Enabled' : 'Disabled'; + }, + 'filter' => [ + 1 => Yii::t('plugin', 'Enabled'), + 0 => Yii::t('plugin', 'Disabled') + ], + 'format' => "raw" + ], + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {view} {delete}', + 'options' => ['style' => 'width: 100px;'], + 'buttons' => [ + 'update' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-primary', + 'title' => Yii::t('plugin', 'Update'), + ]); + }, + 'view' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-warning', + 'title' => Yii::t('plugin', 'View'), + ]); + }, + 'delete' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-danger', + 'data-method' => 'post', + 'data-confirm' => Yii::t('plugin', 'Are you sure to delete this item?'), + 'title' => Yii::t('plugin', 'Delete'), + ]); + }, + ] + ], + ], + ]); ?> + +
diff --git a/views/event/update.php b/views/event/update.php new file mode 100644 index 0000000..d6ed727 --- /dev/null +++ b/views/event/update.php @@ -0,0 +1,23 @@ +title = Yii::t('plugin', 'Update {modelClass}: ', [ + 'modelClass' => 'Event', +]) . ' ' . $model->plugin->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Events'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]]; +$this->params['breadcrumbs'][] = Yii::t('plugin', 'Update'); +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/views/event/view.php b/views/event/view.php new file mode 100644 index 0000000..ddd976b --- /dev/null +++ b/views/event/view.php @@ -0,0 +1,43 @@ +title = $model->id; +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Events'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ +

+ $model->id], ['class' => 'btn btn-primary']) ?> + $model->id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('plugin', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> +

+ + $model, + 'attributes' => [ + 'id', + 'plugin_id', + 'trigger_class', + 'trigger_event', + 'plugin.handler_class', + 'handler_method', + 'data', + 'pos', + 'status', + ], + ]) ?> + +
diff --git a/views/item/_form.php b/views/item/_form.php new file mode 100644 index 0000000..1ccdb5d --- /dev/null +++ b/views/item/_form.php @@ -0,0 +1,38 @@ + + +
+ + + + field($model, 'name')->textInput(['maxlength' => true]) ?> + + field($model, 'url')->textInput(['maxlength' => true]) ?> + + field($model, 'version')->textInput(['maxlength' => true]) ?> + + field($model, 'text')->textarea(['rows' => 6]) ?> + + field($model, 'author')->textInput(['maxlength' => true]) ?> + + field($model, 'author_url')->textInput(['maxlength' => true]) ?> + + field($model, 'status')->dropDownList([ + $model::STATUS_INACTIVE => Yii::t('plugin', 'Disabled'), + $model::STATUS_ACTIVE => Yii::t('plugin', 'Enabled') + ]) ?> + +
+ isNewRecord ? Yii::t('plugin', 'Create') : Yii::t('plugin', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> +
+ + + +
diff --git a/views/item/_item.php b/views/item/_item.php new file mode 100644 index 0000000..e5ec2f5 --- /dev/null +++ b/views/item/_item.php @@ -0,0 +1,11 @@ + + + + + + + + md5($key)], ['class' => 'btn btn-primary']);?> + \ No newline at end of file diff --git a/views/item/_search.php b/views/item/_search.php new file mode 100644 index 0000000..65d6fb8 --- /dev/null +++ b/views/item/_search.php @@ -0,0 +1,43 @@ + + + diff --git a/views/item/create.php b/views/item/create.php new file mode 100644 index 0000000..6148a47 --- /dev/null +++ b/views/item/create.php @@ -0,0 +1,21 @@ +title = Yii::t('plugin', 'Create Item'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Items'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/views/item/find.php b/views/item/find.php new file mode 100644 index 0000000..da10a7b --- /dev/null +++ b/views/item/find.php @@ -0,0 +1,52 @@ +title = Yii::t('plugin', 'Install'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Items'), 'url' => ['info']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ render('/_menu') ?> + + + Plugin name + Ver. + Author + Plugin description + + + '; + ?> + $dataProvider, + 'layout' => "$thead{items}", + 'options' => [ + 'tag' => 'table', + 'class' => 'table table-bordered table-striped', + ], + 'itemOptions' => [ + 'tag' => false, + ], + 'itemOptions' => ['class' => 'item'], + 'itemView' => '_item', + /*'itemView' => function ($model, $key, $index, $widget) use ($transportRun) { + // return print_r($model, true); + return $key; + },*/ + ]) ?> + + $dataProvider->pagination, + ]); ?> + + + +
diff --git a/views/item/index.php b/views/item/index.php new file mode 100644 index 0000000..7546469 --- /dev/null +++ b/views/item/index.php @@ -0,0 +1,71 @@ +title = Yii::t('plugin', 'Items'); +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ render('/_menu') ?> + + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + ['class' => 'yii\grid\SerialColumn'], + 'handler_class', + 'name', + 'url:url', + 'version', + 'text:ntext', + [ + 'attribute' => 'status', + 'options' => ['style'=>'width: 75px; align: center;'], + 'value' => function ($model, $key, $index, $column) { + return $model->status == $model::STATUS_ACTIVE ? 'Enabled' : 'Disabled'; + }, + 'filter' => [ + 1 => Yii::t('plugin', 'Enabled'), + 0 => Yii::t('plugin', 'Disabled') + ], + 'format'=>"raw" + ], + + [ + 'class' => 'yii\grid\ActionColumn', + 'template' => '{update} {view} {delete}', + 'options' => ['style'=>'width: 100px;'], + 'buttons' => [ + 'update' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-primary', + 'title' => Yii::t('plugin', 'Update'), + ]); + }, + 'view' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-warning', + 'title' => Yii::t('plugin', 'View'), + ]); + }, + 'delete' => function ($url, $model) { + return Html::a('', $url, [ + 'class' => 'btn btn-xs btn-danger', + 'data-method' => 'post', + 'data-confirm' => Yii::t('plugin', 'Are you sure to delete this item?'), + 'title' => Yii::t('plugin', 'Delete'), + ]); + }, + ] + ], + ], + ]); ?> + +
diff --git a/views/item/info.php b/views/item/info.php new file mode 100644 index 0000000..73dfe1c --- /dev/null +++ b/views/item/info.php @@ -0,0 +1,227 @@ +title = Yii::t('plugin', 'Info'); +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Items'), 'url' => ['info']]; +$this->params['breadcrumbs'][] = $this->title; +?> + +
+ +

title) ?>

+ render('/_menu') ?> + + +
+
+

MVC

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Controller or Module
EVENT_BEFORE_ACTIONbeforeAction
EVENT_AFTER_ACTIONafterAction
Model
EVENT_BEFORE_VALIDATEbeforeValidate
EVENT_AFTER_VALIDATEafterValidate
yii\base\View
EVENT_BEGIN_PAGEbeginPage
EVENT_END_PAGEendPage
EVENT_BEFORE_RENDERbeforeRender
EVENT_AFTER_RENDERafterRender
yii\web\View
EVENT_BEGIN_BODYbeginBody
EVENT_END_BODYendBody
+ +

Components

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MessageSource
EVENT_MISSING_TRANSLATIONmissingTranslation
BaseMailer
EVENT_BEFORE_SENDbeforeSend
EVENT_AFTER_SENDafterSend
User
EVENT_BEFORE_LOGINbeforeLogin
EVENT_AFTER_LOGINafterLogin
EVENT_BEFORE_LOGOUTbeforeLogout
EVENT_AFTER_LOGOUTafterLogout
+
+ +
+

Database

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BaseActiveRecord
EVENT_INIT + + init
EVENT_AFTER_FINDafterFind
EVENT_BEFORE_INSERTbeforeInsert
EVENT_AFTER_INSERTafterInsert
EVENT_BEFORE_UPDATEbeforeUpdate
EVENT_AFTER_UPDATEafterUpdate
EVENT_BEFORE_DELETEbeforeDelete
EVENT_AFTER_DELETEafterDelete
ActiveQuery
EVENT_INITinit
Connection
EVENT_AFTER_OPENafterOpen
EVENT_BEGIN_TRANSACTIONbeginTransaction
EVENT_COMMIT_TRANSACTIONcommitTransaction
EVENT_ROLLBACK_TRANSACTIONrollbackTransaction
+ +

Request

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
yii\base\Application
EVENT_BEFORE_REQUESTbeforeRequest
EVENT_AFTER_REQUESTafterRequest
Response
EVENT_BEFORE_SENDbeforeSend
EVENT_AFTER_SENDafterSend
EVENT_AFTER_PREPAREafterPrepare
+
+ +
+ + + +
diff --git a/views/item/update.php b/views/item/update.php new file mode 100644 index 0000000..a541381 --- /dev/null +++ b/views/item/update.php @@ -0,0 +1,23 @@ +title = Yii::t('plugin', 'Update {modelClass}: ', [ + 'modelClass' => 'Item', +]) . ' ' . $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Items'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]]; +$this->params['breadcrumbs'][] = Yii::t('plugin', 'Update'); +?> +
+ +

title) ?>

+ + render('_form', [ + 'model' => $model, + ]) ?> + +
diff --git a/views/item/view.php b/views/item/view.php new file mode 100644 index 0000000..03b1e2b --- /dev/null +++ b/views/item/view.php @@ -0,0 +1,42 @@ +title = $model->name; +$this->params['breadcrumbs'][] = ['label' => Yii::t('plugin', 'Items'), 'url' => ['index']]; +$this->params['breadcrumbs'][] = $this->title; +?> +
+ +

title) ?>

+ +

+ $model->id], ['class' => 'btn btn-primary']) ?> + $model->id], [ + 'class' => 'btn btn-danger', + 'data' => [ + 'confirm' => Yii::t('plugin', 'Are you sure you want to delete this item?'), + 'method' => 'post', + ], + ]) ?> +

+ + $model, + 'attributes' => [ + 'id', + 'name', + 'url:url', + 'version', + 'text:ntext', + 'author', + 'author_url:url', + 'status', + ], + ]) ?> + +