diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/BlogPostsApiController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/BlogPostsApiController.php
index 6ee00d0..9d0a782 100644
--- a/plugins/ahmadfatoni/apigenerator/controllers/api/BlogPostsApiController.php
+++ b/plugins/ahmadfatoni/apigenerator/controllers/api/BlogPostsApiController.php
@@ -11,6 +11,11 @@ use Illuminate\Support\Facades\Validator;
use RainLab\Blog\Models\Post;
use TPS\Birzha\Classes\BlogPostResource;
use TPS\Birzha\Models\SliderApp;
+use TPS\Birzha\Models\UserSliders;
+use TPS\Birzha\Models\Product;
+use TPS\Birzha\Models\City;
+use TPS\Birzha\Models\Favourites;
+use TPS\Birzha\Models\Comment;
use RainLab\User\Models\User;
class BlogPostsApiController extends Controller
@@ -19,10 +24,12 @@ class BlogPostsApiController extends Controller
protected $helpers;
- public function __construct(Post $Post, Helpers $helpers)
+ public function __construct(Post $Post, Favourites $Favourites, Comment $Comment, Helpers $helpers)
{
parent::__construct();
$this->Post = $Post;
+ $this->Favourites = $Favourites;
+ $this->Comment = $Comment;
$this->helpers = $helpers;
}
@@ -45,6 +52,184 @@ class BlogPostsApiController extends Controller
->paginate($data['per_page'] ?? 7))->response()->getData(), 200);
}
+ public function getAccountSliders($id)
+ {
+ $data = UserSliders::where('user_id', $id)->paginate(9);
+
+ if($data) {
+ $data->each(function ($item, $key) {
+ $item->img = 'http://78.111.88.8:9086' . \Config::get('cms.storage.media.path') . $item->img;
+ });
+ }
+
+ return response()->json($data, 200);
+ }
+
+
+ public function createComment(Request $request)
+ {
+ $currentUser = \JWTAuth::parseToken()->authenticate();
+
+ $data = $request->all();
+ $validator = Validator::make($data, [
+ 'product_id' => 'required',
+ 'comment' => 'required',
+ 'rating' => 'required|numeric'
+ ]);
+
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+
+
+ $comment = new $this->Comment;
+ //dd($favourite);
+ $comment->user_id = $currentUser->id;
+ $comment->product_id = (int)$data['product_id'];
+ $comment->comment = $data['comment'];
+ $comment->rating = $data['rating'];
+
+ $comment->save();
+
+ return $this->helpers->apiArrayResponseBuilder(201, 'ok', ['comment' => $comment]);
+
+ }
+
+
+ public function getComment(Request $request)
+ {
+ //$data = $request->all();
+ // $validator = Validator::make($data, [
+ // 'product_id' => 'required',
+ // ]);
+
+ // if($validator->fails()) {
+ // return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ // }
+
+ $currentUser = \JWTAuth::parseToken()->authenticate();
+ $comment = $this->Comment::where('user_id', $currentUser->id)->paginate(9);
+ //dd($favourite);
+
+ return response()->json($comment, 200);
+
+ }
+
+ public function getProductComment(Request $request)
+ {
+ $data = $request->all();
+ $validator = Validator::make($data, [
+ 'product_id' => 'required',
+ ]);
+
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+
+ $comment = $this->Comment::where('product_id', $data["product_id"])->where('is_approve', 1)->paginate(15);
+ //dd($favourite);
+
+ return response()->json($comment, 200);
+
+ }
+
+
+ public function createFav(Request $request)
+ {
+ $currentUser = \JWTAuth::parseToken()->authenticate();
+
+ $data = $request->all();
+ $validator = Validator::make($data, [
+ 'product_id' => 'required'
+ ]);
+
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+
+
+ $favourite = new $this->Favourites;
+ //dd($favourite);
+ $favourite->user_id = $currentUser->id;
+ $favourite->product_id = (int)$data['product_id'];
+
+ $favourite->save();
+
+ return $this->helpers->apiArrayResponseBuilder(201, 'ok', ['favourite' => $favourite]);
+
+
+
+ }
+
+
+ public function getFav(Request $request)
+ {
+ $currentUser = \JWTAuth::parseToken()->authenticate();
+
+
+ $favourite = $this->Favourites::where('user_id', $currentUser->id)
+ ->with(['product' => function($q){
+ $q->approved()->with(['images', 'place', 'files', 'translations:locale,model_id,attribute_data']);
+ }])->paginate(9);
+ //dd($favourite);
+
+ return response()->json($favourite, 200);
+
+ }
+
+ public function deleteFav($id){
+
+ $favourite = $this->Favourites->find($id);
+ $favourite->delete();
+
+ return $this->helpers->apiArrayResponseBuilder(200, 'success', ['message' => 'Data has been deleted successfully']);
+ }
+
+
+ public function getAccountProducts($id)
+ {
+ $data = Product::where('vendor_id', $id)
+ ->with('categories:id,name')
+ ->with([
+ 'translations:locale,model_id,attribute_data',
+ 'images:attachment_id,attachment_type,disk_name,file_name',
+ ])
+ ->approved()
+ ->paginate(9);
+
+ return response()->json($data, 200);
+ }
+
+ public function getPlaces()
+ {
+ $data = City::with([
+ 'translations:locale,model_id,attribute_data',
+ ])
+ ->paginate(100);
+
+ return response()->json($data, 200);
+ }
+
+
+ public function getAccountDatas($id)
+ {
+ //$data = User::find($id)->first;
+
+
+ $data = User::where('id', $id)->select('id', 'name', 'email', 'username', 'type', 'logo', 'shop_title', 'slogan', 'work_time', 'description', 'map', 'banner', 'is_instagram')
+ ->with(['categories' => function($q){
+ $q->with('translations:locale,model_id,attribute_data');
+ }])
+ ->with('sliders')
+ ->first();
+
+ $data->pathUrl = 'http://78.111.88.8:9086' . \Config::get('cms.storage.media.path');
+
+ return response()->json($data, 200);
+ }
+
+
+
public function getAccounts(Request $request)
{
@@ -52,7 +237,7 @@ class BlogPostsApiController extends Controller
$dataAccounts = User::select('id', 'name', 'email', 'username', 'type', 'logo', 'shop_title', 'slogan', 'work_time', 'description', 'map', 'banner', 'is_instagram')
->with('categories:id,name,slug,icon')
- ->get();
+ ->paginate(9);
if($data && $data["type"] == "home"){
@@ -94,7 +279,7 @@ class BlogPostsApiController extends Controller
if($validator->fails()) {
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
}
- $dataSlider = SliderApp::where('type', $data['type'])->orderBy('order', 'desc')->select('id', 'img', 'type', 'order')->get();
+ $dataSlider = SliderApp::where('type', $data['type'])->orderBy('order', 'desc')->select('id', 'img', 'type', 'order')->paginate(9);
//$dataSlider["img"] = $path.$dataSlider["img"];
diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/CategoriesAPIController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/CategoriesAPIController.php
index 26e6efd..7e3a799 100644
--- a/plugins/ahmadfatoni/apigenerator/controllers/api/CategoriesAPIController.php
+++ b/plugins/ahmadfatoni/apigenerator/controllers/api/CategoriesAPIController.php
@@ -33,31 +33,30 @@ class CategoriesAPIController extends Controller
if($data["featured"] == 3){
$dataCat = $this->Category
- ->select('id', 'name', 'icon', 'status', 'slug', 'sort_order', 'is_featured')
+ ->select('id', 'name', 'icon', 'status', 'slug', 'sort_order', 'is_featured', 'is_file_category')
->with('translations:locale,model_id,attribute_data')
->orderBy('sort_order', 'asc')
->active()
- ->get();
+ ->paginate(9);
}else{
$dataCat = $this->Category
- ->select('id', 'name', 'icon', 'status', 'slug', 'sort_order', 'is_featured')
+ ->select('id', 'name', 'icon', 'status', 'slug', 'sort_order', 'is_featured', 'is_file_category')
->with('translations:locale,model_id,attribute_data')
->where('is_featured', $data["featured"])
->orderBy('sort_order', 'asc')
->active()
- ->get();
+ ->paginate(9);
}
-
-
if($dataCat) {
$dataCat->each(function ($item, $key) {
- $item->icon = $this->pageUrl('index') . \Config::get('cms.storage.media.path') . $item->icon;
+ $item->icon = 'http://78.111.88.8:9086' . \Config::get('cms.storage.media.path') . $item->icon;
});
}
+
- return $this->helpers->apiArrayResponseBuilder(200, 'success', $dataCat);
+ return response()->json($dataCat, 200);
}
public function show($id){
diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/ProductsApiController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/ProductsApiController.php
index af99c6c..0454655 100644
--- a/plugins/ahmadfatoni/apigenerator/controllers/api/ProductsApiController.php
+++ b/plugins/ahmadfatoni/apigenerator/controllers/api/ProductsApiController.php
@@ -6,6 +6,7 @@ use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use TPS\Birzha\Models\Product;
use TPS\Birzha\Models\Category;
+use TPS\Birzha\Models\City;
use TPS\Birzha\Models\Term;
use TPS\Birzha\Models\Measure;
use TPS\Birzha\Models\Settings;
@@ -28,6 +29,7 @@ class ProductsAPIController extends Controller
}
public function index(){
+ //dd("sdf");
$sortOrderParam = strtolower(input('sort_order'));
// protect from sql injection
@@ -40,13 +42,16 @@ class ProductsAPIController extends Controller
$categoryId = intval(input('category_id')); // intval protects from injection
$perPage = intval(input('custom_per_page')); // intval protects from injection
$productId = intval(input('product_id')); // intval protects from injection
+ $is_home = intval(input('is_home'));
$queryString = input('q');
$locale = input('locale');
// $query = $this->Product::with('categories:id,name')
- $query = $this->Product::with([
+ $query = Product::with([
'translations:locale,model_id,attribute_data',
'images:attachment_id,attachment_type,disk_name,file_name',
+ 'vendor:id,name,email,type,logo,banner,shop_title,slogan,is_instagram',
+ 'place',
])
->approved()
->orderBy('ends_at', $sortOrder);
@@ -66,6 +71,19 @@ class ProductsAPIController extends Controller
$query = null;
}
}
+
+ if($is_home) { // fetch offers by the category of the product
+
+ $query = $this->Product::with([
+ 'translations:locale,model_id,attribute_data',
+ 'images:attachment_id,attachment_type,disk_name,file_name',
+ 'place',
+ 'vendor:id,name,email,type,logo,banner,shop_title,slogan,is_instagram'
+ ])
+ ->where('is_home', 1)
+ ->approved()
+ ->orderBy('ends_at', $sortOrder);
+ }
if($productId) { // fetch offers with products in the same category
@@ -97,7 +115,8 @@ class ProductsAPIController extends Controller
])
->orderBy('ends_at', $sortOrder);
}
-
+
+
$data = $query ? $query->paginate($perPage) : null;
@@ -109,8 +128,11 @@ class ProductsAPIController extends Controller
$data = $this->Product::with([
'translations:locale,model_id,attribute_data',
'images:attachment_id,attachment_type,disk_name,file_name',
- 'vendor:id,name,surname,email,username'
+ 'vendor:id,name,surname,email,username,logo,banner',
+ 'files', 'place'
])->find($id);
+
+ $data->main_path = 'http://78.111.88.8:9086' . \Config::get('cms.storage.media.path');
if ($data && $data->status == 'approved' /*&& $data->ends_at >= \Carbon\Carbon::now()*/){
return response()->json($data, 200);
@@ -123,23 +145,20 @@ class ProductsAPIController extends Controller
// step1
public function store(Request $request){
$data = $request->all();
-
+ // dd($data);
$rules = [
'name_ru' => 'required',
- // 'name_en' => 'required',
'name_tm' => 'required',
-
'price' => 'required|numeric',
- 'place' => 'required',
+ 'place_id' => 'required',
'description_tm' => 'required',
-
'description_ru' => 'required',
-
- 'old_img' => 'array',
'new_img' => 'array',
-
+ 'is_file' => 'required',m
// allowed only jpg & png files with size <= 1024
'new_img.*' => 'mimes:jpg,png|max:1024',
+
+ //'new_file.*' => 'max:9024',
'category_id' => [
'required',
@@ -152,6 +171,9 @@ class ProductsAPIController extends Controller
}
}
],
+
+
+ //'place_id' => 'required',
];
@@ -159,44 +181,105 @@ class ProductsAPIController extends Controller
if($validator->fails()) {
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
}
-
- $category = Category::find($data['category_id']);
-
- if(isset($data['productForEditing'])) { // if product is being edited - not added
-
- if(!$product = $this->checkUserHasProduct($data['productForEditing'])) {
- return $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => "Resource id = {$data['productForEditing']} could not be found"]);
- }
-
- //approved edilen produkty prodlenie uchin drafta tayinlamak, bagly transacsiasyny goparmak,
- if($product && $product->status == 'approved' && $product->transaction)
- {
- $product->transaction()->update(['description'=>"Lot #{$product->id} {$product->name} harydyn onki publikasia tolegidir.(bu haryt prodlenia uchin taze transaksia doredilyar)"]);
- $product->transaction = null;
- }
- } else {
- $product = new $this->Product;
+
+ // validate if no old images and no new images
+ if(!$request->has('new_img')) {
+ return response()->json(ErrorResponseApi::translateErrorMessage('atleast_1_image'), 400);
}
+ // if new_img field is passed but empty
+ if($request->has('new_img')) {
+ $rules = [
+ 'new_img' => 'required'
+ ];
+ $validator = $this->validateForm($data, $rules);
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+ }
+
+
+
+ $category = Category::find($data['category_id']);
+
+
+ $product = new $this->Product;
+
+
+
+
+ $product->translateContext('tm');
+
+ $product->name = $data['name_tm'];
+ $product->setAttributeTranslated('name', $data['name_ru'], 'ru');
+
$product->translateContext('tm');
- $product->name = $data['name_tm'];
- // Sets a single translated attribute for a language
- $product->setAttributeTranslated('name', $data['name_ru'], 'ru');
- // $product->setAttributeTranslated('name', $data['name_en'], 'en');
+ $product->description = $data['description_tm'];
// Sets a single translated attribute for a language
+ $product->setAttributeTranslated('description', $data['description_ru'], 'ru');
+
$product->setAttributeTranslated('slug', \Str::slug($data['name_ru'],'-'), 'ru');
- // $product->setAttributeTranslated('slug', \Str::slug($data['name_en'],'-'), 'en');
$product->slug = \Str::slug($data['name_tm'],'-');
- $product->status = 'draft';
-
+ $product->status = 'new';
+
+ $product->place_id = $data['place_id'];
+ $product->price = $data['price'];
+
+ $product->is_file_product = $data['is_file'];
+
+ if($data['is_file'] == 1){
+ // validate if no old images and no new images
+ if(!$request->has('new_file')) {
+ return response()->json(ErrorResponseApi::translateErrorMessage('atleast_1_file'), 400);
+ }
+
+ // if new_img field is passed but empty
+ if($request->has('new_file')) {
+ $rules = [
+ 'new_file' => 'required'
+ ];
+ $validator = $this->validateForm($data, $rules);
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+ }
+
+
+ try {
+
+ foreach($data['new_file'] ?? [] as $key => $fileq) {
+ $product->files = $fileq;
+ $product->save();
+ }
+
+ } catch(\Throwable $e) {
+
+ return $this->helpers->apiArrayResponseBuilder(500, 'server error', ['message' => 'something went wrong file']);
+ }
+
+ }
$product->vendor_id = \JWTAuth::parseToken()->authenticate()->id;
$product->ends_at = null; // if approved but date was expired
-
-
+
+
+ try {
+
+ foreach($data['new_img'] ?? [] as $key => $img) {
+ $product->images = $img;
+ $product->save();
+ }
+
+ } catch(\Throwable $e) {
+
+ return $this->helpers->apiArrayResponseBuilder(500, 'server error', ['message' => 'qqqsomething went wrong']);
+ }
+
+ // product found, validation passed -> can fill product &save
+
if(!isset($data['productForEditing'])) {
// $product->created_at = Carbon::now('Asia/Ashgabat');
$category->products()->save($product);
@@ -206,6 +289,10 @@ class ProductsAPIController extends Controller
// attach to a new category
$category->products()->save($product);
}
+
+
+ //$produt->save();
+
return $this->helpers->apiArrayResponseBuilder(200, 'ok', ['product' => $product]);
}
@@ -413,16 +500,11 @@ class ProductsAPIController extends Controller
$attachedProduct->description = $data['description_tm'];
// Sets a single translated attribute for a language
$attachedProduct->setAttributeTranslated('description', $data['description_ru'], 'ru');
- $attachedProduct->setAttributeTranslated('description', $data['description_en'], 'en');
+
- $attachedProduct->quantity = $data['quantity'];
+
$attachedProduct->price = $data['price'];
- $attachedProduct->measure_id = $data['measure_id'];
- $attachedProduct->payment_term_id = $data['payment_term_id'];
- $attachedProduct->delivery_term_id = $data['delivery_term_id'];
- $attachedProduct->packaging = $data['packaging'];
$attachedProduct->place = $data['place'];
- $attachedProduct->currency_id = $data['currency_id'];
$attachedProduct->save();
return $attachedProduct;
@@ -430,21 +512,18 @@ class ProductsAPIController extends Controller
public function myProducts()
{
+ dd("sdf");
$perPage = intval(input('custom_per_page')); // intval protects from injection
try {
$products = \JWTAuth::parseToken()->authenticate()
->products()
- ->with('translations:locale,model_id,attribute_data',
- 'images:attachment_id,attachment_type,disk_name,file_name')
+ ->with(['translations:locale,model_id,attribute_data',
+ 'images:attachment_id,attachment_type,disk_name,file_name', 'place', 'files'])
->orderBy('updated_at', 'desc')
->paginate($perPage);
- if($products) {
- foreach ($products as $product) {
- $product->unit = new MeasureResource($product->measure);
- }
- }
+
} catch (\Throwable $th) {
return $this->helpers->apiArrayResponseBuilder(500, 'server error', ['message' => 'Something went wrong']);
}
diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/VendorApiController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/VendorApiController.php
new file mode 100644
index 0000000..8d442ce
--- /dev/null
+++ b/plugins/ahmadfatoni/apigenerator/controllers/api/VendorApiController.php
@@ -0,0 +1,72 @@
+Vendor = $Vendor;
+ $this->helpers = $helpers;
+ }
+
+ public function updateVendor(Request $request){
+
+ $data = $request->all();
+
+ $validator = Validator::make($data, [
+ 'shop_title' => 'required',
+ 'slogan' => 'required',
+ 'work_time' => 'required',
+ 'description' => 'required',
+ ]);
+
+
+
+ // $validator = $this->validateForm($data, $rules);
+ if($validator->fails()) {
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validator->errors() );
+ }
+ //dd($data);
+ $currentUser = \JWTAuth::parseToken()->authenticate();
+ $vendor = Vendor::find($currentUser->id)->first();
+
+ //dd($vendor->type);
+
+ if ($vendor->type != "simple"){
+
+ $vendor->shop_title = $data["shop_title"];
+ $vendor->slogan = $data["slogan"];
+ $vendor->work_time = $data["work_time"];
+ $vendor->description = $data["description"];
+ $vendor->save();
+
+ return $this->helpers->apiArrayResponseBuilder(200, 'ok', [$vendor]);
+
+ }else{
+
+ return $this->helpers->apiArrayResponseBuilder(400, 'fail');
+
+ }
+
+ }
+
+ public static function getAfterFilters() {return [];}
+ public static function getBeforeFilters() {return [];}
+ public static function getMiddleware() {return [];}
+ public function callAction($method, $parameters=false) {
+ return call_user_func_array(array($this, $method), $parameters);
+ }
+
+}
diff --git a/plugins/ahmadfatoni/apigenerator/routes.php b/plugins/ahmadfatoni/apigenerator/routes.php
index ec3aa90..9a933bd 100644
--- a/plugins/ahmadfatoni/apigenerator/routes.php
+++ b/plugins/ahmadfatoni/apigenerator/routes.php
@@ -9,17 +9,12 @@ Route::group(['prefix' =>'api/v1','namespace' =>'AhmadFatoni\ApiGenerator\Contro
// Route::get('categories/{id}/delete', ['as' => 'categories.delete', 'uses' => 'CategoriesAPIController@destroy']);
Route::get('products', ['as' => 'products.index', 'uses' => 'ProductsApiController@index']);
+ // Route::get('products', 'ProductsApiController@index');
Route::get('products/{id}', ['as' => 'products.show', 'uses' => 'ProductsApiController@show']);
Route::get('test',['as' => 'test', 'uses' => 'SmsController@index']);
// Route::get('products/{id}/delete', ['as' => 'products.delete', 'uses' => 'ProductsApiController@destroy']);
- // Route::resource('countries', 'CountriesapiController', ['except' => ['destroy', 'create', 'edit']]);
- // Route::resource('currencies', 'CurrenciesapiController', ['except' => ['destroy', 'create', 'edit']]);
- // Route::resource('measures', 'MeasuresapiController', ['except' => ['destroy', 'create', 'edit']]);
-
-
- // Route::get('measures/{id}/delete', ['as' => 'measures.delete', 'uses' => 'MeasuresapiController@destroy']);
Route::resource('terms', 'TermsapiController', ['except' => ['destroy', 'create', 'edit']]);
// Route::get('terms/{id}/delete', ['as' => 'terms.delete', 'uses' => 'TermsapiController@destroy']);
@@ -30,19 +25,37 @@ Route::group(['prefix' =>'api/v1','namespace' =>'AhmadFatoni\ApiGenerator\Contro
Route::get('news/{id}', 'BlogPostsApiController@show')->where(['id' => '[0-9]+']);
Route::get('sliders', 'BlogPostsApiController@getSliders');
+
+ Route::get('places', 'BlogPostsApiController@getPlaces');
+
Route::get('accounts', 'BlogPostsApiController@getAccounts');
+
+ Route::get('get/product/comments', 'BlogPostsApiController@getProductComment');
+
+// Route::get('account/{id}/sliders', 'BlogPostsApiController@getAccountSliders')->where(['id' => '[0-9]+']);
+ Route::get('account/{id}/products', 'BlogPostsApiController@getAccountProducts')->where(['id' => '[0-9]+']);
+ Route::get('account/{id}/datas', 'BlogPostsApiController@getAccountDatas');
Route::middleware(['\Tymon\JWTAuth\Middleware\GetUserFromToken'])->group(function () {
+ Route::post('update/vendor/data', 'VendorApiController@updateVendor');
+
+ Route::post('create/fav', 'BlogPostsApiController@createFav');
+ Route::get('get/fav', 'BlogPostsApiController@getFav');
+ Route::delete('fav/{id}', 'BlogPostsApiController@deleteFav');
+
+ Route::post('create/comment', 'BlogPostsApiController@createComment');
+ Route::get('get/customer/comments', 'BlogPostsApiController@getComment');
Route::post('products', 'ProductsApiController@store');
- Route::post('products/{id}', 'ProductsApiController@update')
-
- ->where('id', '[0-9]+');
+ Route::post('products/{id}', 'ProductsApiController@update')->where('id', '[0-9]+');
+
Route::delete('products/{id}/image-delete/{image_id}', 'ProductsApiController@imageDelete')
->where(['id' => '[0-9]+', 'image_id' => '[0-9]+']);
Route::post('products/{id}/publish', 'ProductsApiController@publish')
->where('id', '[0-9]+');
- Route::get('my-products/','ProductsApiController@myProducts');
+
+
+ Route::get('my-products','ProductsApiController@customerProducts');
Route::get('my-products/{id}','ProductsApiController@showMyProductById');
Route::delete('my-products/{id}', 'ProductsApiController@delete')
->where('id', '[0-9]+');
diff --git a/plugins/rainlab/notify/LICENSE.md b/plugins/rainlab/notify/LICENSE.md
new file mode 100644
index 0000000..e49b459
--- /dev/null
+++ b/plugins/rainlab/notify/LICENSE.md
@@ -0,0 +1,21 @@
+# MIT license
+
+Copyright (c) 2014-2022 Responsiv Pty Ltd, October CMS
+
+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.
diff --git a/plugins/rainlab/notify/Plugin.php b/plugins/rainlab/notify/Plugin.php
new file mode 100644
index 0000000..783e32d
--- /dev/null
+++ b/plugins/rainlab/notify/Plugin.php
@@ -0,0 +1,74 @@
+ 'Notify',
+ 'description' => 'Notification Services',
+ 'author' => 'Alexey Bobkov, Samuel Georges',
+ 'icon' => 'icon-bullhorn'
+ ];
+ }
+
+ /**
+ * registerSettings
+ */
+ public function registerSettings()
+ {
+ return [
+ 'notifications' => [
+ 'label' => 'Notification Rules',
+ 'description' => 'Manage the events and actions that trigger notifications.',
+ 'category' => SettingsManager::CATEGORY_NOTIFICATIONS,
+ 'icon' => 'icon-bullhorn',
+ 'url' => Backend::url('rainlab/notify/notifications'),
+ 'permissions' => ['rainlab.notify.manage_notifications'],
+ 'order' => 600,
+ 'keywords' => 'notify'
+ ],
+ ];
+ }
+
+ /**
+ * registerNotificationRules
+ */
+ public function registerNotificationRules()
+ {
+ return [
+ 'groups' => [],
+ 'events' => [],
+ 'actions' => [
+ \RainLab\Notify\NotifyRules\SaveDatabaseAction::class,
+ \RainLab\Notify\NotifyRules\SendMailTemplateAction::class,
+ ],
+ 'conditions' => [
+ \RainLab\Notify\NotifyRules\ExecutionContextCondition::class,
+ ],
+ ];
+ }
+
+ /**
+ * registerPermissions
+ */
+ public function registerPermissions()
+ {
+ return [
+ 'rainlab.notify.manage_notifications' => [
+ 'tab' => SettingsManager::CATEGORY_NOTIFICATIONS,
+ 'label' => 'Notifications management'
+ ],
+ ];
+ }
+}
diff --git a/plugins/rainlab/notify/README.md b/plugins/rainlab/notify/README.md
new file mode 100644
index 0000000..d1edf67
--- /dev/null
+++ b/plugins/rainlab/notify/README.md
@@ -0,0 +1,400 @@
+# Notification engine
+
+**Plugin is currently in Beta status. Proceed with caution.**
+
+Adds support for sending notifications across a variety of different channels, including mail, SMS and Slack.
+
+Notifications are managed in the back-end area by navigating to *Settings > Notification rules*.
+
+## Notification workflow
+
+When a notification fires, it uses the following workflow:
+
+1. Plugin registers associated actions, conditions and events using `registerNotificationRules`
+1. A notification class is bound to a system event using `Notifier::bindEvent`
+1. A system event is fired `Event::fire`
+1. The parameters of the event are captured, along with any global context parameters
+1. A command is pushed on the queue to process the notification `Queue::push`
+1. The command finds all notification rules using the notification class and triggers them
+1. The notification conditions are checked and only proceed if met
+1. The notification actions are triggered
+
+Here is an example of a plugin registering notification rules. The `groups` definition will create containers that are used to better organise events. The `presets` definition specifies notification rules defined by the system.
+
+```php
+public function registerNotificationRules()
+{
+ return [
+ 'events' => [
+ \RainLab\User\NotifyRules\UserActivatedEvent::class,
+ ],
+ 'actions' => [
+ \RainLab\User\NotifyRules\SaveToDatabaseAction::class,
+ ],
+ 'conditions' => [
+ \RainLab\User\NotifyRules\UserAttributeCondition::class
+ ],
+ 'groups' => [
+ 'user' => [
+ 'label' => 'User',
+ 'icon' => 'icon-user'
+ ],
+ ],
+ 'presets' => '$/rainlab/user/config/notify_presets.yaml',
+ ];
+}
+```
+
+Here is an example of triggering a notification. The system event `rainlab.user.activate` is bound to the `UserActivatedEvent` class.
+
+```php
+// Bind to a system event
+\RainLab\Notify\Classes\Notifier::bindEvents([
+ 'rainlab.user.activate' => \RainLab\User\NotifyRules\UserActivatedEvent::class
+]);
+
+// Fire the system event
+Event::fire('rainlab.user.activate', [$this]);
+```
+
+Here is an example of registering context parameters, which are available globally to all notifications.
+
+```php
+\RainLab\Notify\Classes\Notifier::instance()->registerCallback(function($manager) {
+ $manager->registerGlobalParams([
+ 'user' => Auth::getUser()
+ ]);
+});
+```
+
+Here is an example of an event preset:
+
+```yaml
+# ===================================
+# Event Presets
+# ===================================
+
+welcome_email:
+ name: Send welcome email to user
+ event: RainLab\User\NotifyRules\UserRegisteredEvent
+ items:
+ - action: RainLab\Notify\NotifyRules\SendMailTemplateAction
+ mail_template: rainlab.user::mail.welcome
+ send_to_mode: user
+ conditions:
+ - condition: RainLab\Notify\NotifyRules\ExecutionContextCondition
+ subcondition: environment
+ operator: is
+ value: dev
+ condition_text: Application environment is dev
+```
+
+## Creating Event classes
+
+An event class is responsible for preparing the parameters passed to the conditions and actions. The static method `makeParamsFromEvent` will take the arguments provided by the system event and convert them in to parameters.
+
+```php
+class UserActivatedEvent extends \RainLab\Notify\Classes\EventBase
+{
+ /**
+ * @var array Local conditions supported by this event.
+ */
+ public $conditions = [
+ \RainLab\User\NotifyRules\UserAttributeCondition::class
+ ];
+
+ /**
+ * Returns information about this event, including name and description.
+ */
+ public function eventDetails()
+ {
+ return [
+ 'name' => 'Activated',
+ 'description' => 'A user is activated',
+ 'group' => 'user'
+ ];
+ }
+
+ /**
+ * Defines the usable parameters provided by this class.
+ */
+ public function defineParams()
+ {
+ return [
+ 'name' => [
+ 'title' => 'Name',
+ 'label' => 'Name of the user',
+ ],
+ // ...
+ ];
+ }
+
+ public static function makeParamsFromEvent(array $args, $eventName = null)
+ {
+ return [
+ 'user' => array_get($args, 0)
+ ];
+ }
+}
+```
+
+## Creating Action classes
+
+Action classes define the final step in a notification and subsequently perform the notification itself. Some examples might be sending and email or writing to the database.
+
+```php
+class SendMailTemplateAction extends \RainLab\Notify\Classes\ActionBase
+{
+ /**
+ * Returns information about this event, including name and description.
+ */
+ public function actionDetails()
+ {
+ return [
+ 'name' => 'Compose a mail message',
+ 'description' => 'Send a message to a recipient',
+ 'icon' => 'icon-envelope'
+ ];
+ }
+
+ /**
+ * Field configuration for the action.
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ public function getText()
+ {
+ $template = $this->host->template_name;
+
+ return 'Send a message using '.$template;
+ }
+
+ /**
+ * Triggers this action.
+ * @param array $params
+ * @return void
+ */
+ public function triggerAction($params)
+ {
+ $email = 'test@email.tld';
+ $template = $this->host->template_name;
+
+ Mail::sendTo($email, $template, $params);
+ }
+}
+```
+
+A form fields definition file is used to provide form fields when the action is established. These values are accessed from condition using the host model via the `$this->host` property.
+
+```yaml
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ template_name:
+ label: Template name
+ type: text
+```
+
+An action may choose to provide no form fields by simply returning false from the `defineFormFields` method.
+
+```php
+public function defineFormFields()
+{
+ return false;
+}
+```
+
+## Creating Condition classes
+
+A condition class should specify how it should appear in the user interface, providing a name, title and summary text. It also must declare an `isTrue` method for evaluating whether the condition is true or not.
+
+```php
+class MyCondition extends \RainLab\Notify\Classes\ConditionBase
+{
+ /**
+ * Return either ConditionBase::TYPE_ANY or ConditionBase::TYPE_LOCAL
+ */
+ public function getConditionType()
+ {
+ // If the condition should appear for all events
+ return ConditionBase::TYPE_ANY;
+
+ // If the condition should appear only for some events
+ return ConditionBase::TYPE_LOCAL;
+ }
+
+ /**
+ * Field configuration for the condition.
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ public function getName()
+ {
+ return 'My condition is checked';
+ }
+
+ public function getTitle()
+ {
+ return 'My condition';
+ }
+
+ public function getText()
+ {
+ $value = $this->host->mycondition;
+
+ return 'My condition is '.$value;
+ }
+
+ /**
+ * Checks whether the condition is TRUE for specified parameters
+ * @param array $params
+ * @return bool
+ */
+ public function isTrue(&$params)
+ {
+ return true;
+ }
+}
+```
+
+A form fields definition file is used to provide form fields when the condition is established. These values are accessed from condition using the host model via the `$this->host` property.
+
+```yaml
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ mycondition:
+ label: My condition
+ type: dropdown
+ options:
+ true: True
+ false: False
+```
+
+## Model attribute condition classes
+
+Model attribute conditions are designed specially for applying conditions to sets of model attributes.
+
+```php
+class UserAttributeCondition extends \RainLab\Notify\Classes\ModelAttributesConditionBase
+{
+ protected $modelClass = \RainLab\User\Models\User::class;
+
+ public function getGroupingTitle()
+ {
+ return 'User attribute';
+ }
+
+ public function getTitle()
+ {
+ return 'User attribute';
+ }
+
+ /**
+ * Checks whether the condition is TRUE for specified parameters
+ * @param array $params Specifies a list of parameters as an associative array.
+ * @return bool
+ */
+ public function isTrue(&$params)
+ {
+ $hostObj = $this->host;
+
+ $attribute = $hostObj->subcondition;
+
+ if (!$user = array_get($params, 'user')) {
+ throw new ApplicationException('Error evaluating the user attribute condition: the user object is not found in the condition parameters.');
+ }
+
+ return parent::evalIsTrue($user);
+ }
+}
+```
+
+An attributes definition file is used to specify which attributes should be included in the condition.
+
+```yaml
+# ===================================
+# Condition Attribute Definitions
+# ===================================
+
+attributes:
+
+ name:
+ label: Name
+
+ email:
+ label: Email address
+
+ country:
+ label: Country
+ type: relation
+ relation:
+ model: RainLab\Location\Models\Country
+ label: Name
+ nameFrom: name
+ keyFrom: id
+```
+
+## Save to database action
+
+There is a dedicated table in the database for storing events and their parameters. This table is accessed using the `RainLab\Notify\Models\Notification` model and can be referenced as a relation from your own models. In this example the `MyProject` model contains its own notification channel called `notifications`.
+
+```php
+class MyProject extends Model
+{
+ // ...
+
+ public $morphMany = [
+ 'my_notifications' => [
+ \RainLab\Notify\Models\Notification::class,
+ 'name' => 'notifiable'
+ ]
+ ];
+}
+```
+
+This channel should be registered with the `RainLab\Notify\NotifyRules\SaveDatabaseAction` so it appears as a related object when selecting the action.
+
+```php
+SaveDatabaseAction::extend(function ($action) {
+ $action->addTableDefinition([
+ 'label' => 'Project activity',
+ 'class' => MyProject::class,
+ 'relation' => 'my_notifications',
+ 'param' => 'project'
+ ]);
+});
+```
+
+The **label** is shown as the related object, the **class** references the model class, the **relation** refers to the relation name. The **param** defines the parameter name, passed to the triggering event.
+
+So essentially if you pass a `project` to the event parameters, or if `project` is a global parameter, a notification model is created with the parameters stored in the `data` attribute. Equivalent to the following code:
+
+```php
+$myproject->my_notifications()->create([
+ // ...
+ 'data' => $params
+]);
+```
+
+## Dynamically adding conditions to events
+
+Events can be extended to include new local conditions. Simply add the condition class to the event `$conditions` array property.
+
+```php
+UserActivatedEvent::extend(function($event) {
+ $event->conditions[] = \RainLab\UserPlus\NotifyRules\UserLocationAttributeCondition::class;
+});
+```
diff --git a/plugins/rainlab/notify/classes/ActionBase.php b/plugins/rainlab/notify/classes/ActionBase.php
new file mode 100644
index 0000000..c64b09e
--- /dev/null
+++ b/plugins/rainlab/notify/classes/ActionBase.php
@@ -0,0 +1,168 @@
+ 'Action',
+ 'description' => 'Action Description',
+ 'icon' => 'icon-dot-circle-o'
+ ];
+ }
+
+ public function __construct($host = null)
+ {
+ /*
+ * Paths
+ */
+ $this->viewPath = $this->configPath = $this->guessConfigPathFrom($this);
+
+ /*
+ * Parse the config, if available
+ */
+ if ($formFields = $this->defineFormFields()) {
+ $this->fieldConfig = $this->makeConfig($formFields);
+ }
+
+ if (!$this->host = $host) {
+ return;
+ }
+
+ $this->boot($host);
+ }
+
+ /**
+ * Boot method called when the condition class is first loaded
+ * with an existing model.
+ * @return array
+ */
+ public function boot($host)
+ {
+ // Set default data
+ if (!$host->exists) {
+ $this->initConfigData($host);
+ }
+
+ // Apply validation rules
+ $host->rules = array_merge($host->rules, $this->defineValidationRules());
+ }
+
+ public function triggerAction($params)
+ {
+ }
+
+ public function getTitle()
+ {
+ return $this->getActionName();
+ }
+
+ public function getText()
+ {
+ return $this->getActionDescription();
+ }
+
+ public function getActionName()
+ {
+ return array_get($this->actionDetails(), 'name');
+ }
+
+ public function getActionDescription()
+ {
+ return array_get($this->actionDetails(), 'description');
+ }
+
+ public function getActionIcon()
+ {
+ return array_get($this->actionDetails(), 'icon', 'icon-dot-circle-o');
+ }
+
+ /**
+ * Extra field configuration for the condition.
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ /**
+ * Determines if this action uses form fields.
+ * @return bool
+ */
+ public function hasFieldConfig()
+ {
+ return !!$this->fieldConfig;
+ }
+
+ /**
+ * Returns the field configuration used by this model.
+ */
+ public function getFieldConfig()
+ {
+ return $this->fieldConfig;
+ }
+
+ /**
+ * Initializes configuration data when the condition is first created.
+ * @param Model $host
+ */
+ public function initConfigData($host) {}
+
+ /**
+ * Defines validation rules for the custom fields.
+ * @return array
+ */
+ public function defineValidationRules()
+ {
+ return [];
+ }
+
+ /**
+ * Spins over types registered in plugin base class with `registerNotificationRules`.
+ * @return array
+ */
+ public static function findActions()
+ {
+ $results = [];
+ $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
+
+ foreach ($bundles as $plugin => $bundle) {
+ foreach ((array) array_get($bundle, 'actions', []) as $conditionClass) {
+ if (!class_exists($conditionClass)) {
+ continue;
+ }
+
+ $obj = new $conditionClass;
+ $results[$conditionClass] = $obj;
+ }
+ }
+
+ return $results;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/CompoundCondition.php b/plugins/rainlab/notify/classes/CompoundCondition.php
new file mode 100644
index 0000000..56c7a8b
--- /dev/null
+++ b/plugins/rainlab/notify/classes/CompoundCondition.php
@@ -0,0 +1,178 @@
+host->condition_type == 0
+ ? __('ALL of subconditions should be') . ' '
+ : __('ANY of subconditions should be') . ' ';
+
+ $result .= $this->host->condition == 'false' ? 'FALSE' : 'TRUE';
+
+ return $result;
+ }
+
+ /**
+ * getJoinText returns the text to use when joining two rules within.
+ * @return string
+ */
+ public function getJoinText()
+ {
+ return $this->host->condition_type == 0 ? 'AND' : 'OR';
+ }
+
+ /**
+ * Returns a list of condition types (`ConditionBase::TYPE_*` constants)
+ * that can be added to this compound condition
+ */
+ public function getAllowedSubtypes()
+ {
+ return [];
+ }
+
+ /**
+ * defineFormFields
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ public function initConfigData($host)
+ {
+ $host->condition_type = 0;
+ $host->condition = 'true';
+ }
+
+ public function getConditionOptions()
+ {
+ $options = [
+ 'true' => 'TRUE',
+ 'false' => 'FALSE'
+ ];
+
+ return $options;
+ }
+
+ public function getConditionTypeOptions()
+ {
+ $options = [
+ '0' => 'ALL subconditions should meet the requirement',
+ '1' => 'ANY subconditions should meet the requirement'
+ ];
+
+ return $options;
+ }
+
+ /**
+ * getChildOptions
+ */
+ public function getChildOptions(array $options)
+ {
+ extract(array_merge([
+ 'extraRules' => [],
+ ], $options));
+
+ $result = [
+ 'Compound condition' => CompoundCondition::class
+ ];
+
+ $classes = $extraRules + self::findConditionsByType(ConditionBase::TYPE_ANY);
+
+ $result = $this->addClassesSubconditions($classes, $result);
+
+ return $result;
+ }
+
+ protected function addClassesSubconditions($classes, $list)
+ {
+ foreach ($classes as $conditionClass => $obj) {
+
+ $subConditions = $obj->listSubconditions();
+
+ if ($subConditions) {
+ $groupName = $obj->getGroupingTitle();
+
+ foreach ($subConditions as $name => $subcondition) {
+ if (!$groupName) {
+ $list[$name] = $conditionClass.':'.$subcondition;
+ }
+ else {
+ if (!array_key_exists($groupName, $list)) {
+ $list[$groupName] = [];
+ }
+
+ $list[$groupName][$name] = $conditionClass.':'.$subcondition;
+ }
+ }
+ }
+ else {
+ $list[$obj->getName()] = $conditionClass;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Checks whether the condition is TRUE for specified parameters.
+ *
+ * @param array $params
+ * @return bool
+ */
+ public function isTrue(&$params)
+ {
+ $hostObj = $this->host;
+
+ $requiredConditionValue = $hostObj->condition == 'true' ? true : false;
+
+ foreach ($hostObj->children as $subcondition) {
+ $subconditionResult = $subcondition->getConditionObject()->isTrue($params) ? true : false;
+
+ /*
+ * All
+ */
+ if ($hostObj->condition_type == 0) {
+ if ($subconditionResult !== $requiredConditionValue) {
+ return false;
+ }
+
+ }
+ /*
+ * Any
+ */
+ else {
+ if ($subconditionResult === $requiredConditionValue) {
+ return true;
+ }
+ }
+ }
+
+ /*
+ * All
+ */
+ if ($hostObj->condition_type == 0) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/ConditionBase.php b/plugins/rainlab/notify/classes/ConditionBase.php
new file mode 100644
index 0000000..81cfaaf
--- /dev/null
+++ b/plugins/rainlab/notify/classes/ConditionBase.php
@@ -0,0 +1,211 @@
+viewPath = $this->configPath = $this->guessConfigPathFrom($this);
+
+ /*
+ * Parse the config
+ */
+ $this->fieldConfig = $this->makeConfig($this->defineFormFields());
+
+ if (!$this->host = $host) {
+ return;
+ }
+
+ $this->boot($host);
+ }
+
+ /**
+ * Boot method called when the condition class is first loaded
+ * with an existing model.
+ * @return array
+ */
+ public function boot($host)
+ {
+ // Set default data
+ if (!$host->exists) {
+ $this->initConfigData($host);
+ }
+
+ // Apply validation rules
+ $host->rules = array_merge($host->rules, $this->defineValidationRules());
+
+ // Inject view paths to the controller through the Form widget
+ $host->bindEvent('model.form.filterFields', function($form) {
+ $form->getController()->addViewPath($this->getViewPaths());
+ });
+ }
+
+ /**
+ * Extra field configuration for the condition.
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ /**
+ * Initializes configuration data when the condition is first created.
+ * @param Model $host
+ */
+ public function initConfigData($host) {}
+
+ /**
+ * Defines validation rules for the custom fields.
+ * @return array
+ */
+ public function defineValidationRules()
+ {
+ return [];
+ }
+
+ public function getText()
+ {
+ return 'Condition Text';
+ }
+
+ /**
+ * Returns a condition name for displaying in the condition selection drop-down menu
+ */
+ public function getName()
+ {
+ return 'Condition';
+ }
+
+ /**
+ * Returns a condition title for displaying in the condition settings form
+ */
+ public function getTitle()
+ {
+ return 'Condition';
+ }
+
+ /**
+ * This function should return one of the `ConditionBase::TYPE_*` constants
+ * depending on a place where the condition is valid
+ */
+ public function getConditionType()
+ {
+ return ConditionBase::TYPE_ANY;
+ }
+
+ public function listSubconditions()
+ {
+ return [];
+ }
+
+ /**
+ * Returns a title to use for grouping subconditions
+ * in the Create Condition drop-down menu
+ */
+ public function getGroupingTitle()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the field configuration used by this model.
+ */
+ public function getFieldConfig()
+ {
+ return $this->fieldConfig;
+ }
+
+ /**
+ * Spins over types registered in plugin base class with `registerNotificationRules`,
+ * checks if the condition type matches and adds it to an array that is returned.
+ *
+ * @param string $type Use `self::TYPE_*` constants
+ * @return array
+ */
+ public static function findConditionsByType($type)
+ {
+ $results = [];
+ $bundles = PluginManager::instance()->getRegistrationMethodValues(static::$registrationMethod);
+
+ foreach ($bundles as $plugin => $bundle) {
+ foreach ((array) array_get($bundle, 'conditions', []) as $conditionClass) {
+ if (!class_exists($conditionClass)) {
+ continue;
+ }
+
+ $obj = new $conditionClass;
+ if ($obj->getConditionType() != $type) {
+ continue;
+ }
+
+ $results[$conditionClass] = $obj;
+ }
+ }
+
+ return $results;
+ }
+
+ public function setFormFields($fields)
+ {
+
+ }
+
+ public function setCustomData()
+ {
+ }
+
+ public function onPreRender($controller, $widget)
+ {
+ }
+
+ /**
+ * Checks whether the condition is TRUE for specified parameters
+ * @param array $params
+ * @return bool
+ */
+ public function isTrue(&$params)
+ {
+ return false;
+ }
+
+ public function getChildOptions(array $options)
+ {
+ return [];
+ }
+}
diff --git a/plugins/rainlab/notify/classes/EventBase.php b/plugins/rainlab/notify/classes/EventBase.php
new file mode 100644
index 0000000..db6241e
--- /dev/null
+++ b/plugins/rainlab/notify/classes/EventBase.php
@@ -0,0 +1,237 @@
+ 'Event',
+ 'description' => 'Event description',
+ 'group' => 'groupcode'
+ ];
+ }
+
+ public function __construct($host = null)
+ {
+ $this->host = $host;
+ }
+
+ /**
+ * Defines the parameters used by this class.
+ * This method should be used as an override in the extended class.
+ */
+ public function defineParams()
+ {
+ return [];
+ }
+
+ /**
+ * Local conditions supported by this event.
+ */
+ public function defineConditions()
+ {
+ return $this->conditions;
+ }
+
+ /**
+ * Sets multiple params.
+ * @param array $params
+ * @return void
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * Returns all params.
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Generates event parameters based on arguments from the triggering system event.
+ * @param array $args
+ * @param string $eventName
+ * @return void
+ */
+ public static function makeParamsFromEvent(array $args, $eventName = null)
+ {
+ }
+
+ public function getEventName()
+ {
+ return Lang::get(array_get($this->eventDetails(), 'name'));
+ }
+
+ public function getEventDescription()
+ {
+ return Lang::get(array_get($this->eventDetails(), 'description'));
+ }
+
+ public function getEventGroup()
+ {
+ return array_get($this->eventDetails(), 'group');
+ }
+
+ /**
+ * Resolves an event or action identifier from a class name or object.
+ * @param mixed Class name or object
+ * @return string Identifier in format of vendor-plugin-class
+ */
+ public function getEventIdentifier()
+ {
+ $namespace = Str::normalizeClassName(get_called_class());
+ if (strpos($namespace, '\\') === null) {
+ return $namespace;
+ }
+
+ $parts = explode('\\', $namespace);
+ $class = array_pop($parts);
+ $slice = array_slice($parts, 1, 2);
+ $code = strtolower(implode('-', $slice) . '-' . $class);
+
+ return $code;
+ }
+
+ /**
+ * Spins over types registered in plugin base class with `registerNotificationRules`.
+ * @return array
+ */
+ public static function findEvents()
+ {
+ $results = [];
+ $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
+
+ foreach ($bundles as $plugin => $bundle) {
+ foreach ((array) array_get($bundle, 'events', []) as $conditionClass) {
+ if (!class_exists($conditionClass)) {
+ continue;
+ }
+
+ $obj = new $conditionClass;
+ $results[$conditionClass] = $obj;
+ }
+ }
+
+ return $results;
+ }
+
+ public static function findEventGroups()
+ {
+ $results = [];
+ $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
+
+ foreach ($bundles as $plugin => $bundle) {
+ if ($groups = array_get($bundle, 'groups')) {
+ $results += $groups;
+ }
+ }
+
+ return $results;
+ }
+
+ public static function findEventsByGroup($group)
+ {
+ $results = [];
+
+ foreach (self::findEvents() as $conditionClass => $obj) {
+ if ($obj->getEventGroup() != $group) {
+ continue;
+ }
+
+ $results[$conditionClass] = $obj;
+ }
+
+ return $results;
+ }
+
+ public static function findEventByIdentifier($identifier)
+ {
+ foreach (self::findEvents() as $class => $obj) {
+ if ($obj->getEventIdentifier() == $identifier) {
+ return $obj;
+ }
+ }
+ }
+
+ /**
+ * Spins over preset registered in plugin base class with `registerNotificationRules`.
+ * @return array
+ */
+ public static function findEventPresets()
+ {
+ $results = [];
+ $bundles = PluginManager::instance()->getRegistrationMethodValues('registerNotificationRules');
+
+ foreach ($bundles as $plugin => $bundle) {
+ if (!$presets = array_get($bundle, 'presets')) {
+ continue;
+ }
+
+ if (!is_array($presets)) {
+ $presets = Yaml::parse(File::get(File::symbolizePath($presets)));
+ }
+
+ if ($presets && is_array($presets)) {
+ $results += $presets;
+ }
+ }
+
+ return $results;
+ }
+
+ public static function findEventPresetsByClass($className)
+ {
+ $results = [];
+
+ foreach (self::findEventPresets() as $code => $definition) {
+ if (!$eventClass = array_get($definition, 'event')) {
+ continue;
+ }
+
+ if ($eventClass != $className) {
+ continue;
+ }
+
+ $results[$code] = $definition;
+ }
+
+ return $results;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/EventParams.php b/plugins/rainlab/notify/classes/EventParams.php
new file mode 100644
index 0000000..cf17577
--- /dev/null
+++ b/plugins/rainlab/notify/classes/EventParams.php
@@ -0,0 +1,59 @@
+eventClass = $eventClass;
+
+ $this->params = $this->serializeParams($params);
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $this->delete();
+
+ Notifier::instance()->fireEvent($this->eventClass, $this->unserializeParams());
+ }
+
+ protected function serializeParams($params)
+ {
+ $result = [];
+
+ foreach ($params as $param => $value) {
+ $result[$param] = $this->getSerializedPropertyValue($value);
+ }
+
+ return $result;
+ }
+
+ protected function unserializeParams()
+ {
+ $result = [];
+
+ foreach ($this->params as $param => $value) {
+ $result[$param] = $this->getRestoredPropertyValue($value);
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php b/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php
new file mode 100644
index 0000000..b6183a6
--- /dev/null
+++ b/plugins/rainlab/notify/classes/ModelAttributesConditionBase.php
@@ -0,0 +1,743 @@
+ 'is',
+ 'is_not' => 'is not',
+ 'equals_or_greater' => 'equals or greater than',
+ 'equals_or_less' => 'equals or less than',
+ 'contains' => 'contains',
+ 'does_not_contain' => 'does not contain',
+ 'greater' => 'greater than',
+ 'less' => 'less than',
+ 'one_of' => 'is one of',
+ 'not_one_of' => 'is not one of'
+ ];
+
+ protected $modelObj = null;
+ protected $referenceInfo = null;
+ protected $modelAttributes = null;
+
+ protected static $modelObjCache = [];
+ protected static $attributeControlTypeCache = [];
+
+ public function __construct($host = null)
+ {
+ parent::__construct($host);
+
+ /*
+ * This is used as a base class, so register view path from here too
+ */
+ $this->addViewPath($this->guessViewPathFrom(__CLASS__));
+ }
+
+ public function initConfigData($host)
+ {
+ $host->operator = 'is';
+ }
+
+ public function setCustomData()
+ {
+ $this->host->condition_control_type = $this->evalControlType();
+ }
+
+ //
+ // Definitions
+ //
+
+ /**
+ * This function should return one of the `ConditionBase::TYPE_*` constants
+ * depending on a place where the condition is valid
+ */
+ public function getConditionType()
+ {
+ return ConditionBase::TYPE_LOCAL;
+ }
+
+ public function defineModelAttributes($type = null)
+ {
+ return 'attributes.yaml';
+ }
+
+ public function defineValidationRules()
+ {
+ return [
+ 'value' => 'required'
+ ];
+ }
+
+ public function defineFormFields()
+ {
+ return plugins_path('rainlab/notify/classes/modelattributesconditionbase/fields.yaml');
+ }
+
+ //
+ // Text helpers
+ //
+
+ public function getText()
+ {
+ $host = $this->host;
+ $attributes = $this->listModelAttributes();
+
+ if (isset($attributes[$host->subcondition])) {
+ $result = $this->getConditionTextPrefix($host, $attributes);
+ }
+ else {
+ $result = __('Unknown Attribute');
+ }
+
+ $result .= ' '.array_get($this->operators, $host->operator, $host->operator).' ';
+
+ $controlType = $this->getValueControlType();
+
+ if ($controlType == 'text') {
+ $result .= $host->value;
+ }
+ else {
+ $textValue = $this->getCustomTextValue();
+ if ($textValue !== false) {
+ return $result.' '.$textValue;
+ }
+
+ $referenceInfo = $this->prepareReferenceListInfo();
+ $modelObj = $referenceInfo->referenceModel;
+
+ if (!count($referenceInfo->columns)) {
+ return $result;
+ }
+
+ if (!strlen($host->value)) {
+ return $result .= '?';
+ }
+
+ $visibleField = $referenceInfo->primaryColumn;
+
+ if ($controlType == 'dropdown') {
+ $obj = $modelObj->where('id', $host->value)->first();
+ if ($obj) {
+ $result .= e($obj->{$visibleField});
+ }
+ }
+ else {
+ $ids = explode(',', $host->value);
+ foreach ($ids as &$id) {
+ $id = trim(e($id));
+ }
+
+ $records = $modelObj
+ ->whereIn('id', $ids)
+ ->orderBy($visibleField)
+ ->get();
+
+ $recordNames = [];
+ foreach ($records as $record) {
+ $recordNames[] = $record->{$visibleField};
+ }
+
+ $result .= '('.implode(', ', $recordNames).')';
+ }
+ }
+
+ return $result;
+ }
+
+ protected function getConditionTextPrefix($parametersHost, $attributes)
+ {
+ return $attributes[$parametersHost->subcondition];
+ }
+
+ public function getCustomTextValue()
+ {
+ return false;
+ }
+
+ //
+ // Options
+ //
+
+ /**
+ * getSubconditionOptions
+ */
+ public function getSubconditionOptions()
+ {
+ return $this->listModelAttributes();
+ }
+
+ /**
+ * getOperatorOptions
+ */
+ public function getOperatorOptions()
+ {
+ $hostObj = $this->host;
+ $options = [];
+ $attribute = $hostObj->subcondition;
+
+ $currentOperatorValue = $hostObj->operator;
+
+ $model = $this->getModelObj();
+ $definitions = $this->listModelAttributeInfo();
+
+ if (!isset($definitions[$attribute])) {
+ $options = ['none' => 'Unknown attribute selected'];
+ }
+ else {
+ $columnType = array_get($definitions[$attribute], 'type');
+
+ if ($columnType != ConditionBase::CAST_RELATION) {
+ if ($columnType == ConditionBase::CAST_STRING) {
+ $options = [
+ 'is' => 'is',
+ 'is_not' => 'is not',
+ 'contains' => 'contains',
+ 'does_not_contain' => 'does not contain'
+ ];
+ }
+ else {
+ $options = [
+ 'is' => 'is',
+ 'is_not' => 'is not',
+ 'equals_or_greater' => 'equals or greater than',
+ 'equals_or_less' => 'equals or less than',
+ 'greater' => 'greater than',
+ 'less' => 'less than'
+ ];
+ }
+ }
+ else {
+ $options = [
+ 'is' => 'is',
+ 'is_not' => 'is not',
+ 'one_of' => 'is one of',
+ 'not_one_of' => 'is not one of'
+ ];
+ }
+ }
+
+ if (!array_key_exists($currentOperatorValue, $options)) {
+ $keys = array_keys($options);
+ if (count($keys)) {
+ $hostObj->operator = $options[$keys[0]];
+ }
+ else {
+ $hostObj->operator = null;
+ }
+ }
+
+ return $options;
+ }
+
+ public function getValueDropdownOptions()
+ {
+ $hostObj = $this->host;
+ $attribute = $hostObj->subcondition;
+ $definitions = $this->listModelAttributeInfo();
+
+ if (!isset($definitions[$attribute])) {
+ return [];
+ }
+
+ $columnType = array_get($definitions[$attribute], 'type');
+
+ if ($columnType != ConditionBase::CAST_RELATION) {
+ return [];
+ }
+
+ $referenceInfo = $this->prepareReferenceListInfo();
+ $referenceModel = $referenceInfo->referenceModel;
+ $nameFrom = $referenceInfo->primaryColumn;
+ $keyFrom = $referenceInfo->primaryKey;
+
+ // Determine if the model uses a tree trait
+ $treeTraits = [
+ \October\Rain\Database\Traits\NestedTree::class,
+ \October\Rain\Database\Traits\SimpleTree::class
+ ];
+ $usesTree = count(array_intersect($treeTraits, class_uses($referenceModel))) > 0;
+
+ $results = $referenceModel->get();
+
+ return $usesTree
+ ? $results->listsNested($nameFrom, $keyFrom)
+ : $results->lists($nameFrom, $keyFrom);
+ }
+
+ //
+ // Control type
+ //
+
+ protected function evalControlType()
+ {
+ $hostObj = $this->host;
+ $attribute = $hostObj->subcondition;
+ $operator = $hostObj->operator;
+
+ $definitions = $this->listModelAttributeInfo();
+
+ if (!isset($definitions[$attribute])) {
+ return 'text';
+ }
+
+ $columnType = array_get($definitions[$attribute], 'type');
+
+ if ($columnType != ConditionBase::CAST_RELATION) {
+ return 'text';
+ }
+ else {
+ if ($operator == 'is' || $operator == 'is_not') {
+ return 'dropdown';
+ }
+
+ return 'multi_value';
+ }
+ }
+
+ public function getValueControlType()
+ {
+ if (App::runningInBackend()) {
+ return $this->evalControlType();
+ }
+
+ $hostObj = $this->host;
+
+ if ($controlType = $hostObj->condition_control_type) {
+ return $controlType;
+ }
+
+ $controlType = $this->evalControlType();
+
+ $this->getModelObj()
+ ->where('id', $hostObj->id)
+ ->update(['condition_control_type' => $controlType]);
+
+ return $hostObj->condition_control_type = $controlType;
+ }
+
+ //
+ // Attributes
+ //
+
+ public function listSubconditions()
+ {
+ $attributes = $this->listModelAttributes();
+
+ $result = [];
+
+ foreach ($attributes as $name => $code) {
+ $result[$code] = $name;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the supported attributes by a condition as an array.
+ * The key is the attribute and the value is the label.
+ *
+ * @return array
+ */
+ protected function listModelAttributes()
+ {
+ $attributeInfo = $this->listModelAttributeInfo();
+
+ foreach ($attributeInfo as $attribute => $info) {
+ $attributes[$attribute] = array_get($info, 'label');
+ }
+
+ asort($attributes);
+
+ return $attributes;
+ }
+
+ protected function listModelAttributeInfo()
+ {
+ if ($this->modelAttributes) {
+ return $this->modelAttributes;
+ }
+
+ $config = $this->makeConfig($this->defineModelAttributes($this->getConditionType()));
+
+ $attributes = $config->attributes ?? [];
+
+ /*
+ * Set defaults
+ */
+ foreach ($attributes as $attribute => $info) {
+ if (!isset($info['type'])) {
+ $attributes[$attribute]['type'] = 'string';
+ }
+ }
+
+ return $this->modelAttributes = $attributes;
+ }
+
+ //
+ // Relation based attributes
+ //
+
+ public function getReferencePrimaryColumn($record)
+ {
+ $referenceInfo = $this->prepareReferenceListInfo();
+
+ return $record->{$referenceInfo->primaryColumn};
+ }
+
+ public function listSelectedReferenceRecords()
+ {
+ $referenceInfo = $this->prepareReferenceListInfo();
+ $model = $referenceInfo->referenceModel;
+
+ $value = $this->host->value;
+ $keys = strlen($value) ? explode(',', $value) : [];
+
+ if (count($keys)) {
+ $model = $model->whereIn('id', $keys);
+ }
+ else {
+ $model = $model->whereRaw('id <> id');
+ }
+
+ $orderField = $referenceInfo->primaryColumn;
+
+ return $model->orderBy($orderField)->get();
+ }
+
+ public function prepareReferenceListInfo()
+ {
+ if (!is_null($this->referenceInfo)) {
+ return $this->referenceInfo;
+ }
+
+ $model = $this->getModelObj();
+ $attribute = $this->host->subcondition;
+ $definitions = $this->listModelAttributeInfo();
+ $definition = array_get($definitions, $attribute);
+
+ $columns = array_get($definition, 'columns');
+ $primaryColumn = array_get($definition, 'relation.nameFrom', 'name');
+
+ if ($model->hasRelation($attribute)) {
+ $relationType = $model->getRelationType($attribute);
+ $relationModel = $model->makeRelation($attribute);
+ $relationObject = $model->{$attribute}();
+
+ // Some simpler relations can specify a custom local or foreign "other" key,
+ // which can be detected and implemented here automagically.
+ $primaryKey = in_array($relationType, ['hasMany', 'belongsTo', 'hasOne'])
+ ? $relationObject->getOtherKey()
+ : $relationModel->getKeyName();
+ }
+ elseif ($relationClass = array_get($definition, 'relation.model')) {
+ $relationModel = new $relationClass;
+ $primaryKey = array_get($definition, 'relation.keyFrom', 'id');
+ }
+ else {
+ throw new SystemException(sprintf('Model %s does not contain a relation "%s"', get_class($model), $attribute));
+ }
+
+ if (!$columns) {
+ $columns = [
+ $primaryColumn => [
+ 'label' => array_get($definition, 'relation.label', '?'),
+ 'searchable' => true
+ ]
+ ];
+ }
+
+ $this->referenceInfo = [];
+ $this->referenceInfo['referenceModel'] = $relationModel;
+ $this->referenceInfo['primaryKey'] = $primaryKey;
+ $this->referenceInfo['primaryColumn'] = $primaryColumn;
+ $this->referenceInfo['columns'] = $columns;
+
+ return $this->referenceInfo = (object) $this->referenceInfo;
+ }
+
+ public function onPreRender($controller, $widget)
+ {
+ $controlType = $this->getValueControlType();
+
+ if ($controlType != 'multi_value') {
+ return;
+ }
+
+ $selectionColumn = [
+ '_select_record' => [
+ 'label' => '',
+ 'sortable' => false,
+ 'type' => 'partial',
+ 'width' => '10px',
+ 'path' => plugins_path('rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm')
+ ]
+ ];
+
+ $referenceInfo = $this->prepareReferenceListInfo();
+ $filterModel = $referenceInfo->referenceModel;
+ $filterColumns = $selectionColumn + $referenceInfo->columns;
+
+ /*
+ * List widget
+ */
+ $config = $this->makeConfig();
+ $config->columns = $filterColumns;
+ $config->model = $filterModel;
+ $config->alias = $widget->alias . 'List';
+ $config->showSetup = false;
+ $config->showCheckboxes = false;
+ $config->recordsPerPage = 6;
+ $listWidget = $controller->makeWidget('Backend\Widgets\Lists', $config);
+
+ /*
+ * Search widget
+ */
+ $config = $this->makeConfig();
+ $config->alias = $widget->alias . 'Search';
+ $config->growable = false;
+ $config->prompt = 'backend::lang.list.search_prompt';
+ $searchWidget = $controller->makeWidget('Backend\Widgets\Search', $config);
+ $searchWidget->cssClasses[] = 'condition-filter-search';
+
+ $listWidget->bindToController();
+ $searchWidget->bindToController();
+
+ /*
+ * Extend list query
+ */
+ $listWidget->bindEvent('list.extendQueryBefore', function ($query) use ($filterModel) {
+ $this->prepareFilterQuery($query, $filterModel);
+ });
+
+ /*
+ * Link the Search Widget to the List Widget
+ */
+ $listWidget->setSearchTerm($searchWidget->getActiveTerm());
+
+ $searchWidget->bindEvent('search.submit', function () use (&$searchWidget, $listWidget) {
+ $listWidget->setSearchTerm($searchWidget->getActiveTerm());
+ return $listWidget->onRefresh();
+ });
+
+ $controller->vars['listWidget'] = $listWidget;
+ $controller->vars['searchWidget'] = $searchWidget;
+ $controller->vars['filterHostModel'] = $this->host;
+ }
+
+ public function prepareFilterQuery($query, $model)
+ {
+ }
+
+ //
+ // Condition check
+ //
+
+ /**
+ * Checks whether the condition is TRUE for a specified model
+ * @return bool
+ */
+ public function evalIsTrue($model, $customValue = '__no_eval_value__')
+ {
+ $hostObj = $this->host;
+
+ $operator = $hostObj->operator;
+ $attribute = $hostObj->subcondition;
+
+ $conditionValue = $hostObj->value;
+ $conditionValue = trim(mb_strtolower($conditionValue));
+
+ $controlType = $this->getValueControlType();
+
+ if ($controlType == 'text') {
+ if ($customValue === self::NO_EVAL_VALUE) {
+ $modelValue = trim(mb_strtolower($model->{$attribute}));
+ }
+ else {
+ $modelValue = trim(mb_strtolower($customValue));
+ }
+
+ if ($operator == 'is') {
+ return $modelValue == $conditionValue;
+ }
+
+ if ($operator == 'is_not') {
+ return $modelValue != $conditionValue;
+ }
+
+ if ($operator == 'contains') {
+ return mb_strpos($modelValue, $conditionValue) !== false;
+ }
+
+ if ($operator == 'does_not_contain') {
+ return mb_strpos($modelValue, $conditionValue) === false;
+ }
+
+ if ($operator == 'equals_or_greater') {
+ return $modelValue >= $conditionValue;
+ }
+
+ if ($operator == 'equals_or_less') {
+ return $modelValue <= $conditionValue;
+ }
+
+ if ($operator == 'greater') {
+ return $modelValue > $conditionValue;
+ }
+
+ if ($operator == 'less') {
+ return $modelValue < $conditionValue;
+ }
+ }
+
+ if ($controlType == 'dropdown') {
+ if ($customValue === self::NO_EVAL_VALUE) {
+ $modelValue = $model->{$attribute};
+ }
+ else {
+ $modelValue = $customValue;
+ }
+
+ if ($operator == 'is') {
+ if ($modelValue == null) {
+ return false;
+ }
+
+ if ($modelValue instanceof EloquentModel) {
+ return $modelValue->getKey() == $conditionValue;
+ }
+
+ if (
+ is_array($modelValue) &&
+ count($modelValue) == 1 &&
+ array_key_exists(0, $modelValue)
+ ) {
+ return $modelValue[0] == $conditionValue;
+ }
+
+ if ($modelValue instanceof EloquentCollection) {
+ if ($modelValue->count() != 1) {
+ return false;
+ }
+
+ return $modelValue[0]->getKey() == $conditionValue;
+ }
+ }
+
+ if ($operator == 'is_not') {
+ if ($modelValue == null) {
+ return true;
+ }
+
+ if ($modelValue instanceof EloquentModel) {
+ return $modelValue->getKey() != $conditionValue;
+ }
+
+ if (is_array($modelValue)) {
+ if (count($modelValue) != 1) {
+ return true;
+ }
+
+ if (!array_key_exists(0, $modelValue)) {
+ return true;
+ }
+
+ return $modelValue[0] != $conditionValue;
+ }
+
+ if ($modelValue instanceof EloquentCollection) {
+ if (!$modelValue->count() || $modelValue->count() > 1) {
+ return true;
+ }
+
+ return $modelValue->first()->getKey() != $conditionValue;
+ }
+ }
+ }
+
+ if ($controlType == 'multi_value') {
+ if ($customValue === self::NO_EVAL_VALUE) {
+ $modelValue = $model->{$attribute};
+ }
+ else {
+ $modelValue = $customValue;
+ }
+
+ if (
+ (!$modelValue instanceof EloquentCollection) &&
+ (!$modelValue instanceof EloquentModel) &&
+ !is_array($modelValue)
+ ) {
+ return false;
+ }
+
+ if (strlen($conditionValue)) {
+ $conditionValues = explode(',', $conditionValue);
+ foreach ($conditionValues as &$value) {
+ $value = trim($value);
+ }
+ } else {
+ $conditionValues = [];
+ }
+
+ if ($modelValue instanceof EloquentCollection) {
+ $modelKeys = array_keys($modelValue->lists('id', 'id'));
+ }
+ elseif ($modelValue instanceof EloquentModel) {
+ $modelKeys = [$modelValue->getKey()];
+ }
+ else {
+ $modelKeys = $modelValue;
+ }
+
+ if ($operator == 'is') {
+ $operator = 'one_of';
+ }
+ elseif ($operator == 'is_not') {
+ $operator = 'not_one_of';
+ }
+
+ if ($operator == 'one_of') {
+ return count(array_intersect($conditionValues, $modelKeys)) ? true : false;
+ }
+
+ if ($operator == 'not_one_of') {
+ return count(array_intersect($conditionValues, $modelKeys)) ? false : true;
+ }
+ }
+
+ return false;
+ }
+
+ //
+ // Helpers
+ //
+
+ public function getModelObj()
+ {
+ if ($this->modelObj === null) {
+ if (array_key_exists($this->modelClass, self::$modelObjCache)) {
+ $this->modelObj = self::$modelObjCache[$this->modelClass];
+ }
+ else {
+ $this->modelObj = self::$modelObjCache[$this->modelClass] = new $this->modelClass;
+ }
+ }
+
+ return $this->modelObj;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/Notifier.php b/plugins/rainlab/notify/classes/Notifier.php
new file mode 100644
index 0000000..de0f520
--- /dev/null
+++ b/plugins/rainlab/notify/classes/Notifier.php
@@ -0,0 +1,134 @@
+registerGlobalParams([...]);
+ * });
+ *
+ * @param callable $callback A callable function.
+ */
+ public function registerCallback(callable $callback)
+ {
+ $this->callbacks[] = $callback;
+ }
+
+ /**
+ * Helper to process callbacks once and once only.
+ * @return void
+ */
+ protected function processCallbacks()
+ {
+ if ($this->registered) {
+ return;
+ }
+
+ foreach ($this->callbacks as $callback) {
+ $callback($this);
+ }
+
+ $this->registered = true;
+ }
+
+ //
+ // Event binding
+ //
+
+ public static function bindEvents(array $events)
+ {
+ foreach ($events as $event => $class) {
+ self::bindEvent($event, $class);
+ }
+ }
+
+ public static function bindEvent($systemEventName, $notifyEventClass)
+ {
+ Event::listen($systemEventName, function() use ($notifyEventClass, $systemEventName) {
+ $params = $notifyEventClass::makeParamsFromEvent(func_get_args(), $systemEventName);
+
+ self::instance()->queueEvent($notifyEventClass, $params);
+ });
+ }
+
+ public function queueEvent($eventClass, array $params)
+ {
+ $params += $this->getContextVars();
+
+ // Use queue
+ if (true) {
+ Queue::push(new EventParams($eventClass, $params));
+ }
+ else {
+ $this->fireEvent($eventClass, $params);
+ }
+ }
+
+ public function fireEvent($eventClass, array $params)
+ {
+ $models = NotificationRuleModel::listRulesForEvent($eventClass);
+
+ foreach ($models as $model) {
+ $model->setParams($params);
+ $model->triggerRule();
+ }
+ }
+
+ public function registerGlobalParams(array $params)
+ {
+ if (!$this->registeredGlobalParams) {
+ $this->registeredGlobalParams = [];
+ }
+
+ $this->registeredGlobalParams = $params + $this->registeredGlobalParams;
+ }
+
+ public function getContextVars()
+ {
+ $this->processCallbacks();
+
+ $globals = $this->registeredGlobalParams ?: [];
+
+ return [
+ 'isBackend' => App::runningInBackend() ? 1 : 0,
+ 'isConsole' => App::runningInConsole() ? 1 : 0,
+ 'appLocale' => App::getLocale(),
+ 'sender' => null // unsafe:BackendAuth::getUser()
+ ] + $globals;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/ScheduledAction.php b/plugins/rainlab/notify/classes/ScheduledAction.php
new file mode 100644
index 0000000..bfde132
--- /dev/null
+++ b/plugins/rainlab/notify/classes/ScheduledAction.php
@@ -0,0 +1,63 @@
+action = $action->id;
+ $this->params = $this->serializeParams($params);
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $this->delete();
+
+ if (! $actionClass = RuleAction::find($this->action)) {
+ return traceLog('Error: Could not restore action #' . $this->action);
+ }
+
+ $params = $this->unserializeParams();
+ $actionClass->triggerAction($params, false);
+ }
+
+ protected function serializeParams($params)
+ {
+ $result = [];
+
+ foreach ($params as $param => $value) {
+ $result[$param] = $this->getSerializedPropertyValue($value);
+ }
+
+ return $result;
+ }
+
+ protected function unserializeParams()
+ {
+ $result = [];
+
+ foreach ($this->params as $param => $value) {
+ $result[$param] = $this->getRestoredPropertyValue($value);
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/rainlab/notify/classes/compoundcondition/fields.yaml b/plugins/rainlab/notify/classes/compoundcondition/fields.yaml
new file mode 100644
index 0000000..551ae81
--- /dev/null
+++ b/plugins/rainlab/notify/classes/compoundcondition/fields.yaml
@@ -0,0 +1,14 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ condition_type:
+ label: Condition Type
+ span: auto
+ type: dropdown
+
+ condition:
+ label: Required Value
+ span: auto
+ type: dropdown
diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm b/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm
new file mode 100644
index 0000000..a218745
--- /dev/null
+++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/_column_select_record.htm
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm b/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm
new file mode 100644
index 0000000..1ac013d
--- /dev/null
+++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/_field_value.htm
@@ -0,0 +1,107 @@
+subcondition;
+ $controlType = $formModel->getValueControlType();
+?>
+
+
+
+
+
+ getValueDropdownOptions();
+ ?>
+
+ $option): ?>
+ isSelected($value) ? 'selected="selected"' : '' ?>
+ value="= $value ?>">= e(trans($option)) ?>
+
+
+
+
+ listSelectedReferenceRecords();
+ $hasData = $selectedRecords->count();
+ ?>
+
+
+
+ = $searchWidget->render() ?>
+ = $listWidget->render() ?>
+
+
+
= e(__('Selected Records')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = e($formModel->getReferencePrimaryColumn($record)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml b/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml
new file mode 100644
index 0000000..d0c4981
--- /dev/null
+++ b/plugins/rainlab/notify/classes/modelattributesconditionbase/fields.yaml
@@ -0,0 +1,22 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ subcondition:
+ label: Attribute
+ span: auto
+ type: dropdown
+
+ operator:
+ label: Operator
+ span: auto
+ type: dropdown
+ dependsOn: subcondition
+
+ value:
+ label: Value
+ dependsOn: [subcondition, operator]
+ type: partial
+ path: field_value
diff --git a/plugins/rainlab/notify/composer.json b/plugins/rainlab/notify/composer.json
new file mode 100644
index 0000000..f55725b
--- /dev/null
+++ b/plugins/rainlab/notify/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "rainlab/notify-plugin",
+ "type": "october-plugin",
+ "description": "Notify plugin for October CMS",
+ "homepage": "https://octobercms.com/plugin/rainlab-pages",
+ "keywords": ["october", "octobercms", "pages"],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Alexey Bobkov",
+ "email": "aleksey.bobkov@gmail.com",
+ "role": "Co-founder"
+ },
+ {
+ "name": "Samuel Georges",
+ "email": "daftspunky@gmail.com",
+ "role": "Co-founder"
+ }
+ ],
+ "require": {
+ "php": ">=5.5.9",
+ "composer/installers": "~1.0"
+ },
+ "minimum-stability": "dev"
+}
diff --git a/plugins/rainlab/notify/controllers/Notifications.php b/plugins/rainlab/notify/controllers/Notifications.php
new file mode 100644
index 0000000..20e65f6
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/Notifications.php
@@ -0,0 +1,130 @@
+asExtension('ListController')->index();
+ }
+
+ public function create($eventAlias = null)
+ {
+ try {
+ if (!$eventAlias) {
+ throw new ApplicationException('Missing a rule code');
+ }
+
+ $this->eventAlias = $eventAlias;
+ $this->bodyClass = 'compact-container breadcrumb-fancy';
+ $this->asExtension('FormController')->create();
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+ }
+
+ public function update($recordId = null, $context = null)
+ {
+ $this->bodyClass = 'compact-container breadcrumb-fancy';
+ $this->asExtension('FormController')->update($recordId, $context);
+ }
+
+ public function formExtendModel($model)
+ {
+ if (!$model->exists) {
+ $model->applyEventClass($this->getEventClass());
+ $model->name = $model->getEventDescription();
+ }
+
+ return $model;
+ }
+
+ // public function formBeforeSave($model)
+ // {
+ // $model->is_custom = 1;
+ // }
+
+ public function index_onLoadRuleGroupForm()
+ {
+ try {
+ $groups = EventBase::findEventGroups();
+ $this->vars['eventGroups'] = $groups;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('add_rule_group_form');
+ }
+
+ /**
+ * This handler requires the group code passed from `onLoadRuleGroupForm`
+ */
+ public function index_onLoadRuleEventForm()
+ {
+ try {
+ if (!$code = post('code')) {
+ throw new ApplicationException('Missing event group code');
+ }
+
+ $events = EventBase::findEventsByGroup($code);
+ $this->vars['events'] = $events;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('add_rule_event_form');
+ }
+
+ protected function getEventClass()
+ {
+ $alias = post('event_alias', $this->eventAlias);
+
+ if ($this->eventClass !== null) {
+ return $this->eventClass;
+ }
+
+ if (!$event = EventBase::findEventByIdentifier($alias)) {
+ throw new ApplicationException('Unable to find event with alias: '. $alias);
+ }
+
+ return $this->eventClass = get_class($event);
+ }
+}
diff --git a/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm b/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm
new file mode 100644
index 0000000..8e939e2
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/_add_rule_event_form.htm
@@ -0,0 +1,39 @@
+= Form::open(['id' => 'addRuleEventForm']) ?>
+
+
+
+ fatalError): ?>
+
= $fatalError ?>
+
+
+
+
+
+
+
+
+
+= Form::close() ?>
diff --git a/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm b/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm
new file mode 100644
index 0000000..23507f4
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/_add_rule_group_form.htm
@@ -0,0 +1,47 @@
+= Form::open(['id' => 'addRuleGroupForm']) ?>
+
+
+
+ fatalError): ?>
+
= $fatalError ?>
+
+
+
+
+
+
+
+
+
+= Form::close() ?>
diff --git a/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm b/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm
new file mode 100644
index 0000000..f2277a3
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/_form_toolbar.htm
@@ -0,0 +1,39 @@
+formGetContext() == 'create';
+ $pageUrl = isset($pageUrl) ? $pageUrl : null;
+?>
+
diff --git a/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm b/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm
new file mode 100644
index 0000000..52030b6
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/_list_toolbar.htm
@@ -0,0 +1,9 @@
+
diff --git a/plugins/rainlab/notify/controllers/notifications/config_form.yaml b/plugins/rainlab/notify/controllers/notifications/config_form.yaml
new file mode 100644
index 0000000..2e2bda0
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/config_form.yaml
@@ -0,0 +1,16 @@
+# ===================================
+# Form Behavior Config
+# ===================================
+
+name: Notification Rule
+form: $/rainlab/notify/models/notificationrule/fields.yaml
+modelClass: RainLab\Notify\Models\NotificationRule
+defaultRedirect: rainlab/notify/notifications
+
+create:
+ redirect: rainlab/notify/notifications/update/:id
+ redirectClose: rainlab/notify/notifications
+
+update:
+ redirect: rainlab/notify/notifications
+ redirectClose: rainlab/notify/notifications
diff --git a/plugins/rainlab/notify/controllers/notifications/config_list.yaml b/plugins/rainlab/notify/controllers/notifications/config_list.yaml
new file mode 100644
index 0000000..1d923a2
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/config_list.yaml
@@ -0,0 +1,20 @@
+# ===================================
+# List Behavior Config
+# ===================================
+
+title: Notification Rules
+list: $/rainlab/notify/models/notificationrule/columns.yaml
+modelClass: RainLab\Notify\Models\NotificationRule
+recordUrl: rainlab/notify/notifications/update/:id
+noRecordsMessage: backend::lang.list.no_records
+recordsPerPage: 30
+showSetup: true
+showCheckboxes: true
+defaultSort:
+ column: count
+ direction: desc
+
+toolbar:
+ buttons: list_toolbar
+ search:
+ prompt: backend::lang.list.search_prompt
diff --git a/plugins/rainlab/notify/controllers/notifications/create.htm b/plugins/rainlab/notify/controllers/notifications/create.htm
new file mode 100644
index 0000000..c1d8cf2
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/create.htm
@@ -0,0 +1,31 @@
+
+
+
+
+fatalError): ?>
+
+
+ = Form::open([
+ 'class' => 'layout',
+ 'data-change-monitor' => 'true',
+ 'data-window-close-confirm' => e(__('Are you sure?')),
+ 'id' => 'post-form'
+ ]) ?>
+
+
+ = $this->formRender() ?>
+
+ = Form::close() ?>
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/controllers/notifications/index.htm b/plugins/rainlab/notify/controllers/notifications/index.htm
new file mode 100644
index 0000000..766877d
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/index.htm
@@ -0,0 +1,2 @@
+
+= $this->listRender() ?>
diff --git a/plugins/rainlab/notify/controllers/notifications/update.htm b/plugins/rainlab/notify/controllers/notifications/update.htm
new file mode 100644
index 0000000..ab37689
--- /dev/null
+++ b/plugins/rainlab/notify/controllers/notifications/update.htm
@@ -0,0 +1,30 @@
+
+
+
+
+fatalError): ?>
+
+
+ = Form::open([
+ 'class' => 'layout',
+ 'data-change-monitor' => 'true',
+ 'data-window-close-confirm' => e(__('Are you sure?')),
+ 'id' => 'post-form'
+ ]) ?>
+
+ = $this->formRender() ?>
+
+ = Form::close() ?>
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/ActionBuilder.php b/plugins/rainlab/notify/formwidgets/ActionBuilder.php
new file mode 100644
index 0000000..02d3933
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/ActionBuilder.php
@@ -0,0 +1,388 @@
+fillFromConfig([
+ // ]);
+
+ if ($widget = $this->makeActionFormWidget()) {
+ $widget->bindToController();
+ }
+
+ if ($widget = $this->makeActionScheduleFormWidget()) {
+ $widget->bindToController();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function loadAssets()
+ {
+ $this->addJs('js/actions.js', 'RainLab.Notify');
+ $this->addCss('css/actions.css', 'RainLab.Notify');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function render()
+ {
+ $this->prepareVars();
+
+ return $this->makePartial('actions_container');
+ }
+
+ /**
+ * Prepares the list data
+ */
+ public function prepareVars()
+ {
+ $this->vars['name'] = $this->getFieldName();
+ $this->vars['formModel'] = $this->model;
+ $this->vars['actions'] = $this->getActions();
+ $this->vars['actionFormWidget'] = $this->actionFormWidget;
+ $connection = config('queue.default');
+ $this->vars['queueDriver'] = config("queue.connections.{$connection}.driver");
+ $this->vars['actionScheduleFormWidget'] = $this->actionScheduleFormWidget;
+ $this->vars['availableTags'] = $this->getAvailableTags();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSaveValue($value)
+ {
+ $this->model->bindEvent('model.afterSave', function() {
+ $this->processSave();
+ });
+
+ return FormField::NO_SAVE_DATA;
+ }
+
+ protected function processSave()
+ {
+ $cache = $this->getCacheActionDataPayload();
+
+ foreach ($cache as $id => $data) {
+ $action = $this->findActionObj($id);
+
+ if ($attributes = $this->getCacheActionAttributes($action)) {
+ $action->fill($attributes);
+ }
+
+ $action->save(null, $this->sessionKey);
+ }
+ }
+
+ //
+ // AJAX
+ //
+
+ public function onLoadCreateActionForm()
+ {
+ try {
+ $actions = ActionBase::findActions();
+ $this->vars['actions'] = $actions;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('create_action_form');
+ }
+
+ public function onSaveAction()
+ {
+ $this->restoreCacheActionDataPayload();
+
+ $action = $this->findActionObj();
+
+ $data = post('Action', []);
+ $schedule = post('ActionSchedule', []);
+ $action->fill($data);
+ $action->fill($schedule);
+ $action->validate();
+ $action->action_text = $action->getActionObject()->getText();
+
+ $action->applyCustomData();
+
+ $this->setCacheActionData($action);
+
+ return $this->renderActions($action);
+ }
+
+ public function onLoadActionSetup()
+ {
+ try {
+ $action = $this->findActionObj();
+
+ $data = $this->getCacheActionAttributes($action);
+
+ if (!is_null($this->actionFormWidget)) {
+ $this->actionFormWidget->setFormValues($data);
+ }
+ $this->actionScheduleFormWidget->setFormValues($data);
+
+ $this->prepareVars();
+ $this->vars['action'] = $action;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('action_settings_form');
+ }
+
+ public function onCreateAction()
+ {
+ if (!$className = post('action_class')) {
+ throw new ApplicationException('Please specify an action');
+ }
+
+ $this->restoreCacheActionDataPayload();
+
+ $newAction = $this->getRelationModel();
+ $newAction->class_name = $className;
+ $newAction->save();
+
+ $this->model->rule_actions()->add($newAction, post('_session_key'));
+
+ $this->vars['newActionId'] = $newAction->id;
+
+ return $this->renderActions();
+ }
+
+ public function onDeleteAction()
+ {
+ $action = $this->findActionObj();
+
+ $this->model->rule_actions()->remove($action, post('_session_key'));
+
+ return $this->renderActions();
+ }
+
+ public function onCancelActionSettings()
+ {
+ $action = $this->findActionObj(post('new_action_id'));
+
+ $action->delete();
+
+ return $this->renderActions();
+ }
+
+ //
+ // Postback deferring
+ //
+
+ public function getCacheActionAttributes($action)
+ {
+ return array_get($this->getCacheActionData($action), 'attributes');
+ }
+
+ public function getCacheActionTitle($action)
+ {
+ return array_get($this->getCacheActionData($action), 'title');
+ }
+
+ public function getCacheActionText($action)
+ {
+ return array_get($this->getCacheActionData($action), 'text');
+ }
+
+ public function getCacheActionData($action, $default = null)
+ {
+ $cache = post('action_data', []);
+
+ if (is_array($cache) && array_key_exists($action->id, $cache)) {
+ return json_decode($cache[$action->id], true);
+ }
+
+ if ($default === false) {
+ return null;
+ }
+
+ return $this->makeCacheActionData($action);
+ }
+
+ public function makeCacheActionData($action)
+ {
+ $data = [
+ 'attributes' => $action->config_data,
+ 'title' => $action->getTitle(),
+ 'text' => $action->getText(),
+ ];
+
+ return $data;
+ }
+
+ public function setCacheActionData($action)
+ {
+ $cache = post('action_data', []);
+
+ $cache[$action->id] = json_encode($this->makeCacheActionData($action));
+
+ Request::merge([
+ 'action_data' => $cache
+ ]);
+ }
+
+ public function restoreCacheActionDataPayload()
+ {
+ Request::merge([
+ 'action_data' => json_decode(post('current_action_data'), true)
+ ]);
+ }
+
+ public function getCacheActionDataPayload()
+ {
+ return post('action_data', []);
+ }
+
+ //
+ // Helpers
+ //
+
+ protected function getAvailableTags()
+ {
+ $tags = [];
+
+ if ($this->model->methodExists('defineParams')) {
+ $params = $this->model->defineParams();
+
+ foreach ($params as $param => $definition) {
+ $tags[$param] = array_get($definition, 'label');
+ }
+ }
+
+ return $tags;
+ }
+
+ /**
+ * Updates the primary rule actions container
+ * @return array
+ */
+ protected function renderActions()
+ {
+ $this->prepareVars();
+
+ return [
+ '#'.$this->getId() => $this->makePartial('actions')
+ ];
+ }
+
+ protected function makeActionFormWidget()
+ {
+ if ($this->actionFormWidget !== null) {
+ return $this->actionFormWidget;
+ }
+
+ if (!$model = $this->findActionObj(null, false)) {
+ return null;
+ }
+
+ if (!$model->hasFieldConfig()) {
+ return null;
+ }
+
+ $config = $model->getFieldConfig();
+ $config->model = $model;
+ $config->alias = $this->alias . 'Form';
+ $config->arrayName = 'Action';
+
+ $widget = $this->makeWidget('Backend\Widgets\Form', $config);
+
+ return $this->actionFormWidget = $widget;
+ }
+
+ protected function makeActionScheduleFormWidget()
+ {
+ if ($this->actionScheduleFormWidget !== null) {
+ return $this->actionScheduleFormWidget;
+ }
+
+ if (!$model = $this->findActionObj(null, false)) {
+ return null;
+ }
+
+ $config = $this->makeConfig('$/rainlab/notify/models/ruleaction/fields_schedule.yaml');
+ $config->model = $model;
+ $config->alias = $this->alias . 'ScheduleForm';
+ $config->arrayName = 'ActionSchedule';
+
+ $widget = $this->makeWidget('Backend\Widgets\Form', $config);
+
+ return $this->actionScheduleFormWidget = $widget;
+ }
+
+ protected function getActions()
+ {
+ if ($this->actionsCache !== false) {
+ return $this->actionsCache;
+ }
+
+ $relationObject = $this->getRelationObject();
+ $actions = $relationObject->withDeferred($this->sessionKey)->get();
+
+ return $this->actionsCache = $actions ?: null;
+ }
+
+ protected function findActionObj($actionId = null, $throw = true)
+ {
+ $actionId = $actionId ? $actionId : post('current_action_id');
+
+ $action = null;
+
+ if (strlen($actionId)) {
+ $action = $this->getRelationModel()->find($actionId);
+ }
+
+ if ($throw && !$action) {
+ throw new ApplicationException('Action not found');
+ }
+
+ return $action;
+ }
+}
diff --git a/plugins/rainlab/notify/formwidgets/ConditionBuilder.php b/plugins/rainlab/notify/formwidgets/ConditionBuilder.php
new file mode 100644
index 0000000..083716d
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/ConditionBuilder.php
@@ -0,0 +1,455 @@
+fillFromConfig([
+ 'conditionsRuleType',
+ ]);
+
+ if ($widget = $this->makeConditionFormWidget()) {
+ $widget->bindToController();
+ }
+
+ $this->initRootCondition();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function loadAssets()
+ {
+ $this->addJs('js/conditions.js', 'RainLab.Notify');
+ $this->addJs('js/conditions.multivalue.js', 'RainLab.Notify');
+ $this->addCss('css/conditions.css', 'RainLab.Notify');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function render()
+ {
+ $this->prepareVars();
+
+ return $this->makePartial('conditions_container');
+ }
+
+ /**
+ * Prepares the list data
+ */
+ public function prepareVars()
+ {
+ $this->vars['name'] = $this->getFieldName();
+ $this->vars['rootCondition'] = $this->getConditionsRoot();
+ $this->vars['conditionFormWidget'] = $this->conditionFormWidget;
+ }
+
+ public function initRootCondition()
+ {
+ if ($this->getConditionsRoot()) {
+ return;
+ }
+
+ $relationObject = $this->getRelationObject();
+
+ $rootRule = $this->getRelationModel();
+ $rootRule->rule_host_type = $this->conditionsRuleType;
+ $rootRule->class_name = $rootRule->getRootConditionClass();
+ $rootRule->save();
+
+ $relationObject->add($rootRule, $this->sessionKey);
+
+ $this->conditionsRoot = $rootRule;
+ }
+
+ public function isRootCondition($condition)
+ {
+ if ($root = $this->getConditionsRoot()) {
+ return $condition->id === $root->id;
+ }
+
+ return false;
+ }
+
+ public function getConditionsRoot()
+ {
+ if ($this->conditionsRoot !== false) {
+ return $this->conditionsRoot;
+ }
+
+ $relationObject = $this->getRelationObject();
+ $rootCondition = $relationObject->withDeferred($this->sessionKey)->first();
+
+ return $this->conditionsRoot = $rootCondition ?: null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSaveValue($value)
+ {
+ $this->model->bindEvent('model.afterSave', function() {
+ $this->processSave();
+ });
+
+ return FormField::NO_SAVE_DATA;
+ }
+
+ protected function processSave()
+ {
+ $cache = $this->getCacheConditionDataPayload();
+
+ foreach ($cache as $id => $data) {
+ $condition = $this->findConditionObj($id);
+ $attributes = $this->getCacheConditionAttributes($condition);
+ $condition->fill($attributes);
+ $condition->save(null, $this->sessionKey.'_'.$condition->id);
+ }
+ }
+
+ //
+ // AJAX
+ //
+
+ public function onLoadConditionSetup()
+ {
+ try {
+ $condition = $this->findConditionObj();
+
+ $this->prepareVars();
+
+ $this->vars['condition'] = $condition;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('condition_settings_form');
+ }
+
+ public function onLoadCreateChildCondition()
+ {
+ try {
+ $condition = $this->findConditionObj();
+
+ /*
+ * Look up parents
+ */
+ $parents = [$condition->id];
+ $parentsArray = post('condition_parent_id', []);
+ $currentId = $condition->id;
+
+ while (array_key_exists($currentId, $parentsArray) && $parentsArray[$currentId]) {
+ $parents[] = $currentId = $parentsArray[$currentId];
+ }
+
+ /*
+ * Custom rules provided by model
+ */
+ $extraRules = [];
+ if ($this->model->methodExists('getExtraConditionRules')) {
+ $extraRules = $this->model->getExtraConditionRules();
+ }
+
+ /*
+ * Look up conditions
+ */
+ $options = $condition->getChildOptions([
+ 'ruleType' => $this->conditionsRuleType,
+ 'parentIds' => $parents,
+ 'extraRules' => $extraRules
+ ]);
+
+ $this->prepareVars();
+ $this->vars['condition'] = $condition;
+ $this->vars['options'] = $options;
+ }
+ catch (Exception $ex) {
+ $this->handleError($ex);
+ }
+
+ return $this->makePartial('create_child_form');
+ }
+
+ public function onSaveCondition()
+ {
+ $this->restoreCacheConditionDataPayload();
+
+ $condition = $this->findConditionObj();
+
+ $data = post('Condition', []);
+ $condition->fill($data);
+ $condition->validate();
+ $condition->condition_text = $condition->getConditionObject()->getText();
+
+ $condition->applyCustomData();
+
+ $this->setCacheConditionData($condition);
+
+ return $this->renderConditions($condition);
+ }
+
+ public function onCreateCondition()
+ {
+ if (!$className = post('condition_class')) {
+ throw new ValidationException(['condition_class' => 'Please specify a condition']);
+ }
+
+ $this->restoreCacheConditionDataPayload();
+
+ $subcondition = null;
+
+ $parts = explode(':', $className);
+ if (count($parts) > 1) {
+ $subcondition = $parts[1];
+ $className = $parts[0];
+ }
+
+ $parentCondition = $this->findConditionObj();
+
+ $newCondition = $this->getRelationModel();
+ $newCondition->class_name = $className;
+ $newCondition->rule_host_type = $parentCondition->rule_host_type;
+
+ if ($subcondition) {
+ $newCondition->subcondition = $subcondition;
+ }
+
+ $newCondition->save();
+
+ $parentCondition->children()->add($newCondition, post('_session_key').'_'.$parentCondition->id);
+
+ $this->vars['newConditionId'] = $newCondition->id;
+
+ return $this->renderConditions($parentCondition);
+ }
+
+ public function onDeleteCondition()
+ {
+ $parentCondition = null;
+
+ $condition = $this->findConditionObj();
+
+ if ($parentId = $this->getParentIdFromCondition($condition)) {
+ $parentCondition = $this->findConditionObj($parentId);
+
+ if ($parentCondition) {
+ $parentCondition->children()->remove($condition, post('_session_key').'_'.$parentCondition->id);
+ }
+ }
+
+ return $this->renderConditions($parentCondition);
+ }
+
+ public function onCancelConditionSettings()
+ {
+ $condition = $this->findConditionObj(post('new_condition_id'));
+
+ $condition->delete();
+
+ return $this->renderConditions();
+ }
+
+ //
+ // Postback deferring
+ //
+
+ public function getCacheConditionAttributes($condition)
+ {
+ return array_get($this->getCacheConditionData($condition), 'attributes');
+ }
+
+ public function getCacheConditionText($condition)
+ {
+ return array_get($this->getCacheConditionData($condition), 'text');
+ }
+
+ public function getCacheConditionJoinText($condition)
+ {
+ return array_get($this->getCacheConditionData($condition), 'joinText');
+ }
+
+ public function getCacheConditionData($condition, $default = null)
+ {
+ $cache = post('condition_data', []);
+
+ if (is_array($cache) && array_key_exists($condition->id, $cache)) {
+ return json_decode($cache[$condition->id], true);
+ }
+
+ if ($default === false) {
+ return null;
+ }
+
+ return $this->makeCacheConditionData($condition);
+ }
+
+ public function makeCacheConditionData($condition)
+ {
+ $data = [
+ 'attributes' => $condition->config_data,
+ 'text' => $condition->getText()
+ ];
+
+ if ($condition->isCompound()) {
+ $data['joinText'] = $condition->getJoinText();
+ }
+
+ return $data;
+ }
+
+ public function setCacheConditionData($condition)
+ {
+ $cache = post('condition_data', []);
+
+ $cache[$condition->id] = json_encode($this->makeCacheConditionData($condition));
+
+ Request::merge([
+ 'condition_data' => $cache
+ ]);
+ }
+
+ public function restoreCacheConditionDataPayload()
+ {
+ Request::merge([
+ 'condition_data' => json_decode(post('current_condition_data', []), true)
+ ]);
+ }
+
+ public function getCacheConditionDataPayload()
+ {
+ return post('condition_data');
+ }
+
+ //
+ // Helpers
+ //
+
+ public function getParentIdFromCondition($condition)
+ {
+ if ($parentId = post('current_parent_id')) {
+ return $parentId;
+ }
+
+ $parentIds = post('condition_parent_id', []);
+
+ if (isset($parentIds[$condition->id])) {
+ return $parentIds[$condition->id];
+ }
+ }
+
+ /**
+ * Updates the primary rule conditions container
+ * @return array
+ */
+ protected function renderConditions($currentCondition = null)
+ {
+ if ($currentCondition && $this->isRootCondition($currentCondition)) {
+ $condition = $currentCondition;
+ }
+ else {
+ $condition = $this->getConditionsRoot();
+ }
+
+ return [
+ '#'.$this->getId() => $this->makePartial('conditions', ['condition' => $condition])
+ ];
+ }
+
+ protected function makeConditionFormWidget()
+ {
+ if ($this->conditionFormWidget !== null) {
+ return $this->conditionFormWidget;
+ }
+
+ if (!$model = $this->findConditionObj(null, false)) {
+ return null;
+ }
+
+ $config = $model->getFieldConfig();
+ $config->model = $model;
+ $config->alias = $this->alias . 'Form';
+ $config->arrayName = 'Condition';
+
+ $widget = $this->makeWidget('Backend\Widgets\Form', $config);
+
+ /*
+ * Set form values based on postback or cached attributes
+ */
+ if (!$data = post('Condition')) {
+ $data = $this->getCacheConditionAttributes($model);
+ }
+
+ $widget->setFormValues($data);
+
+ /*
+ * Allow conditions to register their own widgets
+ */
+ $model->onPreRender($this->controller, $this);
+
+ return $this->conditionFormWidget = $widget;
+ }
+
+ protected function findConditionObj($conditionId = null, $throw = true)
+ {
+ $conditionId = $conditionId ? $conditionId : post('current_condition_id');
+
+ $condition = null;
+
+ if (strlen($conditionId)) {
+ $condition = $this->getRelationModel()->find($conditionId);
+ }
+
+ if ($throw && !$condition) {
+ throw new ApplicationException('Condition not found');
+ }
+
+ return $condition;
+ }
+}
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css
new file mode 100644
index 0000000..817a940
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/css/actions.css
@@ -0,0 +1,227 @@
+.action-set {
+ margin-top: 10px;
+}
+.action-set ol {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.action-set ol > li {
+ -webkit-transition: width 1s;
+ transition: width 1s;
+}
+.action-set ol > li > div {
+ font-size: 14px;
+ font-weight: normal;
+ position: relative;
+}
+.action-set ol > li > div > a,
+.action-set ol > li > div div.content {
+ color: #2b3e50;
+ padding: 10px 45px 10px 91px;
+ display: block;
+ line-height: 150%;
+ text-decoration: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.action-set ol > li > div span.icon {
+ display: block;
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ left: 20px;
+ top: 15px;
+ text-align: center;
+ font-weight: 500;
+ line-height: 22px;
+ color: #999;
+}
+.action-set ol > li > div span.icon > i {
+ line-height: 22px;
+ font-weight: normal;
+ font-size: 22px;
+ color: #999;
+}
+.action-set ol > li > div:not(.no-hover):hover,
+.action-set ol > li > div.popover-highlight {
+ background-color: #4ea5e0 !important;
+}
+.action-set ol > li > div:not(.no-hover):hover span.icon > i,
+.action-set ol > li > div.popover-highlight span.icon > i {
+ color: #fff !important;
+}
+.action-set ol > li > div:not(.no-hover):hover > a,
+.action-set ol > li > div.popover-highlight > a {
+ color: #ffffff !important;
+}
+.action-set ol > li > div:not(.no-hover):hover:before,
+.action-set ol > li > div.popover-highlight:before {
+ background-position: 0px -80px;
+}
+.action-set ol > li > div:not(.no-hover):hover:after,
+.action-set ol > li > div.popover-highlight:after {
+ top: 0 !important;
+ bottom: 0 !important;
+}
+.action-set ol > li > div:not(.no-hover):hover span,
+.action-set ol > li > div.popover-highlight span {
+ color: #ffffff !important;
+}
+.action-set ol > li > div:not(.no-hover):hover span.drag-handle,
+.action-set ol > li > div.popover-highlight span.drag-handle {
+ cursor: move;
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+.action-set ol > li > div:not(.no-hover):active {
+ background-color: #3498db !important;
+}
+.action-set ol > li > div:not(.no-hover):active > a {
+ color: #ffffff !important;
+}
+.action-set ol > li > div span.icon:first-child {
+ font-size: 11px;
+ color: #ccc;
+}
+.action-set ol > li > div span.icon:first-child > i {
+ color: #ccc;
+ font-size: 15px;
+}
+.action-set ol > li > div span.icon:last-child {
+ left: 52px;
+}
+.action-set ol > li > div span.comment {
+ display: block;
+ font-weight: 400;
+ color: #95a5a6;
+ font-size: 13px;
+ margin-top: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.action-set ol > li > div > .subpanel {
+ right: 0;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 200;
+ padding: 0 20px;
+}
+.action-set ol > li > div > ul.submenu {
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 0;
+ list-style: none;
+ z-index: 200;
+ height: 100%;
+ display: none;
+ margin: 0;
+ font-size: 0;
+}
+.action-set ol > li > div > ul.submenu li {
+ font-size: 12px;
+ height: 100%;
+ display: inline-block;
+ background: #2581b8;
+ border-right: 1px solid #328ec8;
+}
+.action-set ol > li > div > ul.submenu li p {
+ display: table;
+ height: 100%;
+ padding: 0;
+ margin: 0;
+}
+.action-set ol > li > div > ul.submenu li p a {
+ display: table-cell;
+ vertical-align: middle;
+ height: 100%;
+ padding: 0 20px;
+ font-size: 13px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ color: #ffffff;
+ text-decoration: none;
+}
+.action-set ol > li > div > ul.submenu li p a i.control-icon {
+ font-size: 22px;
+}
+.action-set ol > li > div:hover > ul.submenu {
+ display: block;
+}
+.action-set ol > li > div .checkbox {
+ position: absolute;
+ top: -2px;
+ right: 0;
+}
+.action-set ol > li > div .checkbox label {
+ margin-right: 0;
+}
+.action-set ol > li > div .checkbox label:before {
+ border-color: #cccccc;
+}
+.action-set ol > li > div.popover-highlight {
+ background-color: #4ea5e0 !important;
+}
+.action-set ol > li > div.popover-highlight:before {
+ background-position: 0px -80px;
+}
+.action-set ol > li > div.popover-highlight > a {
+ color: #ffffff !important;
+ cursor: default;
+}
+.action-set ol > li > div.popover-highlight span {
+ color: #ffffff !important;
+}
+.action-set ol > li > div.popover-highlight > ul.submenu,
+.action-set ol > li > div.popover-highlight > span.drag-handle {
+ display: none !important;
+}
+.action-set ol > li.active > div {
+ background: #dddddd;
+}
+.action-set ol > li.active > div:after {
+ position: absolute;
+ width: 4px;
+ left: 0;
+ top: -1px;
+ bottom: -1px;
+ background: #e67e22;
+ display: block;
+ content: ' ';
+}
+.action-set ol > li.active > div > span.comment,
+.action-set ol > li.active > div > span.expand {
+ color: #8f8f8f;
+}
+.action-set a.menu-control {
+ display: block;
+ margin: 20px;
+ padding: 13px 15px;
+ border: dotted 2px #ebebeb;
+ color: #bdc3c7;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ border-radius: 5px;
+ vertical-align: middle;
+}
+.action-set a.menu-control:hover,
+.action-set a.menu-control:focus {
+ text-decoration: none;
+ background-color: #4ea5e0;
+ color: #ffffff;
+ border: none;
+ padding: 15px 17px;
+}
+.action-set a.menu-control:active {
+ background: #3498db;
+ color: #ffffff;
+}
+.action-set a.menu-control i {
+ margin-right: 10px;
+ font-size: 14px;
+}
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js
new file mode 100644
index 0000000..ea7a12d
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/js/actions.js
@@ -0,0 +1,156 @@
+/*
+ * Global helpers
+ */
+function showActionSettings(id) {
+ var $control = $('[data-action-id='+id+']').closest('[data-control="ruleactions"]')
+
+ $control.ruleActions('onShowNewActionSettings', id)
+}
+
+/*
+ * Plugin definition
+ */
++function ($) { "use strict";
+ var Base = $.oc.foundation.base,
+ BaseProto = Base.prototype
+
+ var RuleActions = function (element, options) {
+ this.$el = $(element)
+ this.options = options || {}
+
+ $.oc.foundation.controlUtils.markDisposable(element)
+ Base.call(this)
+ this.init()
+ }
+
+ RuleActions.prototype = Object.create(BaseProto)
+ RuleActions.prototype.constructor = RuleActions
+
+ RuleActions.prototype.init = function() {
+ this.$el.on('click', '[data-actions-settings]', this.proxy(this.onShowSettings))
+ this.$el.on('click', '[data-actions-delete]', this.proxy(this.onDeleteAction))
+ this.$el.one('dispose-control', this.proxy(this.dispose))
+ }
+
+ RuleActions.prototype.dispose = function() {
+ this.$el.off('click', '[data-actions-settings]', this.proxy(this.onShowSettings))
+ this.$el.off('click', '[data-actions-delete]', this.proxy(this.onDeleteAction))
+ this.$el.off('dispose-control', this.proxy(this.dispose))
+ this.$el.removeData('oc.ruleActions')
+
+ this.$el = null
+
+ // In some cases options could contain callbacks,
+ // so it's better to clean them up too.
+ this.options = null
+
+ BaseProto.dispose.call(this)
+ }
+
+ RuleActions.prototype.onDeleteAction = function(event) {
+ var $el = $(event.target),
+ actionId = getActionIdFromElement($el)
+
+ $el.request(this.options.deleteHandler, {
+ data: { current_action_id: actionId },
+ confirm: 'Do you really want to delete this action?'
+ })
+ }
+
+ RuleActions.prototype.onShowNewActionSettings = function(actionId) {
+ var $el = $('[data-action-id='+actionId+']')
+
+ var popup_size = 'giant';
+ // Action does not use settings
+ if ($el.hasClass('no-form')) {
+ // Popup will contain scheduling option only
+ popup_size = 'medium'
+ }
+
+ $el.popup({
+ handler: this.options.settingsHandler,
+ extraData: { current_action_id: actionId },
+ size: popup_size
+ })
+
+ // This will not fire on successful save because the target element
+ // is replaced by the time the popup loader has finished to call it
+ $el.one('hide.oc.popup', this.proxy(this.onCancelAction))
+ }
+
+ RuleActions.prototype.onCancelAction = function(event) {
+ var $el = $(event.target),
+ actionId = getActionIdFromElement($el)
+
+ $el.request(this.options.cancelHandler, {
+ data: { new_action_id: actionId }
+ })
+
+ return false
+ }
+
+ RuleActions.prototype.onShowSettings = function(event) {
+ var $el = $(event.target),
+ actionId = getActionIdFromElement($el)
+
+ var popup_size = 'giant';
+ // Action does not use settings
+ if ($el.closest('li.action-item').hasClass('no-form')) {
+ popup_size = 'medium'
+ }
+
+ $el.popup({
+ handler: this.options.settingsHandler,
+ extraData: { current_action_id: actionId },
+ size: popup_size
+ })
+
+ return false
+ }
+
+ function getActionIdFromElement($el) {
+ var $item = $el.closest('li.action-item')
+
+ return $item.data('action-id')
+ }
+
+ RuleActions.DEFAULTS = {
+ settingsHandler: null,
+ deleteHandler: null,
+ cancelHandler: null,
+ createHandler: null
+ }
+
+ // PLUGIN DEFINITION
+ // ============================
+
+ var old = $.fn.ruleActions
+
+ $.fn.ruleActions = function (option) {
+ var args = Array.prototype.slice.call(arguments, 1), items, result
+
+ items = this.each(function () {
+ var $this = $(this)
+ var data = $this.data('oc.ruleActions')
+ var options = $.extend({}, RuleActions.DEFAULTS, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('oc.ruleActions', (data = new RuleActions(this, options)))
+ if (typeof option == 'string') result = data[option].apply(data, args)
+ if (typeof result != 'undefined') return false
+ })
+
+ return result ? result : items
+ }
+
+ $.fn.ruleActions.Constructor = RuleActions
+
+ $.fn.ruleActions.noConflict = function () {
+ $.fn.ruleActions = old
+ return this
+ }
+
+ // Add this only if required
+ $(document).render(function (){
+ $('[data-control="ruleactions"]').ruleActions()
+ })
+
+}(window.jQuery);
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less
new file mode 100644
index 0000000..16440bb
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/assets/less/actions.less
@@ -0,0 +1,273 @@
+@import "../../../../../../../modules/backend/assets/less/core/boot.less";
+
+//
+// Actions
+// --------------------------------------------------
+
+@color-actions-item-bg: #ffffff;
+@color-actions-item-title: #2b3e50;
+@color-actions-item-comment: #95a5a6;
+@color-actions-control: #bdc3c7;
+@color-actions-hover-bg: @highlight-hover-bg;
+@color-actions-hover-text: @highlight-hover-text;
+@color-actions-active-bg: @highlight-active-bg;
+@color-actions-active-text: @highlight-active-text;
+@color-actions-item-active-comment: #8f8f8f;
+@color-actions-submenu-text: #ffffff;
+@color-actions-light-submenu-bg: #2581b8;
+@color-actions-light-submenu-border: #328ec8;
+
+.action-set {
+ margin-top: 10px;
+
+ ol {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+
+ > li {
+ .transition(width 1s);
+
+ > div {
+ font-size: @font-size-base;
+ font-weight: normal;
+ position: relative;
+
+ > a, div.content {
+ color: @color-actions-item-title;
+ padding: 10px 45px 10px 91px;
+ display: block;
+ line-height: 150%;
+ text-decoration: none;
+ .box-sizing(border-box);
+ }
+
+ span.icon {
+ display: block;
+ position: absolute;
+ width: 22px;
+ height: 22px;
+ left: 20px;
+ top: 15px;
+ text-align: center;
+ font-weight: 500;
+ line-height: 22px;
+ color: #999;
+
+ > i {
+ line-height: 22px;
+ font-weight: normal;
+ font-size: 22px;
+ color: #999;
+ }
+ }
+
+ &:not(.no-hover):hover, &.popover-highlight {
+ background-color: @color-actions-hover-bg !important;
+
+ span.icon > i {
+ color: #fff !important;
+ }
+
+ > a {
+ color: @color-actions-hover-text !important;
+ }
+
+ &:before {
+ background-position: 0px -80px;
+ }
+
+ &:after {
+ top: 0 !important;
+ bottom: 0 !important;
+ }
+
+ span {
+ color: @color-actions-hover-text !important;
+
+ &.drag-handle {
+ cursor: move;
+ .opacity(1);
+ }
+ }
+ }
+
+ &:not(.no-hover):active {
+ background-color: @color-actions-active-bg !important;
+
+ > a {
+ color: @color-actions-active-text !important;
+ }
+ }
+
+ span.icon:first-child {
+ font-size: 11px;
+ color: #ccc;
+
+ > i {
+ color: #ccc;
+ font-size: 15px;
+ }
+ }
+ span.icon:last-child {
+ left: 52px;
+ }
+
+ span.comment {
+ display: block;
+ font-weight: 400;
+ color: @color-actions-item-comment;
+ font-size: @font-size-base - 1;
+ margin-top: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ > .subpanel {
+ right: 0;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 200;
+ padding: 0 20px;
+ }
+
+ > ul.submenu {
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 0;
+ list-style: none;
+ z-index: 200;
+ height: 100%;
+ display: none;
+ margin: 0;
+ font-size: 0;
+
+ li {
+ font-size: @font-size-base - 2;
+ height: 100%;
+ display: inline-block;
+ background: @color-actions-light-submenu-bg;
+ border-right: 1px solid @color-actions-light-submenu-border;
+
+ p {
+ display: table;
+ height: 100%;
+ padding: 0;
+ margin: 0;
+
+ a {
+ display: table-cell;
+ vertical-align: middle;
+ height: 100%;
+ padding: 0 20px;
+ font-size: @font-size-base - 1;
+ .box-sizing(border-box);
+ color: @color-actions-submenu-text;
+ text-decoration: none;
+
+ i.control-icon {
+ font-size: 22px;
+ }
+ }
+ }
+ }
+ }
+
+ &:hover {
+ > ul.submenu {
+ display: block;
+ }
+ }
+
+ .checkbox {
+ position: absolute;
+ top: -2px;
+ right: 0;
+
+ label {
+ margin-right: 0;
+
+ &:before {
+ border-color: @color-filelist-cb-border;
+ }
+ }
+ }
+
+ &.popover-highlight {
+ background-color: @color-actions-hover-bg !important;
+
+ &:before {
+ background-position: 0px -80px;
+ }
+
+ > a {
+ color: @color-actions-hover-text !important;
+ cursor: default;
+ }
+
+ span {
+ color: @color-actions-hover-text !important;
+ }
+
+ > ul.submenu, > span.drag-handle {
+ display: none !important;
+ }
+ }
+ }
+
+ &.active {
+ > div {
+ background: @color-list-active;
+
+ &:after {
+ position: absolute;
+ width: 4px;
+ left: 0;
+ top: -1px;
+ bottom: -1px;
+ background: @color-list-active-border;
+ display: block;
+ content: ' ';
+ }
+
+ > span.comment, > span.expand {
+ color: @color-actions-item-active-comment;
+ }
+ }
+ }
+ }
+ }
+
+ a.menu-control {
+ display: block;
+ margin: 20px;
+ padding: 13px 15px;
+ border: dotted 2px #ebebeb;
+ color: #bdc3c7;
+ font-size: @font-size-base - 2;
+ font-weight: 600;
+ text-transform: uppercase;
+ border-radius: 5px;
+ vertical-align: middle;
+
+ &:hover, &:focus {
+ text-decoration: none;
+ background-color: @color-actions-hover-bg;
+ color: @color-actions-hover-text;
+ border: none;
+ padding: 15px 17px;
+ }
+
+ &:active {
+ background: @color-actions-active-bg;
+ color: @color-actions-active-text;
+ }
+
+ i {
+ margin-right: 10px;
+ font-size: 14px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm
new file mode 100644
index 0000000..ecda30b
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action.htm
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm
new file mode 100644
index 0000000..5ca84f8
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_action_settings_form.htm
@@ -0,0 +1,77 @@
+= Form::open(['id' => 'propertyForm']) ?>
+
+
+
+ fatalError): ?>
+
+
+
+
+
+
+
+
+ = $actionFormWidget->render() ?>
+
+
+ = $this->makePartial('available_tags') ?>
+
+ = $this->makePartial('schedule') ?>
+
+
+
+ = $this->makePartial('schedule') ?>
+
+
+
+
+
+
+
+
= e(trans($this->fatalError)) ?>
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm
new file mode 100644
index 0000000..afabf2d
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions.htm
@@ -0,0 +1,31 @@
+
+
+
+
+
+ = e(__('ON')) ?>
+
+
+
+ = e($formModel->getEventName()) ?>
+
+
+
+
+
+ = $this->makePartial('action', ['action' => $action]) ?>
+
+
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm
new file mode 100644
index 0000000..3e50fe9
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_actions_container.htm
@@ -0,0 +1,14 @@
+
+
+ = $this->makePartial('actions', ['actions' => $actions]) ?>
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm
new file mode 100644
index 0000000..324de43
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_available_tags.htm
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm
new file mode 100644
index 0000000..01b04d9
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_create_action_form.htm
@@ -0,0 +1,46 @@
+= Form::open(['id' => 'addRuleActionForm']) ?>
+
+
+
+
+
+
+ fatalError): ?>
+
= $fatalError ?>
+
+
+
+
+
+
+
+
+
+= Form::close() ?>
diff --git a/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm
new file mode 100644
index 0000000..d011fde
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/actionbuilder/partials/_schedule.htm
@@ -0,0 +1,22 @@
+
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css
new file mode 100644
index 0000000..84cba64
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/css/conditions.css
@@ -0,0 +1,227 @@
+ul.condition-set {
+ padding: 5px 0 0;
+ margin: 0;
+ list-style: none;
+}
+ul.condition-set ul {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+ul.condition-set li {
+ margin: 0;
+ padding: 0;
+}
+ul.condition-set .condition-item {
+ border-radius: 4px;
+}
+ul.condition-set .condition-item.is-root {
+ border-radius: 0;
+}
+ul.condition-set .condition-item.collapsed .compound-content {
+ border-left-color: transparent !important;
+}
+ul.condition-set .condition-item.collapsed .condition-set {
+ display: none;
+}
+ul.condition-set .condition-item.collapsed.is-inner .compound-add-item {
+ display: none;
+}
+ul.condition-set .condition-item .condition-delete {
+ position: absolute;
+ display: block;
+ width: 10px;
+ height: 10px;
+ right: 7px;
+ top: 7px;
+}
+ul.condition-set a.condition-collapse {
+ text-decoration: none;
+ display: block;
+ position: absolute;
+ right: 8px;
+ top: 2px;
+}
+ul.condition-set a.condition-collapse > i {
+ -webkit-transition: transform 0.3s;
+ transition: transform 0.3s;
+ width: 15px;
+ height: 15px;
+ color: #bdc3c7;
+ font-size: 12px;
+ display: block;
+}
+ul.condition-set a.condition-collapse:hover > i {
+ color: #999;
+}
+ul.condition-set li.collapsed a.condition-collapse > i {
+ -webkit-transform: scale(1, -1);
+ -ms-transform: scale(1, -1);
+ transform: scale(1, -1);
+}
+ul.condition-set .compound-join {
+ text-align: center;
+ padding: 3px 0 2px 0;
+}
+ul.condition-set .compound-join a {
+ color: #333333;
+ text-decoration: none;
+}
+ul.condition-set .compound-join a:hover {
+ text-decoration: underline;
+}
+ul.condition-set .condition-item.is-single {
+ padding: 8px 20px 9px 24px;
+ background: #fff;
+ border: 1px solid #eee;
+ position: relative;
+}
+ul.condition-set .condition-item.is-single:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ text-decoration: inherit;
+ -webkit-font-smoothing: antialiased;
+ *margin-right: .3em;
+ content: "\f126";
+ line-height: 100%;
+ font-size: 16px;
+ color: #bdc3c7;
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ left: 12px;
+ top: 11px;
+}
+ul.condition-set .condition-item.is-single .condition-text {
+ padding-left: 10px;
+}
+ul.condition-set .condition-item.is-single .condition-text a {
+ color: #333333;
+ text-decoration: none;
+}
+ul.condition-set .condition-item.is-single .condition-text a:hover {
+ text-decoration: underline;
+}
+ul.condition-set .condition-item.is-single .condition-text a span.operator {
+ font-weight: 500;
+}
+ul.condition-set .condition-item.is-single a.condition-delete {
+ right: 12px;
+ top: 9px;
+}
+ul.condition-set .condition-item.is-compound .compound-content {
+ position: relative;
+ border-left: 1px solid #dbdee0;
+ margin-left: 5px;
+}
+ul.condition-set .condition-item.is-compound .compound-content:before {
+ color: #bdc3c7;
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ text-decoration: inherit;
+ -webkit-font-smoothing: antialiased;
+ *margin-right: .3em;
+ content: "\f111";
+ font-size: 8px;
+ position: absolute;
+ left: -4px;
+ top: -2px;
+}
+ul.condition-set .condition-item.is-compound .compound-content h4 {
+ padding: 0 30px 5px 5px;
+ margin: 0;
+ position: relative;
+ top: -5px;
+ font-size: 13px;
+ line-height: 130%;
+ font-weight: 400;
+ color: #666;
+}
+ul.condition-set .condition-item.is-compound .compound-content h4 a.condition-text {
+ color: #333333;
+ padding-left: 10px;
+}
+ul.condition-set .condition-item.is-compound .compound-content h4 a.condition-text:hover {
+ text-decoration: underline;
+}
+ul.condition-set .condition-item.is-compound .compound-content .condition-set {
+ padding-left: 15px;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item {
+ position: relative;
+ margin-top: 10px;
+ margin-left: 20px;
+ border: 2px dotted #e0e0e0;
+ border-radius: 5px;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:before {
+ color: #bdc3c7;
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ text-decoration: inherit;
+ -webkit-font-smoothing: antialiased;
+ *margin-right: .3em;
+ content: "\f067";
+ font-size: 16px;
+ position: absolute;
+ left: -23px;
+ top: -11px;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item > a {
+ color: #bdc3c7;
+ text-align: center;
+ display: block;
+ text-decoration: none;
+ padding: 13px 15px;
+ text-transform: uppercase;
+ font-weight: 600;
+ font-size: 12px;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:hover,
+ul.condition-set .condition-item.is-compound .compound-add-item:focus {
+ background-color: #4ea5e0;
+ border-color: #4ea5e0;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:hover:before,
+ul.condition-set .condition-item.is-compound .compound-add-item:focus:before {
+ color: #999;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:hover > a,
+ul.condition-set .condition-item.is-compound .compound-add-item:focus > a {
+ color: #ffffff;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:active {
+ background: #3498db;
+ border-color: #3498db;
+}
+ul.condition-set .condition-item.is-compound .compound-add-item:active > a {
+ color: #ffffff;
+}
+ul.condition-set .condition-item.is-compound.is-inner {
+ padding-right: 20px;
+}
+ul.condition-set .condition-item.is-compound.is-inner a.condition-delete-compound {
+ right: -7px;
+ top: 0;
+}
+.condition-multi-value .condition-filter-search {
+ background-position: right -81px !important;
+ border-top: none !important;
+ border-left: none !important;
+ border-right: none !important;
+ border-radius: 0;
+ padding-left: 20px;
+}
+.condition-multi-value .filter-list {
+ margin: 0 -20px;
+}
+.condition-multi-value .filter-list .list-footer {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+.condition-multi-value .added-filter-list {
+ margin: 0 -20px;
+}
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js
new file mode 100644
index 0000000..e4a7379
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.js
@@ -0,0 +1,184 @@
+/*
+ * Global helpers
+ */
+function showConditionSettings(id) {
+ var $control = $('[data-condition-id='+id+']').closest('[data-control="ruleconditions"]')
+
+ $control.ruleConditions('onShowNewConditionSettings', id)
+}
+
+/*
+ * Plugin definition
+ */
++function ($) { "use strict";
+ var Base = $.oc.foundation.base,
+ BaseProto = Base.prototype
+
+ var RuleConditions = function (element, options) {
+ this.$el = $(element)
+ this.options = options || {}
+
+ $.oc.foundation.controlUtils.markDisposable(element)
+ Base.call(this)
+ this.init()
+ }
+
+ RuleConditions.prototype = Object.create(BaseProto)
+ RuleConditions.prototype.constructor = RuleConditions
+
+ RuleConditions.prototype.init = function() {
+ this.$el.on('click', '[data-conditions-collapse]', this.proxy(this.onConditionsToggle))
+ this.$el.on('click', '[data-conditions-settings]', this.proxy(this.onShowSettings))
+ this.$el.on('click', '[data-conditions-create]', this.proxy(this.onCreateChildCondition))
+ this.$el.on('click', '[data-conditions-delete]', this.proxy(this.onDeleteCondition))
+ this.$el.one('dispose-control', this.proxy(this.dispose))
+ }
+
+ RuleConditions.prototype.dispose = function() {
+ this.$el.off('click', '[data-conditions-collapse]', this.proxy(this.onConditionsToggle))
+ this.$el.off('click', '[data-conditions-settings]', this.proxy(this.onShowSettings))
+ this.$el.off('click', '[data-conditions-create]', this.proxy(this.onCreateChildCondition))
+ this.$el.off('click', '[data-conditions-delete]', this.proxy(this.onDeleteCondition))
+ this.$el.off('dispose-control', this.proxy(this.dispose))
+ this.$el.removeData('oc.ruleConditions')
+
+ this.$el = null
+
+ // In some cases options could contain callbacks,
+ // so it's better to clean them up too.
+ this.options = null
+
+ BaseProto.dispose.call(this)
+ }
+
+ RuleConditions.prototype.onDeleteCondition = function(event) {
+ var $el = $(event.target),
+ conditionId = getConditionIdFromElement($el)
+
+ $el.request(this.options.deleteHandler, {
+ data: { current_condition_id: conditionId },
+ confirm: 'Do you really want to delete this condition?'
+ })
+ }
+
+ RuleConditions.prototype.onCreateChildCondition = function(event) {
+ var $el = $(event.target),
+ conditionId = getConditionIdFromElement($el)
+
+ $el.popup({
+ handler: this.options.createHandler,
+ extraData: { current_condition_id: conditionId },
+ size: 'large'
+ })
+
+ return false
+ }
+
+ RuleConditions.prototype.onConditionsToggle = function(event) {
+ var $el = $(event.target),
+ $item = $el.closest('li'),
+ newStatusValue = $item.hasClass('collapsed') ? 0 : 1,
+ conditionId = getConditionIdFromElement($el)
+
+ $el.request(this.options.collapseHandler, {
+ data: {
+ status: newStatusValue,
+ group: conditionId
+ }
+ })
+
+ if (newStatusValue) {
+ $el.parents('li:first').addClass('collapsed');
+ }
+ else {
+ $el.parents('li:first').removeClass('collapsed');
+ }
+
+ return false
+ }
+
+ RuleConditions.prototype.onShowNewConditionSettings = function(conditionId) {
+ var $el = $('[data-condition-id='+conditionId+']')
+
+ $el.popup({
+ handler: this.options.settingsHandler,
+ extraData: { current_condition_id: conditionId },
+ size: 'large'
+ })
+
+ // This will not fire on successful save because the target element
+ // is replaced by the time the popup loader has finished to call it
+ $el.one('hide.oc.popup', this.proxy(this.onCancelCondition))
+ }
+
+ RuleConditions.prototype.onCancelCondition = function(event) {
+ var $el = $(event.target),
+ conditionId = getConditionIdFromElement($el)
+
+ $el.request(this.options.cancelHandler, {
+ data: { new_condition_id: conditionId }
+ })
+
+ return false
+ }
+
+ RuleConditions.prototype.onShowSettings = function(event) {
+ var $el = $(event.target),
+ conditionId = getConditionIdFromElement($el)
+
+ $el.popup({
+ handler: this.options.settingsHandler,
+ extraData: { current_condition_id: conditionId },
+ size: 'large'
+ })
+
+ return false
+ }
+
+ function getConditionIdFromElement($el) {
+ var $item = $el.closest('li.condition-item')
+
+ return $item.data('condition-id')
+ }
+
+ RuleConditions.DEFAULTS = {
+ collapseHandler: null,
+ settingsHandler: null,
+ deleteHandler: null,
+ cancelHandler: null,
+ createHandler: null
+ }
+
+ // PLUGIN DEFINITION
+ // ============================
+
+ var old = $.fn.ruleConditions
+
+ $.fn.ruleConditions = function (option) {
+ var args = Array.prototype.slice.call(arguments, 1), items, result
+
+ items = this.each(function () {
+ var $this = $(this)
+ var data = $this.data('oc.ruleConditions')
+ var options = $.extend({}, RuleConditions.DEFAULTS, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('oc.ruleConditions', (data = new RuleConditions(this, options)))
+ if (typeof option == 'string') result = data[option].apply(data, args)
+ if (typeof result != 'undefined') return false
+ })
+
+ return result ? result : items
+ }
+
+ $.fn.ruleConditions.Constructor = RuleConditions
+
+ $.fn.ruleConditions.noConflict = function () {
+ $.fn.ruleConditions = old
+ return this
+ }
+
+ // Add this only if required
+ $(document).render(function (){
+ $('[data-control="ruleconditions"]').ruleConditions()
+ })
+
+}(window.jQuery);
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js
new file mode 100644
index 0000000..f87e2f8
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/js/conditions.multivalue.js
@@ -0,0 +1,139 @@
+/*
+ * Plugin definition
+ */
++function ($) { "use strict";
+ var Base = $.oc.foundation.base,
+ BaseProto = Base.prototype
+
+ var ConditionMultiValue = function (element, options) {
+ this.$el = $(element)
+ this.options = options || {}
+ this.$template = $('[data-record-template]', this.$el)
+ this.$emptyTemplate = $('[data-empty-record-template]', this.$el)
+ this.$addedRecords = $('[data-added-records]', this.$el)
+
+ $.oc.foundation.controlUtils.markDisposable(element)
+ Base.call(this)
+
+ this.init()
+ this.recompileData()
+ }
+
+ ConditionMultiValue.prototype = Object.create(BaseProto)
+ ConditionMultiValue.prototype.constructor = ConditionMultiValue
+
+ ConditionMultiValue.prototype.init = function() {
+ this.$el.on('click', '[data-multivalue-add-record]', this.proxy(this.onAddRecord))
+ this.$el.on('click', '[data-multivalue-remove-record]', this.proxy(this.onRemoveRecord))
+ this.$el.one('dispose-control', this.proxy(this.dispose))
+ }
+
+ ConditionMultiValue.prototype.dispose = function() {
+ this.$el.off('click', '[data-multivalue-add-record]', this.proxy(this.onAddRecord))
+ this.$el.off('click', '[data-multivalue-remove-record]', this.proxy(this.onRemoveRecord))
+ this.$el.off('dispose-control', this.proxy(this.dispose))
+ this.$el.removeData('oc.conditionMultiValue')
+
+ this.$el = null
+
+ // In some cases options could contain callbacks,
+ // so it's better to clean them up too.
+ this.options = null
+
+ BaseProto.dispose.call(this)
+ }
+
+ ConditionMultiValue.prototype.onAddRecord = function(event) {
+ var $el = $(event.target),
+ recordId = $el.closest('[data-record-key]').data('record-key'),
+ recordValue = $el.closest('a').data('record-value')
+
+ if (!!$('[data-record-key='+recordId+']', this.$addedRecords).length) {
+ return
+ }
+
+ this.$addedRecords.append(this.renderTemplate({
+ key: recordId,
+ value: recordValue
+ }))
+
+ this.recompileData()
+ }
+
+ ConditionMultiValue.prototype.onRemoveRecord = function(event) {
+ var $el = $(event.target),
+ recordId = $el.closest('[data-record-key]').data('record-key')
+
+ $('[data-record-key='+recordId+']', this.$addedRecords).remove()
+
+ this.recompileData()
+ }
+
+ ConditionMultiValue.prototype.recompileData = function(params) {
+ var $recordElements = $('[data-record-key]', this.$addedRecords),
+ hasData = !!$recordElements.length
+
+ if (hasData) {
+ $('[data-no-record-data]', this.$addedRecords).remove()
+ }
+ else {
+ this.$addedRecords.append(this.renderEmptyTemplate())
+ }
+
+ if (this.options.dataLocker) {
+ var $locker = $(this.options.dataLocker),
+ selectedIds = []
+
+ $recordElements.each(function(key, record) {
+ selectedIds.push($(record).data('record-key'))
+ })
+
+ $locker.val(selectedIds.join(','))
+ }
+ }
+
+ ConditionMultiValue.prototype.renderTemplate = function(params) {
+ return Mustache.render(this.$template.html(), params)
+ }
+
+ ConditionMultiValue.prototype.renderEmptyTemplate = function() {
+ return this.$emptyTemplate.html()
+ }
+
+ ConditionMultiValue.DEFAULTS = {
+ dataLocker: null
+ }
+
+ // PLUGIN DEFINITION
+ // ============================
+
+ var old = $.fn.conditionMultiValue
+
+ $.fn.conditionMultiValue = function (option) {
+ var args = Array.prototype.slice.call(arguments, 1), items, result
+
+ items = this.each(function () {
+ var $this = $(this)
+ var data = $this.data('oc.conditionMultiValue')
+ var options = $.extend({}, ConditionMultiValue.DEFAULTS, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('oc.conditionMultiValue', (data = new ConditionMultiValue(this, options)))
+ if (typeof option == 'string') result = data[option].apply(data, args)
+ if (typeof result != 'undefined') return false
+ })
+
+ return result ? result : items
+ }
+
+ $.fn.conditionMultiValue.Constructor = ConditionMultiValue
+
+ $.fn.conditionMultiValue.noConflict = function () {
+ $.fn.conditionMultiValue = old
+ return this
+ }
+
+ // Add this only if required
+ $(document).render(function (){
+ $('[data-control="condition-multivalue"]').conditionMultiValue()
+ })
+
+}(window.jQuery);
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less
new file mode 100644
index 0000000..e406b20
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/assets/less/conditions.less
@@ -0,0 +1,286 @@
+@import "../../../../../../../modules/backend/assets/less/core/boot.less";
+
+//
+// Conditions
+// --------------------------------------------------
+
+ul.condition-set {
+ padding: 5px 0 0;
+ margin: 0;
+ list-style: none;
+
+ ul {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ }
+
+ li {
+ margin: 0;
+ padding: 0;
+ }
+
+ .condition-item {
+ border-radius: 4px;
+
+ &.is-root {
+ border-radius: 0;
+ }
+
+ &.is-inner {}
+
+ &.collapsed {
+ .compound-content {
+ border-left-color: transparent !important;
+ }
+
+ .condition-set {
+ display: none;
+ }
+
+ &.is-inner .compound-add-item {
+ display: none;
+ }
+ }
+
+ .condition-delete {
+ position: absolute;
+ display: block;
+ width: 10px;
+ height: 10px;
+ right: 7px;
+ top: 7px;
+ }
+ }
+
+ //
+ // Collapse
+ //
+
+ a.condition-collapse {
+ text-decoration: none;
+ display: block;
+ position: absolute;
+ right: 8px;
+ top: 2px;
+
+ > i {
+ .transition(~'transform 0.3s');
+ width: 15px;
+ height: 15px;
+ color: #bdc3c7;
+ font-size: 12px;
+ display: block;
+ }
+
+ &:hover {
+ > i {
+ color: #999;
+ }
+ }
+ }
+
+ li.collapsed a.condition-collapse {
+ > i {
+ .transform(scale(1,-1));
+ }
+ }
+
+ //
+ // Joins
+ //
+
+ .compound-join {
+ text-align: center;
+ padding: 3px 0 2px 0;
+ a {
+ color: #333333;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ //
+ // Single
+ //
+
+ .condition-item.is-single {
+ padding: 8px 20px 9px 24px;
+ background: #fff;
+ border: 1px solid #eee;
+ position: relative;
+
+ &:before {
+ .icon(@code-fork);
+ line-height: 100%;
+ font-size: 16px;
+ color: #bdc3c7;
+
+ position: absolute;
+ width: 15px;
+ height: 15px;
+ left: 12px;
+ top: 11px;
+ }
+
+ .condition-text {
+ padding-left: 10px;
+
+ a {
+ color: #333333;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+
+ span.operator {
+ font-weight: 500;
+ }
+ }
+ }
+
+ a.condition-delete {
+ right: 12px;
+ top: 9px;
+ }
+ }
+
+ //
+ // Compound
+ //
+
+ .condition-item.is-compound {
+ .compound-content {
+ position: relative;
+ border-left: 1px solid #dbdee0;
+ margin-left: 5px;
+
+ &:before {
+ color: #bdc3c7;
+ .icon(@circle);
+ font-size: 8px;
+ position: absolute;
+ left: -4px;
+ top: -2px;
+ }
+
+ h4 {
+ padding: 0 30px 5px 5px;
+ margin: 0;
+ position: relative;
+ top: -5px;
+ font-size: 13px;
+ line-height: 130%;
+ font-weight: 400;
+ color: #666;
+ a.condition-text {
+ color: #333333;
+ padding-left: 10px;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .condition-set {
+ padding-left: 15px;
+ }
+ }
+
+ .condition-item.is-compound {
+
+ }
+
+ .compound-add-item {
+ position: relative;
+ margin-top: 10px;
+ margin-left: 20px;
+ border: 2px dotted #e0e0e0;
+ border-radius: 5px;
+
+ &:before {
+ color: #bdc3c7;
+ .icon(@plus);
+ font-size: 16px;
+ position: absolute;
+ left: -23px;
+ top: -11px;
+ }
+
+ > a {
+ color: #bdc3c7;
+ text-align: center;
+ display: block;
+ text-decoration: none;
+ padding: 13px 15px;
+ text-transform: uppercase;
+ font-weight: 600;
+ font-size: @font-size-base - 2;
+ }
+
+ &:hover, &:focus {
+ background-color: @highlight-hover-bg;
+ border-color: @highlight-hover-bg;
+
+ &:before {
+ color: #999;
+ }
+
+ > a {
+ color: @highlight-hover-text;
+ }
+ }
+
+ &:active {
+ background: @highlight-active-bg;
+ border-color: @highlight-active-bg;
+ > a {
+ color: @highlight-active-text;
+ }
+ }
+ }
+ }
+
+ //
+ // Compound (Inner)
+ //
+
+ .condition-item.is-compound.is-inner {
+ padding-right: 20px;
+
+ a.condition-delete-compound {
+ right: -7px;
+ top: 0;
+ }
+ }
+}
+
+//
+// Multi value
+//
+
+.condition-multi-value {
+ .condition-filter-search {
+ background-position: right -81px !important;
+ border-top: none !important;
+ border-left: none !important;
+ border-right: none !important;
+ border-radius: 0;
+ padding-left: 20px;
+ }
+
+ .filter-list {
+ margin: 0 -20px;
+
+ .list-footer {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ }
+
+ .added-filter-list {
+ margin: 0 -20px;
+ }
+}
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm
new file mode 100644
index 0000000..95bb61d
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition.htm
@@ -0,0 +1,99 @@
+isCompound();
+ $isRoot = $treeLevel == 0;
+ $collapsed = $this->getCollapseStatus($condition->id, false);
+?>
+
+
+
+
+
+
+
+ children()->withDeferred($this->sessionKey.'_'.$condition->id)->get();
+ $lastIndex = $children->count() - 1;
+ ?>
+ $childCondition): ?>
+ = $this->makePartial('condition', [
+ 'condition' => $childCondition,
+ 'treeLevel' => $treeLevel + 1,
+ 'parentCondition' => $condition
+ ]) ?>
+
+
+
+ = e($this->getCacheConditionJoinText($condition)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm
new file mode 100644
index 0000000..77e0d84
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_condition_settings_form.htm
@@ -0,0 +1,68 @@
+= Form::open(['id' => 'propertyForm']) ?>
+
+
+
+ fatalError): ?>
+
+
+
+
+
+
+ = $conditionFormWidget->render() ?>
+
+
+
+
+
+
+
= e(trans($this->fatalError)) ?>
+
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm
new file mode 100644
index 0000000..8cc8c4b
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions.htm
@@ -0,0 +1,10 @@
+
+ = $this->makePartial('condition', [
+ 'condition' => $condition,
+ 'treeLevel' => 0
+ ]) ?>
+
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm
new file mode 100644
index 0000000..737b072
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_conditions_container.htm
@@ -0,0 +1,15 @@
+
+
+ = $this->makePartial('conditions', ['condition' => $rootCondition]) ?>
+
+
+
+
diff --git a/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm
new file mode 100644
index 0000000..3e9e863
--- /dev/null
+++ b/plugins/rainlab/notify/formwidgets/conditionbuilder/partials/_create_child_form.htm
@@ -0,0 +1,67 @@
+= Form::open(['id' => 'propertyForm']) ?>
+
+
+
+ fatalError): ?>
+
+
+
+
+
+
+ = e(__('Condition')) ?>
+
+
+ $class): ?>
+
+ = e($name) ?>
+
+
+ $subClass): ?>
+ = e($subName) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
= e(trans($this->fatalError)) ?>
+
+
+
+
+
+= Form::close() ?>
diff --git a/plugins/rainlab/notify/interfaces/Action.php b/plugins/rainlab/notify/interfaces/Action.php
new file mode 100644
index 0000000..011cc13
--- /dev/null
+++ b/plugins/rainlab/notify/interfaces/Action.php
@@ -0,0 +1,31 @@
+ [],
+ ];
+
+ /**
+ * Mark the notification as read.
+ *
+ * @return void
+ */
+ public function markAsRead()
+ {
+ if (is_null($this->read_at)) {
+ $this->forceFill(['read_at' => $this->freshTimestamp()])->save();
+ }
+ }
+
+ /**
+ * Determine if a notification has been read.
+ *
+ * @return bool
+ */
+ public function read()
+ {
+ return $this->read_at !== null;
+ }
+
+ /**
+ * Determine if a notification has not been read.
+ *
+ * @return bool
+ */
+ public function unread()
+ {
+ return $this->read_at === null;
+ }
+
+ /**
+ * Get the entity's unread notifications.
+ */
+ public function scopeApplyUnread($query)
+ {
+ return $query->whereNull('read_at');
+ }
+
+ /**
+ * Get the entity's read notifications.
+ */
+ public function scopeApplyRead($query)
+ {
+ return $query->whereNotNull('read_at');
+ }
+
+ /**
+ * Get the parsed body of the announcement.
+ *
+ * @return string
+ */
+ public function getParsedBodyAttribute()
+ {
+ return Markdown::parse($this->body);
+ }
+}
diff --git a/plugins/rainlab/notify/models/NotificationRule.php b/plugins/rainlab/notify/models/NotificationRule.php
new file mode 100644
index 0000000..8db57a4
--- /dev/null
+++ b/plugins/rainlab/notify/models/NotificationRule.php
@@ -0,0 +1,289 @@
+ 'required'
+ ];
+
+ /**
+ * @var array Relations
+ */
+ public $hasMany = [
+ 'rule_conditions' => [
+ RuleCondition::class,
+ 'key' => 'rule_host_id',
+ 'conditions' => 'rule_parent_id is null',
+ 'delete' => true
+ ],
+ 'rule_actions' => [
+ RuleAction::class,
+ 'key' => 'rule_host_id',
+ 'delete' => true
+ ],
+ ];
+
+ /**
+ * Kicks off this notification rule, fires the event to obtain its parameters,
+ * checks the rule conditions evaluate as true, then spins over each action.
+ */
+ public function triggerRule()
+ {
+ $params = $this->getEventObject()->getParams();
+ $rootCondition = $this->rule_conditions->first();
+
+ if ($rootCondition && !$rootCondition->getConditionObject()->isTrue($params)) {
+ return false;
+ }
+
+ foreach ($this->rule_actions as $action) {
+ $action->setRelation('notification_rule', $this);
+ $action->triggerAction($params);
+ }
+ }
+
+ /**
+ * Returns extra conditions provided by the event.
+ * @return array
+ */
+ public function getExtraConditionRules()
+ {
+ $rules = [];
+
+ $classes = $this->getEventObject()->defineConditions();
+
+ foreach ($classes as $class) {
+ $rules[$class] = new $class;
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Extends this class with the event class
+ * @param string $class Class name
+ * @return boolean
+ */
+ public function applyEventClass($class = null)
+ {
+ if (!$class) {
+ $class = $this->class_name;
+ }
+
+ if (!$class) {
+ return false;
+ }
+
+ if (!$this->isClassExtendedWith($class)) {
+ $this->extendClassWith($class);
+ }
+
+ $this->class_name = $class;
+ $this->event_name = Lang::get(array_get($this->eventDetails(), 'name', 'Unknown'));
+ return true;
+ }
+
+ /**
+ * Returns the event class extension object.
+ * @return \RainLab\Notify\Classes\NotificationEvent
+ */
+ public function getEventObject()
+ {
+ $this->applyEventClass();
+
+ return $this->asExtension($this->getEventClass());
+ }
+
+ public function getEventClass()
+ {
+ return $this->class_name;
+ }
+
+ //
+ // Events
+ //
+
+ public function afterFetch()
+ {
+ $this->applyEventClass();
+ }
+
+ public function beforeValidate()
+ {
+ if (!$this->applyEventClass()) {
+ return;
+ }
+ }
+
+ //
+ // Scopes
+ //
+
+ public function scopeApplyEnabled($query)
+ {
+ return $query->where('is_enabled', true);
+ }
+
+ public function scopeApplyClass($query, $class)
+ {
+ if (!is_string($class)) {
+ $class = get_class($class);
+ }
+
+ return $query->where('class_name', $class);
+ }
+
+ //
+ // Presets
+ //
+
+ /**
+ * Returns an array of rule codes and descriptions.
+ * @return array
+ */
+ public static function listRulesForEvent($eventClass)
+ {
+ $results = [];
+
+ $dbRules = self::applyClass($eventClass)->get();
+ $presets = (array) EventBase::findEventPresetsByClass($eventClass);
+
+ foreach ($dbRules as $dbRule) {
+ if ($dbRule->code) {
+ unset($presets[$dbRule->code]);
+ }
+
+ if ($dbRule->is_enabled) {
+ $results[] = $dbRule;
+ }
+ }
+
+ foreach ($presets as $code => $preset) {
+ if ($newPreset = self::createFromPreset($code, $preset)) {
+ $results[] = $newPreset;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Syncronise all file-based presets to the database.
+ * @return void
+ */
+ public static function syncAll()
+ {
+ $presets = (array) EventBase::findEventPresets();
+ $dbRules = self::where('code', '!=', '')->whereNotNull('code')->lists('is_custom', 'code');
+ $newRules = array_diff_key($presets, $dbRules);
+
+ /*
+ * Clean up non-customized templates
+ */
+ foreach ($dbRules as $code => $isCustom) {
+ if ($isCustom) {
+ continue;
+ }
+
+ if (!array_key_exists($code, $presets) && ($record = self::whereCode($code)->first())) {
+ $record->delete();
+ }
+ }
+
+ /*
+ * Create new rules
+ */
+ foreach ($newRules as $code => $preset) {
+ self::createFromPreset($code, $preset);
+ }
+ }
+
+ public static function createFromPreset($code, $preset)
+ {
+ $actions = array_get($preset, 'items');
+ if (!$actions || !is_array($actions)) {
+ return;
+ }
+
+ $newRule = new self;
+ $newRule->code = $code;
+ $newRule->is_enabled = 1;
+ $newRule->is_custom = 0;
+ $newRule->name = array_get($preset, 'name');
+ $newRule->class_name = array_get($preset, 'event');
+ $newRule->forceSave();
+
+ // Add the actions
+ foreach ($actions as $action) {
+ $params = array_except($action, 'action');
+
+ $newAction = new RuleAction;
+ $newAction->class_name = array_get($action, 'action');
+ $newAction->notification_rule = $newRule;
+ $newAction->fill($params);
+ $newAction->forceSave();
+ }
+
+ // Add the conditions
+ $conditions = array_get($preset, 'conditions');
+ if (!$conditions || !is_array($conditions)) {
+ return $newRule;
+ }
+
+ // Create the root condition
+ $rootCondition = new RuleCondition();
+ $rootCondition->rule_host_type = ConditionBase::TYPE_ANY;
+ $rootCondition->class_name = $rootCondition->getRootConditionClass();
+ $rootCondition->notification_rule = $newRule;
+ $rootCondition->save();
+
+ // Add the sub conditions
+ foreach ($conditions as $condition) {
+ $params = array_except($condition, 'condition');
+ $newCondition = new RuleCondition();
+ $newCondition->class_name = array_get($condition, 'condition');
+ $newCondition->parent = $rootCondition;
+ $newCondition->fill($params);
+ $newCondition->forceSave();
+ }
+
+ return $newRule;
+ }
+}
diff --git a/plugins/rainlab/notify/models/RuleAction.php b/plugins/rainlab/notify/models/RuleAction.php
new file mode 100644
index 0000000..7d4b42c
--- /dev/null
+++ b/plugins/rainlab/notify/models/RuleAction.php
@@ -0,0 +1,201 @@
+ [NotificationRule::class, 'key' => 'rule_host_id'],
+ ];
+
+ public function triggerAction($params, $scheduled = true)
+ {
+ try {
+ $actionObject = $this->getActionObject();
+
+ // Check if action is muted in which case we don't proceed sending a notification
+ if (method_exists($actionObject, 'isMuted') && $actionObject->isMuted()) {
+ return;
+ }
+
+ // Apply action schedule
+ if ($scheduled && $schedule = $this->getSchedule()) {
+ // We delay the execution using Queues. When dequeued
+ // ScheduledAction will call this triggerAction
+ // function with $scheduled=false
+ Queue::later($schedule, new ScheduledAction($this, $params));
+ }
+ else {
+ // We trigger the action
+ $actionObject->triggerAction($params);
+ }
+ }
+ catch (Exception $ex) {
+ // We could log the error here, for now we should suppress
+ // any exceptions to let other actions proceed as normal
+ traceLog('Error with ' . $this->getActionClass());
+ traceLog($ex);
+ }
+ }
+
+ /**
+ * Extends this model with the action class
+ * @param string $class Class name
+ * @return boolean
+ */
+ public function applyActionClass($class = null)
+ {
+ if (!$class) {
+ $class = $this->class_name;
+ }
+
+ if (!$class) {
+ return false;
+ }
+
+ if (!$this->isClassExtendedWith($class)) {
+ $this->extendClassWith($class);
+ }
+
+ $this->class_name = $class;
+ return true;
+ }
+
+ public function beforeSave()
+ {
+ $this->setCustomData();
+ }
+
+ public function afterSave()
+ {
+ // Make sure that this record is removed from the DB after being removed from a rule
+ $removedFromRule = $this->rule_host_id === null && $this->getOriginal('rule_host_id');
+ if ($removedFromRule && !$this->notification_rule()->withDeferred(post('_session_key'))->exists()) {
+ $this->delete();
+ }
+ }
+
+ public function applyCustomData()
+ {
+ $this->setCustomData();
+ $this->loadCustomData();
+ }
+
+ protected function loadCustomData()
+ {
+ $this->setRawAttributes((array) $this->getAttributes() + (array) $this->config_data, true);
+ }
+
+ protected function setCustomData()
+ {
+ if (!$actionObj = $this->getActionObject()) {
+ throw new SystemException(sprintf('Unable to find action object [%s]', $this->getActionClass()));
+ }
+
+ /*
+ * Spin over each field and add it to config_data
+ */
+
+ $metaAttributes = [
+ 'action_text',
+ 'schedule_type',
+ 'schedule_delay',
+ 'schedule_delay_factor',
+ ];
+
+ $formAttributes = [];
+ $config = $actionObj->getFieldConfig();
+ if (isset($config->fields)) {
+ $formAttributes = array_keys($config->fields);
+ }
+
+ $fieldAttributes = array_merge($formAttributes, $metaAttributes);
+
+ $dynamicAttributes = array_only($this->getAttributes(), $fieldAttributes);
+ $this->config_data = $dynamicAttributes;
+
+ $this->setRawAttributes(array_except($this->getAttributes(), $fieldAttributes));
+ }
+
+ public function afterFetch()
+ {
+ $this->applyActionClass();
+ $this->loadCustomData();
+ }
+
+ public function getText()
+ {
+ if (strlen($this->action_text)) {
+ return $this->action_text;
+ }
+
+ if ($actionObj = $this->getActionObject()) {
+ return $actionObj->getText();
+ }
+ }
+
+ public function getSchedule()
+ {
+ if ($this->schedule_type !== 'delayed') {
+ return false;
+ }
+
+ if (!is_numeric($this->schedule_delay) || !is_numeric($this->schedule_delay_factor)) {
+ return false;
+ }
+
+ $delay = (int) $this->schedule_delay;
+ $delay_factor = (int) $this->schedule_delay_factor;
+
+ return abs($delay * $delay_factor);
+ }
+
+ public function getActionObject()
+ {
+ $this->applyActionClass();
+
+ return $this->asExtension($this->getActionClass());
+ }
+
+ public function getActionClass()
+ {
+ return $this->class_name;
+ }
+}
diff --git a/plugins/rainlab/notify/models/RuleCondition.php b/plugins/rainlab/notify/models/RuleCondition.php
new file mode 100644
index 0000000..0282748
--- /dev/null
+++ b/plugins/rainlab/notify/models/RuleCondition.php
@@ -0,0 +1,172 @@
+ [self::class, 'key' => 'rule_parent_id', 'delete' => true],
+ ];
+
+ public $belongsTo = [
+ 'parent' => [self::class, 'key' => 'rule_parent_id'],
+ 'notification_rule' => [NotificationRule::class, 'key'=>'rule_host_id']
+ ];
+
+ public function filterFields($fields, $context)
+ {
+ /*
+ * Let the condition contribute
+ */
+ $this->getConditionObject()->setFormFields($fields);
+ }
+
+ /**
+ * Extends this model with the condition class
+ * @param string $class Class name
+ * @return boolean
+ */
+ public function applyConditionClass($class = null)
+ {
+ if (!$class) {
+ $class = $this->class_name;
+ }
+
+ if (!$class) {
+ return false;
+ }
+
+ if (!$this->isClassExtendedWith($class)) {
+ $this->extendClassWith($class);
+ }
+
+ $this->class_name = $class;
+ return true;
+ }
+
+ public function beforeSave()
+ {
+ $this->setCustomData();
+ }
+
+ public function applyCustomData()
+ {
+ $this->setCustomData();
+ $this->loadCustomData();
+ }
+
+ protected function loadCustomData()
+ {
+ $this->setRawAttributes((array) $this->getAttributes() + (array) $this->config_data, true);
+ }
+
+ protected function setCustomData()
+ {
+ /*
+ * Let the condition contribute
+ */
+ $this->getConditionObject()->setCustomData($this);
+
+ /*
+ * Spin over each field and add it to config_data
+ */
+ $config = $this->getFieldConfig();
+
+ if (!isset($config->fields)) {
+ throw new SystemException('Condition class has no fields.');
+ }
+
+ $staticAttributes = ['condition_text'];
+
+ $fieldAttributes = array_merge($staticAttributes, array_keys($config->fields));
+
+ $dynamicAttributes = array_only($this->getAttributes(), $fieldAttributes);
+
+ $this->config_data = $dynamicAttributes;
+
+ $this->setRawAttributes(array_except($this->getAttributes(), $fieldAttributes));
+ }
+
+ public function afterFetch()
+ {
+ $this->applyConditionClass();
+ $this->loadCustomData();
+ }
+
+ public function afterSave()
+ {
+ // Make sure that this record is removed from the DB after being removed from a rule
+ $removedFromRule = $this->rule_parent_id === null && $this->getOriginal('rule_parent_id');
+ if ($removedFromRule && !$this->notification_rule()->withDeferred(post('_session_key'))->exists()) {
+ $this->delete();
+ }
+ }
+
+ public function getText()
+ {
+ if (strlen($this->condition_text)) {
+ return $this->condition_text;
+ }
+
+ if ($conditionObj = $this->getConditionObject()) {
+ return $conditionObj->getText();
+ }
+ }
+
+ public function isCompound()
+ {
+ return $this->getConditionObject() instanceof CompoundConditionInterface;
+ }
+
+ public function getConditionObject()
+ {
+ $this->applyConditionClass();
+
+ return $this->asExtension($this->getConditionClass());
+ }
+
+ public function getConditionClass()
+ {
+ return $this->class_name;
+ }
+
+ public function getRootConditionClass()
+ {
+ return CompoundCondition::class;
+ }
+}
diff --git a/plugins/rainlab/notify/models/notificationrule/columns.yaml b/plugins/rainlab/notify/models/notificationrule/columns.yaml
new file mode 100644
index 0000000..2e298a3
--- /dev/null
+++ b/plugins/rainlab/notify/models/notificationrule/columns.yaml
@@ -0,0 +1,13 @@
+# ===================================
+# Column Definitions
+# ===================================
+
+columns:
+
+ name:
+ label: Name
+ searchable: true
+
+ code:
+ label: Code
+ searchable: true
diff --git a/plugins/rainlab/notify/models/notificationrule/fields.yaml b/plugins/rainlab/notify/models/notificationrule/fields.yaml
new file mode 100644
index 0000000..b0eec2a
--- /dev/null
+++ b/plugins/rainlab/notify/models/notificationrule/fields.yaml
@@ -0,0 +1,47 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ name:
+ label: Name
+ placeholder: New notification rule name
+ attributes:
+ autofocus: 1
+
+ toolbar:
+ type: partial
+ path: form_toolbar
+ cssClass: collapse-visible
+
+tabs:
+ stretch: true
+ cssClass: master-area
+ paneCssClass:
+ 'Actions': 'pane-compact'
+
+ fields:
+ rule_actions:
+ type: RainLab\Notify\FormWidgets\ActionBuilder
+ tab: Actions
+
+ rule_conditions:
+ type: RainLab\Notify\FormWidgets\ConditionBuilder
+ tab: Conditions
+
+ is_enabled:
+ label: Active
+ type: checkbox
+ tab: Settings
+ default: true
+
+ description:
+ label: Description
+ type: textarea
+ size: tiny
+ tab: Settings
+
+ code:
+ label: API Code
+ span: auto
+ tab: Settings
diff --git a/plugins/rainlab/notify/models/ruleaction/columns.yaml b/plugins/rainlab/notify/models/ruleaction/columns.yaml
new file mode 100644
index 0000000..b11160b
--- /dev/null
+++ b/plugins/rainlab/notify/models/ruleaction/columns.yaml
@@ -0,0 +1,8 @@
+# ===================================
+# List Column Definitions
+# ===================================
+
+columns:
+ id:
+ label: ID
+ searchable: true
diff --git a/plugins/rainlab/notify/models/ruleaction/fields.yaml b/plugins/rainlab/notify/models/ruleaction/fields.yaml
new file mode 100644
index 0000000..c611f31
--- /dev/null
+++ b/plugins/rainlab/notify/models/ruleaction/fields.yaml
@@ -0,0 +1,8 @@
+# ===================================
+# Form Field Definitions
+# ===================================
+
+fields:
+ id:
+ label: ID
+ disabled: true
diff --git a/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml b/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml
new file mode 100644
index 0000000..bd3d80c
--- /dev/null
+++ b/plugins/rainlab/notify/models/ruleaction/fields_schedule.yaml
@@ -0,0 +1,30 @@
+# ===================================
+# Form Field Definitions
+# ===================================
+
+fields:
+ schedule_type:
+ type: dropdown
+ options:
+ disabled: None
+ delayed: Delay execution
+ schedule_delay:
+ type: number
+ span: left
+ trigger:
+ action: show
+ field: schedule_type
+ condition: value[delayed]
+ schedule_delay_factor:
+ type: dropdown
+ span: right
+ options:
+ 1: Seconds
+ 60: Minutes
+ 3600: Hours
+ 86400: Days
+ 2592000: Months
+ trigger:
+ action: show
+ field: schedule_type
+ condition: value[delayed]
diff --git a/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php b/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php
new file mode 100644
index 0000000..32369ca
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/ExecutionContextCondition.php
@@ -0,0 +1,136 @@
+ 'is',
+ 'is_not' => 'is not',
+ ];
+
+ public function getName()
+ {
+ return 'Event is triggered from environment';
+ }
+
+ public function getTitle()
+ {
+ return 'Execution context';
+ }
+
+ public function getText()
+ {
+ $host = $this->host;
+ $value = $host->value;
+ $attribute = $host->subcondition;
+ $subconditions = $this->getSubconditionOptions();
+
+ $result = array_get($subconditions, $attribute, 'Execution context');
+ $result .= ' '.array_get($this->operators, $host->operator, $host->operator).' ';
+
+ if ($attribute == 'locale' || $attribute == 'environment') {
+ $result .= strtolower($value) ?: '?';
+ }
+ elseif ($value) {
+ $options = $this->getValueOptions();
+ $result .= strtolower(array_get($options, $value));
+ }
+ else {
+ $result .= '?';
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a title to use for grouping subconditions
+ * in the Create Condition drop-down menu
+ */
+ public function getGroupingTitle()
+ {
+ return 'Execution context';
+ }
+
+ public function listSubconditions()
+ {
+ return array_flip($this->getSubconditionOptions());
+ }
+
+ public function initConfigData($host)
+ {
+ $host->operator = 'is';
+ }
+
+ public function setFormFields($fields)
+ {
+ $attribute = $fields->subcondition->value;
+
+ if ($attribute == 'locale' || $attribute == 'environment') {
+ $fields->value->type = 'text';
+ }
+ else {
+ $fields->value->type = 'dropdown';
+ }
+ }
+
+ public function getValueOptions()
+ {
+ $attribute = $this->host->subcondition;
+ $result = [];
+
+ if ($attribute == 'context') {
+ $result = [
+ 'backend' => 'Back-end area',
+ 'front' => 'Front-end website',
+ 'console' => 'Command line interface',
+ ];
+ }
+
+ if ($attribute == 'theme') {
+ foreach (Theme::all() as $theme) {
+ $result[$theme->getDirName()] = $theme->getDirName();
+ }
+ }
+
+ return $result;
+ }
+
+ public function getSubconditionOptions()
+ {
+ return [
+ 'environment' => 'Application environment',
+ 'context' => 'Request context',
+ 'theme' => 'Active theme',
+ 'locale' => 'Visitor locale',
+ ];
+ }
+
+ public function getOperatorOptions()
+ {
+ return $this->operators;
+ }
+
+ /**
+ * Checks whether the condition is TRUE for specified parameters
+ * @param array $params
+ * @return bool
+ */
+ public function isTrue(&$params)
+ {
+ $hostObj = $this->host;
+ $attribute = $hostObj->subcondition;
+
+ $conditionValue = $hostObj->value;
+ $conditionValue = trim(mb_strtolower($conditionValue));
+
+ if ($attribute == 'locale') {
+ return array_get($params, 'appLocale') == $conditionValue;
+ } else if ($attribute === 'environment') {
+ return $conditionValue === \App::environment();
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php b/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php
new file mode 100644
index 0000000..6192126
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/SaveDatabaseAction.php
@@ -0,0 +1,119 @@
+ 'Store in database',
+ 'description' => 'Log event data in the notifications activity log',
+ 'icon' => 'icon-database'
+ ];
+ }
+
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ public function getText()
+ {
+ if ($this->host->related_object) {
+ $label = array_get($this->getRelatedObjectOptions(), $this->host->related_object);
+
+ return 'Log event in the '.$label.' log';
+ }
+
+ return parent::getText();
+ }
+
+ /**
+ * Triggers this action.
+ * @param array $params
+ * @return void
+ */
+ public function triggerAction($params)
+ {
+ if (
+ (!$definition = array_get($this->tableDefinitions, $this->host->related_object)) ||
+ (!$param = array_get($definition, 'param')) ||
+ (!$value = array_get($params, $param))
+ ) {
+ throw new ApplicationException('Error evaluating the save database action: the related object is not found in the action parameters.');
+ }
+
+ if (!$value instanceof EloquentModel) {
+ // @todo Perhaps value is an ID or a model array,
+ // look up model $definition[class] from ID ...
+ }
+
+ $rule = $this->host->notification_rule;
+ $relation = array_get($definition, 'relation');
+
+ $value->$relation()->create([
+ 'id' => Uuid::uuid4()->toString(),
+ 'event_type' => $rule->getEventClass(),
+ 'icon' => $this->host->icon,
+ 'type' => $this->host->type,
+ 'body' => $this->host->body,
+ 'data' => $this->getData($params),
+ 'read_at' => null,
+ ]);
+ }
+
+ /**
+ * Get the data for the notification.
+ *
+ * @param array $notifiable
+ * @return array
+ */
+ protected function getData($params)
+ {
+ // This should check for params that cannot be jsonable.
+ return $params;
+ }
+
+ public function getRelatedObjectOptions()
+ {
+ $result = [];
+
+ foreach ($this->tableDefinitions as $key => $definition) {
+ $result[$key] = array_get($definition, 'label');
+ }
+
+ return $result;
+ }
+
+ public function getTableDefinitions()
+ {
+ return $this->tableDefinitions;
+ }
+
+ public function addTableDefinition($options)
+ {
+ if (!$className = array_get($options, 'class')) {
+ throw new ApplicationException('Missing class name from table definition.');
+ }
+
+ $options = array_merge([
+ 'label' => 'Undefined table',
+ 'class' => null,
+ 'param' => null,
+ 'relation' => 'notifications',
+ ], $options);
+
+ $keyName = $className . '@' . array_get($options, 'relation');
+
+ $this->tableDefinitions[$keyName] = $options;
+ }
+}
diff --git a/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php b/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php
new file mode 100644
index 0000000..e7abf4b
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/SendMailTemplateAction.php
@@ -0,0 +1,225 @@
+ 'System default',
+ 'user' => 'User email address (if applicable)',
+ 'sender' => 'Sender user email address (if applicable)',
+ 'admin' => 'Back-end administrators',
+ 'custom' => 'Specific email address',
+ ];
+
+ /**
+ * Returns information about this event, including name and description.
+ */
+ public function actionDetails()
+ {
+ return [
+ 'name' => 'Compose a mail message',
+ 'description' => 'Send a message to a recipient',
+ 'icon' => 'icon-envelope'
+ ];
+ }
+
+ /**
+ * Triggers this action.
+ * @param array $params
+ * @return void
+ */
+ public function triggerAction($params)
+ {
+ $template = $this->host->mail_template;
+
+ $recipient = $this->getRecipientAddress($params);
+
+ $replyTo = $this->getReplyToAddress($params);
+
+ if (!$recipient || !$template) {
+ throw new ApplicationException('Missing valid recipient or mail template');
+ }
+
+ Mail::sendTo($recipient, $template, $params, function($message) use ($replyTo) {
+ if ($replyTo) {
+ $message->replyTo($replyTo);
+ }
+ });
+ }
+
+ /**
+ * Field configuration for the action.
+ */
+ public function defineFormFields()
+ {
+ return 'fields.yaml';
+ }
+
+ /**
+ * Defines validation rules for the custom fields.
+ * @return array
+ */
+ public function defineValidationRules()
+ {
+ return [
+ 'mail_template' => 'required',
+ 'send_to_mode' => 'required',
+ ];
+ }
+
+ public function getTitle()
+ {
+ if ($this->isAdminMode()) {
+ return 'Compose mail to administrators';
+ }
+
+ return parent::getTitle();
+ }
+
+ public function getActionIcon()
+ {
+ if ($this->isAdminMode()) {
+ return 'icon-envelope-square';
+ }
+
+ return parent::getActionIcon();
+ }
+
+ public function getText()
+ {
+ $hostObj = $this->host;
+
+ $recipient = array_get($this->recipientModes, $hostObj->send_to_mode);
+
+ if ($this->isAdminMode()) {
+ if ($groupId = $this->host->send_to_admin) {
+ if ($group = AdminGroupModel::find($groupId)) {
+ $adminText = $group->name;
+ }
+ else {
+ $adminText = '?';
+ }
+
+ $adminText .= ' admin group';
+ }
+ else {
+ $adminText = 'all admins';
+ }
+ return sprintf(
+ 'Send a message to %s using template %s',
+ $adminText,
+ $hostObj->mail_template
+ );
+ }
+
+ if ($hostObj->mail_template) {
+ return sprintf(
+ 'Send a message to %s using template %s',
+ mb_strtolower($recipient),
+ $hostObj->mail_template
+ );
+ }
+
+ return parent::getText();
+ }
+
+ public function getSendToAdminOptions()
+ {
+ $options = ['' => '- All administrators -'];
+
+ $groups = AdminGroupModel::lists('name', 'id');
+
+ return $options + $groups;
+ }
+
+ public function getSendToModeOptions()
+ {
+ $modes = $this->recipientModes;
+
+ unset($modes['system']);
+
+ return $modes;
+ }
+
+ public function getReplyToModeOptions()
+ {
+ $modes = $this->recipientModes;
+
+ unset($modes['admin']);
+
+ return $modes;
+ }
+
+ public function getMailTemplateOptions()
+ {
+ $codes = array_keys(MailTemplate::listAllTemplates());
+
+ $result = array_combine($codes, $codes);
+
+ // Wrap to prevent collisions with language keys
+ array_walk($result, function(&$value, $key) {
+ $value = "[$value]";
+ });
+
+ return $result;
+ }
+
+ protected function getReplyToAddress($params)
+ {
+ $mode = $this->host->reply_to_mode;
+
+ if ($mode == 'custom') {
+ return $this->host->reply_to_custom;
+ }
+
+ if ($mode == 'user' || $mode == 'sender') {
+ $obj = array_get($params, $mode);
+ return $obj->email;
+ }
+ }
+
+ protected function getRecipientAddress($params)
+ {
+ $mode = $this->host->send_to_mode;
+
+ if ($mode == 'custom') {
+ return $this->host->send_to_custom;
+ }
+
+ if ($mode == 'system') {
+ $name = Config::get('mail.from.name', 'Your Site');
+ $address = Config::get('mail.from.address', 'admin@domain.tld');
+ return [$address => $name];
+ }
+
+ if ($mode == 'admin') {
+ if ($groupId = $this->host->send_to_admin) {
+ if (!$group = AdminGroupModel::find($groupId)) {
+ throw new ApplicationException('Unable to find admin group with ID: '.$groupId);
+ }
+
+ return $group->users->lists('full_name', 'email');
+ }
+ else {
+ return AdminUserModel::all()->lists('full_name', 'email');
+ }
+ }
+
+ if ($mode == 'user' || $mode == 'sender') {
+ return array_get($params, $mode);
+ }
+ }
+
+ protected function isAdminMode()
+ {
+ return $this->host->send_to_mode == 'admin';
+ }
+}
diff --git a/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml b/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml
new file mode 100644
index 0000000..9981f87
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/executioncontextcondition/fields.yaml
@@ -0,0 +1,21 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+
+ subcondition:
+ label: Attribute
+ span: auto
+ type: dropdown
+
+ operator:
+ label: Operator
+ span: auto
+ type: dropdown
+ dependsOn: subcondition
+
+ value:
+ label: Value
+ dependsOn: [subcondition, operator]
+ type: dropdown
diff --git a/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml b/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml
new file mode 100644
index 0000000..77c144f
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/savedatabaseaction/fields.yaml
@@ -0,0 +1,8 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ related_object:
+ label: Related object
+ type: dropdown
diff --git a/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml b/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml
new file mode 100644
index 0000000..13aa548
--- /dev/null
+++ b/plugins/rainlab/notify/notifyrules/sendmailtemplateaction/fields.yaml
@@ -0,0 +1,42 @@
+# ===================================
+# Field Definitions
+# ===================================
+
+fields:
+ mail_template:
+ label: Mail template
+ type: dropdown
+ placeholder: Select template
+
+ send_to_mode:
+ label: Send to
+ type: radio
+ span: left
+
+ send_to_custom:
+ cssClass: radio-align
+ trigger:
+ action: show
+ field: send_to_mode
+ condition: value[custom]
+
+ send_to_admin:
+ label: Send to admin group
+ span: right
+ type: dropdown
+ trigger:
+ action: show
+ field: send_to_mode
+ condition: value[admin]
+
+ reply_to_mode:
+ label: Reply-to address
+ type: radio
+ span: left
+
+ reply_to_custom:
+ cssClass: radio-align
+ trigger:
+ action: show
+ field: reply_to_mode
+ condition: value[custom]
diff --git a/plugins/rainlab/notify/updates/create_notification_rules_table.php b/plugins/rainlab/notify/updates/create_notification_rules_table.php
new file mode 100644
index 0000000..2bcc6b0
--- /dev/null
+++ b/plugins/rainlab/notify/updates/create_notification_rules_table.php
@@ -0,0 +1,29 @@
+engine = 'InnoDB';
+ $table->increments('id');
+ $table->string('name')->nullable();
+ $table->string('code')->index()->nullable();
+ $table->string('class_name')->nullable();
+ $table->text('description')->nullable();
+ $table->mediumText('config_data')->nullable();
+ $table->mediumText('condition_data')->nullable();
+ $table->boolean('is_enabled')->default(0);
+ $table->boolean('is_custom')->default(1);
+ $table->timestamps();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('rainlab_notify_notification_rules');
+ }
+}
diff --git a/plugins/rainlab/notify/updates/create_notifications_table.php b/plugins/rainlab/notify/updates/create_notifications_table.php
new file mode 100644
index 0000000..4c73bb4
--- /dev/null
+++ b/plugins/rainlab/notify/updates/create_notifications_table.php
@@ -0,0 +1,28 @@
+uuid('id')->primary();
+ $table->string('event_type');
+ $table->morphs('notifiable');
+ $table->string('icon')->nullable();
+ $table->string('type')->nullable();
+ $table->text('body')->nullable();
+ $table->mediumText('data');
+ $table->timestamp('read_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('rainlab_notify_notifications');
+ }
+}
diff --git a/plugins/rainlab/notify/updates/create_rule_actions_table.php b/plugins/rainlab/notify/updates/create_rule_actions_table.php
new file mode 100644
index 0000000..af89e41
--- /dev/null
+++ b/plugins/rainlab/notify/updates/create_rule_actions_table.php
@@ -0,0 +1,25 @@
+engine = 'InnoDB';
+ $table->increments('id');
+ $table->string('class_name')->nullable();
+ $table->mediumText('config_data')->nullable();
+ $table->integer('rule_host_id')->unsigned()->nullable()->index();
+ $table->timestamps();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('rainlab_notify_rule_actions');
+ }
+}
diff --git a/plugins/rainlab/notify/updates/create_rule_conditions_table.php b/plugins/rainlab/notify/updates/create_rule_conditions_table.php
new file mode 100644
index 0000000..4a0211e
--- /dev/null
+++ b/plugins/rainlab/notify/updates/create_rule_conditions_table.php
@@ -0,0 +1,29 @@
+engine = 'InnoDB';
+ $table->increments('id');
+ $table->string('class_name')->nullable();
+ $table->mediumText('config_data')->nullable();
+ $table->string('condition_control_type', 100)->nullable();
+ $table->string('rule_host_type', 100)->nullable();
+ $table->integer('rule_host_id')->unsigned()->nullable()->index();
+ $table->integer('rule_parent_id')->unsigned()->nullable()->index();
+ $table->index(['rule_host_id', 'rule_host_type'], 'host_rule_id_type');
+ $table->timestamps();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('rainlab_notify_rule_conditions');
+ }
+}
diff --git a/plugins/rainlab/notify/updates/version.yaml b/plugins/rainlab/notify/updates/version.yaml
new file mode 100644
index 0000000..af1dae0
--- /dev/null
+++ b/plugins/rainlab/notify/updates/version.yaml
@@ -0,0 +1,13 @@
+v1.0.1:
+ - First version of Notify
+ - create_notifications_table.php
+ - create_notification_rules_table.php
+ - create_rule_conditions_table.php
+ - create_rule_actions_table.php
+v1.0.2: Fixes crashing bug.
+v1.0.3: Added Turkish & Russian translations, various bug fixes.
+v1.1.0: Fixes support for October CMS 2.0
+v1.1.1: Fixes missing template bug when saving notification rule
+v1.1.2: Fixes collisions with language keys in compose mail action
+v1.2.0: Adds delayed execution to actions
+v1.2.1: Improve support with October v3
diff --git a/plugins/rainlab/user/models/User.php b/plugins/rainlab/user/models/User.php
index 102fded..338cfbe 100644
--- a/plugins/rainlab/user/models/User.php
+++ b/plugins/rainlab/user/models/User.php
@@ -61,6 +61,8 @@ class User extends UserBase
'transactions' => ['TPS\Birzha\Models\Transaction', 'softDelete' => true],
'exchangerequests' => ['TPS\Birzha\Models\Exchangerequest', 'softDelete' => true],
'sliders' => ['TPS\Birzha\Models\UserSliders', 'softDelete' => true],
+ 'favourites' => ['TPS\Birzha\Models\Favourites', 'softDelete' => true, 'table' => 'tps_birzha_favourites'],
+ 'comments' => ['TPS\Birzha\Models\Comment','table' => 'tps_birzha_comments'],
];
diff --git a/plugins/rainlab/userplus/LICENCE.md b/plugins/rainlab/userplus/LICENCE.md
new file mode 100644
index 0000000..d68943e
--- /dev/null
+++ b/plugins/rainlab/userplus/LICENCE.md
@@ -0,0 +1,19 @@
+# MIT license
+
+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/plugins/rainlab/userplus/Plugin.php b/plugins/rainlab/userplus/Plugin.php
new file mode 100644
index 0000000..0c80059
--- /dev/null
+++ b/plugins/rainlab/userplus/Plugin.php
@@ -0,0 +1,124 @@
+ 'rainlab.userplus::lang.plugin.name',
+ 'description' => 'rainlab.userplus::lang.plugin.description',
+ 'author' => 'Alexey Bobkov, Samuel Georges',
+ 'icon' => 'icon-user-plus',
+ 'homepage' => 'https://github.com/rainlab/userplus-plugin'
+ ];
+ }
+
+ public function boot()
+ {
+ $this->extendUserModel();
+ $this->extendUsersController();
+ $this->extendSaveDatabaseAction();
+ $this->extendUserEventBase();
+ }
+
+ public function registerComponents()
+ {
+ return [
+ \RainLab\UserPlus\Components\Notifications::class => 'notifications',
+ ];
+ }
+
+ protected function extendUserModel()
+ {
+ UserModel::extend(function($model) {
+ $model->addFillable([
+ 'phone',
+ 'mobile',
+ 'company',
+ 'street_addr',
+ 'city',
+ 'zip'
+ ]);
+
+ $model->implement[] = 'RainLab.Location.Behaviors.LocationModel';
+
+ $model->morphMany['notifications'] = [
+ NotificationModel::class,
+ 'name' => 'notifiable',
+ 'order' => 'created_at desc'
+ ];
+ });
+ }
+
+ protected function extendUsersController()
+ {
+ UsersController::extendFormFields(function($widget) {
+ // Prevent extending of related form instead of the intended User form
+ if (!$widget->model instanceof UserModel) {
+ return;
+ }
+
+ $configFile = plugins_path('rainlab/userplus/config/profile_fields.yaml');
+ $config = Yaml::parse(File::get($configFile));
+ $widget->addTabFields($config);
+ });
+ }
+
+ public function registerNotificationRules()
+ {
+ return [
+ 'events' => [],
+ 'actions' => [],
+ 'conditions' => [
+ \RainLab\UserPlus\NotifyRules\UserLocationAttributeCondition::class
+ ],
+ 'presets' => '$/rainlab/userplus/config/notify_presets.yaml',
+ ];
+ }
+
+ protected function extendUserEventBase()
+ {
+ if (!class_exists(UserEventBase::class)) {
+ return;
+ }
+
+ UserEventBase::extend(function($event) {
+ $event->conditions[] = \RainLab\UserPlus\NotifyRules\UserLocationAttributeCondition::class;
+ });
+ }
+
+ protected function extendSaveDatabaseAction()
+ {
+ if (!class_exists(SaveDatabaseAction::class)) {
+ return;
+ }
+
+ SaveDatabaseAction::extend(function ($action) {
+ $action->addTableDefinition([
+ 'label' => 'User activity',
+ 'class' => UserModel::class,
+ 'param' => 'user'
+ ]);
+ });
+ }
+}
diff --git a/plugins/rainlab/userplus/README.md b/plugins/rainlab/userplus/README.md
new file mode 100644
index 0000000..8a98daf
--- /dev/null
+++ b/plugins/rainlab/userplus/README.md
@@ -0,0 +1,13 @@
+# User Plus+ plugin
+
+This plugin extends the [User plugin](http://octobercms.com/plugin/rainlab-user) with extra profile fields and features.
+
+* Adds the following extra fields to a user: `phone`, `mobile`, `company`, `street_addr`, `city`, `zip`.
+* A user can belong to a Country and/or State, sourced from the [Location plugin](http://octobercms.com/plugin/rainlab-location).
+
+### Potential features
+
+* A user can befriend other users via a friendship system.
+* A user can earn "Experience Points" by performing predefined activities.
+
+> Note these features may or may not be implemented in the future, but act only as an indicator of the plugin's potential.
diff --git a/plugins/rainlab/userplus/assets/css/notifications.css b/plugins/rainlab/userplus/assets/css/notifications.css
new file mode 100644
index 0000000..234271f
--- /dev/null
+++ b/plugins/rainlab/userplus/assets/css/notifications.css
@@ -0,0 +1,74 @@
+.rainlab-userplus {
+ position: relative;
+}
+.rainlab-userplus .notifications-popover {
+ background: #fff;
+ border: 1px solid #f0f0f0;
+ position: absolute;
+ width: 480px;
+ z-index: 2;
+ right: 0;
+ display: none;
+}
+.rainlab-userplus.active .notifications-popover {
+ display: block;
+}
+.rainlab-userplus .notifications-header {
+ border-bottom: 1px solid #f0f0f0;
+ padding: 15px;
+}
+.rainlab-userplus .notifications-header h4 {
+ padding: 0;
+ margin: 0;
+ display: inline-block;
+}
+.rainlab-userplus .notifications-header .close,
+.rainlab-userplus .notifications-header .mark-all-read {
+ float: right;
+ margin: 0 5px;
+}
+.rainlab-userplus .notifications-content .notifications-loading,
+.notification_area .notifications-loading,
+.accord_notification .notifications-loading {
+ padding: 20px;
+ font-size: 24px;
+ text-align: center;
+}
+.rainlab-userplus .notifications-content .no-notifications,
+.notification_area .no-notifications,
+.accord_notification .no-notifications {
+ text-align: center;
+ padding: 25px;
+ margin: 0;
+}
+.rainlab-userplus .notifications-content > ul {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ overflow: auto;
+ max-height: 400px;
+}
+.rainlab-userplus .notifications-content > ul > li {
+ position: relative;
+ padding: 10px 25px 10px 60px;
+ border-bottom: 1px solid #f9f9f9;
+}
+.rainlab-userplus .notifications-content > ul > li > i {
+ position: absolute;
+ left: 25px;
+ top: 12px;
+ font-size: 18px;
+ opacity: .5;
+}
+.rainlab-userplus .notifications-content > ul > li .parsed-body > *:last-child {
+ margin-bottom: 0;
+}
+.rainlab-userplus .notifications-content > ul > li .date {
+ white-space: nowrap;
+ opacity: .75;
+}
+.rainlab-userplus .notifications-footer {
+ border-top: 1px solid #f0f0f0;
+ padding: 15px;
+ text-align: center;
+}
diff --git a/plugins/rainlab/userplus/assets/js/notifications.js b/plugins/rainlab/userplus/assets/js/notifications.js
new file mode 100644
index 0000000..1faba42
--- /dev/null
+++ b/plugins/rainlab/userplus/assets/js/notifications.js
@@ -0,0 +1,13 @@
+function toggleNotificationsPopover(el) {
+
+ if($(el).next().hasClass('notification_area')) {
+ $(el).request('onLoadNotifications', {
+ update: { '@notifications-list': '#notification_area' }
+ })
+ } else if($(el).next().hasClass('accord_notification')) {
+ $(el).request('onLoadNotifications', {
+ update: { '@notifications-list': '#accord_notification' }
+ })
+ }
+
+}
diff --git a/plugins/rainlab/userplus/assets/less/notifications.less b/plugins/rainlab/userplus/assets/less/notifications.less
new file mode 100644
index 0000000..92fad58
--- /dev/null
+++ b/plugins/rainlab/userplus/assets/less/notifications.less
@@ -0,0 +1,84 @@
+.rainlab-userplus {
+ position: relative;
+
+ .notifications-popover {
+ background: #fff;
+ border: 1px solid #f0f0f0;
+ position: absolute;
+ width: 480px;
+ z-index: 2;
+ right: 0;
+ display: none;
+ }
+
+ &.active .notifications-popover {
+ display: block;
+ }
+
+ .notifications-header {
+ border-bottom: 1px solid #f0f0f0;
+ padding: 15px;
+
+ h4 {
+ padding: 0;
+ margin: 0;
+ display: inline-block;
+ }
+
+ .close, .mark-all-read {
+ float: right;
+ margin: 0 5px;
+ }
+ }
+
+ .notifications-content {
+ .notifications-loading {
+ padding: 20px;
+ font-size: 24px;
+ text-align: center;
+ }
+
+ .no-notifications {
+ text-align: center;
+ padding: 25px;
+ margin: 0;
+ }
+
+ > ul {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ overflow: auto;
+ max-height: 400px;
+ }
+
+ > ul > li {
+ position: relative;
+ padding: 10px 25px 10px 60px;
+ border-bottom: 1px solid #f9f9f9;
+
+ > i {
+ position: absolute;
+ left: 25px;
+ top: 12px;
+ font-size: 18px;
+ opacity: .5;
+ }
+
+ .parsed-body > *:last-child {
+ margin-bottom: 0;
+ }
+
+ .date {
+ white-space: nowrap;
+ opacity: .75;
+ }
+ }
+ }
+
+ .notifications-footer {
+ border-top: 1px solid #f0f0f0;
+ padding: 15px;
+ text-align: center;
+ }
+}
diff --git a/plugins/rainlab/userplus/components/Notifications.php b/plugins/rainlab/userplus/components/Notifications.php
new file mode 100644
index 0000000..f8decf0
--- /dev/null
+++ b/plugins/rainlab/userplus/components/Notifications.php
@@ -0,0 +1,129 @@
+ 'Notifications Component',
+ 'description' => 'Display user and site-wide notifications'
+ ];
+ }
+
+ public function defineProperties()
+ {
+ return [
+ 'recordsPerPage' => [
+ 'title' => 'Records per page',
+ 'comment' => 'Number of notifications to display per page',
+ 'default' => 7
+ ],
+ 'includeAssets' => [
+ 'title' => 'Include assets',
+ 'comment' => 'Inject the JavaScript and Stylesheet used by the default component markup',
+ 'type' => 'checkbox',
+ 'default' => true
+ ],
+ 'frontend' => [
+ 'title' => 'Frontend block',
+ 'description' => 'Choose where in the page to display',
+ 'type' => 'dropdown',
+ 'options' => ['desktop','mobile'],
+ 'default' => 'desktop'
+ ]
+ ];
+ }
+
+ public function onRun()
+ {
+ if (!Auth::getUser()) {
+ return;
+ }
+
+ if ($this->property('includeAssets')) {
+ $this->addCss('assets/css/notifications.css');
+ $this->addJs('assets/js/notifications.js');
+ }
+
+ $this->prepareVars();
+ }
+
+ protected function prepareVars()
+ {
+ $this->page['recordsToDisplay'] = $this->getRecordCountToDisplay();
+ $this->page['hasNotifications'] = $this->hasNotifications();
+ }
+
+ public function hasNotifications()
+ {
+ return $this->getUnreadQuery()->count() > 0;
+ }
+
+ public function unreadNotifications($recordsToDisplay = null)
+ {
+ if (!$recordsToDisplay) {
+ $recordsToDisplay = $this->getRecordCountToDisplay();
+ }
+
+ return $this->getUnreadQuery()->paginate($recordsToDisplay);
+ }
+
+ //
+ // AJAX
+ //
+
+ public function onLoadNotifications()
+ {
+ $this->prepareVars();
+ $this->page['notifications'] = $this->unreadNotifications();
+ }
+
+ public function onLoadOlderNotifications()
+ {
+ $recordsToDisplay = $this->getRecordCountToDisplay() + $this->property('recordsPerPage');
+
+ $this->page['recordsToDisplay'] = $recordsToDisplay;
+ $this->page['notifications'] = $this->unreadNotifications($recordsToDisplay);
+ }
+
+ public function onMarkAllNotificationsAsRead()
+ {
+ $this->getUnreadQuery()->update(['read_at' => Carbon::now()]);
+
+ $this->prepareVars();
+ $this->page['notifications'] = $this->unreadNotifications();
+ }
+
+ public function onMarkNotificationAsRead()
+ {
+ $this->getUnreadQuery()
+ ->where('id', \Input::get('notification_id'))
+ ->update(['read_at' => Carbon::now()]);
+
+ return Redirect::to(\Input::get('redirect_link'));
+ }
+
+ //
+ // Helpers
+ //
+
+ protected function getRecordCountToDisplay()
+ {
+ return ((int) post('records_per_page')) ?: $this->property('recordsPerPage');
+ }
+
+ protected function getUnreadQuery()
+ {
+ if (!$user = Auth::getUser()) {
+ throw new ApplicationException('You must be logged in');
+ }
+
+ return $user->notifications()->applyUnread();
+ }
+}
diff --git a/plugins/rainlab/userplus/components/notifications/default.htm b/plugins/rainlab/userplus/components/notifications/default.htm
new file mode 100644
index 0000000..e4a2115
--- /dev/null
+++ b/plugins/rainlab/userplus/components/notifications/default.htm
@@ -0,0 +1 @@
+{% partial __SELF__~"::"~__SELF__.property('frontend') %}
diff --git a/plugins/rainlab/userplus/components/notifications/desktop.htm b/plugins/rainlab/userplus/components/notifications/desktop.htm
new file mode 100644
index 0000000..504793d
--- /dev/null
+++ b/plugins/rainlab/userplus/components/notifications/desktop.htm
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/components/notifications/mobile.htm b/plugins/rainlab/userplus/components/notifications/mobile.htm
new file mode 100644
index 0000000..b019790
--- /dev/null
+++ b/plugins/rainlab/userplus/components/notifications/mobile.htm
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {{ 'auth.notifications'|_ }}
+
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/components/notifications/notifications-list.htm b/plugins/rainlab/userplus/components/notifications/notifications-list.htm
new file mode 100644
index 0000000..69f69dc
--- /dev/null
+++ b/plugins/rainlab/userplus/components/notifications/notifications-list.htm
@@ -0,0 +1,20 @@
+{% if notifications.total %}
+ {% for notification in notifications %}
+
+
+ {{ notification.description }}
+
+
+ {{ notification.created_at|date('H:i - d.m.Y') }}
+
+
+ {% endfor %}
+
+{% else %}
+ {{ 'auth.no_notifications'|_ }}
+{% endif %}
diff --git a/plugins/rainlab/userplus/composer.json b/plugins/rainlab/userplus/composer.json
new file mode 100644
index 0000000..3cba8bc
--- /dev/null
+++ b/plugins/rainlab/userplus/composer.json
@@ -0,0 +1,8 @@
+{
+ "name": "rainlab/userplus-plugin",
+ "type": "october-plugin",
+ "description": "None",
+ "require": {
+ "composer/installers": "~1.0"
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/config/notify_presets.yaml b/plugins/rainlab/userplus/config/notify_presets.yaml
new file mode 100644
index 0000000..e05e4d8
--- /dev/null
+++ b/plugins/rainlab/userplus/config/notify_presets.yaml
@@ -0,0 +1,11 @@
+# ===================================
+# Event Presets
+# ===================================
+
+welcome_email:
+ name: Send welcome email to user
+ event: RainLab\User\NotifyRules\UserRegisteredEvent
+ items:
+ - action: RainLab\Notify\NotifyRules\SendMailTemplateAction
+ mail_template: rainlab.user::welcome
+ send_to_mode: user
diff --git a/plugins/rainlab/userplus/config/profile_fields.yaml b/plugins/rainlab/userplus/config/profile_fields.yaml
new file mode 100644
index 0000000..eabbe9c
--- /dev/null
+++ b/plugins/rainlab/userplus/config/profile_fields.yaml
@@ -0,0 +1,47 @@
+# ===================================
+# Extended Profile Field Definitions
+# ===================================
+
+phone:
+ label: rainlab.userplus::lang.user.phone
+ tab: rainlab.userplus::lang.tab.profile
+ span: left
+
+mobile:
+ label: rainlab.userplus::lang.user.mobile
+ tab: rainlab.userplus::lang.tab.profile
+ span: right
+
+company:
+ label: rainlab.userplus::lang.user.company
+ tab: rainlab.userplus::lang.tab.profile
+ span: left
+
+street_addr:
+ label: rainlab.userplus::lang.user.street_addr
+ tab: rainlab.userplus::lang.tab.profile
+
+city:
+ label: rainlab.userplus::lang.user.city
+ tab: rainlab.userplus::lang.tab.profile
+ span: left
+
+zip:
+ label: rainlab.userplus::lang.user.zip
+ tab: rainlab.userplus::lang.tab.profile
+ span: right
+
+country:
+ label: rainlab.location::lang.country.label
+ placeholder: rainlab.location::lang.country.select
+ type: dropdown
+ tab: rainlab.userplus::lang.tab.profile
+ span: left
+
+state:
+ label: rainlab.location::lang.state.label
+ placeholder: rainlab.location::lang.state.select
+ type: dropdown
+ tab: rainlab.userplus::lang.tab.profile
+ span: right
+ dependsOn: country
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/lang/cs/lang.php b/plugins/rainlab/userplus/lang/cs/lang.php
new file mode 100644
index 0000000..ec7a1f9
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/cs/lang.php
@@ -0,0 +1,19 @@
+ [
+ 'name' => 'User Plus+',
+ 'description' => 'Přidává uživatelům profilová políčka.',
+ ],
+ 'tab' => [
+ 'profile' => 'Profil',
+ ],
+ 'user' => [
+ 'phone' => 'Telefon',
+ 'mobile' => 'Mobil',
+ 'company' => 'Firma',
+ 'city' => 'Město',
+ 'zip' => 'PSČ',
+ 'street_addr' => 'Ulice',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/en/lang.php b/plugins/rainlab/userplus/lang/en/lang.php
new file mode 100644
index 0000000..cd0dae2
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/en/lang.php
@@ -0,0 +1,19 @@
+ [
+ 'name' => 'User Plus+',
+ 'description' => 'Adds profile fields to users.',
+ ],
+ 'tab' => [
+ 'profile' => 'Profile',
+ ],
+ 'user' => [
+ 'phone' => 'Phone',
+ 'mobile' => 'Mobile',
+ 'company' => 'Company',
+ 'city' => 'City',
+ 'zip' => 'Legalization number',
+ 'street_addr' => 'Street Address',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/fa/lang.php b/plugins/rainlab/userplus/lang/fa/lang.php
new file mode 100644
index 0000000..c78c2f1
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/fa/lang.php
@@ -0,0 +1,16 @@
+ [
+ 'name' => 'گسترش کاربر',
+ 'description' => 'افزودن فیلد به پروفایل کاربران',
+ ],
+ 'user' => [
+ 'phone' => 'تلفن',
+ 'mobile' => 'تلفن همراه',
+ 'company' => 'شرکت',
+ 'city' => 'شهر',
+ 'zip' => 'کد پستی',
+ 'street_addr' => 'آدرس',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/fr/lang.php b/plugins/rainlab/userplus/lang/fr/lang.php
new file mode 100644
index 0000000..4d21d24
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/fr/lang.php
@@ -0,0 +1,15 @@
+ [
+ 'name' => 'User Plus+',
+ 'description' => 'Ajoute des champs de profil aux utilisateurs.',
+ ],
+ 'user' => [
+ 'phone' => 'Téléhone',
+ 'mobile' => 'Mobile',
+ 'company' => 'Société',
+ 'city' => 'Ville',
+ 'zip' => 'Code postal',
+ 'street_addr' => 'Addresse',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/hu/lang.php b/plugins/rainlab/userplus/lang/hu/lang.php
new file mode 100644
index 0000000..1f86db8
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/hu/lang.php
@@ -0,0 +1,19 @@
+ [
+ 'name' => 'Felhasználók Plusz+',
+ 'description' => 'Új személyes mezők a felhasználók adatlapjához.',
+ ],
+ 'tab' => [
+ 'profile' => 'Személyes',
+ ],
+ 'user' => [
+ 'phone' => 'Telefon',
+ 'mobile' => 'Mobil',
+ 'company' => 'Cég',
+ 'city' => 'Város',
+ 'zip' => 'Irszám.',
+ 'street_addr' => 'Postacím',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/pt-br/lang.php b/plugins/rainlab/userplus/lang/pt-br/lang.php
new file mode 100644
index 0000000..524afda
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/pt-br/lang.php
@@ -0,0 +1,19 @@
+ [
+ 'name' => 'Usuário Plus+',
+ 'description' => 'Adiciona campos de perfil aos usuários.',
+ ],
+ 'tab' => [
+ 'profile' => 'Perfil',
+ ],
+ 'user' => [
+ 'phone' => 'Telefone',
+ 'mobile' => 'Celular',
+ 'company' => 'Empresa',
+ 'city' => 'Cidade',
+ 'zip' => 'Cep',
+ 'street_addr' => 'Endereço',
+ ],
+];
diff --git a/plugins/rainlab/userplus/lang/tr/lang.php b/plugins/rainlab/userplus/lang/tr/lang.php
new file mode 100644
index 0000000..6a80d3d
--- /dev/null
+++ b/plugins/rainlab/userplus/lang/tr/lang.php
@@ -0,0 +1,19 @@
+ [
+ 'name' => 'Üye Plus+',
+ 'description' => 'Üyeler için ek profil bilgileri sağlar.',
+ ],
+ 'tab' => [
+ 'profile' => 'Profil',
+ ],
+ 'user' => [
+ 'phone' => 'Telefon',
+ 'mobile' => 'Cep telefon',
+ 'company' => 'Firma',
+ 'city' => 'Şehir',
+ 'zip' => 'Legalizasyon kodu',
+ 'street_addr' => 'Adresi',
+ ],
+];
diff --git a/plugins/rainlab/userplus/notifyrules/UserLocationAttributeCondition.php b/plugins/rainlab/userplus/notifyrules/UserLocationAttributeCondition.php
new file mode 100644
index 0000000..db6701b
--- /dev/null
+++ b/plugins/rainlab/userplus/notifyrules/UserLocationAttributeCondition.php
@@ -0,0 +1,37 @@
+host;
+
+ $attribute = $hostObj->subcondition;
+
+ if (!$user = array_get($params, 'user')) {
+ throw new ApplicationException('Error evaluating the user attribute condition: the user object is not found in the condition parameters.');
+ }
+
+ return parent::evalIsTrue($user);
+ }
+}
diff --git a/plugins/rainlab/userplus/notifyrules/userlocationattributecondition/attributes.yaml b/plugins/rainlab/userplus/notifyrules/userlocationattributecondition/attributes.yaml
new file mode 100644
index 0000000..56c0f98
--- /dev/null
+++ b/plugins/rainlab/userplus/notifyrules/userlocationattributecondition/attributes.yaml
@@ -0,0 +1,23 @@
+# ===================================
+# Condition Attribute Definitions
+# ===================================
+
+attributes:
+
+ country:
+ label: Country
+ type: relation
+ relation:
+ model: RainLab\Location\Models\Country
+ label: Name
+ nameFrom: name
+ keyFrom: id
+
+ state:
+ label: State
+ type: relation
+ relation:
+ model: RainLab\Location\Models\State
+ label: Name
+ nameFrom: name
+ keyFrom: id
diff --git a/plugins/rainlab/userplus/updates/user_add_location_fields.php b/plugins/rainlab/userplus/updates/user_add_location_fields.php
new file mode 100644
index 0000000..3ffc997
--- /dev/null
+++ b/plugins/rainlab/userplus/updates/user_add_location_fields.php
@@ -0,0 +1,29 @@
+integer('state_id')->unsigned()->nullable()->index();
+ $table->integer('country_id')->unsigned()->nullable()->index();
+ });
+ }
+
+ public function down()
+ {
+ if (Schema::hasTable('users')) {
+ Schema::table('users', function ($table) {
+ $table->dropColumn(['state_id', 'country_id']);
+ });
+ }
+ }
+}
diff --git a/plugins/rainlab/userplus/updates/user_add_mobile_field.php b/plugins/rainlab/userplus/updates/user_add_mobile_field.php
new file mode 100644
index 0000000..254bddf
--- /dev/null
+++ b/plugins/rainlab/userplus/updates/user_add_mobile_field.php
@@ -0,0 +1,28 @@
+string('mobile', 100)->nullable();
+ });
+ }
+
+ public function down()
+ {
+ if (Schema::hasTable('users')) {
+ Schema::table('users', function ($table) {
+ $table->dropColumn(['mobile']);
+ });
+ }
+ }
+}
diff --git a/plugins/rainlab/userplus/updates/user_add_profile_fields.php b/plugins/rainlab/userplus/updates/user_add_profile_fields.php
new file mode 100644
index 0000000..1a56fc7
--- /dev/null
+++ b/plugins/rainlab/userplus/updates/user_add_profile_fields.php
@@ -0,0 +1,32 @@
+string('phone', 100)->nullable();
+ $table->string('company', 100)->nullable();
+ $table->string('street_addr')->nullable();
+ $table->string('city', 100)->nullable();
+ $table->string('zip', 20)->nullable();
+ });
+ }
+
+ public function down()
+ {
+ if (Schema::hasTable('users')) {
+ Schema::table('users', function ($table) {
+ $table->dropColumn(['phone', 'company', 'street_addr', 'city', 'zip']);
+ });
+ }
+ }
+}
diff --git a/plugins/rainlab/userplus/updates/version.yaml b/plugins/rainlab/userplus/updates/version.yaml
new file mode 100644
index 0000000..b13960a
--- /dev/null
+++ b/plugins/rainlab/userplus/updates/version.yaml
@@ -0,0 +1,10 @@
+1.0.1: First version of User Profile
+1.0.2:
+ - Reintroduce profile fields that were removed from User plugin
+ - user_add_profile_fields.php
+ - user_add_location_fields.php
+1.0.3:
+ - Add mobile phone user profile field.
+ - user_add_mobile_field.php
+1.0.4: Add various languages and minor bug fixes.
+1.1.0: Compatibility with Notify plugin.
diff --git a/plugins/rainlab/userplus/vendor/autoload.php b/plugins/rainlab/userplus/vendor/autoload.php
new file mode 100644
index 0000000..87b20f4
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/autoload.php
@@ -0,0 +1,7 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see http://www.php-fig.org/psr/psr-0/
+ * @see http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ // PSR-4
+ private $prefixLengthsPsr4 = array();
+ private $prefixDirsPsr4 = array();
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ private $prefixesPsr0 = array();
+ private $fallbackDirsPsr0 = array();
+
+ private $useIncludePath = false;
+ private $classMap = array();
+
+ private $classMapAuthoritative = false;
+
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', $this->prefixesPsr0);
+ }
+
+ return array();
+ }
+
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 base directories
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
+ if ('\\' == $class[0]) {
+ $class = substr($class, 1);
+ }
+
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative) {
+ return false;
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if ($file === null && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if ($file === null) {
+ // Remember that this class does not exist.
+ return $this->classMap[$class] = false;
+ }
+
+ return $file;
+ }
+
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/LICENSE b/plugins/rainlab/userplus/vendor/composer/LICENSE
new file mode 100644
index 0000000..1a28124
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) 2016 Nils Adermann, Jordi Boggiano
+
+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.
+
diff --git a/plugins/rainlab/userplus/vendor/composer/autoload_classmap.php b/plugins/rainlab/userplus/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..7a91153
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/autoload_classmap.php
@@ -0,0 +1,9 @@
+ array($vendorDir . '/composer/installers/src/Composer/Installers'),
+);
diff --git a/plugins/rainlab/userplus/vendor/composer/autoload_real.php b/plugins/rainlab/userplus/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..7b9afb9
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/autoload_real.php
@@ -0,0 +1,45 @@
+ $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+
+ $loader->register(true);
+
+ return $loader;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installed.json b/plugins/rainlab/userplus/vendor/composer/installed.json
new file mode 100644
index 0000000..3635188
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installed.json
@@ -0,0 +1,210 @@
+[
+ {
+ "name": "composer/installers",
+ "version": "v1.3.0",
+ "version_normalized": "1.3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/installers.git",
+ "reference": "79ad876c7498c0bbfe7eed065b8651c93bfd6045"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/installers/zipball/79ad876c7498c0bbfe7eed065b8651c93bfd6045",
+ "reference": "79ad876c7498c0bbfe7eed065b8651c93bfd6045",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0"
+ },
+ "replace": {
+ "roundcube/plugin-installer": "*",
+ "shama/baton": "*"
+ },
+ "require-dev": {
+ "composer/composer": "1.0.*@dev",
+ "phpunit/phpunit": "4.1.*"
+ },
+ "time": "2017-04-24 06:37:16",
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Composer\\Installers\\": "src/Composer/Installers"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kyle Robinson Young",
+ "email": "kyle@dontkry.com",
+ "homepage": "https://github.com/shama"
+ }
+ ],
+ "description": "A multi-framework Composer library installer",
+ "homepage": "https://composer.github.io/installers/",
+ "keywords": [
+ "Craft",
+ "Dolibarr",
+ "Eliasis",
+ "Hurad",
+ "ImageCMS",
+ "Kanboard",
+ "MODX Evo",
+ "Mautic",
+ "Maya",
+ "OXID",
+ "Plentymarkets",
+ "Porto",
+ "RadPHP",
+ "SMF",
+ "Thelia",
+ "WolfCMS",
+ "agl",
+ "aimeos",
+ "annotatecms",
+ "attogram",
+ "bitrix",
+ "cakephp",
+ "chef",
+ "cockpit",
+ "codeigniter",
+ "concrete5",
+ "croogo",
+ "dokuwiki",
+ "drupal",
+ "elgg",
+ "expressionengine",
+ "fuelphp",
+ "grav",
+ "installer",
+ "itop",
+ "joomla",
+ "kohana",
+ "laravel",
+ "lavalite",
+ "lithium",
+ "magento",
+ "mako",
+ "mediawiki",
+ "modulework",
+ "moodle",
+ "phpbb",
+ "piwik",
+ "ppi",
+ "puppet",
+ "reindex",
+ "roundcube",
+ "shopware",
+ "silverstripe",
+ "sydes",
+ "symfony",
+ "typo3",
+ "wordpress",
+ "yawik",
+ "zend",
+ "zikula"
+ ]
+ },
+ {
+ "name": "rainlab/user-plugin",
+ "version": "dev-master",
+ "version_normalized": "9999999-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rainlab/user-plugin.git",
+ "reference": "d270514438c736633790aa575150ac4edfc82912"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rainlab/user-plugin/zipball/d270514438c736633790aa575150ac4edfc82912",
+ "reference": "d270514438c736633790aa575150ac4edfc82912",
+ "shasum": ""
+ },
+ "require": {
+ "composer/installers": "~1.0",
+ "php": ">=5.5.9"
+ },
+ "time": "2017-07-29 00:22:36",
+ "type": "october-plugin",
+ "installation-source": "source",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alexey Bobkov",
+ "email": "aleksey.bobkov@gmail.com",
+ "role": "Co-founder"
+ },
+ {
+ "name": "Samuel Georges",
+ "email": "daftspunky@gmail.com",
+ "role": "Co-founder"
+ }
+ ],
+ "description": "User plugin for October CMS",
+ "homepage": "https://octobercms.com/plugin/rainlab-user",
+ "keywords": [
+ "october",
+ "octobercms",
+ "user"
+ ]
+ },
+ {
+ "name": "rainlab/location-plugin",
+ "version": "dev-master",
+ "version_normalized": "9999999-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rainlab/location-plugin.git",
+ "reference": "39aecc73acd9b6935abdf724251ead4f3b6293cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rainlab/location-plugin/zipball/39aecc73acd9b6935abdf724251ead4f3b6293cf",
+ "reference": "39aecc73acd9b6935abdf724251ead4f3b6293cf",
+ "shasum": ""
+ },
+ "require": {
+ "composer/installers": "~1.0",
+ "php": ">=5.5.9"
+ },
+ "time": "2017-06-21 19:57:10",
+ "type": "october-plugin",
+ "installation-source": "source",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alexey Bobkov",
+ "email": "aleksey.bobkov@gmail.com",
+ "role": "Co-founder"
+ },
+ {
+ "name": "Samuel Georges",
+ "email": "daftspunky@gmail.com",
+ "role": "Co-founder"
+ }
+ ],
+ "description": "Location plugin for October CMS",
+ "homepage": "https://octobercms.com/plugin/rainlab-location",
+ "keywords": [
+ "location",
+ "october",
+ "octobercms"
+ ]
+ }
+]
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/CHANGELOG.md b/plugins/rainlab/userplus/vendor/composer/installers/CHANGELOG.md
new file mode 100644
index 0000000..d5d2619
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/CHANGELOG.md
@@ -0,0 +1,60 @@
+# Change Log
+
+## v1.3.0 - 2017-04-24
+### Added
+* Kanboard plugins installer.
+* Porto-SAP installer.
+* Add `core` to concrete5 installer.
+* Support Moodle "search" plugin type.
+* SyDES installer.
+* iTop installer.
+* Lavalite installer.
+* Module type for Eliasis.
+* Vgmcp installer.
+* OntoWiki installer.
+* The requirements for contributing (CONTRIBUTING.md).
+
+### Changed
+* Concrete5: block & theme install location updates.
+
+## v1.2.0 - 2016-08-13
+### Added
+* Installer for Attogram.
+* Installer for Cockpit.
+* Installer for Plentymarkets.
+* Installer for ReIndex.
+* Installer for Vanilla.
+* Installer for YAWIK.
+* Added missing environments for new Shopware (5.2) Plugin System.
+
+## v1.1.0 - 2016-07-05
+### Added
+* Installer for ReIndex.
+* Installer for RadPHP.
+* Installer for Decibel.
+* Installer for Phifty.
+* Installer for ExpressionEngine.
+
+### Changed
+* New paths for new Bitrix CMS. Old paths is deprecated.
+
+### Deprecated
+* Old paths in Bitrix CMS Installer is deprecated.
+
+## v1.0.25 - 2016-04-13
+### Removed
+* Revert TYPO3 installer deletion.
+
+## v1.0.24 - 2016-04-05
+### Added
+* Installer for ImageCMS.
+* Installer for Mautic.
+* New types in the Kirby installer: `kirby-plugin` and `kirby-field`.
+* New types in the Drupal installer: `custom-theme` and `custom-module`.
+
+### Changed
+* Switch to PSR-4.
+* Update Bitrix Installer: configuration for setting custom path to directory with kernel.
+
+### Removed
+* Remove TYPO3 Extension installers.
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/CONTRIBUTING.md b/plugins/rainlab/userplus/vendor/composer/installers/CONTRIBUTING.md
new file mode 100644
index 0000000..1b1998e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+# Contributing
+
+If you would like to help, please take a look at the list of
+[issues](https://github.com/composer/installers/issues).
+
+## Pull requests
+
+* [Fork and clone](https://help.github.com/articles/fork-a-repo).
+* Run the command `php composer.phar install` to install the dependencies.
+ This will also install the dev dependencies. See [Composer](https://getcomposer.org/doc/03-cli.md#install).
+* Use the command `phpunit` to run the tests. See [PHPUnit](http://phpunit.de).
+* Create a branch, commit, push and send us a
+ [pull request](https://help.github.com/articles/using-pull-requests).
+
+To ensure a consistent code base, you should make sure the code follows the
+coding standards [PSR-1](http://www.php-fig.org/psr/psr-1/) and
+[PSR-2](http://www.php-fig.org/psr/psr-2/).
+
+### Create a new Installer
+
+* Create class extends `Composer\Installers\BaseInstaller` with your Installer.
+* Create unit tests as a separate class or as part of a `Composer\Installers\Test\InstallerTest`.
+* Add information about your Installer in `README.md` in section "Current Supported Package Types".
+* Run the tests.
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/LICENSE b/plugins/rainlab/userplus/vendor/composer/installers/LICENSE
new file mode 100644
index 0000000..85f97fc
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Kyle Robinson Young
+
+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/plugins/rainlab/userplus/vendor/composer/installers/README.md b/plugins/rainlab/userplus/vendor/composer/installers/README.md
new file mode 100644
index 0000000..332142c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/README.md
@@ -0,0 +1,214 @@
+# A Multi-Framework [Composer](http://getcomposer.org) Library Installer
+
+[](http://travis-ci.org/composer/installers)
+
+This is for PHP package authors to require in their `composer.json`. It will
+install their package to the correct location based on the specified package
+type.
+
+The goal of Installers is to be a simple package type to install path map.
+Users can also customize the install path per package and package authors can
+modify the package name upon installing.
+
+Installers isn't intended on replacing all custom installers. If your
+package requires special installation handling then by all means, create a
+custom installer to handle it.
+
+**Natively Supported Frameworks**:
+
+The following frameworks natively work with Composer and will be
+installed to the default `vendor` directory. `composer/installers`
+is not needed to install packages with these frameworks:
+
+* Aura
+* Symfony2
+* Yii
+* Yii2
+
+## Current Supported Package Types
+
+> Stable types are marked as **bold**, this means that installation paths
+> for those type will not be changed. Any adjustment for those types would
+> require creation of brand new type that will cover required changes.
+
+| Framework | Types
+| --------- | -----
+| Aimeos | `aimeos-extension`
+| Asgard | `asgard-module` `asgard-theme`
+| Attogram | `attogram-module`
+| AGL | `agl-module`
+| Bonefish | `bonefish-package`
+| AnnotateCms | `annotatecms-module` `annotatecms-component` `annotatecms-service`
+| Bitrix | `bitrix-module` (deprecated) `bitrix-component` (deprecated) `bitrix-theme` (deprecated) `bitrix-d7-module` `bitrix-d7-component` `bitrix-d7-template`
+| CakePHP 2+ | **`cakephp-plugin`**
+| Chef | `chef-cookbook` `chef-role`
+| CCFramework | `ccframework-ship` `ccframework-theme`
+| Cockpit | `cockpit-module`
+| CodeIgniter | `codeigniter-library` `codeigniter-third-party` `codeigniter-module`
+| concrete5 | `concrete5-core` `concrete5-package` `concrete5-theme` `concrete5-block` `concrete5-update`
+| Craft | `craft-plugin`
+| Croogo | `croogo-plugin` `croogo-theme`
+| Decibel | `decibel-app`
+| DokuWiki | `dokuwiki-plugin` `dokuwiki-template`
+| Dolibarr | `dolibarr-module`
+| Drupal | `drupal-core` `drupal-module` `drupal-theme` `drupal-library` `drupal-profile` `drupal-drush`
+| Elgg | `elgg-plugin`
+| Eliasis | `eliasis-module`
+| ExpressionEngine 3 | `ee3-addon` `ee3-theme`
+| FuelPHP v1.x | `fuel-module` `fuel-package` `fuel-theme`
+| FuelPHP v2.x | `fuelphp-component`
+| Grav | `grav-plugin` `grav-theme`
+| Hurad | `hurad-plugin` `hurad-theme`
+| ImageCMS | `imagecms-template` `imagecms-module` `imagecms-library`
+| iTop | `itop-extension`
+| Joomla | `joomla-component` `joomla-module` `joomla-template` `joomla-plugin` `joomla-library`
+| Kanboard | `kanboard-plugin`
+| Kirby | **`kirby-plugin`** `kirby-field` `kirby-tag`
+| KodiCMS | `kodicms-plugin` `kodicms-media`
+| Kohana | **`kohana-module`**
+| Laravel | `laravel-library`
+| Lavalite | `lavalite-theme` `lavalite-package`
+| Lithium | **`lithium-library` `lithium-source`**
+| Magento | `magento-library` `magento-skin` `magento-theme`
+| Mako | `mako-package`
+| Mautic | `mautic-plugin` `mautic-theme`
+| Maya | `maya-module`
+| MODX Evo | `modxevo-snippet` `modxevo-plugin` `modxevo-module` `modxevo-template` `modxevo-lib`
+| MediaWiki | `mediawiki-extension`
+| October | **`october-module` `october-plugin` `october-theme`**
+| OntoWiki | `ontowiki-extension` `ontowiki-theme` `ontowiki-translation`
+| OXID | `oxid-module` `oxid-theme` `oxid-out`
+| MODULEWork | `modulework-module`
+| Moodle | `moodle-*` (Please [check source](https://raw.githubusercontent.com/composer/installers/master/src/Composer/Installers/MoodleInstaller.php) for all supported types)
+| Piwik | `piwik-plugin`
+| phpBB | `phpbb-extension` `phpbb-style` `phpbb-language`
+| Pimcore | `pimcore-plugin`
+| Plentymarkets | `plentymarkets-plugin`
+| PPI | **`ppi-module`**
+| Puppet | `puppet-module`
+| Porto | `porto-container`
+| RadPHP | `radphp-bundle`
+| REDAXO | `redaxo-addon`
+| ReIndex | **`reindex-plugin`** **`reindex-theme`**
+| Roundcube | `roundcube-plugin`
+| shopware | `shopware-backend-plugin` `shopware-core-plugin` `shopware-frontend-plugin` `shopware-theme` `shopware-plugin` `shopware-frontend-theme`
+| SilverStripe | `silverstripe-module` `silverstripe-theme`
+| SMF | `smf-module` `smf-theme`
+| SyDES | `sydes-module` `sydes-theme`
+| symfony1 | **`symfony1-plugin`**
+| Tusk | `tusk-task` `tusk-command` `tusk-asset`
+| TYPO3 Flow | `typo3-flow-package` `typo3-flow-framework` `typo3-flow-plugin` `typo3-flow-site` `typo3-flow-boilerplate` `typo3-flow-build`
+| TYPO3 CMS | `typo3-cms-extension` (Deprecated in this package, use the [TYPO3 CMS Installers](https://packagist.org/packages/typo3/cms-composer-installers) instead)
+| Vanilla | `vanilla-plugin` `vanilla-theme`
+| Vgmcp | `vgmcp-bundle` `vgmcp-theme`
+| Wolf CMS | `wolfcms-plugin`
+| WordPress | `wordpress-plugin` `wordpress-theme` `wordpress-muplugin`
+| YAWIK | `yawik-module`
+| Zend | `zend-library` `zend-extra` `zend-module`
+| Zikula | `zikula-module` `zikula-theme`
+| Prestashop | `prestashop-module` `prestashop-theme`
+| Phifty | `phifty-bundle` `phifty-framework` `phifty-library`
+
+## Example `composer.json` File
+
+This is an example for a CakePHP plugin. The only important parts to set in your
+composer.json file are `"type": "cakephp-plugin"` which describes what your
+package is and `"require": { "composer/installers": "~1.0" }` which tells composer
+to load the custom installers.
+
+```json
+{
+ "name": "you/ftp",
+ "type": "cakephp-plugin",
+ "require": {
+ "composer/installers": "~1.0"
+ }
+}
+```
+
+This would install your package to the `Plugin/Ftp/` folder of a CakePHP app
+when a user runs `php composer.phar install`.
+
+So submit your packages to [packagist.org](http://packagist.org)!
+
+## Custom Install Paths
+
+If you are consuming a package that uses the `composer/installers` you can
+override the install path with the following extra in your `composer.json`:
+
+```json
+{
+ "extra": {
+ "installer-paths": {
+ "your/custom/path/{$name}/": ["shama/ftp", "vendor/package"]
+ }
+ }
+}
+```
+
+A package type can have a custom installation path with a `type:` prefix.
+
+``` json
+{
+ "extra": {
+ "installer-paths": {
+ "your/custom/path/{$name}/": ["type:wordpress-plugin"]
+ }
+ }
+}
+```
+
+You can also have the same vendor packages with a custom installation path by
+using the `vendor:` prefix.
+
+``` json
+{
+ "extra": {
+ "installer-paths": {
+ "your/custom/path/{$name}/": ["vendor:my_organization"]
+ }
+ }
+}
+```
+
+These would use your custom path for each of the listed packages. The available
+variables to use in your paths are: `{$name}`, `{$vendor}`, `{$type}`.
+
+## Custom Install Names
+
+If you're a package author and need your package to be named differently when
+installed consider using the `installer-name` extra.
+
+For example you have a package named `shama/cakephp-ftp` with the type
+`cakephp-plugin`. Installing with `composer/installers` would install to the
+path `Plugin/CakephpFtp`. Due to the strict naming conventions, you as a
+package author actually need the package to be named and installed to
+`Plugin/Ftp`. Using the following config within your **package** `composer.json`
+will allow this:
+
+```json
+{
+ "name": "shama/cakephp-ftp",
+ "type": "cakephp-plugin",
+ "extra": {
+ "installer-name": "Ftp"
+ }
+}
+```
+
+Please note the name entered into `installer-name` will be the final and will
+not be inflected.
+
+## Should we allow dynamic package types or paths? No.
+
+What are they? The ability for a package author to determine where a package
+will be installed either through setting the path directly in their
+`composer.json` or through a dynamic package type: `"type":
+"framework-install-here"`.
+
+It has been proposed many times. Even implemented once early on and then
+removed. Installers won't do this because it would allow a single package
+author to wipe out entire folders without the user's consent. That user would
+then come here to yell at us.
+
+Anyone still wanting this capability should consider requiring https://github.com/oomphinc/composer-installers-extender.
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/composer.json b/plugins/rainlab/userplus/vendor/composer/installers/composer.json
new file mode 100644
index 0000000..4dd0b90
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/composer.json
@@ -0,0 +1,99 @@
+{
+ "name": "composer/installers",
+ "type": "composer-plugin",
+ "license": "MIT",
+ "description": "A multi-framework Composer library installer",
+ "keywords": [
+ "installer",
+ "Aimeos",
+ "AGL",
+ "AnnotateCms",
+ "Attogram",
+ "Bitrix",
+ "CakePHP",
+ "Chef",
+ "Cockpit",
+ "CodeIgniter",
+ "concrete5",
+ "Craft",
+ "Croogo",
+ "DokuWiki",
+ "Dolibarr",
+ "Drupal",
+ "Elgg",
+ "Eliasis",
+ "ExpressionEngine",
+ "FuelPHP",
+ "Grav",
+ "Hurad",
+ "ImageCMS",
+ "iTop",
+ "Joomla",
+ "Kanboard",
+ "Kohana",
+ "Laravel",
+ "Lavalite",
+ "Lithium",
+ "Magento",
+ "Mako",
+ "Mautic",
+ "Maya",
+ "MODX Evo",
+ "MediaWiki",
+ "OXID",
+ "MODULEWork",
+ "Moodle",
+ "Piwik",
+ "phpBB",
+ "Plentymarkets",
+ "PPI",
+ "Puppet",
+ "Porto",
+ "RadPHP",
+ "ReIndex",
+ "Roundcube",
+ "shopware",
+ "SilverStripe",
+ "SMF",
+ "SyDES",
+ "symfony",
+ "Thelia",
+ "TYPO3",
+ "WolfCMS",
+ "WordPress",
+ "YAWIK",
+ "Zend",
+ "Zikula"
+ ],
+ "homepage": "https://composer.github.io/installers/",
+ "authors": [
+ {
+ "name": "Kyle Robinson Young",
+ "email": "kyle@dontkry.com",
+ "homepage": "https://github.com/shama"
+ }
+ ],
+ "autoload": {
+ "psr-4": { "Composer\\Installers\\": "src/Composer/Installers" }
+ },
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "replace": {
+ "shama/baton": "*",
+ "roundcube/plugin-installer": "*"
+ },
+ "require": {
+ "composer-plugin-api": "^1.0"
+ },
+ "require-dev": {
+ "composer/composer": "1.0.*@dev",
+ "phpunit/phpunit": "4.1.*"
+ },
+ "scripts": {
+ "test": "phpunit"
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/phpunit.xml.dist b/plugins/rainlab/userplus/vendor/composer/installers/phpunit.xml.dist
new file mode 100644
index 0000000..cc5cc99
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/phpunit.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ tests/Composer/Installers
+
+
+
+
+
+ src/Composer/Installers
+
+
+
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AglInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AglInstaller.php
new file mode 100644
index 0000000..01b8a41
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AglInstaller.php
@@ -0,0 +1,21 @@
+ 'More/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = preg_replace_callback('/(?:^|_|-)(.?)/', function ($matches) {
+ return strtoupper($matches[1]);
+ }, $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php
new file mode 100644
index 0000000..79a0e95
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php
@@ -0,0 +1,9 @@
+ 'ext/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php
new file mode 100644
index 0000000..89d7ad9
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php
@@ -0,0 +1,11 @@
+ 'addons/modules/{$name}/',
+ 'component' => 'addons/components/{$name}/',
+ 'service' => 'addons/services/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php
new file mode 100644
index 0000000..22dad1b
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php
@@ -0,0 +1,49 @@
+ 'Modules/{$name}/',
+ 'theme' => 'Themes/{$name}/'
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type asgard-module, cut off a trailing '-plugin' if present.
+ *
+ * For package type asgard-theme, cut off a trailing '-theme' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'asgard-module') {
+ return $this->inflectPluginVars($vars);
+ }
+
+ if ($vars['type'] === 'asgard-theme') {
+ return $this->inflectThemeVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectPluginVars($vars)
+ {
+ $vars['name'] = preg_replace('/-module$/', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+
+ protected function inflectThemeVars($vars)
+ {
+ $vars['name'] = preg_replace('/-theme$/', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php
new file mode 100644
index 0000000..d62fd8f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php
@@ -0,0 +1,9 @@
+ 'modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php
new file mode 100644
index 0000000..7082bf2
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php
@@ -0,0 +1,136 @@
+composer = $composer;
+ $this->package = $package;
+ $this->io = $io;
+ }
+
+ /**
+ * Return the install path based on package type.
+ *
+ * @param PackageInterface $package
+ * @param string $frameworkType
+ * @return string
+ */
+ public function getInstallPath(PackageInterface $package, $frameworkType = '')
+ {
+ $type = $this->package->getType();
+
+ $prettyName = $this->package->getPrettyName();
+ if (strpos($prettyName, '/') !== false) {
+ list($vendor, $name) = explode('/', $prettyName);
+ } else {
+ $vendor = '';
+ $name = $prettyName;
+ }
+
+ $availableVars = $this->inflectPackageVars(compact('name', 'vendor', 'type'));
+
+ $extra = $package->getExtra();
+ if (!empty($extra['installer-name'])) {
+ $availableVars['name'] = $extra['installer-name'];
+ }
+
+ if ($this->composer->getPackage()) {
+ $extra = $this->composer->getPackage()->getExtra();
+ if (!empty($extra['installer-paths'])) {
+ $customPath = $this->mapCustomInstallPaths($extra['installer-paths'], $prettyName, $type, $vendor);
+ if ($customPath !== false) {
+ return $this->templatePath($customPath, $availableVars);
+ }
+ }
+ }
+
+ $packageType = substr($type, strlen($frameworkType) + 1);
+ $locations = $this->getLocations();
+ if (!isset($locations[$packageType])) {
+ throw new \InvalidArgumentException(sprintf('Package type "%s" is not supported', $type));
+ }
+
+ return $this->templatePath($locations[$packageType], $availableVars);
+ }
+
+ /**
+ * For an installer to override to modify the vars per installer.
+ *
+ * @param array $vars
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ return $vars;
+ }
+
+ /**
+ * Gets the installer's locations
+ *
+ * @return array
+ */
+ public function getLocations()
+ {
+ return $this->locations;
+ }
+
+ /**
+ * Replace vars in a path
+ *
+ * @param string $path
+ * @param array $vars
+ * @return string
+ */
+ protected function templatePath($path, array $vars = array())
+ {
+ if (strpos($path, '{') !== false) {
+ extract($vars);
+ preg_match_all('@\{\$([A-Za-z0-9_]*)\}@i', $path, $matches);
+ if (!empty($matches[1])) {
+ foreach ($matches[1] as $var) {
+ $path = str_replace('{$' . $var . '}', $$var, $path);
+ }
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * Search through a passed paths array for a custom install path.
+ *
+ * @param array $paths
+ * @param string $name
+ * @param string $type
+ * @param string $vendor = NULL
+ * @return string
+ */
+ protected function mapCustomInstallPaths(array $paths, $name, $type, $vendor = NULL)
+ {
+ foreach ($paths as $path => $names) {
+ if (in_array($name, $names) || in_array('type:' . $type, $names) || in_array('vendor:' . $vendor, $names)) {
+ return $path;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php
new file mode 100644
index 0000000..e80cd1e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php
@@ -0,0 +1,126 @@
+.`.
+ * - `bitrix-d7-component` — copy the component to directory `bitrix/components//`.
+ * - `bitrix-d7-template` — copy the template to directory `bitrix/templates/_`.
+ *
+ * You can set custom path to directory with Bitrix kernel in `composer.json`:
+ *
+ * ```json
+ * {
+ * "extra": {
+ * "bitrix-dir": "s1/bitrix"
+ * }
+ * }
+ * ```
+ *
+ * @author Nik Samokhvalov
+ * @author Denis Kulichkin
+ */
+class BitrixInstaller extends BaseInstaller
+{
+ protected $locations = array(
+ 'module' => '{$bitrix_dir}/modules/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken)
+ 'component' => '{$bitrix_dir}/components/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken)
+ 'theme' => '{$bitrix_dir}/templates/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken)
+ 'd7-module' => '{$bitrix_dir}/modules/{$vendor}.{$name}/',
+ 'd7-component' => '{$bitrix_dir}/components/{$vendor}/{$name}/',
+ 'd7-template' => '{$bitrix_dir}/templates/{$vendor}_{$name}/',
+ );
+
+ /**
+ * @var array Storage for informations about duplicates at all the time of installation packages.
+ */
+ private static $checkedDuplicates = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($this->composer->getPackage()) {
+ $extra = $this->composer->getPackage()->getExtra();
+
+ if (isset($extra['bitrix-dir'])) {
+ $vars['bitrix_dir'] = $extra['bitrix-dir'];
+ }
+ }
+
+ if (!isset($vars['bitrix_dir'])) {
+ $vars['bitrix_dir'] = 'bitrix';
+ }
+
+ return parent::inflectPackageVars($vars);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function templatePath($path, array $vars = array())
+ {
+ $templatePath = parent::templatePath($path, $vars);
+ $this->checkDuplicates($templatePath, $vars);
+
+ return $templatePath;
+ }
+
+ /**
+ * Duplicates search packages.
+ *
+ * @param string $path
+ * @param array $vars
+ */
+ protected function checkDuplicates($path, array $vars = array())
+ {
+ $packageType = substr($vars['type'], strlen('bitrix') + 1);
+ $localDir = explode('/', $vars['bitrix_dir']);
+ array_pop($localDir);
+ $localDir[] = 'local';
+ $localDir = implode('/', $localDir);
+
+ $oldPath = str_replace(
+ array('{$bitrix_dir}', '{$name}'),
+ array($localDir, $vars['name']),
+ $this->locations[$packageType]
+ );
+
+ if (in_array($oldPath, static::$checkedDuplicates)) {
+ return;
+ }
+
+ if ($oldPath !== $path && file_exists($oldPath) && $this->io && $this->io->isInteractive()) {
+
+ $this->io->writeError(' Duplication of packages: ');
+ $this->io->writeError(' Package ' . $oldPath . ' will be called instead package ' . $path . ' ');
+
+ while (true) {
+ switch ($this->io->ask(' Delete ' . $oldPath . ' [y,n,?]? ', '?')) {
+ case 'y':
+ $fs = new Filesystem();
+ $fs->removeDirectory($oldPath);
+ break 2;
+
+ case 'n':
+ break 2;
+
+ case '?':
+ default:
+ $this->io->writeError(array(
+ ' y - delete package ' . $oldPath . ' and to continue with the installation',
+ ' n - don\'t delete and to continue with the installation',
+ ));
+ $this->io->writeError(' ? - print help');
+ break;
+ }
+ }
+ }
+
+ static::$checkedDuplicates[] = $oldPath;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php
new file mode 100644
index 0000000..da3aad2
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php
@@ -0,0 +1,9 @@
+ 'Packages/{$vendor}/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php
new file mode 100644
index 0000000..96e987a
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php
@@ -0,0 +1,83 @@
+ 'Plugin/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($this->matchesCakeVersion('>=', '3.0.0')) {
+ return $vars;
+ }
+
+ $nameParts = explode('/', $vars['name']);
+ foreach ($nameParts as &$value) {
+ $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value));
+ $value = str_replace(array('-', '_'), ' ', $value);
+ $value = str_replace(' ', '', ucwords($value));
+ }
+ $vars['name'] = implode('/', $nameParts);
+
+ return $vars;
+ }
+
+ /**
+ * Change the default plugin location when cakephp >= 3.0
+ */
+ public function getLocations()
+ {
+ if ($this->matchesCakeVersion('>=', '3.0.0')) {
+ $this->locations['plugin'] = $this->composer->getConfig()->get('vendor-dir') . '/{$vendor}/{$name}/';
+ }
+ return $this->locations;
+ }
+
+ /**
+ * Check if CakePHP version matches against a version
+ *
+ * @param string $matcher
+ * @param string $version
+ * @return bool
+ */
+ protected function matchesCakeVersion($matcher, $version)
+ {
+ if (class_exists('Composer\Semver\Constraint\MultiConstraint')) {
+ $multiClass = 'Composer\Semver\Constraint\MultiConstraint';
+ $constraintClass = 'Composer\Semver\Constraint\Constraint';
+ } else {
+ $multiClass = 'Composer\Package\LinkConstraint\MultiConstraint';
+ $constraintClass = 'Composer\Package\LinkConstraint\VersionConstraint';
+ }
+
+ $repositoryManager = $this->composer->getRepositoryManager();
+ if ($repositoryManager) {
+ $repos = $repositoryManager->getLocalRepository();
+ if (!$repos) {
+ return false;
+ }
+ $cake3 = new $multiClass(array(
+ new $constraintClass($matcher, $version),
+ new $constraintClass('!=', '9999999-dev'),
+ ));
+ $pool = new Pool('dev');
+ $pool->addRepository($repos);
+ $packages = $pool->whatProvides('cakephp/cakephp');
+ foreach ($packages as $package) {
+ $installed = new $constraintClass('=', $package->getVersion());
+ if ($cake3->matches($installed)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php
new file mode 100644
index 0000000..ab2f9aa
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php
@@ -0,0 +1,11 @@
+ 'Chef/{$vendor}/{$name}/',
+ 'role' => 'Chef/roles/{$name}/',
+ );
+}
+
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php
new file mode 100644
index 0000000..c887815
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php
@@ -0,0 +1,10 @@
+ 'CCF/orbit/{$name}/',
+ 'theme' => 'CCF/app/themes/{$name}/',
+ );
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php
new file mode 100644
index 0000000..c7816df
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php
@@ -0,0 +1,34 @@
+ 'cockpit/modules/addons/{$name}/',
+ );
+
+ /**
+ * Format module name.
+ *
+ * Strip `module-` prefix from package name.
+ *
+ * @param array @vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] == 'cockpit-module') {
+ return $this->inflectModuleVars($vars);
+ }
+
+ return $vars;
+ }
+
+ public function inflectModuleVars($vars)
+ {
+ $vars['name'] = ucfirst(preg_replace('/cockpit-/i', '', $vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php
new file mode 100644
index 0000000..3b4a4ec
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php
@@ -0,0 +1,11 @@
+ 'application/libraries/{$name}/',
+ 'third-party' => 'application/third_party/{$name}/',
+ 'module' => 'application/modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php
new file mode 100644
index 0000000..5c01baf
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php
@@ -0,0 +1,13 @@
+ 'concrete/',
+ 'block' => 'application/blocks/{$name}/',
+ 'package' => 'packages/{$name}/',
+ 'theme' => 'application/themes/{$name}/',
+ 'update' => 'updates/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php
new file mode 100644
index 0000000..d37a77a
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php
@@ -0,0 +1,35 @@
+ 'craft/plugins/{$name}/',
+ );
+
+ /**
+ * Strip `craft-` prefix and/or `-plugin` suffix from package names
+ *
+ * @param array $vars
+ *
+ * @return array
+ */
+ final public function inflectPackageVars($vars)
+ {
+ return $this->inflectPluginVars($vars);
+ }
+
+ private function inflectPluginVars($vars)
+ {
+ $vars['name'] = preg_replace('/-' . self::NAME_SUFFIX . '$/i', '', $vars['name']);
+ $vars['name'] = preg_replace('/^' . self::NAME_PREFIX . '-/i', '', $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php
new file mode 100644
index 0000000..d94219d
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php
@@ -0,0 +1,21 @@
+ 'Plugin/{$name}/',
+ 'theme' => 'View/Themed/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower(str_replace(array('-', '_'), ' ', $vars['name']));
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php
new file mode 100644
index 0000000..f4837a6
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php
@@ -0,0 +1,10 @@
+ 'app/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php
new file mode 100644
index 0000000..cfd638d
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php
@@ -0,0 +1,50 @@
+ 'lib/plugins/{$name}/',
+ 'template' => 'lib/tpl/{$name}/',
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type dokuwiki-plugin, cut off a trailing '-plugin',
+ * or leading dokuwiki_ if present.
+ *
+ * For package type dokuwiki-template, cut off a trailing '-template' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+
+ if ($vars['type'] === 'dokuwiki-plugin') {
+ return $this->inflectPluginVars($vars);
+ }
+
+ if ($vars['type'] === 'dokuwiki-template') {
+ return $this->inflectTemplateVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectPluginVars($vars)
+ {
+ $vars['name'] = preg_replace('/-plugin$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectTemplateVars($vars)
+ {
+ $vars['name'] = preg_replace('/-template$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']);
+
+ return $vars;
+ }
+
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php
new file mode 100644
index 0000000..21f7e8e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php
@@ -0,0 +1,16 @@
+
+ */
+class DolibarrInstaller extends BaseInstaller
+{
+ //TODO: Add support for scripts and themes
+ protected $locations = array(
+ 'module' => 'htdocs/custom/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php
new file mode 100644
index 0000000..a41ee2e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php
@@ -0,0 +1,16 @@
+ 'core/',
+ 'module' => 'modules/{$name}/',
+ 'theme' => 'themes/{$name}/',
+ 'library' => 'libraries/{$name}/',
+ 'profile' => 'profiles/{$name}/',
+ 'drush' => 'drush/{$name}/',
+ 'custom-theme' => 'themes/custom/{$name}/',
+ 'custom-module' => 'modules/custom/{$name}',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php
new file mode 100644
index 0000000..c0bb609
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php
@@ -0,0 +1,9 @@
+ 'mod/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php
new file mode 100644
index 0000000..2be4e3c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php
@@ -0,0 +1,9 @@
+ 'modules/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php
new file mode 100644
index 0000000..d5321a8
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php
@@ -0,0 +1,29 @@
+ 'system/expressionengine/third_party/{$name}/',
+ 'theme' => 'themes/third_party/{$name}/',
+ );
+
+ private $ee3Locations = array(
+ 'addon' => 'system/user/addons/{$name}/',
+ 'theme' => 'themes/user/{$name}/',
+ );
+
+ public function getInstallPath(PackageInterface $package, $frameworkType = '')
+ {
+
+ $version = "{$frameworkType}Locations";
+ $this->locations = $this->$version;
+
+ return parent::getInstallPath($package, $frameworkType);
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php
new file mode 100644
index 0000000..6eba2e3
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php
@@ -0,0 +1,11 @@
+ 'fuel/app/modules/{$name}/',
+ 'package' => 'fuel/packages/{$name}/',
+ 'theme' => 'fuel/app/themes/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php
new file mode 100644
index 0000000..29d980b
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php
@@ -0,0 +1,9 @@
+ 'components/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/GravInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/GravInstaller.php
new file mode 100644
index 0000000..dbe63e0
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/GravInstaller.php
@@ -0,0 +1,30 @@
+ 'user/plugins/{$name}/',
+ 'theme' => 'user/themes/{$name}/',
+ );
+
+ /**
+ * Format package name
+ *
+ * @param array $vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $restrictedWords = implode('|', array_keys($this->locations));
+
+ $vars['name'] = strtolower($vars['name']);
+ $vars['name'] = preg_replace('/^(?:grav-)?(?:(?:'.$restrictedWords.')-)?(.*?)(?:-(?:'.$restrictedWords.'))?$/ui',
+ '$1',
+ $vars['name']
+ );
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php
new file mode 100644
index 0000000..8fe017f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php
@@ -0,0 +1,25 @@
+ 'plugins/{$name}/',
+ 'theme' => 'plugins/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $nameParts = explode('/', $vars['name']);
+ foreach ($nameParts as &$value) {
+ $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value));
+ $value = str_replace(array('-', '_'), ' ', $value);
+ $value = str_replace(' ', '', ucwords($value));
+ }
+ $vars['name'] = implode('/', $nameParts);
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php
new file mode 100644
index 0000000..5e2142e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php
@@ -0,0 +1,11 @@
+ 'templates/{$name}/',
+ 'module' => 'application/modules/{$name}/',
+ 'library' => 'application/libraries/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Installer.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Installer.php
new file mode 100644
index 0000000..10f28bb
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Installer.php
@@ -0,0 +1,197 @@
+ 'AimeosInstaller',
+ 'asgard' => 'AsgardInstaller',
+ 'attogram' => 'AttogramInstaller',
+ 'agl' => 'AglInstaller',
+ 'annotatecms' => 'AnnotateCmsInstaller',
+ 'bitrix' => 'BitrixInstaller',
+ 'bonefish' => 'BonefishInstaller',
+ 'cakephp' => 'CakePHPInstaller',
+ 'chef' => 'ChefInstaller',
+ 'ccframework' => 'ClanCatsFrameworkInstaller',
+ 'cockpit' => 'CockpitInstaller',
+ 'codeigniter' => 'CodeIgniterInstaller',
+ 'concrete5' => 'Concrete5Installer',
+ 'craft' => 'CraftInstaller',
+ 'croogo' => 'CroogoInstaller',
+ 'dokuwiki' => 'DokuWikiInstaller',
+ 'dolibarr' => 'DolibarrInstaller',
+ 'decibel' => 'DecibelInstaller',
+ 'drupal' => 'DrupalInstaller',
+ 'elgg' => 'ElggInstaller',
+ 'eliasis' => 'EliasisInstaller',
+ 'ee3' => 'ExpressionEngineInstaller',
+ 'ee2' => 'ExpressionEngineInstaller',
+ 'fuel' => 'FuelInstaller',
+ 'fuelphp' => 'FuelphpInstaller',
+ 'grav' => 'GravInstaller',
+ 'hurad' => 'HuradInstaller',
+ 'imagecms' => 'ImageCMSInstaller',
+ 'itop' => 'ItopInstaller',
+ 'joomla' => 'JoomlaInstaller',
+ 'kanboard' => 'KanboardInstaller',
+ 'kirby' => 'KirbyInstaller',
+ 'kodicms' => 'KodiCMSInstaller',
+ 'kohana' => 'KohanaInstaller',
+ 'laravel' => 'LaravelInstaller',
+ 'lavalite' => 'LavaLiteInstaller',
+ 'lithium' => 'LithiumInstaller',
+ 'magento' => 'MagentoInstaller',
+ 'mako' => 'MakoInstaller',
+ 'maya' => 'MayaInstaller',
+ 'mautic' => 'MauticInstaller',
+ 'mediawiki' => 'MediaWikiInstaller',
+ 'microweber' => 'MicroweberInstaller',
+ 'modulework' => 'MODULEWorkInstaller',
+ 'modxevo' => 'MODXEvoInstaller',
+ 'moodle' => 'MoodleInstaller',
+ 'october' => 'OctoberInstaller',
+ 'ontowiki' => 'OntoWikiInstaller',
+ 'oxid' => 'OxidInstaller',
+ 'phpbb' => 'PhpBBInstaller',
+ 'pimcore' => 'PimcoreInstaller',
+ 'piwik' => 'PiwikInstaller',
+ 'plentymarkets'=> 'PlentymarketsInstaller',
+ 'ppi' => 'PPIInstaller',
+ 'puppet' => 'PuppetInstaller',
+ 'radphp' => 'RadPHPInstaller',
+ 'phifty' => 'PhiftyInstaller',
+ 'porto' => 'PortoInstaller',
+ 'redaxo' => 'RedaxoInstaller',
+ 'reindex' => 'ReIndexInstaller',
+ 'roundcube' => 'RoundcubeInstaller',
+ 'shopware' => 'ShopwareInstaller',
+ 'silverstripe' => 'SilverStripeInstaller',
+ 'smf' => 'SMFInstaller',
+ 'sydes' => 'SyDESInstaller',
+ 'symfony1' => 'Symfony1Installer',
+ 'thelia' => 'TheliaInstaller',
+ 'tusk' => 'TuskInstaller',
+ 'typo3-cms' => 'TYPO3CmsInstaller',
+ 'typo3-flow' => 'TYPO3FlowInstaller',
+ 'vanilla' => 'VanillaInstaller',
+ 'whmcs' => 'WHMCSInstaller',
+ 'wolfcms' => 'WolfCMSInstaller',
+ 'wordpress' => 'WordPressInstaller',
+ 'yawik' => 'YawikInstaller',
+ 'zend' => 'ZendInstaller',
+ 'zikula' => 'ZikulaInstaller',
+ 'prestashop' => 'PrestashopInstaller'
+ );
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getInstallPath(PackageInterface $package)
+ {
+ $type = $package->getType();
+ $frameworkType = $this->findFrameworkType($type);
+
+ if ($frameworkType === false) {
+ throw new \InvalidArgumentException(
+ 'Sorry the package type of this package is not yet supported.'
+ );
+ }
+
+ $class = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType];
+ $installer = new $class($package, $this->composer, $this->getIO());
+
+ return $installer->getInstallPath($package, $frameworkType);
+ }
+
+ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
+ {
+ if (!$repo->hasPackage($package)) {
+ throw new \InvalidArgumentException('Package is not installed: '.$package);
+ }
+
+ $repo->removePackage($package);
+
+ $installPath = $this->getInstallPath($package);
+ $this->io->write(sprintf('Deleting %s - %s', $installPath, $this->filesystem->removeDirectory($installPath) ? 'deleted ' : 'not deleted '));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function supports($packageType)
+ {
+ $frameworkType = $this->findFrameworkType($packageType);
+
+ if ($frameworkType === false) {
+ return false;
+ }
+
+ $locationPattern = $this->getLocationPattern($frameworkType);
+
+ return preg_match('#' . $frameworkType . '-' . $locationPattern . '#', $packageType, $matches) === 1;
+ }
+
+ /**
+ * Finds a supported framework type if it exists and returns it
+ *
+ * @param string $type
+ * @return string
+ */
+ protected function findFrameworkType($type)
+ {
+ $frameworkType = false;
+
+ krsort($this->supportedTypes);
+
+ foreach ($this->supportedTypes as $key => $val) {
+ if ($key === substr($type, 0, strlen($key))) {
+ $frameworkType = substr($type, 0, strlen($key));
+ break;
+ }
+ }
+
+ return $frameworkType;
+ }
+
+ /**
+ * Get the second part of the regular expression to check for support of a
+ * package type
+ *
+ * @param string $frameworkType
+ * @return string
+ */
+ protected function getLocationPattern($frameworkType)
+ {
+ $pattern = false;
+ if (!empty($this->supportedTypes[$frameworkType])) {
+ $frameworkClass = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType];
+ /** @var BaseInstaller $framework */
+ $framework = new $frameworkClass(null, $this->composer, $this->getIO());
+ $locations = array_keys($framework->getLocations());
+ $pattern = $locations ? '(' . implode('|', $locations) . ')' : false;
+ }
+
+ return $pattern ? : '(\w+)';
+ }
+
+ /**
+ * Get I/O object
+ *
+ * @return IOInterface
+ */
+ private function getIO()
+ {
+ return $this->io;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php
new file mode 100644
index 0000000..c6c1b33
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php
@@ -0,0 +1,9 @@
+ 'extensions/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php
new file mode 100644
index 0000000..9ee7759
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php
@@ -0,0 +1,15 @@
+ 'components/{$name}/',
+ 'module' => 'modules/{$name}/',
+ 'template' => 'templates/{$name}/',
+ 'plugin' => 'plugins/{$name}/',
+ 'library' => 'libraries/{$name}/',
+ );
+
+ // TODO: Add inflector for mod_ and com_ names
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php
new file mode 100644
index 0000000..9cb7b8c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php
@@ -0,0 +1,18 @@
+ 'plugins/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php
new file mode 100644
index 0000000..36b2f84
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php
@@ -0,0 +1,11 @@
+ 'site/plugins/{$name}/',
+ 'field' => 'site/fields/{$name}/',
+ 'tag' => 'site/tags/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php
new file mode 100644
index 0000000..7143e23
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php
@@ -0,0 +1,10 @@
+ 'cms/plugins/{$name}/',
+ 'media' => 'cms/media/vendor/{$name}/'
+ );
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php
new file mode 100644
index 0000000..dcd6d26
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php
@@ -0,0 +1,9 @@
+ 'modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php
new file mode 100644
index 0000000..be4d53a
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php
@@ -0,0 +1,9 @@
+ 'libraries/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php
new file mode 100644
index 0000000..8130b88
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php
@@ -0,0 +1,10 @@
+ 'packages/{$name}/',
+ 'theme' => 'public/themes/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php
new file mode 100644
index 0000000..47bbd4c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php
@@ -0,0 +1,10 @@
+ 'libraries/{$name}/',
+ 'source' => 'libraries/_source/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php
new file mode 100644
index 0000000..9c2e9fb
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php
@@ -0,0 +1,9 @@
+ 'modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php
new file mode 100644
index 0000000..5a66460
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php
@@ -0,0 +1,16 @@
+ 'assets/snippets/{$name}/',
+ 'plugin' => 'assets/plugins/{$name}/',
+ 'module' => 'assets/modules/{$name}/',
+ 'template' => 'assets/templates/{$name}/',
+ 'lib' => 'assets/lib/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php
new file mode 100644
index 0000000..cf18e94
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php
@@ -0,0 +1,11 @@
+ 'app/design/frontend/{$name}/',
+ 'skin' => 'skin/frontend/default/{$name}/',
+ 'library' => 'lib/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php
new file mode 100644
index 0000000..ca3cfac
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php
@@ -0,0 +1,9 @@
+ 'app/packages/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php
new file mode 100644
index 0000000..3e1ce2b
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php
@@ -0,0 +1,25 @@
+ 'plugins/{$name}/',
+ 'theme' => 'themes/{$name}/',
+ );
+
+ /**
+ * Format package name of mautic-plugins to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] == 'mautic-plugin') {
+ $vars['name'] = preg_replace_callback('/(-[a-z])/', function ($matches) {
+ return strtoupper($matches[0][1]);
+ }, ucfirst($vars['name']));
+ }
+
+ return $vars;
+ }
+
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php
new file mode 100644
index 0000000..30a9167
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php
@@ -0,0 +1,33 @@
+ 'modules/{$name}/',
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type maya-module, cut off a trailing '-module' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'maya-module') {
+ return $this->inflectModuleVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectModuleVars($vars)
+ {
+ $vars['name'] = preg_replace('/-module$/', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php
new file mode 100644
index 0000000..01008c6
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php
@@ -0,0 +1,50 @@
+ 'extensions/{$name}/',
+ 'skin' => 'skins/{$name}/',
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type mediawiki-extension, cut off a trailing '-extension' if present and transform
+ * to CamelCase keeping existing uppercase chars.
+ *
+ * For package type mediawiki-skin, cut off a trailing '-skin' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+
+ if ($vars['type'] === 'mediawiki-extension') {
+ return $this->inflectExtensionVars($vars);
+ }
+
+ if ($vars['type'] === 'mediawiki-skin') {
+ return $this->inflectSkinVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectExtensionVars($vars)
+ {
+ $vars['name'] = preg_replace('/-extension$/', '', $vars['name']);
+ $vars['name'] = str_replace('-', ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+
+ protected function inflectSkinVars($vars)
+ {
+ $vars['name'] = preg_replace('/-skin$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php
new file mode 100644
index 0000000..4bbbec8
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php
@@ -0,0 +1,111 @@
+ 'userfiles/modules/{$name}/',
+ 'module-skin' => 'userfiles/modules/{$name}/templates/',
+ 'template' => 'userfiles/templates/{$name}/',
+ 'element' => 'userfiles/elements/{$name}/',
+ 'vendor' => 'vendor/{$name}/',
+ 'components' => 'components/{$name}/'
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type microweber-module, cut off a trailing '-module' if present
+ *
+ * For package type microweber-template, cut off a trailing '-template' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'microweber-template') {
+ return $this->inflectTemplateVars($vars);
+ }
+ if ($vars['type'] === 'microweber-templates') {
+ return $this->inflectTemplatesVars($vars);
+ }
+ if ($vars['type'] === 'microweber-core') {
+ return $this->inflectCoreVars($vars);
+ }
+ if ($vars['type'] === 'microweber-adapter') {
+ return $this->inflectCoreVars($vars);
+ }
+ if ($vars['type'] === 'microweber-module') {
+ return $this->inflectModuleVars($vars);
+ }
+ if ($vars['type'] === 'microweber-modules') {
+ return $this->inflectModulesVars($vars);
+ }
+ if ($vars['type'] === 'microweber-skin') {
+ return $this->inflectSkinVars($vars);
+ }
+ if ($vars['type'] === 'microweber-element' or $vars['type'] === 'microweber-elements') {
+ return $this->inflectElementVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectTemplateVars($vars)
+ {
+ $vars['name'] = preg_replace('/-template$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/template-$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectTemplatesVars($vars)
+ {
+ $vars['name'] = preg_replace('/-templates$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/templates-$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectCoreVars($vars)
+ {
+ $vars['name'] = preg_replace('/-providers$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/-provider$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/-adapter$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectModuleVars($vars)
+ {
+ $vars['name'] = preg_replace('/-module$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/module-$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectModulesVars($vars)
+ {
+ $vars['name'] = preg_replace('/-modules$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/modules-$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectSkinVars($vars)
+ {
+ $vars['name'] = preg_replace('/-skin$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/skin-$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectElementVars($vars)
+ {
+ $vars['name'] = preg_replace('/-elements$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/elements-$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/-element$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/element-$/', '', $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php
new file mode 100644
index 0000000..a89c82f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php
@@ -0,0 +1,57 @@
+ 'mod/{$name}/',
+ 'admin_report' => 'admin/report/{$name}/',
+ 'atto' => 'lib/editor/atto/plugins/{$name}/',
+ 'tool' => 'admin/tool/{$name}/',
+ 'assignment' => 'mod/assignment/type/{$name}/',
+ 'assignsubmission' => 'mod/assign/submission/{$name}/',
+ 'assignfeedback' => 'mod/assign/feedback/{$name}/',
+ 'auth' => 'auth/{$name}/',
+ 'availability' => 'availability/condition/{$name}/',
+ 'block' => 'blocks/{$name}/',
+ 'booktool' => 'mod/book/tool/{$name}/',
+ 'cachestore' => 'cache/stores/{$name}/',
+ 'cachelock' => 'cache/locks/{$name}/',
+ 'calendartype' => 'calendar/type/{$name}/',
+ 'format' => 'course/format/{$name}/',
+ 'coursereport' => 'course/report/{$name}/',
+ 'datafield' => 'mod/data/field/{$name}/',
+ 'datapreset' => 'mod/data/preset/{$name}/',
+ 'editor' => 'lib/editor/{$name}/',
+ 'enrol' => 'enrol/{$name}/',
+ 'filter' => 'filter/{$name}/',
+ 'gradeexport' => 'grade/export/{$name}/',
+ 'gradeimport' => 'grade/import/{$name}/',
+ 'gradereport' => 'grade/report/{$name}/',
+ 'gradingform' => 'grade/grading/form/{$name}/',
+ 'local' => 'local/{$name}/',
+ 'logstore' => 'admin/tool/log/store/{$name}/',
+ 'ltisource' => 'mod/lti/source/{$name}/',
+ 'ltiservice' => 'mod/lti/service/{$name}/',
+ 'message' => 'message/output/{$name}/',
+ 'mnetservice' => 'mnet/service/{$name}/',
+ 'plagiarism' => 'plagiarism/{$name}/',
+ 'portfolio' => 'portfolio/{$name}/',
+ 'qbehaviour' => 'question/behaviour/{$name}/',
+ 'qformat' => 'question/format/{$name}/',
+ 'qtype' => 'question/type/{$name}/',
+ 'quizaccess' => 'mod/quiz/accessrule/{$name}/',
+ 'quiz' => 'mod/quiz/report/{$name}/',
+ 'report' => 'report/{$name}/',
+ 'repository' => 'repository/{$name}/',
+ 'scormreport' => 'mod/scorm/report/{$name}/',
+ 'search' => 'search/engine/{$name}/',
+ 'theme' => 'theme/{$name}/',
+ 'tinymce' => 'lib/editor/tinymce/plugins/{$name}/',
+ 'profilefield' => 'user/profile/field/{$name}/',
+ 'webservice' => 'webservice/{$name}/',
+ 'workshopallocation' => 'mod/workshop/allocation/{$name}/',
+ 'workshopeval' => 'mod/workshop/eval/{$name}/',
+ 'workshopform' => 'mod/workshop/form/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php
new file mode 100644
index 0000000..6bf53fd
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php
@@ -0,0 +1,46 @@
+ 'modules/{$name}/',
+ 'plugin' => 'plugins/{$vendor}/{$name}/',
+ 'theme' => 'themes/{$name}/'
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type october-plugin, cut off a trailing '-plugin' if present.
+ *
+ * For package type october-theme, cut off a trailing '-theme' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'october-plugin') {
+ return $this->inflectPluginVars($vars);
+ }
+
+ if ($vars['type'] === 'october-theme') {
+ return $this->inflectThemeVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectPluginVars($vars)
+ {
+ $vars['name'] = preg_replace('/-plugin$/', '', $vars['name']);
+
+ return $vars;
+ }
+
+ protected function inflectThemeVars($vars)
+ {
+ $vars['name'] = preg_replace('/-theme$/', '', $vars['name']);
+
+ return $vars;
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php
new file mode 100644
index 0000000..5dd3438
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php
@@ -0,0 +1,24 @@
+ 'extensions/{$name}/',
+ 'theme' => 'extensions/themes/{$name}/',
+ 'translation' => 'extensions/translations/{$name}/',
+ );
+
+ /**
+ * Format package name to lower case and remove ".ontowiki" suffix
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower($vars['name']);
+ $vars['name'] = preg_replace('/.ontowiki$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/-theme$/', '', $vars['name']);
+ $vars['name'] = preg_replace('/-translation$/', '', $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php
new file mode 100644
index 0000000..49940ff
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php
@@ -0,0 +1,59 @@
+.+)\/.+/';
+
+ protected $locations = array(
+ 'module' => 'modules/{$name}/',
+ 'theme' => 'application/views/{$name}/',
+ 'out' => 'out/{$name}/',
+ );
+
+ /**
+ * getInstallPath
+ *
+ * @param PackageInterface $package
+ * @param string $frameworkType
+ * @return void
+ */
+ public function getInstallPath(PackageInterface $package, $frameworkType = '')
+ {
+ $installPath = parent::getInstallPath($package, $frameworkType);
+ $type = $this->package->getType();
+ if ($type === 'oxid-module') {
+ $this->prepareVendorDirectory($installPath);
+ }
+ return $installPath;
+ }
+
+ /**
+ * prepareVendorDirectory
+ *
+ * Makes sure there is a vendormetadata.php file inside
+ * the vendor folder if there is a vendor folder.
+ *
+ * @param string $installPath
+ * @return void
+ */
+ protected function prepareVendorDirectory($installPath)
+ {
+ $matches = '';
+ $hasVendorDirectory = preg_match(self::VENDOR_PATTERN, $installPath, $matches);
+ if (!$hasVendorDirectory) {
+ return;
+ }
+
+ $vendorDirectory = $matches['vendor'];
+ $vendorPath = getcwd() . '/modules/' . $vendorDirectory;
+ if (!file_exists($vendorPath)) {
+ mkdir($vendorPath, 0755, true);
+ }
+
+ $vendorMetaDataPath = $vendorPath . '/vendormetadata.php';
+ touch($vendorMetaDataPath);
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php
new file mode 100644
index 0000000..170136f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php
@@ -0,0 +1,9 @@
+ 'modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php
new file mode 100644
index 0000000..4e59a8a
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php
@@ -0,0 +1,11 @@
+ 'bundles/{$name}/',
+ 'library' => 'libraries/{$name}/',
+ 'framework' => 'frameworks/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php
new file mode 100644
index 0000000..deb2b77
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php
@@ -0,0 +1,11 @@
+ 'ext/{$vendor}/{$name}/',
+ 'language' => 'language/{$name}/',
+ 'style' => 'styles/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php
new file mode 100644
index 0000000..4781fa6
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php
@@ -0,0 +1,21 @@
+ 'plugins/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name']));
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php
new file mode 100644
index 0000000..c17f457
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php
@@ -0,0 +1,32 @@
+ 'plugins/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ * @param array $vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name']));
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php
new file mode 100644
index 0000000..903e55f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php
@@ -0,0 +1,29 @@
+ '{$name}/'
+ );
+
+ /**
+ * Remove hyphen, "plugin" and format to camelcase
+ * @param array $vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = explode("-", $vars['name']);
+ foreach ($vars['name'] as $key => $name) {
+ $vars['name'][$key] = ucfirst($vars['name'][$key]);
+ if (strcasecmp($name, "Plugin") == 0) {
+ unset($vars['name'][$key]);
+ }
+ }
+ $vars['name'] = implode("",$vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Plugin.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Plugin.php
new file mode 100644
index 0000000..5eb04af
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Plugin.php
@@ -0,0 +1,17 @@
+getInstallationManager()->addInstaller($installer);
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php
new file mode 100644
index 0000000..dbf85e6
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php
@@ -0,0 +1,9 @@
+ 'app/Containers/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php
new file mode 100644
index 0000000..4c8421e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php
@@ -0,0 +1,10 @@
+ 'modules/{$name}/',
+ 'theme' => 'themes/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php
new file mode 100644
index 0000000..77cc3dd
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php
@@ -0,0 +1,11 @@
+ 'modules/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php
new file mode 100644
index 0000000..0f78b5c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php
@@ -0,0 +1,24 @@
+ 'src/{$name}/'
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $nameParts = explode('/', $vars['name']);
+ foreach ($nameParts as &$value) {
+ $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value));
+ $value = str_replace(array('-', '_'), ' ', $value);
+ $value = str_replace(' ', '', ucwords($value));
+ }
+ $vars['name'] = implode('/', $nameParts);
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php
new file mode 100644
index 0000000..252c733
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php
@@ -0,0 +1,10 @@
+ 'themes/{$name}/',
+ 'plugin' => 'plugins/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php
new file mode 100644
index 0000000..0954457
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php
@@ -0,0 +1,10 @@
+ 'redaxo/include/addons/{$name}/',
+ 'bestyle-plugin' => 'redaxo/include/addons/be_style/plugins/{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php
new file mode 100644
index 0000000..d8d795b
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php
@@ -0,0 +1,22 @@
+ 'plugins/{$name}/',
+ );
+
+ /**
+ * Lowercase name and changes the name to a underscores
+ *
+ * @param array $vars
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower(str_replace('-', '_', $vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php
new file mode 100644
index 0000000..1acd3b1
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php
@@ -0,0 +1,10 @@
+ 'Sources/{$name}/',
+ 'theme' => 'Themes/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php
new file mode 100644
index 0000000..d0193cb
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php
@@ -0,0 +1,60 @@
+ 'engine/Shopware/Plugins/Local/Backend/{$name}/',
+ 'core-plugin' => 'engine/Shopware/Plugins/Local/Core/{$name}/',
+ 'frontend-plugin' => 'engine/Shopware/Plugins/Local/Frontend/{$name}/',
+ 'theme' => 'templates/{$name}/',
+ 'plugin' => 'custom/plugins/{$name}/',
+ 'frontend-theme' => 'themes/Frontend/{$name}/',
+ );
+
+ /**
+ * Transforms the names
+ * @param array $vars
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'shopware-theme') {
+ return $this->correctThemeName($vars);
+ } else {
+ return $this->correctPluginName($vars);
+ }
+ }
+
+ /**
+ * Changes the name to a camelcased combination of vendor and name
+ * @param array $vars
+ * @return array
+ */
+ private function correctPluginName($vars)
+ {
+ $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) {
+ return strtoupper($matches[0][1]);
+ }, $vars['name']);
+
+ $vars['name'] = ucfirst($vars['vendor']) . ucfirst($camelCasedName);
+
+ return $vars;
+ }
+
+ /**
+ * Changes the name to a underscore separated name
+ * @param array $vars
+ * @return array
+ */
+ private function correctThemeName($vars)
+ {
+ $vars['name'] = str_replace('-', '_', $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php
new file mode 100644
index 0000000..17ca543
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php
@@ -0,0 +1,36 @@
+ '{$name}/',
+ 'theme' => 'themes/{$name}/',
+ );
+
+ /**
+ * Return the install path based on package type.
+ *
+ * Relies on built-in BaseInstaller behaviour with one exception: silverstripe/framework
+ * must be installed to 'sapphire' and not 'framework' if the version is <3.0.0
+ *
+ * @param PackageInterface $package
+ * @param string $frameworkType
+ * @return string
+ */
+ public function getInstallPath(PackageInterface $package, $frameworkType = '')
+ {
+ if (
+ $package->getName() == 'silverstripe/framework'
+ && preg_match('/^\d+\.\d+\.\d+/', $package->getVersion())
+ && version_compare($package->getVersion(), '2.999.999') < 0
+ ) {
+ return $this->templatePath($this->locations['module'], array('name' => 'sapphire'));
+ } else {
+ return parent::getInstallPath($package, $frameworkType);
+ }
+
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php
new file mode 100644
index 0000000..83ef9d0
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php
@@ -0,0 +1,49 @@
+ 'app/modules/{$name}/',
+ 'theme' => 'themes/{$name}/',
+ );
+
+ /**
+ * Format module name.
+ *
+ * Strip `sydes-` prefix and a trailing '-theme' or '-module' from package name if present.
+ *
+ * @param array @vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] == 'sydes-module') {
+ return $this->inflectModuleVars($vars);
+ }
+
+ if ($vars['type'] === 'sydes-theme') {
+ return $this->inflectThemeVars($vars);
+ }
+
+ return $vars;
+ }
+
+ public function inflectModuleVars($vars)
+ {
+ $vars['name'] = preg_replace('/(^sydes-|-module$)/i', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+
+ protected function inflectThemeVars($vars)
+ {
+ $vars['name'] = preg_replace('/(^sydes-|-theme$)/', '', $vars['name']);
+ $vars['name'] = strtolower($vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php
new file mode 100644
index 0000000..1675c4f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php
@@ -0,0 +1,26 @@
+
+ */
+class Symfony1Installer extends BaseInstaller
+{
+ protected $locations = array(
+ 'plugin' => 'plugins/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = preg_replace_callback('/(-[a-z])/', function ($matches) {
+ return strtoupper($matches[0][1]);
+ }, $vars['name']);
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php
new file mode 100644
index 0000000..b1663e8
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php
@@ -0,0 +1,16 @@
+
+ */
+class TYPO3CmsInstaller extends BaseInstaller
+{
+ protected $locations = array(
+ 'extension' => 'typo3conf/ext/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php
new file mode 100644
index 0000000..42572f4
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php
@@ -0,0 +1,38 @@
+ 'Packages/Application/{$name}/',
+ 'framework' => 'Packages/Framework/{$name}/',
+ 'plugin' => 'Packages/Plugins/{$name}/',
+ 'site' => 'Packages/Sites/{$name}/',
+ 'boilerplate' => 'Packages/Boilerplates/{$name}/',
+ 'build' => 'Build/{$name}/',
+ );
+
+ /**
+ * Modify the package name to be a TYPO3 Flow style key.
+ *
+ * @param array $vars
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $autoload = $this->package->getAutoload();
+ if (isset($autoload['psr-0']) && is_array($autoload['psr-0'])) {
+ $namespace = key($autoload['psr-0']);
+ $vars['name'] = str_replace('\\', '.', $namespace);
+ }
+ if (isset($autoload['psr-4']) && is_array($autoload['psr-4'])) {
+ $namespace = key($autoload['psr-4']);
+ $vars['name'] = rtrim(str_replace('\\', '.', $namespace), '.');
+ }
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php
new file mode 100644
index 0000000..158af52
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php
@@ -0,0 +1,12 @@
+ 'local/modules/{$name}/',
+ 'frontoffice-template' => 'templates/frontOffice/{$name}/',
+ 'backoffice-template' => 'templates/backOffice/{$name}/',
+ 'email-template' => 'templates/email/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php
new file mode 100644
index 0000000..7c0113b
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php
@@ -0,0 +1,14 @@
+
+ */
+ class TuskInstaller extends BaseInstaller
+ {
+ protected $locations = array(
+ 'task' => '.tusk/tasks/{$name}/',
+ 'command' => '.tusk/commands/{$name}/',
+ 'asset' => 'assets/tusk/{$name}/',
+ );
+ }
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php
new file mode 100644
index 0000000..24ca645
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php
@@ -0,0 +1,10 @@
+ 'plugins/{$name}/',
+ 'theme' => 'themes/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php
new file mode 100644
index 0000000..7d90c5e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php
@@ -0,0 +1,49 @@
+ 'src/{$vendor}/{$name}/',
+ 'theme' => 'themes/{$name}/'
+ );
+
+ /**
+ * Format package name.
+ *
+ * For package type vgmcp-bundle, cut off a trailing '-bundle' if present.
+ *
+ * For package type vgmcp-theme, cut off a trailing '-theme' if present.
+ *
+ */
+ public function inflectPackageVars($vars)
+ {
+ if ($vars['type'] === 'vgmcp-bundle') {
+ return $this->inflectPluginVars($vars);
+ }
+
+ if ($vars['type'] === 'vgmcp-theme') {
+ return $this->inflectThemeVars($vars);
+ }
+
+ return $vars;
+ }
+
+ protected function inflectPluginVars($vars)
+ {
+ $vars['name'] = preg_replace('/-bundle$/', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+
+ protected function inflectThemeVars($vars)
+ {
+ $vars['name'] = preg_replace('/-theme$/', '', $vars['name']);
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php
new file mode 100644
index 0000000..2cbb4a4
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php
@@ -0,0 +1,10 @@
+ 'modules/gateways/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php
new file mode 100644
index 0000000..cb38788
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php
@@ -0,0 +1,9 @@
+ 'wolf/plugins/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php
new file mode 100644
index 0000000..b03219c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php
@@ -0,0 +1,11 @@
+ 'wp-content/plugins/{$name}/',
+ 'theme' => 'wp-content/themes/{$name}/',
+ 'muplugin' => 'wp-content/mu-plugins/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php
new file mode 100644
index 0000000..27f429f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php
@@ -0,0 +1,32 @@
+ 'module/{$name}/',
+ );
+
+ /**
+ * Format package name to CamelCase
+ * @param array $vars
+ *
+ * @return array
+ */
+ public function inflectPackageVars($vars)
+ {
+ $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name']));
+ $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']);
+ $vars['name'] = str_replace(' ', '', ucwords($vars['name']));
+
+ return $vars;
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php
new file mode 100644
index 0000000..bde9bc8
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php
@@ -0,0 +1,11 @@
+ 'library/{$name}/',
+ 'extra' => 'extras/library/{$name}/',
+ 'module' => 'module/{$name}/',
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php
new file mode 100644
index 0000000..56cdf5d
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php
@@ -0,0 +1,10 @@
+ 'modules/{$vendor}-{$name}/',
+ 'theme' => 'themes/{$vendor}-{$name}/'
+ );
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/src/bootstrap.php b/plugins/rainlab/userplus/vendor/composer/installers/src/bootstrap.php
new file mode 100644
index 0000000..0de276e
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/src/bootstrap.php
@@ -0,0 +1,13 @@
+installer = new AsgardInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ array('name' => $expected, 'type' => $type),
+ $this->installer->inflectPackageVars(array('name' => $name, 'type' => $type))
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ // Should keep module name StudlyCase
+ array(
+ 'asgard-module',
+ 'user-profile',
+ 'UserProfile'
+ ),
+ array(
+ 'asgard-module',
+ 'asgard-module',
+ 'Asgard'
+ ),
+ array(
+ 'asgard-module',
+ 'blog',
+ 'Blog'
+ ),
+ // tests that exactly one '-module' is cut off
+ array(
+ 'asgard-module',
+ 'some-module-module',
+ 'SomeModule',
+ ),
+ // tests that exactly one '-theme' is cut off
+ array(
+ 'asgard-theme',
+ 'some-theme-theme',
+ 'SomeTheme',
+ ),
+ // tests that names without '-theme' suffix stay valid
+ array(
+ 'asgard-theme',
+ 'someothertheme',
+ 'Someothertheme',
+ ),
+ // Should keep theme name StudlyCase
+ array(
+ 'asgard-theme',
+ 'adminlte-advanced',
+ 'AdminlteAdvanced'
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/BitrixInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/BitrixInstallerTest.php
new file mode 100644
index 0000000..e909bb4
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/BitrixInstallerTest.php
@@ -0,0 +1,76 @@
+composer = new Composer();
+ }
+
+ /**
+ * @param string $vars
+ * @param string $expectedVars
+ *
+ * @covers ::inflectPackageVars
+ *
+ * @dataProvider provideExpectedInflectionResults
+ */
+ final public function testInflectPackageVars($vars, $expectedVars)
+ {
+
+ $this->installer = new BitrixInstaller(
+ new Package($vars['name'], '4.2', '4.2'),
+ $this->composer
+ );
+ $actual = $this->installer->inflectPackageVars($vars);
+ $this->assertEquals($actual, $expectedVars);
+ }
+
+ /**
+ * Provides various parameters for packages and the expected result after inflection
+ *
+ * @return array
+ */
+ final public function provideExpectedInflectionResults()
+ {
+ return array(
+ //check bitrix-dir is correct
+ array(
+ array('name' => 'Nyan/Cat'),
+ array('name' => 'Nyan/Cat', 'bitrix_dir' => 'bitrix')
+ ),
+ array(
+ array('name' => 'Nyan/Cat', 'bitrix_dir' => 'bitrix'),
+ array('name' => 'Nyan/Cat', 'bitrix_dir' => 'bitrix')
+ ),
+ array(
+ array('name' => 'Nyan/Cat', 'bitrix_dir' => 'local'),
+ array('name' => 'Nyan/Cat', 'bitrix_dir' => 'local')
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CakePHPInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CakePHPInstallerTest.php
new file mode 100644
index 0000000..523e847
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CakePHPInstallerTest.php
@@ -0,0 +1,115 @@
+package = new Package('CamelCased', '1.0', '1.0');
+ $this->io = $this->getMock('Composer\IO\PackageInterface');
+ $this->composer = new Composer();
+ $this->composer->setConfig(new Config(false));
+ }
+
+ /**
+ * testInflectPackageVars
+ *
+ * @return void
+ */
+ public function testInflectPackageVars()
+ {
+ $installer = new CakePHPInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'CamelCased'));
+ $this->assertEquals($result, array('name' => 'CamelCased'));
+
+ $installer = new CakePHPInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'with-dash'));
+ $this->assertEquals($result, array('name' => 'WithDash'));
+
+ $installer = new CakePHPInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'with_underscore'));
+ $this->assertEquals($result, array('name' => 'WithUnderscore'));
+
+ $installer = new CakePHPInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'cake/acl'));
+ $this->assertEquals($result, array('name' => 'Cake/Acl'));
+
+ $installer = new CakePHPInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'cake/debug-kit'));
+ $this->assertEquals($result, array('name' => 'Cake/DebugKit'));
+ }
+
+ /**
+ * Test getLocations returning appropriate values based on CakePHP version
+ *
+ */
+ public function testGetLocations() {
+ $package = new RootPackage('CamelCased', '1.0', '1.0');
+ $composer = $this->composer;
+ $rm = new RepositoryManager(
+ $this->getMock('Composer\IO\IOInterface'),
+ $this->getMock('Composer\Config')
+ );
+ $composer->setRepositoryManager($rm);
+ $installer = new CakePHPInstaller($package, $composer);
+
+ // 2.0 < cakephp < 3.0
+ $this->setCakephpVersion($rm, '2.0.0');
+ $result = $installer->getLocations();
+ $this->assertContains('Plugin/', $result['plugin']);
+
+ $this->setCakephpVersion($rm, '2.5.9');
+ $result = $installer->getLocations();
+ $this->assertContains('Plugin/', $result['plugin']);
+
+ $this->setCakephpVersion($rm, '~2.5');
+ $result = $installer->getLocations();
+ $this->assertContains('Plugin/', $result['plugin']);
+
+ // special handling for 2.x versions when 3.x is still in development
+ $this->setCakephpVersion($rm, 'dev-master');
+ $result = $installer->getLocations();
+ $this->assertContains('Plugin/', $result['plugin']);
+
+ $this->setCakephpVersion($rm, '>=2.5');
+ $result = $installer->getLocations();
+ $this->assertContains('Plugin/', $result['plugin']);
+
+ // cakephp >= 3.0
+ $this->setCakephpVersion($rm, '3.0.*-dev');
+ $result = $installer->getLocations();
+ $this->assertContains('vendor/{$vendor}/{$name}/', $result['plugin']);
+
+ $this->setCakephpVersion($rm, '~8.8');
+ $result = $installer->getLocations();
+ $this->assertContains('vendor/{$vendor}/{$name}/', $result['plugin']);
+ }
+
+ protected function setCakephpVersion($rm, $version) {
+ $parser = new VersionParser();
+ list(, $version) = explode(' ', $parser->parseConstraints($version));
+ $installed = new InstalledArrayRepository();
+ $package = new Package('cakephp/cakephp', $version, $version);
+ $installed->addPackage($package);
+ $rm->setLocalRepository($installed);
+ }
+
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CraftInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CraftInstallerTest.php
new file mode 100644
index 0000000..31ccecd
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/CraftInstallerTest.php
@@ -0,0 +1,83 @@
+installer = new CraftInstaller();
+ }
+
+ /**
+ * @param string $packageName
+ * @param string $expectedName
+ *
+ * @covers ::inflectPackageVars
+ *
+ * @dataProvider provideExpectedInflectionResults
+ */
+ final public function testInflectPackageVars($packageName, $expectedName)
+ {
+ $installer = $this->installer;
+
+ $vars = array('name' => $packageName);
+ $expected = array('name' => $expectedName);
+
+ $actual = $installer->inflectPackageVars($vars);
+
+ $this->assertEquals($actual, $expected);
+ }
+
+ /**
+ * Provides various names for packages and the expected result after inflection
+ *
+ * @return array
+ */
+ final public function provideExpectedInflectionResults()
+ {
+ return array(
+ // lowercase
+ array('foo', 'foo'),
+ array('craftfoo', 'craftfoo'),
+ array('fooplugin', 'fooplugin'),
+ array('craftfooplugin', 'craftfooplugin'),
+ // lowercase - dash
+ array('craft-foo', 'foo'),
+ array('foo-plugin', 'foo'),
+ array('craft-foo-plugin', 'foo'),
+ // lowercase - underscore
+ array('craft_foo', 'craft_foo'),
+ array('foo_plugin', 'foo_plugin'),
+ array('craft_foo_plugin', 'craft_foo_plugin'),
+ // CamelCase
+ array('Foo', 'Foo'),
+ array('CraftFoo', 'CraftFoo'),
+ array('FooPlugin', 'FooPlugin'),
+ array('CraftFooPlugin', 'CraftFooPlugin'),
+ // CamelCase - Dash
+ array('Craft-Foo', 'Foo'),
+ array('Foo-Plugin', 'Foo'),
+ array('Craft-Foo-Plugin', 'Foo'),
+ // CamelCase - underscore
+ array('Craft_Foo', 'Craft_Foo'),
+ array('Foo_Plugin', 'Foo_Plugin'),
+ array('Craft_Foo_Plugin', 'Craft_Foo_Plugin'),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/DokuWikiInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/DokuWikiInstallerTest.php
new file mode 100644
index 0000000..9e385e6
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/DokuWikiInstallerTest.php
@@ -0,0 +1,89 @@
+installer = new DokuWikiInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ $this->installer->inflectPackageVars(array('name' => $name, 'type'=>$type)),
+ array('name' => $expected, 'type'=>$type)
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ array(
+ 'dokuwiki-plugin',
+ 'dokuwiki-test-plugin',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-plugin',
+ 'test-plugin',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-plugin',
+ 'dokuwiki_test',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-plugin',
+ 'test',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-plugin',
+ 'test-template',
+ 'test-template',
+ ),
+ array(
+ 'dokuwiki-template',
+ 'dokuwiki-test-template',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-template',
+ 'test-template',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-template',
+ 'dokuwiki_test',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-template',
+ 'test',
+ 'test',
+ ),
+ array(
+ 'dokuwiki-template',
+ 'test-plugin',
+ 'test-plugin',
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/GravInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/GravInstallerTest.php
new file mode 100644
index 0000000..b757799
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/GravInstallerTest.php
@@ -0,0 +1,63 @@
+composer = new Composer();
+ }
+
+ public function testInflectPackageVars()
+ {
+ $package = $this->getPackage('vendor/name', '0.0.0');
+ $installer = new GravInstaller($package, $this->composer);
+ $packageVars = $this->getPackageVars($package);
+
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => 'test')));
+ $this->assertEquals('test', $result['name']);
+
+ foreach ($installer->getLocations() as $name => $location) {
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "$name-test")));
+ $this->assertEquals('test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "test-$name")));
+ $this->assertEquals('test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "$name-test-test")));
+ $this->assertEquals('test-test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "test-test-$name")));
+ $this->assertEquals('test-test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "grav-$name-test")));
+ $this->assertEquals('test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "grav-test-$name")));
+ $this->assertEquals('test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "grav-$name-test-test")));
+ $this->assertEquals('test-test', $result['name']);
+ $result = $installer->inflectPackageVars(array_merge($packageVars, array('name' => "grav-test-test-$name")));
+ $this->assertEquals('test-test', $result['name']);
+ }
+ }
+
+ /**
+ * @param $package \Composer\Package\PackageInterface
+ */
+ public function getPackageVars($package)
+ {
+ $type = $package->getType();
+
+ $prettyName = $package->getPrettyName();
+ if (strpos($prettyName, '/') !== false) {
+ list($vendor, $name) = explode('/', $prettyName);
+ } else {
+ $vendor = '';
+ $name = $prettyName;
+ }
+
+ return compact('name', 'vendor', 'type');
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/InstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/InstallerTest.php
new file mode 100644
index 0000000..91a303a
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/InstallerTest.php
@@ -0,0 +1,512 @@
+fs = new Filesystem;
+
+ $this->composer = new Composer();
+ $this->config = new Config();
+ $this->composer->setConfig($this->config);
+
+ $this->vendorDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'baton-test-vendor';
+ $this->ensureDirectoryExistsAndClear($this->vendorDir);
+
+ $this->binDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'baton-test-bin';
+ $this->ensureDirectoryExistsAndClear($this->binDir);
+
+ $this->config->merge(array(
+ 'config' => array(
+ 'vendor-dir' => $this->vendorDir,
+ 'bin-dir' => $this->binDir,
+ ),
+ ));
+
+ $this->dm = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->composer->setDownloadManager($this->dm);
+
+ $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
+ $this->io = $this->getMock('Composer\IO\IOInterface');
+ }
+
+ /**
+ * tearDown
+ *
+ * @return void
+ */
+ public function tearDown()
+ {
+ $this->fs->removeDirectory($this->vendorDir);
+ $this->fs->removeDirectory($this->binDir);
+ }
+
+ /**
+ * testSupports
+ *
+ * @return void
+ *
+ * @dataProvider dataForTestSupport
+ */
+ public function testSupports($type, $expected)
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $this->assertSame($expected, $installer->supports($type), sprintf('Failed to show support for %s', $type));
+ }
+
+ /**
+ * dataForTestSupport
+ */
+ public function dataForTestSupport()
+ {
+ return array(
+ array('agl-module', true),
+ array('aimeos-extension', true),
+ array('annotatecms-module', true),
+ array('annotatecms-component', true),
+ array('annotatecms-service', true),
+ array('attogram-module', true),
+ array('bitrix-module', true),
+ array('bitrix-component', true),
+ array('bitrix-theme', true),
+ array('bonefish-package', true),
+ array('cakephp', false),
+ array('cakephp-', false),
+ array('cakephp-app', false),
+ array('cakephp-plugin', true),
+ array('chef-cookbook', true),
+ array('chef-role', true),
+ array('cockpit-module', true),
+ array('codeigniter-app', false),
+ array('codeigniter-library', true),
+ array('codeigniter-third-party', true),
+ array('codeigniter-module', true),
+ array('concrete5-block', true),
+ array('concrete5-package', true),
+ array('concrete5-theme', true),
+ array('concrete5-core', true),
+ array('concrete5-update', true),
+ array('craft-plugin', true),
+ array('croogo-plugin', true),
+ array('croogo-theme', true),
+ array('decibel-app', true),
+ array('dokuwiki-plugin', true),
+ array('dokuwiki-template', true),
+ array('drupal-module', true),
+ array('dolibarr-module', true),
+ array('ee3-theme', true),
+ array('ee3-addon', true),
+ array('ee2-theme', true),
+ array('ee2-addon', true),
+ array('elgg-plugin', true),
+ array('eliasis-module', true),
+ array('fuel-module', true),
+ array('fuel-package', true),
+ array('fuel-theme', true),
+ array('fuelphp-component', true),
+ array('hurad-plugin', true),
+ array('hurad-theme', true),
+ array('imagecms-template', true),
+ array('imagecms-module', true),
+ array('imagecms-library', true),
+ array('itop-extension', true),
+ array('joomla-library', true),
+ array('kanboard-plugin', true),
+ array('kirby-plugin', true),
+ array('kohana-module', true),
+ array('laravel-library', true),
+ array('lavalite-theme', true),
+ array('lavalite-package', true),
+ array('lithium-library', true),
+ array('magento-library', true),
+ array('mako-package', true),
+ array('modxevo-snippet', true),
+ array('modxevo-plugin', true),
+ array('modxevo-module', true),
+ array('modxevo-template', true),
+ array('modxevo-lib', true),
+ array('mediawiki-extension', true),
+ array('mediawiki-skin', true),
+ array('microweber-module', true),
+ array('modulework-module', true),
+ array('moodle-mod', true),
+ array('october-module', true),
+ array('october-plugin', true),
+ array('piwik-plugin', true),
+ array('phpbb-extension', true),
+ array('pimcore-plugin', true),
+ array('plentymarkets-plugin', true),
+ array('ppi-module', true),
+ array('prestashop-module', true),
+ array('prestashop-theme', true),
+ array('puppet-module', true),
+ array('porto-container', true),
+ array('radphp-bundle', true),
+ array('redaxo-addon', true),
+ array('redaxo-bestyle-plugin', true),
+ array('reindex-theme', true),
+ array('reindex-plugin', true),
+ array('roundcube-plugin', true),
+ array('shopware-backend-plugin', true),
+ array('shopware-core-plugin', true),
+ array('shopware-frontend-plugin', true),
+ array('shopware-theme', true),
+ array('shopware-plugin', true),
+ array('shopware-frontend-theme', true),
+ array('silverstripe-module', true),
+ array('silverstripe-theme', true),
+ array('smf-module', true),
+ array('smf-theme', true),
+ array('sydes-module', true),
+ array('sydes-theme', true),
+ array('symfony1-plugin', true),
+ array('thelia-module', true),
+ array('thelia-frontoffice-template', true),
+ array('thelia-backoffice-template', true),
+ array('thelia-email-template', true),
+ array('tusk-task', true),
+ array('tusk-asset', true),
+ array('typo3-flow-plugin', true),
+ array('typo3-cms-extension', true),
+ array('vanilla-plugin', true),
+ array('vanilla-theme', true),
+ array('whmcs-gateway', true),
+ array('wolfcms-plugin', true),
+ array('wordpress-plugin', true),
+ array('wordpress-core', false),
+ array('yawik-module', true),
+ array('zend-library', true),
+ array('zikula-module', true),
+ array('zikula-theme', true),
+ array('kodicms-plugin', true),
+ array('kodicms-media', true),
+ array('phifty-bundle', true),
+ array('phifty-library', true),
+ array('phifty-framework', true),
+ );
+ }
+
+ /**
+ * testInstallPath
+ *
+ * @dataProvider dataForTestInstallPath
+ */
+ public function testInstallPath($type, $path, $name, $version = '1.0.0')
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package($name, $version, $version);
+
+ $package->setType($type);
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals($path, $result);
+ }
+
+ /**
+ * dataFormTestInstallPath
+ */
+ public function dataForTestInstallPath()
+ {
+ return array(
+ array('agl-module', 'More/MyTestPackage/', 'agl/my_test-package'),
+ array('aimeos-extension', 'ext/ai-test/', 'author/ai-test'),
+ array('annotatecms-module', 'addons/modules/my_module/', 'vysinsky/my_module'),
+ array('annotatecms-component', 'addons/components/my_component/', 'vysinsky/my_component'),
+ array('annotatecms-service', 'addons/services/my_service/', 'vysinsky/my_service'),
+ array('attogram-module', 'modules/my_module/', 'author/my_module'),
+ array('bitrix-module', 'bitrix/modules/my_module/', 'author/my_module'),
+ array('bitrix-component', 'bitrix/components/my_component/', 'author/my_component'),
+ array('bitrix-theme', 'bitrix/templates/my_theme/', 'author/my_theme'),
+ array('bitrix-d7-module', 'bitrix/modules/author.my_module/', 'author/my_module'),
+ array('bitrix-d7-component', 'bitrix/components/author/my_component/', 'author/my_component'),
+ array('bitrix-d7-template', 'bitrix/templates/author_my_template/', 'author/my_template'),
+ array('bonefish-package', 'Packages/bonefish/package/', 'bonefish/package'),
+ array('cakephp-plugin', 'Plugin/Ftp/', 'shama/ftp'),
+ array('chef-cookbook', 'Chef/mre/my_cookbook/', 'mre/my_cookbook'),
+ array('chef-role', 'Chef/roles/my_role/', 'mre/my_role'),
+ array('cockpit-module', 'cockpit/modules/addons/My_module/', 'piotr-cz/cockpit-my_module'),
+ array('codeigniter-library', 'application/libraries/my_package/', 'shama/my_package'),
+ array('codeigniter-module', 'application/modules/my_package/', 'shama/my_package'),
+ array('concrete5-block', 'application/blocks/concrete5_block/', 'remo/concrete5_block'),
+ array('concrete5-package', 'packages/concrete5_package/', 'remo/concrete5_package'),
+ array('concrete5-theme', 'application/themes/concrete5_theme/', 'remo/concrete5_theme'),
+ array('concrete5-core', 'concrete/', 'concrete5/core'),
+ array('concrete5-update', 'updates/concrete5/', 'concrete5/concrete5'),
+ array('craft-plugin', 'craft/plugins/my_plugin/', 'mdcpepper/my_plugin'),
+ array('croogo-plugin', 'Plugin/Sitemaps/', 'fahad19/sitemaps'),
+ array('croogo-theme', 'View/Themed/Readable/', 'rchavik/readable'),
+ array('decibel-app', 'app/someapp/', 'author/someapp'),
+ array('dokuwiki-plugin', 'lib/plugins/someplugin/', 'author/someplugin'),
+ array('dokuwiki-template', 'lib/tpl/sometemplate/', 'author/sometemplate'),
+ array('dolibarr-module', 'htdocs/custom/my_module/', 'shama/my_module'),
+ array('drupal-module', 'modules/my_module/', 'shama/my_module'),
+ array('drupal-theme', 'themes/my_module/', 'shama/my_module'),
+ array('drupal-profile', 'profiles/my_module/', 'shama/my_module'),
+ array('drupal-drush', 'drush/my_module/', 'shama/my_module'),
+ array('elgg-plugin', 'mod/sample_plugin/', 'test/sample_plugin'),
+ array('eliasis-module', 'modules/my_module/', 'shama/my_module'),
+ array('ee3-addon', 'system/user/addons/ee_theme/', 'author/ee_theme'),
+ array('ee3-theme', 'themes/user/ee_package/', 'author/ee_package'),
+ array('ee2-addon', 'system/expressionengine/third_party/ee_theme/', 'author/ee_theme'),
+ array('ee2-theme', 'themes/third_party/ee_package/', 'author/ee_package'),
+ array('fuel-module', 'fuel/app/modules/module/', 'fuel/module'),
+ array('fuel-package', 'fuel/packages/orm/', 'fuel/orm'),
+ array('fuel-theme', 'fuel/app/themes/theme/', 'fuel/theme'),
+ array('fuelphp-component', 'components/demo/', 'fuelphp/demo'),
+ array('hurad-plugin', 'plugins/Akismet/', 'atkrad/akismet'),
+ array('hurad-theme', 'plugins/Hurad2013/', 'atkrad/Hurad2013'),
+ array('imagecms-template', 'templates/my_template/', 'shama/my_template'),
+ array('imagecms-module', 'application/modules/my_module/', 'shama/my_module'),
+ array('imagecms-library', 'application/libraries/my_library/', 'shama/my_library'),
+ array('itop-extension', 'extensions/my_extension/', 'shama/my_extension'),
+ array('joomla-plugin', 'plugins/my_plugin/', 'shama/my_plugin'),
+ array('kanboard-plugin', 'plugins/my_plugin/', 'shama/my_plugin'),
+ array('kirby-plugin', 'site/plugins/my_plugin/', 'shama/my_plugin'),
+ array('kohana-module', 'modules/my_package/', 'shama/my_package'),
+ array('laravel-library', 'libraries/my_package/', 'shama/my_package'),
+ array('lavalite-theme', 'public/themes/my_theme/', 'shama/my_theme'),
+ array('lavalite-package', 'packages/my_package/', 'shama/my_package'),
+ array('lithium-library', 'libraries/li3_test/', 'user/li3_test'),
+ array('magento-library', 'lib/foo/', 'test/foo'),
+ array('modxevo-snippet', 'assets/snippets/my_snippet/', 'shama/my_snippet'),
+ array('modxevo-plugin', 'assets/plugins/my_plugin/', 'shama/my_plugin'),
+ array('modxevo-module', 'assets/modules/my_module/', 'shama/my_module'),
+ array('modxevo-template', 'assets/templates/my_template/', 'shama/my_template'),
+ array('modxevo-lib', 'assets/lib/my_lib/', 'shama/my_lib'),
+ array('mako-package', 'app/packages/my_package/', 'shama/my_package'),
+ array('mediawiki-extension', 'extensions/APC/', 'author/APC'),
+ array('mediawiki-extension', 'extensions/APC/', 'author/APC-extension'),
+ array('mediawiki-extension', 'extensions/UploadWizard/', 'author/upload-wizard'),
+ array('mediawiki-extension', 'extensions/SyntaxHighlight_GeSHi/', 'author/syntax-highlight_GeSHi'),
+ array('mediawiki-skin', 'skins/someskin/', 'author/someskin-skin'),
+ array('mediawiki-skin', 'skins/someskin/', 'author/someskin'),
+ array('microweber-module', 'userfiles/modules/my-thing/', 'author/my-thing-module'),
+ array('modulework-module', 'modules/my_package/', 'shama/my_package'),
+ array('moodle-mod', 'mod/my_package/', 'shama/my_package'),
+ array('october-module', 'modules/my_plugin/', 'shama/my_plugin'),
+ array('october-plugin', 'plugins/shama/my_plugin/', 'shama/my_plugin'),
+ array('october-theme', 'themes/my_theme/', 'shama/my_theme'),
+ array('piwik-plugin', 'plugins/VisitSummary/', 'shama/visit-summary'),
+ array('prestashop-module', 'modules/a-module/', 'vendor/a-module'),
+ array('prestashop-theme', 'themes/a-theme/', 'vendor/a-theme'),
+ array('phpbb-extension', 'ext/test/foo/', 'test/foo'),
+ array('phpbb-style', 'styles/foo/', 'test/foo'),
+ array('phpbb-language', 'language/foo/', 'test/foo'),
+ array('pimcore-plugin', 'plugins/MyPlugin/', 'ubikz/my_plugin'),
+ array('plentymarkets-plugin', 'HelloWorld/', 'plugin-hello-world'),
+ array('ppi-module', 'modules/foo/', 'test/foo'),
+ array('puppet-module', 'modules/puppet-name/', 'puppet/puppet-name'),
+ array('porto-container', 'app/Containers/container-name/', 'test/container-name'),
+ array('radphp-bundle', 'src/Migration/', 'atkrad/migration'),
+ array('redaxo-addon', 'redaxo/include/addons/my_plugin/', 'shama/my_plugin'),
+ array('redaxo-bestyle-plugin', 'redaxo/include/addons/be_style/plugins/my_plugin/', 'shama/my_plugin'),
+ array('reindex-theme', 'themes/my_module/', 'author/my_module'),
+ array('reindex-plugin', 'plugins/my_module/', 'author/my_module'),
+ array('roundcube-plugin', 'plugins/base/', 'test/base'),
+ array('roundcube-plugin', 'plugins/replace_dash/', 'test/replace-dash'),
+ array('shopware-backend-plugin', 'engine/Shopware/Plugins/Local/Backend/ShamaMyBackendPlugin/', 'shama/my-backend-plugin'),
+ array('shopware-core-plugin', 'engine/Shopware/Plugins/Local/Core/ShamaMyCorePlugin/', 'shama/my-core-plugin'),
+ array('shopware-frontend-plugin', 'engine/Shopware/Plugins/Local/Frontend/ShamaMyFrontendPlugin/', 'shama/my-frontend-plugin'),
+ array('shopware-theme', 'templates/my_theme/', 'shama/my-theme'),
+ array('shopware-frontend-theme', 'themes/Frontend/ShamaMyFrontendTheme/', 'shama/my-frontend-theme'),
+ array('shopware-plugin', 'custom/plugins/ShamaMyPlugin/', 'shama/my-plugin'),
+ array('silverstripe-module', 'my_module/', 'shama/my_module'),
+ array('silverstripe-module', 'sapphire/', 'silverstripe/framework', '2.4.0'),
+ array('silverstripe-module', 'framework/', 'silverstripe/framework', '3.0.0'),
+ array('silverstripe-module', 'framework/', 'silverstripe/framework', '3.0.0-rc1'),
+ array('silverstripe-module', 'framework/', 'silverstripe/framework', 'my/branch'),
+ array('silverstripe-theme', 'themes/my_theme/', 'shama/my_theme'),
+ array('smf-module', 'Sources/my_module/', 'shama/my_module'),
+ array('smf-theme', 'Themes/my_theme/', 'shama/my_theme'),
+ array('symfony1-plugin', 'plugins/sfShamaPlugin/', 'shama/sfShamaPlugin'),
+ array('symfony1-plugin', 'plugins/sfShamaPlugin/', 'shama/sf-shama-plugin'),
+ array('thelia-module', 'local/modules/my_module/', 'shama/my_module'),
+ array('thelia-frontoffice-template', 'templates/frontOffice/my_template_fo/', 'shama/my_template_fo'),
+ array('thelia-backoffice-template', 'templates/backOffice/my_template_bo/', 'shama/my_template_bo'),
+ array('thelia-email-template', 'templates/email/my_template_email/', 'shama/my_template_email'),
+ array('tusk-task', '.tusk/tasks/my_task/', 'shama/my_task'),
+ array('typo3-flow-package', 'Packages/Application/my_package/', 'shama/my_package'),
+ array('typo3-flow-build', 'Build/my_package/', 'shama/my_package'),
+ array('typo3-cms-extension', 'typo3conf/ext/my_extension/', 'shama/my_extension'),
+ array('vanilla-plugin', 'plugins/my_plugin/', 'shama/my_plugin'),
+ array('vanilla-theme', 'themes/my_theme/', 'shama/my_theme'),
+ array('whmcs-gateway', 'modules/gateways/gateway_name/', 'vendor/gateway_name'),
+ array('wolfcms-plugin', 'wolf/plugins/my_plugin/', 'shama/my_plugin'),
+ array('wordpress-plugin', 'wp-content/plugins/my_plugin/', 'shama/my_plugin'),
+ array('wordpress-muplugin', 'wp-content/mu-plugins/my_plugin/', 'shama/my_plugin'),
+ array('zend-extra', 'extras/library/zend_test/', 'shama/zend_test'),
+ array('zikula-module', 'modules/my-test_module/', 'my/test_module'),
+ array('zikula-theme', 'themes/my-test_theme/', 'my/test_theme'),
+ array('kodicms-media', 'cms/media/vendor/my_media/', 'shama/my_media'),
+ array('kodicms-plugin', 'cms/plugins/my_plugin/', 'shama/my_plugin'),
+ array('phifty-bundle', 'bundles/core/', 'shama/core'),
+ array('phifty-library', 'libraries/my-lib/', 'shama/my-lib'),
+ array('phifty-framework', 'frameworks/my-framework/', 'shama/my-framework'),
+ array('yawik-module', 'module/MyModule/', 'shama/my_module'),
+ );
+ }
+
+ /**
+ * testGetCakePHPInstallPathException
+ *
+ * @return void
+ *
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGetCakePHPInstallPathException()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('shama/ftp', '1.0.0', '1.0.0');
+
+ $package->setType('cakephp-whoops');
+ $result = $installer->getInstallPath($package);
+ }
+
+ /**
+ * testCustomInstallPath
+ */
+ public function testCustomInstallPath()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('shama/ftp', '1.0.0', '1.0.0');
+ $package->setType('cakephp-plugin');
+ $consumerPackage = new RootPackage('foo/bar', '1.0.0', '1.0.0');
+ $this->composer->setPackage($consumerPackage);
+ $consumerPackage->setExtra(array(
+ 'installer-paths' => array(
+ 'my/custom/path/{$name}/' => array(
+ 'shama/ftp',
+ 'foo/bar',
+ ),
+ ),
+ ));
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('my/custom/path/Ftp/', $result);
+ }
+
+ /**
+ * testCustomInstallerName
+ */
+ public function testCustomInstallerName()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('shama/cakephp-ftp-plugin', '1.0.0', '1.0.0');
+ $package->setType('cakephp-plugin');
+ $package->setExtra(array(
+ 'installer-name' => 'FTP',
+ ));
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('Plugin/FTP/', $result);
+ }
+
+ /**
+ * testCustomTypePath
+ */
+ public function testCustomTypePath()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('slbmeh/my_plugin', '1.0.0', '1.0.0');
+ $package->setType('wordpress-plugin');
+ $consumerPackage = new RootPackage('foo/bar', '1.0.0', '1.0.0');
+ $this->composer->setPackage($consumerPackage);
+ $consumerPackage->setExtra(array(
+ 'installer-paths' => array(
+ 'my/custom/path/{$name}/' => array(
+ 'type:wordpress-plugin'
+ ),
+ ),
+ ));
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('my/custom/path/my_plugin/', $result);
+ }
+
+ /**
+ * testVendorPath
+ */
+ public function testVendorPath()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('penyaskito/my_module', '1.0.0', '1.0.0');
+ $package->setType('drupal-module');
+ $consumerPackage = new RootPackage('drupal/drupal', '1.0.0', '1.0.0');
+ $this->composer->setPackage($consumerPackage);
+ $consumerPackage->setExtra(array(
+ 'installer-paths' => array(
+ 'modules/custom/{$name}/' => array(
+ 'vendor:penyaskito'
+ ),
+ ),
+ ));
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('modules/custom/my_module/', $result);
+ }
+
+ /**
+ * testNoVendorName
+ */
+ public function testNoVendorName()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('sfPhpunitPlugin', '1.0.0', '1.0.0');
+
+ $package->setType('symfony1-plugin');
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('plugins/sfPhpunitPlugin/', $result);
+ }
+
+ /**
+ * testTypo3Inflection
+ */
+ public function testTypo3Inflection()
+ {
+ $installer = new Installer($this->io, $this->composer);
+ $package = new Package('typo3/fluid', '1.0.0', '1.0.0');
+
+ $package->setAutoload(array(
+ 'psr-0' => array(
+ 'TYPO3\\Fluid' => 'Classes',
+ ),
+ ));
+
+ $package->setType('typo3-flow-package');
+ $result = $installer->getInstallPath($package);
+ $this->assertEquals('Packages/Application/TYPO3.Fluid/', $result);
+ }
+
+ public function testUninstallAndDeletePackageFromLocalRepo()
+ {
+ $package = new Package('foo', '1.0.0', '1.0.0');
+
+ $installer = $this->getMock('Composer\Installers\Installer', array('getInstallPath'), array($this->io, $this->composer));
+ $installer->expects($this->once())->method('getInstallPath')->with($package)->will($this->returnValue(sys_get_temp_dir().'/foo'));
+
+ $repo = $this->getMock('Composer\Repository\InstalledRepositoryInterface');
+ $repo->expects($this->once())->method('hasPackage')->with($package)->will($this->returnValue(true));
+ $repo->expects($this->once())->method('removePackage')->with($package);
+
+ $installer->uninstall($repo, $package);
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MayaInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MayaInstallerTest.php
new file mode 100644
index 0000000..f800c61
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MayaInstallerTest.php
@@ -0,0 +1,61 @@
+installer = new MayaInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ array('name' => $expected, 'type' => $type),
+ $this->installer->inflectPackageVars(array('name' => $name, 'type' => $type))
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ // Should keep module name StudlyCase
+ array(
+ 'maya-module',
+ 'user-profile',
+ 'UserProfile'
+ ),
+ array(
+ 'maya-module',
+ 'maya-module',
+ 'Maya'
+ ),
+ array(
+ 'maya-module',
+ 'blog',
+ 'Blog'
+ ),
+ // tests that exactly one '-module' is cut off
+ array(
+ 'maya-module',
+ 'some-module-module',
+ 'SomeModule',
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MediaWikiInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MediaWikiInstallerTest.php
new file mode 100644
index 0000000..3675e18
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/MediaWikiInstallerTest.php
@@ -0,0 +1,66 @@
+installer = new MediaWikiInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ $this->installer->inflectPackageVars(array('name' => $name, 'type'=>$type)),
+ array('name' => $expected, 'type'=>$type)
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ array(
+ 'mediawiki-extension',
+ 'sub-page-list',
+ 'SubPageList',
+ ),
+ array(
+ 'mediawiki-extension',
+ 'sub-page-list-extension',
+ 'SubPageList',
+ ),
+ array(
+ 'mediawiki-extension',
+ 'semantic-mediawiki',
+ 'SemanticMediawiki',
+ ),
+ // tests that exactly one '-skin' is cut off, and that skins do not get ucwords treatment like extensions
+ array(
+ 'mediawiki-skin',
+ 'some-skin-skin',
+ 'some-skin',
+ ),
+ // tests that names without '-skin' suffix stay valid
+ array(
+ 'mediawiki-skin',
+ 'someotherskin',
+ 'someotherskin',
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OctoberInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OctoberInstallerTest.php
new file mode 100644
index 0000000..fd427cd
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OctoberInstallerTest.php
@@ -0,0 +1,66 @@
+installer = new OctoberInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ $this->installer->inflectPackageVars(array('name' => $name, 'type' => $type)),
+ array('name' => $expected, 'type' => $type)
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ array(
+ 'october-plugin',
+ 'subpagelist',
+ 'subpagelist',
+ ),
+ array(
+ 'october-plugin',
+ 'subpagelist-plugin',
+ 'subpagelist',
+ ),
+ array(
+ 'october-plugin',
+ 'semanticoctober',
+ 'semanticoctober',
+ ),
+ // tests that exactly one '-theme' is cut off
+ array(
+ 'october-theme',
+ 'some-theme-theme',
+ 'some-theme',
+ ),
+ // tests that names without '-theme' suffix stay valid
+ array(
+ 'october-theme',
+ 'someothertheme',
+ 'someothertheme',
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OntoWikiInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OntoWikiInstallerTest.php
new file mode 100644
index 0000000..24c498c
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/OntoWikiInstallerTest.php
@@ -0,0 +1,85 @@
+installer = new OntoWikiInstaller();
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ $this->installer->inflectPackageVars(array('name' => $name, 'type'=>$type)),
+ array('name' => $expected, 'type'=>$type)
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ array(
+ 'ontowiki-extension',
+ 'CSVImport.ontowiki',
+ 'csvimport',
+ ),
+ array(
+ 'ontowiki-extension',
+ 'csvimport',
+ 'csvimport',
+ ),
+ array(
+ 'ontowiki-extension',
+ 'some_ontowiki_extension',
+ 'some_ontowiki_extension',
+ ),
+ array(
+ 'ontowiki-extension',
+ 'some_ontowiki_extension.ontowiki',
+ 'some_ontowiki_extension',
+ ),
+ array(
+ 'ontowiki-translation',
+ 'de-translation.ontowiki',
+ 'de',
+ ),
+ array(
+ 'ontowiki-translation',
+ 'en-US-translation.ontowiki',
+ 'en-us',
+ ),
+ array(
+ 'ontowiki-translation',
+ 'en-US-translation',
+ 'en-us',
+ ),
+ array(
+ 'ontowiki-theme',
+ 'blue-theme.ontowiki',
+ 'blue',
+ ),
+ array(
+ 'ontowiki-theme',
+ 'blue-theme',
+ 'blue',
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PimcoreInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PimcoreInstallerTest.php
new file mode 100644
index 0000000..ea79374
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PimcoreInstallerTest.php
@@ -0,0 +1,44 @@
+package = new Package('CamelCased', '1.0', '1.0');
+ $this->io = $this->getMock('Composer\IO\PackageInterface');
+ $this->composer = new Composer();
+ }
+
+ /**
+ * testInflectPackageVars
+ *
+ * @return void
+ */
+ public function testInflectPackageVars()
+ {
+ $installer = new PimcoreInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'CamelCased'));
+ $this->assertEquals($result, array('name' => 'CamelCased'));
+
+ $installer = new PimcoreInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'with-dash'));
+ $this->assertEquals($result, array('name' => 'WithDash'));
+
+ $installer = new PimcoreInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'with_underscore'));
+ $this->assertEquals($result, array('name' => 'WithUnderscore'));
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PiwikInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PiwikInstallerTest.php
new file mode 100644
index 0000000..8d9ff3f
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/PiwikInstallerTest.php
@@ -0,0 +1,63 @@
+package = new Package('VisitSummary', '1.0', '1.0');
+ $this->io = $this->getMock('Composer\IO\PackageInterface');
+ $this->composer = new Composer();
+ }
+
+ /**
+ * testInflectPackageVars
+ *
+ * @return void
+ */
+ public function testInflectPackageVars()
+ {
+ $installer = new PiwikInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'VisitSummary'));
+ $this->assertEquals($result, array('name' => 'VisitSummary'));
+
+ $installer = new PiwikInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'visit-summary'));
+ $this->assertEquals($result, array('name' => 'VisitSummary'));
+
+ $installer = new PiwikInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => 'visit_summary'));
+ $this->assertEquals($result, array('name' => 'VisitSummary'));
+ }
+
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/SyDESInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/SyDESInstallerTest.php
new file mode 100644
index 0000000..1521fbf
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/SyDESInstallerTest.php
@@ -0,0 +1,81 @@
+installer = new SyDESInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ array('name' => $expected, 'type' => $type),
+ $this->installer->inflectPackageVars(array('name' => $name, 'type' => $type))
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ // modules
+ array(
+ 'sydes-module',
+ 'name',
+ 'Name'
+ ),
+ array(
+ 'sydes-module',
+ 'sample-name',
+ 'SampleName'
+ ),
+ array(
+ 'sydes-module',
+ 'sydes-name',
+ 'Name'
+ ),
+ array(
+ 'sydes-module',
+ 'sample-name-module',
+ 'SampleName',
+ ),
+ array(
+ 'sydes-module',
+ 'sydes-sample-name-module',
+ 'SampleName'
+ ),
+ // themes
+ array(
+ 'sydes-theme',
+ 'some-theme-theme',
+ 'some-theme',
+ ),
+ array(
+ 'sydes-theme',
+ 'sydes-sometheme',
+ 'sometheme',
+ ),
+ array(
+ 'sydes-theme',
+ 'Sample-Name',
+ 'sample-name'
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/TestCase.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/TestCase.php
new file mode 100644
index 0000000..6418a03
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/TestCase.php
@@ -0,0 +1,64 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Installers\Test;
+
+use Composer\Package\Version\VersionParser;
+use Composer\Package\Package;
+use Composer\Package\AliasPackage;
+use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\Util\Filesystem;
+
+abstract class TestCase extends \PHPUnit_Framework_TestCase
+{
+ private static $parser;
+
+ protected static function getVersionParser()
+ {
+ if (!self::$parser) {
+ self::$parser = new VersionParser();
+ }
+
+ return self::$parser;
+ }
+
+ protected function getVersionConstraint($operator, $version)
+ {
+ return new VersionConstraint(
+ $operator,
+ self::getVersionParser()->normalize($version)
+ );
+ }
+
+ protected function getPackage($name, $version)
+ {
+ $normVersion = self::getVersionParser()->normalize($version);
+
+ return new Package($name, $normVersion, $version);
+ }
+
+ protected function getAliasPackage($package, $version)
+ {
+ $normVersion = self::getVersionParser()->normalize($version);
+
+ return new AliasPackage($package, $normVersion, $version);
+ }
+
+ protected function ensureDirectoryExistsAndClear($directory)
+ {
+ $fs = new Filesystem();
+ if (is_dir($directory)) {
+ $fs->removeDirectory($directory);
+ }
+ mkdir($directory, 0777, true);
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/VgmcpInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/VgmcpInstallerTest.php
new file mode 100644
index 0000000..8b33ab8
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/VgmcpInstallerTest.php
@@ -0,0 +1,79 @@
+installer = new VgmcpInstaller(
+ new Package('NyanCat', '4.2', '4.2'),
+ new Composer()
+ );
+ }
+
+ /**
+ * @dataProvider packageNameInflectionProvider
+ */
+ public function testInflectPackageVars($type, $name, $expected)
+ {
+ $this->assertEquals(
+ array('name' => $expected, 'type' => $type),
+ $this->installer->inflectPackageVars(array('name' => $name, 'type' => $type))
+ );
+ }
+
+ public function packageNameInflectionProvider()
+ {
+ return array(
+ // Should keep bundle name StudlyCase
+ array(
+ 'vgmcp-bundle',
+ 'user-profile',
+ 'UserProfile'
+ ),
+ array(
+ 'vgmcp-bundle',
+ 'vgmcp-bundle',
+ 'Vgmcp'
+ ),
+ array(
+ 'vgmcp-bundle',
+ 'blog',
+ 'Blog'
+ ),
+ // tests that exactly one '-bundle' is cut off
+ array(
+ 'vgmcp-bundle',
+ 'some-bundle-bundle',
+ 'SomeBundle',
+ ),
+ // tests that exactly one '-theme' is cut off
+ array(
+ 'vgmcp-theme',
+ 'some-theme-theme',
+ 'SomeTheme',
+ ),
+ // tests that names without '-theme' suffix stay valid
+ array(
+ 'vgmcp-theme',
+ 'someothertheme',
+ 'Someothertheme',
+ ),
+ // Should keep theme name StudlyCase
+ array(
+ 'vgmcp-theme',
+ 'adminlte-advanced',
+ 'AdminlteAdvanced'
+ ),
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/YawikInstallerTest.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/YawikInstallerTest.php
new file mode 100644
index 0000000..d8d35ff
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/Composer/Installers/Test/YawikInstallerTest.php
@@ -0,0 +1,64 @@
+package = new Package('YawikCompanyRegistration', '1.0', '1.0');
+ $this->io = $this->getMock('Composer\IO\PackageInterface');
+ $this->composer = new Composer();
+ }
+
+ /**
+ * testInflectPackageVars
+ *
+ * @dataProvider packageNameProvider
+ * @return void
+ */
+ public function testInflectPackageVars($input)
+ {
+ $installer = new YawikInstaller($this->package, $this->composer);
+ $result = $installer->inflectPackageVars(array('name' => $input));
+ $this->assertEquals($result, array('name' => 'YawikCompanyRegistration'));
+ }
+
+ public function packageNameProvider()
+ {
+ return array(
+ array('yawik-company-registration'),
+ array('yawik_company_registration'),
+ array('YawikCompanyRegistration')
+ );
+ }
+}
diff --git a/plugins/rainlab/userplus/vendor/composer/installers/tests/bootstrap.php b/plugins/rainlab/userplus/vendor/composer/installers/tests/bootstrap.php
new file mode 100644
index 0000000..30c8fdc
--- /dev/null
+++ b/plugins/rainlab/userplus/vendor/composer/installers/tests/bootstrap.php
@@ -0,0 +1,4 @@
+add('Composer\Installers\Test', __DIR__);
diff --git a/plugins/tps/birzha/classes/ProductResource.php b/plugins/tps/birzha/classes/ProductResource.php
index 6b4768f..7d6a45b 100644
--- a/plugins/tps/birzha/classes/ProductResource.php
+++ b/plugins/tps/birzha/classes/ProductResource.php
@@ -18,21 +18,14 @@ class ProductResource extends JsonResource
'id' => $this->id,
'name_ru' => $this->getAttributeTranslated('name', 'ru'),
'name_en' => $this->getAttributeTranslated('name', 'en'),
- 'name_tm' => $this->getAttributeTranslated('name', 'tm'),
'category_id' => $this->categories()->first()->id,
'productForEditing' => $this->id,
'quantity' => $this->quantity,
'price' => $this->price,
'place' => $this->place,
'description_tm' => $this->getAttributeTranslated('description', 'tm'),
- 'description_en' => $this->getAttributeTranslated('description', 'en'),
'description_ru' => $this->getAttributeTranslated('description', 'ru'),
- 'payment_term_id' => $this->payment_term ? $this->payment_term->id : null,
- 'packaging' => $this->packaging,
- 'delivery_term_id' => $this->delivery_term ? $this->delivery_term->id : null,
- 'currency_id' => $this->currency ? $this->currency->id : null,
- 'measure_id' => $this->measure ? $this->measure->id : null,
- 'old_img' => ImageResource::collection($this->images)
+
];
}
}
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/City.php b/plugins/tps/birzha/controllers/City.php
new file mode 100644
index 0000000..fc1bd47
--- /dev/null
+++ b/plugins/tps/birzha/controllers/City.php
@@ -0,0 +1,18 @@
+
+ = e(trans('backend::lang.form.create')) ?>
+
+ = e(trans('backend::lang.list.delete_selected')) ?>
+
+
diff --git a/plugins/tps/birzha/controllers/city/config_form.yaml b/plugins/tps/birzha/controllers/city/config_form.yaml
new file mode 100644
index 0000000..15a7ca2
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/config_form.yaml
@@ -0,0 +1,10 @@
+name: City
+form: $/tps/birzha/models/city/fields.yaml
+modelClass: TPS\Birzha\Models\City
+defaultRedirect: tps/birzha/city
+create:
+ redirect: 'tps/birzha/city/update/:id'
+ redirectClose: tps/birzha/city
+update:
+ redirect: tps/birzha/city
+ redirectClose: tps/birzha/city
diff --git a/plugins/tps/birzha/controllers/city/config_list.yaml b/plugins/tps/birzha/controllers/city/config_list.yaml
new file mode 100644
index 0000000..7cd2d6b
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/config_list.yaml
@@ -0,0 +1,12 @@
+list: $/tps/birzha/models/city/columns.yaml
+modelClass: TPS\Birzha\Models\City
+title: City
+noRecordsMessage: 'backend::lang.list.no_records'
+showSetup: true
+showCheckboxes: true
+recordsPerPage: 20
+toolbar:
+ buttons: list_toolbar
+ search:
+ prompt: 'backend::lang.list.search_prompt'
+recordUrl: 'tps/birzha/city/update/:id'
diff --git a/plugins/tps/birzha/controllers/city/create.htm b/plugins/tps/birzha/controllers/city/create.htm
new file mode 100644
index 0000000..45b11d5
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/create.htm
@@ -0,0 +1,46 @@
+
+
+ City
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/city/index.htm b/plugins/tps/birzha/controllers/city/index.htm
new file mode 100644
index 0000000..ea43a36
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/index.htm
@@ -0,0 +1 @@
+= $this->listRender() ?>
diff --git a/plugins/tps/birzha/controllers/city/preview.htm b/plugins/tps/birzha/controllers/city/preview.htm
new file mode 100644
index 0000000..905b12f
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/preview.htm
@@ -0,0 +1,22 @@
+
+
+ City
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+
+ = $this->formRenderPreview() ?>
+
+
+
+ = e($this->fatalError) ?>
+
+
+
+
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/city/update.htm b/plugins/tps/birzha/controllers/city/update.htm
new file mode 100644
index 0000000..d30bfba
--- /dev/null
+++ b/plugins/tps/birzha/controllers/city/update.htm
@@ -0,0 +1,54 @@
+
+
+ City
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/comment/_list_toolbar.htm b/plugins/tps/birzha/controllers/comment/_list_toolbar.htm
new file mode 100644
index 0000000..f7d8c5b
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/_list_toolbar.htm
@@ -0,0 +1,18 @@
+
diff --git a/plugins/tps/birzha/controllers/comment/config_form.yaml b/plugins/tps/birzha/controllers/comment/config_form.yaml
new file mode 100644
index 0000000..5fc9716
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/config_form.yaml
@@ -0,0 +1,10 @@
+name: Comment
+form: $/tps/birzha/models/comment/fields.yaml
+modelClass: TPS\Birzha\Models\Comment
+defaultRedirect: tps/birzha/comment
+create:
+ redirect: 'tps/birzha/comment/update/:id'
+ redirectClose: tps/birzha/comment
+update:
+ redirect: tps/birzha/comment
+ redirectClose: tps/birzha/comment
diff --git a/plugins/tps/birzha/controllers/comment/config_list.yaml b/plugins/tps/birzha/controllers/comment/config_list.yaml
new file mode 100644
index 0000000..8af8954
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/config_list.yaml
@@ -0,0 +1,12 @@
+list: $/tps/birzha/models/comment/columns.yaml
+modelClass: TPS\Birzha\Models\Comment
+title: Comment
+noRecordsMessage: 'backend::lang.list.no_records'
+showSetup: true
+showCheckboxes: true
+recordsPerPage: 20
+toolbar:
+ buttons: list_toolbar
+ search:
+ prompt: 'backend::lang.list.search_prompt'
+recordUrl: 'tps/birzha/comment/update/:id'
diff --git a/plugins/tps/birzha/controllers/comment/create.htm b/plugins/tps/birzha/controllers/comment/create.htm
new file mode 100644
index 0000000..c54b7b8
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/create.htm
@@ -0,0 +1,46 @@
+
+
+ Comment
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/comment/index.htm b/plugins/tps/birzha/controllers/comment/index.htm
new file mode 100644
index 0000000..ea43a36
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/index.htm
@@ -0,0 +1 @@
+= $this->listRender() ?>
diff --git a/plugins/tps/birzha/controllers/comment/preview.htm b/plugins/tps/birzha/controllers/comment/preview.htm
new file mode 100644
index 0000000..50a84bb
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/preview.htm
@@ -0,0 +1,22 @@
+
+
+ Comment
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+
+ = $this->formRenderPreview() ?>
+
+
+
+ = e($this->fatalError) ?>
+
+
+
+
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/comment/update.htm b/plugins/tps/birzha/controllers/comment/update.htm
new file mode 100644
index 0000000..edc865b
--- /dev/null
+++ b/plugins/tps/birzha/controllers/comment/update.htm
@@ -0,0 +1,54 @@
+
+
+ Comment
+ = e($this->pageTitle) ?>
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/favourites/_list_toolbar.htm b/plugins/tps/birzha/controllers/favourites/_list_toolbar.htm
new file mode 100644
index 0000000..643f97a
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/_list_toolbar.htm
@@ -0,0 +1,18 @@
+
diff --git a/plugins/tps/birzha/controllers/favourites/config_form.yaml b/plugins/tps/birzha/controllers/favourites/config_form.yaml
new file mode 100644
index 0000000..ca5403d
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/config_form.yaml
@@ -0,0 +1,10 @@
+name: Favourites
+form: $/tps/birzha/models/favourites/fields.yaml
+modelClass: TPS\Birzha\Models\Favourites
+defaultRedirect: tps/birzha/favourites
+create:
+ redirect: 'tps/birzha/favourites/update/:id'
+ redirectClose: tps/birzha/favourites
+update:
+ redirect: tps/birzha/favourites
+ redirectClose: tps/birzha/favourites
diff --git a/plugins/tps/birzha/controllers/favourites/config_list.yaml b/plugins/tps/birzha/controllers/favourites/config_list.yaml
new file mode 100644
index 0000000..2c099a1
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/config_list.yaml
@@ -0,0 +1,12 @@
+list: $/tps/birzha/models/favourites/columns.yaml
+modelClass: TPS\Birzha\Models\Favourites
+title: Favourites
+noRecordsMessage: 'backend::lang.list.no_records'
+showSetup: true
+showCheckboxes: true
+recordsPerPage: 20
+toolbar:
+ buttons: list_toolbar
+ search:
+ prompt: 'backend::lang.list.search_prompt'
+recordUrl: 'tps/birzha/favourites/update/:id'
diff --git a/plugins/tps/birzha/controllers/favourites/create.htm b/plugins/tps/birzha/controllers/favourites/create.htm
new file mode 100644
index 0000000..7695062
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/create.htm
@@ -0,0 +1,46 @@
+
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/favourites/index.htm b/plugins/tps/birzha/controllers/favourites/index.htm
new file mode 100644
index 0000000..ea43a36
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/index.htm
@@ -0,0 +1 @@
+= $this->listRender() ?>
diff --git a/plugins/tps/birzha/controllers/favourites/preview.htm b/plugins/tps/birzha/controllers/favourites/preview.htm
new file mode 100644
index 0000000..47b0d65
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/preview.htm
@@ -0,0 +1,22 @@
+
+
+
+
+fatalError): ?>
+
+
+ = $this->formRenderPreview() ?>
+
+
+
+ = e($this->fatalError) ?>
+
+
+
+
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/controllers/favourites/update.htm b/plugins/tps/birzha/controllers/favourites/update.htm
new file mode 100644
index 0000000..136cec3
--- /dev/null
+++ b/plugins/tps/birzha/controllers/favourites/update.htm
@@ -0,0 +1,54 @@
+
+
+
+
+fatalError): ?>
+
+ = Form::open(['class' => 'layout']) ?>
+
+
+ = $this->formRender() ?>
+
+
+
+ = Form::close() ?>
+
+
+ = e(trans($this->fatalError)) ?>
+ = e(trans('backend::lang.form.return_to_list')) ?>
+
\ No newline at end of file
diff --git a/plugins/tps/birzha/models/City.php b/plugins/tps/birzha/models/City.php
new file mode 100644
index 0000000..1e44e6d
--- /dev/null
+++ b/plugins/tps/birzha/models/City.php
@@ -0,0 +1,35 @@
+ [
+ 'TPS\Birzha\Models\Product',
+ ]
+ ];
+ /**
+ * @var string The database table used by the model.
+ */
+ public $table = 'tps_birzha_cities';
+
+ /**
+ * @var array Validation rules
+ */
+ public $rules = [
+ ];
+}
diff --git a/plugins/tps/birzha/models/Comment.php b/plugins/tps/birzha/models/Comment.php
new file mode 100644
index 0000000..42c584e
--- /dev/null
+++ b/plugins/tps/birzha/models/Comment.php
@@ -0,0 +1,32 @@
+ ['RainLab\User\Models\User', 'table' => 'users'],
+ 'product' => ['TPS\Birzha\Models\Product', 'table' => 'tps_birzha_products'],
+ ];
+
+ /**
+ * @var array Validation rules
+ */
+ public $rules = [
+ ];
+}
diff --git a/plugins/tps/birzha/models/Favourites.php b/plugins/tps/birzha/models/Favourites.php
new file mode 100644
index 0000000..b3dee79
--- /dev/null
+++ b/plugins/tps/birzha/models/Favourites.php
@@ -0,0 +1,34 @@
+ ['RainLab\User\Models\User', 'table' => 'users'],
+ 'product' => ['TPS\Birzha\Models\Product', 'table' => 'tps_birzha_products'],
+ ];
+
+
+ /**
+ * @var array Validation rules
+ */
+ public $rules = [
+ ];
+}
diff --git a/plugins/tps/birzha/models/Product.php b/plugins/tps/birzha/models/Product.php
index e1aaf0d..0755090 100644
--- a/plugins/tps/birzha/models/Product.php
+++ b/plugins/tps/birzha/models/Product.php
@@ -5,6 +5,7 @@ use October\Rain\Support\Facades\Event;
use RainLab\User\Models\User;
use Carbon\Carbon;
use TPS\Birzha\Models\Settings;
+use TPS\Birzha\Models\City;
/**
* Model
@@ -45,17 +46,24 @@ class Product extends Model
];
public $belongsToMany = [
- 'categories' => ['TPS\Birzha\Models\Category','table' => 'tps_birzha_product_categories']
+ 'categories' => ['TPS\Birzha\Models\Category','table' => 'tps_birzha_product_categories'],
+ //'favourites' => ['TPS\Birzha\Models\Favourites','table' => 'tps_birzha_favourites'],
+
];
public $belongsTo = [
- // 'country' => ['TPS\Birzha\Models\Country'],
+ 'place' => City::class,
//'measure' => ['TPS\Birzha\Models\Measure','key' => 'measure_id'],
//'currency' => ['TPS\Birzha\Models\Currency'],
//'payment_term' => ['TPS\Birzha\Models\Term','key' => 'payment_term_id'],
//'delivery_term' => ['TPS\Birzha\Models\Term','key' => 'delivery_term_id'],
'vendor' => User::class,
- 'payment' => ['TPS\Birzha\Models\Payment'],
+ //'payment' => ['TPS\Birzha\Models\Payment'],
+ ];
+
+ public $hasMany = [
+ 'favourites' => ['TPS\Birzha\Models\Favourites', 'table' => 'tps_birzha_favourites'],
+ 'comments' => ['TPS\Birzha\Models\Comment','table' => 'tps_birzha_comments'],
];
public $morphOne = [
@@ -63,7 +71,8 @@ class Product extends Model
];
public $attachMany = [
- 'images' => 'System\Models\File'
+ 'images' => 'System\Models\File',
+ 'files' => 'System\Models\File'
];
public $translatable = [
diff --git a/plugins/tps/birzha/models/ProductComments.php b/plugins/tps/birzha/models/ProductComments.php
new file mode 100644
index 0000000..3beef51
--- /dev/null
+++ b/plugins/tps/birzha/models/ProductComments.php
@@ -0,0 +1,27 @@
+engine = 'InnoDB';
+ $table->increments('id')->unsigned();
+ $table->timestamp('created_at')->nullable();
+ $table->timestamp('updated_at')->nullable();
+ $table->timestamp('deleted_at')->nullable();
+ $table->string('name')->nullable();
+ $table->text('note')->nullable();
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('tps_birzha_cities');
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_create_tps_birzha_comments.php b/plugins/tps/birzha/updates/builder_table_create_tps_birzha_comments.php
new file mode 100644
index 0000000..4d5510d
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_create_tps_birzha_comments.php
@@ -0,0 +1,27 @@
+engine = 'InnoDB';
+ $table->increments('id')->unsigned();
+ $table->timestamp('created_at')->nullable();
+ $table->timestamp('updated_at')->nullable();
+ $table->timestamp('deleted_at')->nullable();
+ $table->integer('product_id');
+ $table->integer('user_id');
+ $table->text('comment');
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('tps_birzha_comments');
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_create_tps_birzha_favourites.php b/plugins/tps/birzha/updates/builder_table_create_tps_birzha_favourites.php
new file mode 100644
index 0000000..ea9c4c2
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_create_tps_birzha_favourites.php
@@ -0,0 +1,26 @@
+engine = 'InnoDB';
+ $table->increments('id')->unsigned();
+ $table->timestamp('created_at')->nullable();
+ $table->timestamp('updated_at')->nullable();
+ $table->timestamp('deleted_at')->nullable();
+ $table->integer('user_id');
+ $table->integer('product_id');
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('tps_birzha_favourites');
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_categories_3.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_categories_3.php
new file mode 100644
index 0000000..d431974
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_categories_3.php
@@ -0,0 +1,23 @@
+boolean('is_file_category')->nullable()->default(0);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_categories', function($table)
+ {
+ $table->dropColumn('is_file_category');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_cities.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_cities.php
new file mode 100644
index 0000000..b23bfb7
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_cities.php
@@ -0,0 +1,23 @@
+integer('order')->nullable();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_cities', function($table)
+ {
+ $table->dropColumn('order');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments.php
new file mode 100644
index 0000000..cb88ba9
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments.php
@@ -0,0 +1,23 @@
+text('comment')->nullable()->change();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_comments', function($table)
+ {
+ $table->text('comment')->nullable(false)->change();
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_2.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_2.php
new file mode 100644
index 0000000..6dc816f
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_2.php
@@ -0,0 +1,23 @@
+integer('rating')->nullable()->default(0);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_comments', function($table)
+ {
+ $table->dropColumn('rating');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_3.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_3.php
new file mode 100644
index 0000000..69878c2
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_comments_3.php
@@ -0,0 +1,23 @@
+boolean('is_approve')->nullable()->default(0);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_comments', function($table)
+ {
+ $table->dropColumn('is_approve');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_30.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_30.php
new file mode 100644
index 0000000..7967963
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_30.php
@@ -0,0 +1,23 @@
+dropColumn('place');
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_products', function($table)
+ {
+ $table->string('place', 191)->nullable();
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_31.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_31.php
new file mode 100644
index 0000000..85620f8
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_31.php
@@ -0,0 +1,23 @@
+integer('place')->nullable();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_products', function($table)
+ {
+ $table->dropColumn('place');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_32.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_32.php
new file mode 100644
index 0000000..42fc1a3
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_32.php
@@ -0,0 +1,23 @@
+renameColumn('place', 'place_id');
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_products', function($table)
+ {
+ $table->renameColumn('place_id', 'place');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_33.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_33.php
new file mode 100644
index 0000000..3e2b8c4
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_33.php
@@ -0,0 +1,23 @@
+boolean('is_file')->nullable()->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_products', function($table)
+ {
+ $table->dropColumn('is_file');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_34.php b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_34.php
new file mode 100644
index 0000000..afd766c
--- /dev/null
+++ b/plugins/tps/birzha/updates/builder_table_update_tps_birzha_products_34.php
@@ -0,0 +1,23 @@
+renameColumn('is_file', 'is_file_product');
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('tps_birzha_products', function($table)
+ {
+ $table->renameColumn('is_file_product', 'is_file');
+ });
+ }
+}
diff --git a/plugins/tps/birzha/updates/version.yaml b/plugins/tps/birzha/updates/version.yaml
index 007fe47..9d74043 100644
--- a/plugins/tps/birzha/updates/version.yaml
+++ b/plugins/tps/birzha/updates/version.yaml
@@ -347,3 +347,42 @@
1.0.122:
- 'Updated table tps_birzha_terms'
- builder_table_update_tps_birzha_terms.php
+1.0.123:
+ - 'Created table tps_birzha_cities'
+ - builder_table_create_tps_birzha_cities.php
+1.0.124:
+ - 'Updated table tps_birzha_cities'
+ - builder_table_update_tps_birzha_cities.php
+1.0.125:
+ - 'Updated table tps_birzha_products'
+ - builder_table_update_tps_birzha_products_30.php
+1.0.126:
+ - 'Updated table tps_birzha_products'
+ - builder_table_update_tps_birzha_products_31.php
+1.0.127:
+ - 'Updated table tps_birzha_products'
+ - builder_table_update_tps_birzha_products_32.php
+1.0.128:
+ - 'Updated table tps_birzha_products'
+ - builder_table_update_tps_birzha_products_33.php
+1.0.129:
+ - 'Updated table tps_birzha_products'
+ - builder_table_update_tps_birzha_products_34.php
+1.0.130:
+ - 'Updated table tps_birzha_categories'
+ - builder_table_update_tps_birzha_categories_3.php
+1.0.131:
+ - 'Created table tps_birzha_favourites'
+ - builder_table_create_tps_birzha_favourites.php
+1.0.132:
+ - 'Created table tps_birzha_comments'
+ - builder_table_create_tps_birzha_comments.php
+1.0.133:
+ - 'Updated table tps_birzha_comments'
+ - builder_table_update_tps_birzha_comments.php
+1.0.134:
+ - 'Updated table tps_birzha_comments'
+ - builder_table_update_tps_birzha_comments_2.php
+1.0.135:
+ - 'Updated table tps_birzha_comments'
+ - builder_table_update_tps_birzha_comments_3.php
diff --git a/plugins/vdomah/jwtauth/routes.php b/plugins/vdomah/jwtauth/routes.php
index 2cac5bb..c1e09c4 100644
--- a/plugins/vdomah/jwtauth/routes.php
+++ b/plugins/vdomah/jwtauth/routes.php
@@ -4,6 +4,15 @@ use RainLab\User\Models\User as UserModel;
use Vdomah\JWTAuth\Models\Settings;
Route::group(['prefix' => 'api'], function() {
+
+ Route::get('me', function() {
+
+ $me = \JWTAuth::parseToken()->authenticate()
+ ->only(['name','surname','email','username','type', 'logo', 'banner', 'shop_title', 'slogan', 'is_instagram']);
+
+ return Response::json(compact('me'));
+
+ })->middleware('\Tymon\JWTAuth\Middleware\GetUserFromToken');
Route::post('login', function (Request $request) {
if (Settings::get('is_login_disabled'))