update
This commit is contained in:
124
inc/Hura8/System/Controller/DomainController.php
Normal file
124
inc/Hura8/System/Controller/DomainController.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\System\Model\DomainModel;
|
||||
|
||||
class DomainController
|
||||
{
|
||||
protected $objDomainModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->objDomainModel = new DomainModel();
|
||||
}
|
||||
|
||||
protected $layout_options = [
|
||||
"pc" => "Chỉ cho PC",
|
||||
"mobile" => "Chỉ cho Mobile",
|
||||
//"amp" => "Chỉ cho AMP",
|
||||
"all" => "Cả PC & Mobile",
|
||||
];
|
||||
|
||||
|
||||
public function buildDomainConfig() {
|
||||
$domain_per_languages = $this->getList('');
|
||||
|
||||
$config_domain_list = []; //all domains and attributes, so we can know the info of currently visited domain
|
||||
$config_domain_languages = []; //domains per language, so we can redirect to main domain of a specific language
|
||||
|
||||
foreach ($domain_per_languages as $lang => $list_domains) {
|
||||
foreach ($list_domains as $_item) {
|
||||
$config_domain_languages[$lang][] = [
|
||||
"domain" => $_item['domain'],
|
||||
"is_main" => $_item['isMain'],
|
||||
"layout" => ($_item['layout']) ? $_item['layout'] : 'pc',
|
||||
];
|
||||
|
||||
$config_domain_list[$_item['domain']] = [
|
||||
"lang" => $_item['lang'],
|
||||
"is_main" => $_item['isMain'],
|
||||
"layout" => ($_item['layout']) ? $_item['layout'] : 'pc',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"language" => $config_domain_languages,
|
||||
"list" => $config_domain_list,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function getLayoutOption(){
|
||||
return $this->layout_options;
|
||||
}
|
||||
|
||||
|
||||
public function getList($language = '') {
|
||||
|
||||
$item_list = $this->objDomainModel->getList([
|
||||
"language" => $language,
|
||||
"numPerPage" => 100,
|
||||
]);
|
||||
|
||||
$result = array();
|
||||
foreach ( $item_list as $rs ) {
|
||||
if(!$rs['lang']) $rs['lang'] = DEFAULT_LANGUAGE;
|
||||
$result[$rs['lang']][] = $rs;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function addNewDomain($domain, $language = DEFAULT_LANGUAGE){
|
||||
$this->objDomainModel->addNewDomain($this->cleanDomain($domain), $language);
|
||||
|
||||
$this->rebuildConfigFile();
|
||||
}
|
||||
|
||||
|
||||
public function deleteDomain($domain){
|
||||
$this->objDomainModel->deleteDomain($this->cleanDomain($domain));
|
||||
|
||||
$this->rebuildConfigFile();
|
||||
}
|
||||
|
||||
|
||||
public function setDomainMain($domain, $language){
|
||||
$this->objDomainModel->setDomainMain($this->cleanDomain($domain), $language);
|
||||
|
||||
$this->rebuildConfigFile();
|
||||
}
|
||||
|
||||
|
||||
public function setDomainLayout($domain, $layout = 'pc'){
|
||||
$layout_option = $this->getLayoutOption();
|
||||
if(!isset($layout_option[$layout])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->objDomainModel->setDomainLayout($this->cleanDomain($domain), $layout);
|
||||
$this->rebuildConfigFile();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
protected function cleanDomain($domain) {
|
||||
$domain_element = parse_url($domain);
|
||||
|
||||
$scheme = isset($domain_element['scheme']) ? $domain_element['scheme'] . '://' : '';
|
||||
$host = $domain_element['host'] ?? '';
|
||||
$port = isset($domain_element['port']) ? ':' . $domain_element['port'] : '';
|
||||
|
||||
return strtolower(trim($scheme . $host . $port));
|
||||
}
|
||||
|
||||
|
||||
protected function rebuildConfigFile() {
|
||||
$objSettingController = new SettingController();
|
||||
$objSettingController->create_config_file_n_upload();
|
||||
}
|
||||
}
|
||||
43
inc/Hura8/System/Controller/EntityPermissionController.php
Normal file
43
inc/Hura8/System/Controller/EntityPermissionController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\iEntityPermission;
|
||||
|
||||
class EntityPermissionController implements iEntityPermission
|
||||
{
|
||||
|
||||
public function __construct($entity_type) {
|
||||
|
||||
}
|
||||
|
||||
public function canCreate(): bool
|
||||
{
|
||||
// TODO: Implement canCreate() method.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canUpdate(): bool
|
||||
{
|
||||
// TODO: Implement canUpdate() method.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canDelete(): bool
|
||||
{
|
||||
// TODO: Implement canDelete() method.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canView(): bool
|
||||
{
|
||||
// TODO: Implement canView() method.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canApprove(): bool
|
||||
{
|
||||
// TODO: Implement canApprove() method.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
82
inc/Hura8/System/Controller/RelationController.php
Normal file
82
inc/Hura8/System/Controller/RelationController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\System\Model\RelationModel;
|
||||
use Hura8\Traits\ClassCacheTrait;
|
||||
|
||||
class RelationController
|
||||
{
|
||||
use ClassCacheTrait;
|
||||
|
||||
protected $objRelationModel;
|
||||
|
||||
private $main_item_type = '';
|
||||
private $main_item_id = 0;
|
||||
|
||||
public function __construct($main_item_type, $main_item_id)
|
||||
{
|
||||
$this->objRelationModel = new RelationModel($main_item_type, $main_item_id);
|
||||
$this->main_item_type = $main_item_type;
|
||||
$this->main_item_id = $main_item_id;
|
||||
}
|
||||
|
||||
|
||||
public function updateOrdering($related_item_id, $new_order) {
|
||||
return $this->objRelationModel->updateOrdering($related_item_id, $new_order);
|
||||
}
|
||||
|
||||
|
||||
//@warn: this does not check if records exist.
|
||||
public function create(array $related_items, $both_way_relation = true) {
|
||||
return $this->objRelationModel->create($related_items, $both_way_relation);
|
||||
}
|
||||
|
||||
|
||||
public function checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id){
|
||||
return $this->objRelationModel->checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id);
|
||||
}
|
||||
|
||||
//remove a related-item
|
||||
public function remove($related_item_type, $related_item_id, $remove_both_way = true) {
|
||||
return $this->objRelationModel->remove($related_item_type, $related_item_id, $remove_both_way);
|
||||
}
|
||||
|
||||
|
||||
//remove all relate items
|
||||
public function truncate() {
|
||||
$this->objRelationModel->truncate();
|
||||
}
|
||||
|
||||
|
||||
public function getRelatedItems(array $related_item_types = []) {
|
||||
return $this->objRelationModel->getRelatedItems($related_item_types);
|
||||
}
|
||||
|
||||
public function getRelatedItemsForList(array $main_item_list_ids, array $related_item_types = []) {
|
||||
return $this->objRelationModel->getRelatedItemsForList($main_item_list_ids, $related_item_types);
|
||||
}
|
||||
|
||||
//count related items
|
||||
public function getRelatedItemCount() {
|
||||
return $this->objRelationModel->getRelatedItemCount();
|
||||
}
|
||||
|
||||
public static function findItemUrl($item_type, $item_id) {
|
||||
$url_config = array(
|
||||
"product" => "/admin/?opt=product&view=form&id=".$item_id."&part=relation&l=vi&popup=".POPUP,
|
||||
"product-category" => "?opt=product&view=category-form&id=".$item_id."",
|
||||
|
||||
"article-article" => "?opt=article&view=form&id=".$item_id."&l=&popup=".POPUP,
|
||||
"article-category" => "?opt=article&view=category-form&id=".$item_id."&l=&popup=".POPUP,
|
||||
|
||||
"album" => "?opt=album&view=form&id=".$item_id,
|
||||
"banner" => "?opt=banner&view=upload&id=".$item_id,
|
||||
"page" => "?opt=page&view=form&id=".$item_id,
|
||||
);
|
||||
|
||||
return (isset($url_config[$item_type])) ? $url_config[$item_type] : null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
144
inc/Hura8/System/Controller/SettingController.php
Normal file
144
inc/Hura8/System/Controller/SettingController.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Components\Template\AdminController\ATemplateSetController;
|
||||
use Hura8\System\Model\SettingModel;
|
||||
|
||||
class SettingController
|
||||
{
|
||||
|
||||
static $file_folder = "media/settings";
|
||||
|
||||
protected $objSettingModel;
|
||||
|
||||
// reserved special keys which should not be created by admin (but the system)
|
||||
protected $special_keys = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->objSettingModel = new SettingModel();
|
||||
$this->special_keys = include CONFIG_DIR . "/system/special_settings_keys.php";
|
||||
}
|
||||
|
||||
|
||||
public function getAll(){
|
||||
return $this->objSettingModel->getAll();
|
||||
}
|
||||
|
||||
|
||||
public function getSpecialKeys() {
|
||||
$group_keys = $this->special_keys;
|
||||
return array_merge($group_keys['design'], $group_keys['system']);
|
||||
}
|
||||
|
||||
|
||||
public function get($key, $default =null){
|
||||
return $this->objSettingModel->get($key, $default);
|
||||
}
|
||||
|
||||
|
||||
public function delete($key){
|
||||
return $this->objSettingModel->delete($key);
|
||||
}
|
||||
|
||||
|
||||
// update bulk key-values
|
||||
public function updateBulk(array $key_values){
|
||||
foreach ($key_values as $key => $value) {
|
||||
$this->objSettingModel->updateOrCreate($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updateOrCreate($key, $value, $comment = '') {
|
||||
return $this->objSettingModel->updateOrCreate($key, $value, $comment );
|
||||
}
|
||||
|
||||
|
||||
public function getList(array $keys) {
|
||||
return $this->objSettingModel->getList($keys);
|
||||
}
|
||||
|
||||
|
||||
public function populateSpecialKeys() {
|
||||
$keys = [];
|
||||
foreach ($this->special_keys as $group => $_list) {
|
||||
$keys = array_merge($keys, $_list);
|
||||
}
|
||||
|
||||
return $this->objSettingModel->populateKeys($keys);
|
||||
}
|
||||
|
||||
|
||||
//tao config file tu database va upload vao thu muc website
|
||||
// Global config variables available to the whole website
|
||||
// includes:
|
||||
/*
|
||||
* - domain setting
|
||||
* - main domain
|
||||
* - template_set
|
||||
* - exchange rate
|
||||
* - password to unlock website
|
||||
* - google analytic verification
|
||||
* - google webmaster tool verification
|
||||
* - number of products to display
|
||||
* - default product display type: list|grid
|
||||
*
|
||||
* */
|
||||
public function create_config_file_n_upload(){
|
||||
|
||||
$objDomainController = new DomainController();
|
||||
$config_domains = $objDomainController->buildDomainConfig();
|
||||
|
||||
$config = [];
|
||||
|
||||
// * - domain setting
|
||||
$config['domains'] = $config_domains;
|
||||
|
||||
// * - template_set
|
||||
$objATemplateSetController = new ATemplateSetController();
|
||||
$config['template_set'] = $objATemplateSetController->getActivatedSet();
|
||||
|
||||
// * - exchange rate
|
||||
// * - password to unlock website
|
||||
// * - google analytic verification
|
||||
// * - google webmaster tool verification
|
||||
// * - number of products to display
|
||||
// * - default product display type: list|grid
|
||||
$config['setup'] = $this->getList(array(
|
||||
"web_close_pass" ,
|
||||
"web_close_message" ,
|
||||
"exchange_rate" ,
|
||||
"google_domain_verify" ,
|
||||
"product_per_page" ,
|
||||
"product_default_order",
|
||||
"site_manager" ,
|
||||
"site_manager_access_key",
|
||||
));
|
||||
|
||||
$config_file = CONFIG_DIR . "/build/store.config.php";
|
||||
|
||||
$config_head = "<?php
|
||||
/**
|
||||
* Author: HuraSoft
|
||||
* Generated time : ".date("d-m-Y, g:ia")."
|
||||
*/
|
||||
";
|
||||
$config_content = "return ".var_export($config, true) .";";
|
||||
|
||||
@file_put_contents($config_file, $config_head. $this->_minifyCode($config_content));
|
||||
}
|
||||
|
||||
|
||||
//this is a greatly simplified version
|
||||
protected function _minifyCode($text) {
|
||||
//remove line break
|
||||
$text = str_replace(["\n", "\r", "\t"], " ", $text);
|
||||
//remove double spacings
|
||||
$text = preg_replace("/(\s+)/i", " ", $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
}
|
||||
242
inc/Hura8/System/Controller/UrlManagerController.php
Normal file
242
inc/Hura8/System/Controller/UrlManagerController.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\AppResponse;
|
||||
use Hura8\System\IDGenerator;
|
||||
use Hura8\System\Language;
|
||||
use Hura8\System\Model\UrlModel;
|
||||
use Hura8\System\Url;
|
||||
use Hura8\Traits\ClassCacheTrait;
|
||||
|
||||
|
||||
class UrlManagerController
|
||||
{
|
||||
|
||||
use ClassCacheTrait;
|
||||
|
||||
/* @var UrlModel $objUrlModel */
|
||||
protected $objUrlModel;
|
||||
|
||||
public function __construct() {
|
||||
$this->objUrlModel = new UrlModel();
|
||||
}
|
||||
|
||||
|
||||
public function createRedirect($info) {
|
||||
$request_path = $info['request_path'] ?? '';
|
||||
$redirect_code = $info['redirect_code'] ?? 0;
|
||||
$redirect_url = $info['redirect_url'] ?? '';
|
||||
|
||||
if(!$request_path || !$redirect_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request_path_element = Url::parse($request_path);
|
||||
|
||||
$request_path_path = $request_path_element['path'];
|
||||
|
||||
// home page or itself is forbidden
|
||||
if($request_path_path == '/' || $request_path_path == $redirect_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return $this->objUrlModel->create([
|
||||
"url_type" => "redirect",
|
||||
"request_path" => $request_path_path,
|
||||
"redirect_code" => $redirect_code,
|
||||
"redirect_url" => $redirect_url,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function getInfo($id) : ?array {
|
||||
return $this->objUrlModel->getInfo($id);
|
||||
}
|
||||
|
||||
|
||||
public function getEmptyInfo($addition_field_value = []) : array {
|
||||
return $this->objUrlModel->getEmptyInfo($addition_field_value);
|
||||
}
|
||||
|
||||
|
||||
public function getList(array $condition) : array
|
||||
{
|
||||
return $this->objUrlModel->getList($condition);
|
||||
}
|
||||
|
||||
|
||||
public function getTotal(array $condition) : int
|
||||
{
|
||||
return $this->objUrlModel->getTotal($condition);
|
||||
}
|
||||
|
||||
|
||||
public function deleteByRequestPath($request_path) {
|
||||
$this->objUrlModel->deleteByRequestPath($request_path);
|
||||
}
|
||||
|
||||
|
||||
public static function translateRequestPathConfig(
|
||||
$request_path_config, // "/%extra_path%/%item_index%/ac%item_id%.html",/
|
||||
$item_id = '',
|
||||
$item_index = '',
|
||||
$extra_path = ''
|
||||
) {
|
||||
|
||||
$item_index = static::create_url_index($item_index); //reclean url index
|
||||
|
||||
$new_url = str_replace(
|
||||
array('%item_id%', '%item_index%', '%extra_path%',),
|
||||
array($item_id, $item_index, $extra_path),
|
||||
$request_path_config
|
||||
);
|
||||
|
||||
return str_replace("//","/",$new_url);
|
||||
}
|
||||
|
||||
|
||||
public static function create_url_index($name, $vietnamese=true){
|
||||
if($vietnamese) $name = Language::chuyenKhongdau($name);
|
||||
|
||||
$name = preg_replace("/[^a-z0-9\s_\-]/i", " ", $name);
|
||||
$name = preg_replace("/\s+/i", " ", $name);
|
||||
$name = str_replace(" ","-", trim($name));
|
||||
$name = preg_replace("/-+/","-", $name);
|
||||
|
||||
if (!defined("BUILD_URL_INDEX_LOWERCASE") || BUILD_URL_INDEX_LOWERCASE) {
|
||||
return strtolower($name);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
|
||||
public function getUrlMetaInfo($request_path){
|
||||
return $this->objUrlModel->getUrlMetaInfo($request_path);
|
||||
}
|
||||
|
||||
|
||||
public function createUrlMeta(array $info){
|
||||
return $this->objUrlModel->createUrlMeta($info);
|
||||
}
|
||||
|
||||
|
||||
public function getUrlInfoByRequestPath($request_path) {
|
||||
$info = $this->objUrlModel->getUrlByRequestPath($request_path);
|
||||
if($info){
|
||||
|
||||
$id_path_content = $this->analyze_url_id_path($info['id_path']);
|
||||
|
||||
$id_path_content['option'] = $id_path_content['module'];
|
||||
//$id_path_content['redirect_code'] = $info['redirect_code'];
|
||||
//$id_path_content['request_path'] = $info['request_path'];
|
||||
|
||||
return array_merge($info, $id_path_content);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
///module:product/view:category/view_id:1/brand_id:55/type:hello-ther
|
||||
//return:
|
||||
/*
|
||||
[
|
||||
"module" => "product",
|
||||
"view" => "category",
|
||||
"view_id" => "1",
|
||||
'query' => [
|
||||
"brand_id" => 55,
|
||||
"type" => "hello-ther",
|
||||
],
|
||||
],
|
||||
* */
|
||||
protected function analyze_url_id_path($id_path) : array
|
||||
{
|
||||
$id_path_ele = explode("/", $id_path);
|
||||
|
||||
$result = array();
|
||||
$query_string = array();
|
||||
|
||||
foreach ( $id_path_ele as $ele ) {
|
||||
if(!$ele) continue;
|
||||
|
||||
$ele_part = explode(":", $ele);
|
||||
if(!in_array($ele_part[0], array('module', 'view', 'view_id'))) {
|
||||
$query_string[$ele_part[0]] = $ele_part[1];
|
||||
}else{
|
||||
$result[$ele_part[0]] = $ele_part[1];
|
||||
}
|
||||
}
|
||||
|
||||
$result['query'] = $query_string;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public static function createIdPath($module, $view, $view_id, array $other_list = []): string
|
||||
{
|
||||
$parameter_constr = [
|
||||
"module:".$module,
|
||||
"view:".$view,
|
||||
"view_id:".$view_id,
|
||||
];
|
||||
foreach($other_list as $arg => $value) {
|
||||
$parameter_constr[] = $arg.":".$value;
|
||||
}
|
||||
|
||||
return "/".join("/", $parameter_constr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description create or update url's $request_path by checking $id_path. $new_request_path will return to be updated to item's entity
|
||||
* @param string $url_type $module:$view
|
||||
* @param string $wanted_request_path
|
||||
* @param string $id_path
|
||||
* @param string $redirect_code
|
||||
* @return ?string $wanted_request_path or new $request_path if the path already exists
|
||||
*/
|
||||
public function createUrl(string $url_type, string $wanted_request_path, string $id_path, $redirect_code=0) : ?string
|
||||
{
|
||||
$new_request_path = $this->createUniqueRequestPath($wanted_request_path, $id_path);
|
||||
|
||||
$check_id_path_exist = $this->objUrlModel->getUrlByIdPath($id_path);
|
||||
if($check_id_path_exist) {
|
||||
$res = $this->objUrlModel->update($check_id_path_exist['id'], [
|
||||
'request_path' => $new_request_path ,
|
||||
]);
|
||||
} else {
|
||||
$res = $this->objUrlModel->create([
|
||||
'url_type' => $url_type,
|
||||
'request_path' => $new_request_path ,
|
||||
'id_path' => $id_path,
|
||||
'redirect_code' => $redirect_code,
|
||||
]);
|
||||
}
|
||||
|
||||
return ($res->getStatus() == AppResponse::SUCCESS) ? $new_request_path : null;
|
||||
}
|
||||
|
||||
|
||||
//create a unique request-path
|
||||
protected function createUniqueRequestPath(string $wanted_request_path, string $id_path) : string
|
||||
{
|
||||
$check_exist = $this->objUrlModel->getUrlByRequestPath($wanted_request_path);
|
||||
|
||||
//ok, can use this one
|
||||
if(!$check_exist || $check_exist['id_path'] == $id_path ) {
|
||||
return $wanted_request_path;
|
||||
}
|
||||
|
||||
//other case, create new path and check again
|
||||
$random_suffix = IDGenerator::createStringId(4, true);
|
||||
$new_request_path = $wanted_request_path . '-'.$random_suffix;
|
||||
|
||||
return $this->createUniqueRequestPath($new_request_path, $id_path);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
44
inc/Hura8/System/Controller/ViewHistoryController.php
Normal file
44
inc/Hura8/System/Controller/ViewHistoryController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
|
||||
use Hura8\System\Model\WebUserModel;
|
||||
|
||||
class ViewHistoryController
|
||||
{
|
||||
protected $history = []; //type => [id1, id2]
|
||||
|
||||
protected $objWebUserModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->objWebUserModel = new WebUserModel(WebUserController::getUserId());
|
||||
}
|
||||
|
||||
|
||||
public function addHistory($item_type, $item_id) {
|
||||
$current_list = $this->getHistory($item_type);
|
||||
|
||||
// if exist, remove it
|
||||
$search_key = array_search($item_id, $current_list, true);
|
||||
if($search_key !== false) {
|
||||
array_splice($current_list, $search_key, 1);
|
||||
}
|
||||
|
||||
// add to front
|
||||
array_unshift($current_list, $item_id);
|
||||
|
||||
$this->history[$item_type] = $current_list;
|
||||
|
||||
// save to db
|
||||
$this->objWebUserModel->setValue("view-history", $this->history);
|
||||
}
|
||||
|
||||
|
||||
public function getHistory($item_type) {
|
||||
$history = $this->objWebUserModel->getValue("view-history");
|
||||
return (isset($history[$item_type])) ? $history[$item_type] : [];
|
||||
}
|
||||
|
||||
}
|
||||
27
inc/Hura8/System/Controller/WebUserController.php
Normal file
27
inc/Hura8/System/Controller/WebUserController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\System\Model\WebUserModel;
|
||||
|
||||
class WebUserController
|
||||
{
|
||||
const USER_BROWSER_COOKIE_NAME = 'uID';
|
||||
|
||||
protected $objWebUserModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->objWebUserModel = new WebUserModel(self::getUserId());
|
||||
}
|
||||
|
||||
// prefer cookie-id, if set ID in url parameter then use it
|
||||
public static function getUserId() {
|
||||
$url_user_id = getRequest("uid", "");
|
||||
$cookie_user_id = (isset($_COOKIE[self::USER_BROWSER_COOKIE_NAME])) ? $_COOKIE[self::USER_BROWSER_COOKIE_NAME] : '';
|
||||
|
||||
return ($url_user_id) ?: $cookie_user_id;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
78
inc/Hura8/System/Controller/aAdminEntityBaseController.php
Normal file
78
inc/Hura8/System/Controller/aAdminEntityBaseController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\AppResponse;
|
||||
use Hura8\Interfaces\iEntityController;
|
||||
|
||||
abstract class aAdminEntityBaseController extends aEntityBaseController implements iEntityController
|
||||
{
|
||||
|
||||
public function updateFields($id, array $info): AppResponse
|
||||
{
|
||||
return $this->iEntityModel->updateFields($id, $info);
|
||||
}
|
||||
|
||||
|
||||
public function updateField($id, $field, $value): AppResponse
|
||||
{
|
||||
|
||||
$fields_list = [];
|
||||
$fields_list[$field] = $value;
|
||||
|
||||
return $this->iEntityModel->updateFields($id, $fields_list);
|
||||
}
|
||||
|
||||
|
||||
public function updateFeatured($id, $new_status): AppResponse
|
||||
{
|
||||
$status = ($new_status == 'on' || $new_status == 1) ? 1 : 0;
|
||||
|
||||
return $this->iEntityModel->updateFields($id, ['is_featured' => $status]);
|
||||
}
|
||||
|
||||
|
||||
public function updateStatus($id, $new_status): AppResponse
|
||||
{
|
||||
$status = ($new_status == 'on' || $new_status == 1) ? 1 : 0;
|
||||
|
||||
return $this->iEntityModel->updateFields($id, ['status' => $status]);
|
||||
}
|
||||
|
||||
|
||||
public function create(array $info): AppResponse
|
||||
{
|
||||
return $this->iEntityModel->create($info);
|
||||
}
|
||||
|
||||
|
||||
public function update($id, array $info): AppResponse
|
||||
{
|
||||
if($this->iEntityLanguageModel) {
|
||||
return $this->iEntityLanguageModel->update($id, $info);
|
||||
}
|
||||
|
||||
return $this->iEntityModel->update($id, $info);
|
||||
}
|
||||
|
||||
|
||||
abstract protected function deleteFileBeforeDeleteItem($item_id) : bool;
|
||||
|
||||
|
||||
public function delete($id): AppResponse
|
||||
{
|
||||
if($this->deleteFileBeforeDeleteItem($id)) {
|
||||
return $this->iEntityModel->delete($id);
|
||||
}
|
||||
|
||||
return new AppResponse('error', 'Cannot delete '.$id);
|
||||
}
|
||||
|
||||
|
||||
public function getEmptyInfo(array $additional_fields = []) : array
|
||||
{
|
||||
return $this->iEntityModel->getEmptyInfo($additional_fields);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
304
inc/Hura8/System/Controller/aCategoryBaseController.php
Normal file
304
inc/Hura8/System/Controller/aCategoryBaseController.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\AppResponse;
|
||||
use Hura8\Interfaces\iEntityCategoryModel;
|
||||
use Hura8\Interfaces\iEntityLanguageModel;
|
||||
use Hura8\Traits\ClassCacheTrait;
|
||||
|
||||
abstract class aCategoryBaseController
|
||||
{
|
||||
|
||||
use ClassCacheTrait;
|
||||
|
||||
/* @var iEntityCategoryModel $iEntityCategoryModel */
|
||||
protected $iEntityCategoryModel;
|
||||
|
||||
/* @var ?iEntityLanguageModel $iEntityLanguageModel */
|
||||
protected $iEntityLanguageModel = null;
|
||||
|
||||
protected $view_language = LANGUAGE;
|
||||
|
||||
public static $public_fields = [
|
||||
"id", "title", "cat_path", "request_path", "image", "url", "not_translated", "children"
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
iEntityCategoryModel $iEntityCategoryModel,
|
||||
?iEntityLanguageModel $iEntityLanguageModel = null
|
||||
) {
|
||||
$this->iEntityCategoryModel = $iEntityCategoryModel;
|
||||
|
||||
if(!$this->isDefaultLanguage() && $iEntityLanguageModel instanceof iEntityLanguageModel) {
|
||||
$this->iEntityLanguageModel = $iEntityLanguageModel;
|
||||
$this->iEntityLanguageModel->setLanguage($this->view_language);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function create(array $info) : AppResponse
|
||||
{
|
||||
return $this->iEntityCategoryModel->create($info);
|
||||
}
|
||||
|
||||
|
||||
public function update($id, array $info) : AppResponse
|
||||
{
|
||||
if($this->iEntityLanguageModel) {
|
||||
return $this->iEntityLanguageModel->update($id, $info, $info['title']);
|
||||
}
|
||||
|
||||
return $this->iEntityCategoryModel->update($id, $info);
|
||||
}
|
||||
|
||||
|
||||
public function delete($id) : AppResponse
|
||||
{
|
||||
return $this->iEntityCategoryModel->delete($id);
|
||||
}
|
||||
|
||||
|
||||
public function updateFields($id, array $info) : AppResponse
|
||||
{
|
||||
return $this->iEntityCategoryModel->updateFields($id, $info);
|
||||
}
|
||||
|
||||
|
||||
public function isDefaultLanguage() : bool
|
||||
{
|
||||
return IS_DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
|
||||
public function getListByIds(array $list_id, array $condition = array()) : array
|
||||
{
|
||||
$item_list = $this->iEntityCategoryModel->getListByIds($list_id, $condition);
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id);
|
||||
|
||||
$final_list = [];
|
||||
foreach ($item_list as $item) {
|
||||
$item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true];
|
||||
$final_list[] = $this->formatItemInList(array_merge($item, $item_language_info));
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
return array_map(function ($item){
|
||||
return $this->formatItemInList($item);
|
||||
}, $item_list);
|
||||
}
|
||||
|
||||
|
||||
protected function formatItemInList(array $info): array
|
||||
{
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
public function getActualFilterCondition(array $raw_filter_condition) : array
|
||||
{
|
||||
return $raw_filter_condition;
|
||||
}
|
||||
|
||||
|
||||
public function getModelFilterCondition(array $raw_filter_condition) : array
|
||||
{
|
||||
return $this->iEntityCategoryModel->getQueryCondition( $raw_filter_condition);
|
||||
}
|
||||
|
||||
|
||||
public function getList(array $condition) : array
|
||||
{
|
||||
$item_list = $this->iEntityCategoryModel->getList($condition);
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
$list_ids = array_map(function ($item){ return $item['id'];}, $item_list);
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_ids);
|
||||
|
||||
$final_list = [];
|
||||
foreach ($item_list as $item) {
|
||||
$item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true];
|
||||
$final_list[] = $this->formatItemInList(array_merge($item, $item_language_info));
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
return array_map(function ($item){
|
||||
return $this->formatItemInList($item);
|
||||
}, $item_list);
|
||||
}
|
||||
|
||||
|
||||
public function getTotal(array $condition) : int
|
||||
{
|
||||
return $this->iEntityCategoryModel->getTotal($condition);
|
||||
}
|
||||
|
||||
|
||||
protected function formatItemInfo(?array $info) : ?array
|
||||
{
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
public function getInfo($id) : ?array
|
||||
{
|
||||
if(!$id) return null;
|
||||
|
||||
return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
$info = $this->iEntityCategoryModel->getInfo($id);
|
||||
$item_language_info = $this->iEntityLanguageModel->getInfo($id);
|
||||
if($item_language_info) {
|
||||
return $this->formatItemInfo(array_merge($info, $item_language_info));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->formatItemInfo($this->iEntityCategoryModel->getInfo($id));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function getEmptyInfo(array $additional_fields = []) : array
|
||||
{
|
||||
return $this->iEntityCategoryModel->getEmptyInfo($additional_fields);
|
||||
}
|
||||
|
||||
|
||||
// all categories and nested by parent
|
||||
public function getNestedCategories($is_public = false) {
|
||||
|
||||
$cache_key = "getNestedCategories-".$this->iEntityCategoryModel->getEntityType()."-".($is_public ? 1: 0);
|
||||
|
||||
return self::getCache($cache_key, function () use ($is_public){
|
||||
$all_categories = $this->getAllParent([
|
||||
'status' => ($is_public) ? 1 : 0
|
||||
]);
|
||||
|
||||
$result = [];
|
||||
if(isset($all_categories[0])) {
|
||||
foreach ($all_categories[0] as $index => $info) {
|
||||
$info['children'] = ($info['is_parent']) ? $this->getChildren($info['id'], $all_categories, 1) : [];
|
||||
$result[] = $this->formatItemInList($info);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function getChildren($parent_id, $all_categories, $current_level = 1){
|
||||
// dont allow too many nested level
|
||||
$max_deep_level_allow = 5;
|
||||
if($current_level == $max_deep_level_allow) return [];
|
||||
|
||||
$result = [];
|
||||
if(isset($all_categories[$parent_id])) {
|
||||
foreach ($all_categories[$parent_id] as $id => $info) {
|
||||
$info['children'] = $this->getChildren($id, $all_categories, $current_level + 1);
|
||||
$result[] = $this->formatItemInList($info);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function showPath($catPathId, $p_category){
|
||||
|
||||
if(!$catPathId){
|
||||
$cat_info = $this->getInfo($p_category);
|
||||
$catPathId = $cat_info['cat_path'];
|
||||
}
|
||||
|
||||
$result = array();
|
||||
$path_build = array();
|
||||
|
||||
if(strlen($catPathId) > 0){
|
||||
$cat_id_list = array_filter(explode(":", $catPathId));
|
||||
|
||||
$cat_list_info = $this->getListByIds($cat_id_list);
|
||||
|
||||
//reverse cat id order because the parent in the right most
|
||||
krsort($cat_id_list);
|
||||
|
||||
$start_count = 0;
|
||||
$count_cat = sizeof($cat_id_list);
|
||||
|
||||
foreach($cat_id_list as $catId){
|
||||
|
||||
$_cat_info = $cat_list_info[$catId] ?? null;
|
||||
if(!$_cat_info) continue;
|
||||
|
||||
$start_count ++;
|
||||
$path_build[] = "<a href=\"".$_cat_info['url']."\">".$_cat_info['title']."</a> ";
|
||||
|
||||
$result[] = array(
|
||||
'id' => $catId,
|
||||
'url' => $_cat_info['url'],
|
||||
'title' => $_cat_info['title'],
|
||||
);
|
||||
|
||||
if($start_count < $count_cat) $path_build[] = " >> ";
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'path' => $result,
|
||||
'path_url' => join("", $path_build),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function getAll(array $condition = []) {
|
||||
return $this->iEntityCategoryModel->getAll($condition);
|
||||
}
|
||||
|
||||
|
||||
public function getAllParent(array $condition = []) {
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
|
||||
$all_categories = $this->getAll($condition);
|
||||
|
||||
$item_list_ids = array_map(function ($item){ return $item['id']; }, $all_categories);
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($item_list_ids);
|
||||
|
||||
$translated_list = [];
|
||||
foreach ($all_categories as $item) {
|
||||
$item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true];
|
||||
$item = array_merge($item, $item_language_info);
|
||||
|
||||
$translated_list[] = array_merge($item, $item_language_info);
|
||||
}
|
||||
|
||||
// get group by parent
|
||||
$final_list = [];
|
||||
foreach ( $translated_list as $item ) {
|
||||
$final_list[$item['parent_id']][$item['id']] = $this->formatItemInList($item);
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
// else
|
||||
$all_categories = $this->getAll($condition);
|
||||
$final_list = [];
|
||||
foreach ( $all_categories as $item ) {
|
||||
$final_list[$item['parent_id']][$item['id']] = $this->formatItemInList($item);
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
197
inc/Hura8/System/Controller/aERPController.php
Normal file
197
inc/Hura8/System/Controller/aERPController.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Database\ConnectDB;
|
||||
|
||||
abstract class aERPController implements iClientERP
|
||||
{
|
||||
|
||||
protected $provider;
|
||||
protected $erp_config;
|
||||
|
||||
protected $tb_product = TableName::PRODUCT;
|
||||
protected $tb_erp_product = "erp_product";
|
||||
|
||||
// this table is the exact copy of $tb_erp_product
|
||||
// it's used when call the syncProductToWeb method to make sure that no new products are added in the syncing process
|
||||
protected $tb_erp_product_copy = "erp_product_tmp_copy";
|
||||
|
||||
protected $tb_log = "erp_log";
|
||||
|
||||
/* @var $objERPProvider iERPProvider */
|
||||
protected $objERPProvider;
|
||||
|
||||
/* @var $db ConnectDB */
|
||||
protected $db;
|
||||
|
||||
protected $get_erp_product_options = [];
|
||||
|
||||
|
||||
public function __construct($provider)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
|
||||
$provider_config_file = CONFIG_DIR . '/provider/'.$provider.'_config.php';
|
||||
if(file_exists($provider_config_file)) {
|
||||
$this->erp_config = include $provider_config_file;
|
||||
|
||||
// create an instance of provider
|
||||
$this->erpFactory();
|
||||
|
||||
$this->db = ConnectDB::getInstance('');
|
||||
}else{
|
||||
die("Cannot load /config/provider/".$provider."_config.php ");
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function customSyncProductToWeb();
|
||||
abstract protected function formatProductListFromERP(array $product_list);
|
||||
|
||||
/**
|
||||
* @overwrite on implemeting class if needed
|
||||
*/
|
||||
public function getProductListPerPage($page, $debug = false) {
|
||||
return $this->getProductList(['page' => $page], $debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iERPProvider
|
||||
*/
|
||||
public function getERPInstance()
|
||||
{
|
||||
return $this->objERPProvider;
|
||||
}
|
||||
|
||||
public function createOrder(array $order_info)
|
||||
{
|
||||
return $this->objERPProvider->createOrder($order_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
public function syncProductToWeb(array $options = [])
|
||||
{
|
||||
$this->beforeSyncProductToWeb();
|
||||
$this->customSyncProductToWeb();
|
||||
$this->afterSyncProductToWeb();
|
||||
}
|
||||
|
||||
protected function beforeSyncProductToWeb() {
|
||||
/*$this->db->runQuery("CREATE TABLE `" . $this->tb_erp_product_copy . "` LIKE ".$this->tb_erp_product." ");
|
||||
$this->db->runQuery("INSERT INTO `" . $this->tb_erp_product_copy . "` SELECT * FROM ".$this->tb_erp_product." ");
|
||||
$this->db->runQuery("UPDATE `" . $this->tb_erp_product_copy . "` e, idv_sell_product_store p SET
|
||||
e.product_id = p.id
|
||||
WHERE e.sku = p.storeSKU AND LENGTH(e.sku) > 0 ");
|
||||
$this->db->runQuery("DELETE FROM `" . $this->tb_erp_product_copy . "` WHERE `product_id` = 0 ");*/
|
||||
|
||||
$this->db->multi_query([
|
||||
"CREATE TABLE IF NOT EXISTS `" . $this->tb_erp_product_copy . "` LIKE ".$this->tb_erp_product." ",
|
||||
"INSERT INTO `" . $this->tb_erp_product_copy . "` SELECT * FROM ".$this->tb_erp_product." ",
|
||||
"UPDATE `" . $this->tb_erp_product_copy . "` e, ".$this->tb_product." p SET
|
||||
e.product_id = p.id
|
||||
WHERE e.sku = p.storeSKU AND LENGTH(e.sku) > 0 ",
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function afterSyncProductToWeb() {
|
||||
$this->db->runQuery("DROP TABLE `" . $this->tb_erp_product_copy . "` ");
|
||||
}
|
||||
|
||||
public function setERPProductOptions(array $options = []) {
|
||||
foreach ($options as $key => $value) {
|
||||
$this->get_erp_product_options[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: clean any existing data before populate new ones
|
||||
* @return void
|
||||
*/
|
||||
public function cleanExistingData()
|
||||
{
|
||||
$this->db->runQuery("TRUNCATE `" . $this->tb_erp_product . "` ");
|
||||
}
|
||||
|
||||
public function getAllStore()
|
||||
{
|
||||
return $this->objERPProvider->getAllStore();
|
||||
}
|
||||
|
||||
|
||||
public function getProductSummary()
|
||||
{
|
||||
return $this->objERPProvider->getProductSummary();
|
||||
}
|
||||
|
||||
|
||||
public function getProductList(array $options = [], $debug = false)
|
||||
{
|
||||
return $this->formatProductListFromERP($this->objERPProvider->getProductList($options, $debug));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get log data
|
||||
*/
|
||||
public function getLog($type, $limit = 50) {
|
||||
$query = $this->db->runQuery("SELECT `id`, `data`, `log_time` FROM `".$this->tb_log."` WHERE `type` = ? ORDER BY `id` DESC LIMIT ".$limit, ['s'], [$type]);
|
||||
|
||||
return array_map(function ($item){
|
||||
$copy = $item;
|
||||
$copy['data'] = $item['data'] ? \json_decode($item['data'], true) : [];
|
||||
|
||||
return $copy;
|
||||
}, $this->db->fetchAll($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* log data
|
||||
*/
|
||||
public function updateLogData($id, $new_data) {
|
||||
|
||||
$query = $this->db->runQuery("SELECT `data` FROM `".$this->tb_log."` WHERE `id` = ? LIMIT 1", ['d'], [$id]);
|
||||
|
||||
if($item_info = $this->db->fetchAssoc($query)) {
|
||||
$current_data = $item_info['data'] ? \json_decode($item_info['data'], true) : [];
|
||||
$updated_info = array_merge($current_data, $new_data);
|
||||
|
||||
return $this->db->update($this->tb_log, ['data' => $updated_info], ['id' => $id]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* log data
|
||||
*/
|
||||
public function log($type, array $data) {
|
||||
$info = [];
|
||||
$info['type'] = $type;
|
||||
$info['data'] = $data;
|
||||
$info['log_time'] = CURRENT_TIME;
|
||||
|
||||
return $this->db->insert($this->tb_log, $info);
|
||||
}
|
||||
|
||||
protected function erpFactory()
|
||||
{
|
||||
if(!$this->provider) {
|
||||
die("No provider found!");
|
||||
}
|
||||
|
||||
$provider_class = 'Provider\\ERPProviders\\'.ucfirst($this->provider);
|
||||
|
||||
try {
|
||||
|
||||
$this->objERPProvider = (new \ReflectionClass($provider_class))->newInstance($this->erp_config);
|
||||
|
||||
} catch (\ReflectionException $e) {
|
||||
die("aClientERP/erpFactory: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
262
inc/Hura8/System/Controller/aEntityBaseController.php
Normal file
262
inc/Hura8/System/Controller/aEntityBaseController.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\iEntityLanguageModel;
|
||||
use Hura8\Interfaces\iEntityModel;
|
||||
use Hura8\System\Security\DataClean;
|
||||
use Hura8\System\Security\DataType;
|
||||
use Hura8\Traits\ClassCacheTrait;
|
||||
|
||||
|
||||
/**
|
||||
* @description shared controller for aAdminEntityBaseController and aPublicEntityBaseController
|
||||
* DO NOT EXTEND THIS CLASS from other classes
|
||||
*/
|
||||
abstract class aEntityBaseController
|
||||
{
|
||||
use ClassCacheTrait;
|
||||
|
||||
/* @var iEntityModel $iEntityModel */
|
||||
protected $iEntityModel;
|
||||
|
||||
/* @var ?iEntityLanguageModel $iEntityLanguageModel */
|
||||
protected $iEntityLanguageModel = null;
|
||||
|
||||
protected $view_language = LANGUAGE;
|
||||
|
||||
public function __construct(
|
||||
iEntityModel $iEntityModel,
|
||||
?iEntityLanguageModel $iEntityLanguageModel = null
|
||||
) {
|
||||
|
||||
$this->iEntityModel = $iEntityModel;
|
||||
|
||||
if(!$this->isDefaultLanguage() && $iEntityLanguageModel instanceof iEntityLanguageModel) {
|
||||
$this->iEntityLanguageModel = $iEntityLanguageModel;
|
||||
|
||||
// only controller allow to control the language for the model
|
||||
$this->iEntityLanguageModel->setLanguage($this->view_language);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getLanguage() {
|
||||
return $this->view_language;
|
||||
}
|
||||
|
||||
|
||||
public function isDefaultLanguage() {
|
||||
return IS_DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
|
||||
public function getFilterConditions() : array {
|
||||
return array_merge(
|
||||
$this->baseFilterConditions(),
|
||||
$this->extendFilterConditions()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// this is supposed to be overwritten by the extending class if required
|
||||
protected function extendFilterConditions() : array {
|
||||
return [];
|
||||
}
|
||||
|
||||
// just here to show the reserved keys. That's all
|
||||
protected function baseFilterConditions() {
|
||||
$base_filter = [
|
||||
'q' => getRequest("q", ''), // keyword search
|
||||
'q_options' => [
|
||||
'field_filters' => [],
|
||||
'fulltext_fields' => [],
|
||||
'limit_result' => 2000
|
||||
], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000)
|
||||
'featured' => getRequestInt("featured", 0), // 1|-1
|
||||
'status' => getRequestInt("status", 0), // 1|-1
|
||||
'excluded_ids' => null, // [id1, id2, ...]
|
||||
'included_ids' => null,// [id1, id2, ...]
|
||||
|
||||
// to special filters for language
|
||||
'translated' => getRequestInt("translated", 0), // 1|-1
|
||||
|
||||
// for sorting
|
||||
'sort_by' => getRequest('sort', ''),
|
||||
|
||||
// for pagination, not exactly for filter but put here to reserve the keys
|
||||
'numPerPage' => getRequestInt("show", 20),
|
||||
'page' => getPageId(),
|
||||
];
|
||||
|
||||
if(getRequest("excluded_ids", '') != '') {
|
||||
$base_filter['excluded_ids'] = explode("-", getRequest("excluded_ids", ''));
|
||||
}
|
||||
|
||||
if(getRequest("included_ids", '') != '') {
|
||||
$base_filter['included_ids'] = explode("-", getRequest("included_ids", ''));
|
||||
}
|
||||
|
||||
return $base_filter;
|
||||
}
|
||||
|
||||
|
||||
protected function formatItemInList(array $item_info)
|
||||
{
|
||||
return $item_info;
|
||||
}
|
||||
|
||||
|
||||
protected function formatItemInfo(array $item_info)
|
||||
{
|
||||
return $item_info;
|
||||
}
|
||||
|
||||
|
||||
public function getListByIds(array $list_id, array $condition = array()) : array
|
||||
{
|
||||
$item_list = array_map(function ($item){
|
||||
return $this->formatItemInList($item);
|
||||
}, $this->iEntityModel->getListByIds($list_id, $condition));
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id);
|
||||
|
||||
$final_list = [];
|
||||
foreach ($item_list as $item) {
|
||||
$item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true];
|
||||
$final_list[] = array_merge($item, $item_language_info);
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
return $item_list;
|
||||
}
|
||||
|
||||
|
||||
// for extending controller class to validate and clean data in the $raw_filter_condition (ie. from URL)
|
||||
// before sending to model for data querying
|
||||
// extending controller must overwrite this
|
||||
protected function validateAndCleanFilterCondition(array $raw_filter_condition) : array {
|
||||
|
||||
$clean_values = [];
|
||||
|
||||
foreach ($raw_filter_condition as $key => $value) {
|
||||
// default
|
||||
if(is_array($value)) {
|
||||
$clean_values[$key] = DataClean::makeListOfInputSafe($value, DataType::ID);
|
||||
}else{
|
||||
$clean_values[$key] = DataClean::makeInputSafe($value, DataType::ID);
|
||||
}
|
||||
}
|
||||
|
||||
return $clean_values;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description utility to inspect the actual filters which will be used in getList
|
||||
* make sure to edit the ::_buildQueryConditionExtend method on the Model so the Model will parse the filters provided by controller here
|
||||
* @param array $raw_filter_condition
|
||||
* @return string[]
|
||||
*/
|
||||
public function getActualFilterCondition(array $raw_filter_condition) : array
|
||||
{
|
||||
return $this->buildFilterQuery($raw_filter_condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description utility to inspect the actual filters which will be used in getList by Model
|
||||
* @param array $raw_filter_condition
|
||||
* @return string[]
|
||||
*/
|
||||
public function getModelFilterCondition(array $raw_filter_condition) : array
|
||||
{
|
||||
return $this->iEntityModel->getQueryCondition($this->buildFilterQuery($raw_filter_condition));
|
||||
}
|
||||
|
||||
|
||||
public function getList(array $raw_filter_condition) : array
|
||||
{
|
||||
|
||||
$filter_condition = $this->buildFilterQuery($raw_filter_condition);
|
||||
//debug_var($filter_condition);
|
||||
|
||||
$item_list = array_map(function ($item){
|
||||
return $this->formatItemInList($item);
|
||||
}, $this->iEntityModel->getList($filter_condition));
|
||||
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
|
||||
$item_list_ids = array_map(function ($item){ return $item['id']; }, $item_list);
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($item_list_ids);
|
||||
|
||||
$final_list = [];
|
||||
foreach ($item_list as $item) {
|
||||
$item_language_info = $item_list_language_info[$item['id']] ?? ["not_translated" => true];
|
||||
$final_list[] = array_merge($item, $item_language_info);
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
return $item_list;
|
||||
}
|
||||
|
||||
|
||||
public function getTotal(array $raw_filter_condition) : int
|
||||
{
|
||||
$filter_condition = $this->buildFilterQuery($raw_filter_condition);
|
||||
|
||||
return $this->iEntityModel->getTotal($filter_condition);
|
||||
}
|
||||
|
||||
|
||||
protected function buildFilterQuery(array $raw_filter_condition) : array
|
||||
{
|
||||
$filter_condition = $this->validateAndCleanFilterCondition($raw_filter_condition);
|
||||
|
||||
// special case to filter out which ids have not been translated
|
||||
if(isset($filter_condition['translated']) && $filter_condition['translated'] && $this->iEntityLanguageModel) {
|
||||
if($filter_condition['translated'] == 1) {
|
||||
$filter_condition['included_ids'] = $this->iEntityLanguageModel->getTranslatedIds();
|
||||
}else{
|
||||
$filter_condition['excluded_ids'] = $this->iEntityLanguageModel->getTranslatedIds();
|
||||
}
|
||||
}
|
||||
|
||||
return $filter_condition;
|
||||
}
|
||||
|
||||
|
||||
public function getInfo($id): ?array
|
||||
{
|
||||
if(!$id) return null;
|
||||
|
||||
return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){
|
||||
|
||||
$info = $this->iEntityModel->getInfo($id);
|
||||
|
||||
if($this->iEntityLanguageModel && $info) {
|
||||
$item_language_info = $this->iEntityLanguageModel->getInfo($id);
|
||||
//debug_var($item_language_info);
|
||||
|
||||
if($item_language_info) {
|
||||
return $this->formatItemInfo(array_merge($info, $item_language_info));
|
||||
}else{
|
||||
$info["not_translated"] = true;
|
||||
|
||||
return $this->formatItemInfo($info);
|
||||
}
|
||||
}
|
||||
|
||||
return ($info) ? $this->formatItemInfo($info) : null;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
344
inc/Hura8/System/Controller/aExcelDownloadController.php
Normal file
344
inc/Hura8/System/Controller/aExcelDownloadController.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
/**
|
||||
* Created by Glee Ltd.
|
||||
* User: Hieu
|
||||
* Date: 20-Aug-19
|
||||
* Time: 11:16 AM
|
||||
* Description:
|
||||
*/
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\iExcelDownload;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
|
||||
abstract class aExcelDownloadController implements iExcelDownload
|
||||
{
|
||||
protected static $column_names = [
|
||||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||||
'AA','AB','AC','AD','AE','AF','AG','AH','AI','AJ','AK','AL','AM',
|
||||
'AN','AO','AP','AQ','AR','AS','AT','AU','AV','AW','AX','AY','AZ',
|
||||
//...
|
||||
];
|
||||
|
||||
protected $work_sheet_title = "Danh sách"; //
|
||||
protected $export_file_name = '';
|
||||
|
||||
/* @var $objExcel Spreadsheet */
|
||||
protected $objExcel;
|
||||
|
||||
/* @var $currentActiveSheet Worksheet */
|
||||
protected $currentActiveSheet;
|
||||
|
||||
protected $client_excel_col_config = [
|
||||
/*"A" => array(
|
||||
'name' => 'ID Sản phẩm Web',
|
||||
'width' => '10',
|
||||
'data_field_name' => 'id',
|
||||
),
|
||||
"B" => array(
|
||||
'name' => 'Mã kho (SKU)',
|
||||
'width' => '10',
|
||||
'data_field_name' => 'storeSKU',
|
||||
),*/
|
||||
//...
|
||||
];
|
||||
|
||||
protected $field_column_mappings = [
|
||||
//'name' => "A",
|
||||
//'price' => "B",
|
||||
];
|
||||
|
||||
private $header_row_index = 2;
|
||||
|
||||
// hold cach for some operations
|
||||
protected $cache = [];
|
||||
|
||||
protected $format_item_middlewares = []; // list of middleware object to format item
|
||||
|
||||
|
||||
public function __construct($client_config_file_name='', $export_file_name='', $work_sheet_title = '')
|
||||
{
|
||||
if($client_config_file_name) {
|
||||
$this->setColumnConfigUseConfigFile($client_config_file_name);
|
||||
}
|
||||
|
||||
$this->export_file_name = ($export_file_name) ?: "file_".CURRENT_TIME;
|
||||
$this->work_sheet_title = ($work_sheet_title) ?: "Danh sách";
|
||||
}
|
||||
|
||||
|
||||
protected function setColumnConfigUseConfigFile($client_config_file_name) {
|
||||
//this from a config file for each client
|
||||
$client_config_file = "config/client/excel/".$client_config_file_name;
|
||||
if( ! file_exists(ROOT_DIR .'/'. $client_config_file)) {
|
||||
die("Please create config file: ".$client_config_file);
|
||||
}
|
||||
$client_fields_config = include ROOT_DIR .'/'. $client_config_file;
|
||||
|
||||
// auto add excel column names based on fields' index
|
||||
$this->client_excel_col_config = $this->_make_columns(array_values($client_fields_config));
|
||||
|
||||
// create field-col map
|
||||
$this->createFieldColumnMappings();
|
||||
}
|
||||
|
||||
|
||||
private function createFieldColumnMappings() {
|
||||
$field_column_mappings = [];
|
||||
foreach ($this->client_excel_col_config as $column_name => $_prop) {
|
||||
$field_column_mappings[$_prop['data_field_name']] = $column_name;
|
||||
}
|
||||
$this->field_column_mappings = $field_column_mappings;
|
||||
}
|
||||
|
||||
|
||||
public function setColumnConfigManually(array $col_config) {
|
||||
$this->client_excel_col_config = $this->_make_columns($col_config);
|
||||
// create field-col map
|
||||
$this->createFieldColumnMappings();
|
||||
}
|
||||
|
||||
|
||||
protected function _make_columns($fields_config) {
|
||||
$new_array = [];
|
||||
$total_names = sizeof(static::$column_names);
|
||||
foreach ($fields_config as $index => $config) {
|
||||
if($index >= $total_names) break;
|
||||
$new_array[static::$column_names[$index]] = $config;
|
||||
}
|
||||
|
||||
return $new_array;
|
||||
}
|
||||
|
||||
|
||||
public function start(array $options = [
|
||||
"debug_mode" => '', // show-item-list
|
||||
"excelOption" => [],
|
||||
"sheetOption" => [],
|
||||
"sheetStartRowNumber" => 3,
|
||||
"sheetHeaderRowNumber" => 2,
|
||||
"getItemListOption" => [
|
||||
"brand" => [],
|
||||
"category" => [],
|
||||
"page" => 1,
|
||||
"limit" => 100,
|
||||
],
|
||||
"exportFileOption" => [],
|
||||
]) {
|
||||
|
||||
$debug_mode = $options['debug_mode'] ?? false;
|
||||
// debug mode
|
||||
if($debug_mode) {
|
||||
// show item list
|
||||
if($debug_mode == 'show-item-list') {
|
||||
$item_list = $this->getItemList($options['getItemListOption']);
|
||||
print_r($item_list);
|
||||
}
|
||||
|
||||
// show formatted list
|
||||
if($debug_mode == 'show-formatted-list') {
|
||||
$item_list = $this->formatItemList( $this->getItemList($options['getItemListOption']) );
|
||||
print_r($item_list);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// setup
|
||||
if(isset($options['sheetHeaderRowNumber']) && $options['sheetHeaderRowNumber']) {
|
||||
$this->header_row_index = $options['sheetHeaderRowNumber'];
|
||||
}
|
||||
|
||||
$this->createExcelObject($options['excelOption'] ?? []);
|
||||
$this->createActiveSheet(0, $options['sheetOption'] ?? []);
|
||||
|
||||
// num beforeWriteList
|
||||
$this->beforeWriteList();
|
||||
|
||||
// fetch all items and write till the end
|
||||
$has_fetch_all_items = false;
|
||||
$start_row = (isset($options["sheetStartRowNumber"])) ? $options["sheetStartRowNumber"] : 3;
|
||||
|
||||
$item_list = $this->formatItemList( $this->getItemList($options['getItemListOption']) );
|
||||
|
||||
//debug_var($item_list);
|
||||
//exit;
|
||||
|
||||
$has_fetch_all_items = true;
|
||||
$this->writeItemsToExcel($start_row, $item_list);
|
||||
|
||||
/*$getItemListOption = $options['getItemListOption'];
|
||||
$pageIndex = 0;
|
||||
|
||||
while (!$has_fetch_all_items) {
|
||||
// run from page 1->end
|
||||
$pageIndex += 1;
|
||||
$getItemListOption["page"] = $pageIndex;
|
||||
$item_list = $this->getItemList($getItemListOption);
|
||||
|
||||
// flag
|
||||
if(!sizeof($item_list)) {
|
||||
$has_fetch_all_items = true;
|
||||
}
|
||||
|
||||
// else, start write
|
||||
$last_row = $this->writeItemsToExcel($start_row, $item_list);
|
||||
|
||||
// update $start_row
|
||||
$start_row = $last_row;// + 1
|
||||
}*/
|
||||
|
||||
// export
|
||||
if($has_fetch_all_items) {
|
||||
$this->getExcelFile($options['exportFileOption']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// abstract methods
|
||||
protected function beforeWriteList() { }
|
||||
abstract protected function getItemList(array $options);
|
||||
abstract protected function defaultFormatItemInfo(array $item_info, $index =0);
|
||||
|
||||
protected function registerFormatItemInfoMiddleware($middleware_ojb) {
|
||||
$this->format_item_middlewares[] = $middleware_ojb;
|
||||
}
|
||||
|
||||
protected function formatItemInfo(array $item_info, $index = 0) {
|
||||
|
||||
// apply middleware
|
||||
if(sizeof($this->format_item_middlewares)) {
|
||||
foreach ($this->format_item_middlewares as $_middleware) {
|
||||
$item_info = call_user_func($_middleware, $item_info);
|
||||
}
|
||||
} else {
|
||||
$item_info = $this->defaultFormatItemInfo($item_info, $index);
|
||||
}
|
||||
|
||||
return $item_info;
|
||||
}
|
||||
|
||||
|
||||
protected function createExcelObject(array $options)
|
||||
{
|
||||
// Create new Spreadsheet object
|
||||
$this->objExcel = new Spreadsheet();
|
||||
|
||||
// Set properties
|
||||
$this->objExcel->getProperties()->setCreator("Hurasoft");
|
||||
$this->objExcel->getProperties()->setLastModifiedBy("Hurasoft");
|
||||
$this->objExcel->getProperties()->setTitle("Excel Document");
|
||||
$this->objExcel->getProperties()->setSubject("Excel Document");
|
||||
$this->objExcel->getProperties()->setDescription("Tao file excel");
|
||||
}
|
||||
|
||||
|
||||
protected function createActiveSheet($sheet_index = 0, array $options=[]) {
|
||||
// Create a first sheet, representing sales data
|
||||
$this->objExcel->setActiveSheetIndex($sheet_index);
|
||||
$this->currentActiveSheet = $this->objExcel->getActiveSheet();
|
||||
$this->currentActiveSheet->setCellValueExplicit('A1', $this->work_sheet_title, DataType::TYPE_STRING);
|
||||
$this->currentActiveSheet->getStyle('A1')->getFont()->setSize(16);
|
||||
$this->currentActiveSheet->getStyle('A1')->getFont()->setBold(true);
|
||||
|
||||
// Set header row
|
||||
$row_index = $this->header_row_index;
|
||||
foreach ($this->client_excel_col_config as $col_name => $_prop) {
|
||||
$col_width = (isset($_prop['width']) && intval($_prop['width']) > 0) ? intval($_prop['width']) : 15;
|
||||
$this->currentActiveSheet->getColumnDimension($col_name)->setWidth($col_width);
|
||||
$this->currentActiveSheet->setCellValueExplicit($col_name. $row_index, $_prop["name"], DataType::TYPE_STRING);
|
||||
$this->currentActiveSheet->getStyle($col_name . $row_index)->getFont()->setBold(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatItemList(array $item_list)
|
||||
{
|
||||
$new_list = [];
|
||||
foreach ( $item_list as $index => $item_info ) {
|
||||
$new_list[$index] = $this->formatItemInfo($item_info, $index);
|
||||
}
|
||||
|
||||
return $new_list;
|
||||
}
|
||||
|
||||
|
||||
protected function writeItemsToExcel($start_row = 1, array $item_list=[])
|
||||
{
|
||||
$write_row = $start_row;
|
||||
foreach ( $item_list as $index => $item_info ) {
|
||||
|
||||
// write each field to its corresponding columns
|
||||
foreach ($item_info as $_field => $_value) {
|
||||
if( !isset($this->field_column_mappings[$_field])) continue;
|
||||
|
||||
if(is_array($_value)) $_value = serialize($_value);
|
||||
|
||||
$write_column = $this->field_column_mappings[$_field];
|
||||
|
||||
$this->currentActiveSheet->setCellValueExplicit($write_column . $write_row, $_value, DataType::TYPE_STRING);
|
||||
$this->currentActiveSheet->getStyle($write_column . $write_row)->getAlignment()->setWrapText(true);
|
||||
}
|
||||
|
||||
// next rows
|
||||
$write_row += 1;
|
||||
}
|
||||
|
||||
// get the last row
|
||||
return $write_row;
|
||||
}
|
||||
|
||||
|
||||
protected function getExcelFile(array $options)
|
||||
{
|
||||
// write to a local file
|
||||
$local_file = $options['local_file'] ?? '';
|
||||
if($local_file) {
|
||||
$this->_save_to_file($local_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
// default: export to browser to download
|
||||
$this->_export_to_browser();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function cleanUp(){
|
||||
// clean up
|
||||
$this->objExcel->disconnectWorksheets();
|
||||
unset($this->objExcel);
|
||||
}
|
||||
|
||||
protected function _save_to_file($file_path){
|
||||
|
||||
// delete old file if exist
|
||||
if(file_exists($file_path)) {
|
||||
@unlink($file_path);
|
||||
}
|
||||
|
||||
$writer = new Xlsx($this->objExcel);
|
||||
$writer->save($file_path);
|
||||
$this->cleanUp();
|
||||
}
|
||||
|
||||
protected function _export_to_browser(){
|
||||
// Rename sheet
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header('Content-Disposition: attachment;filename="'.$this->export_file_name.'.xlsx"');
|
||||
header('Cache-Control: max-age=0');
|
||||
|
||||
$writer = new Xlsx($this->objExcel);
|
||||
ob_end_clean();
|
||||
$writer->save('php://output');
|
||||
|
||||
$this->cleanUp();
|
||||
exit();
|
||||
}
|
||||
|
||||
}
|
||||
355
inc/Hura8/System/Controller/aExcelUploadController.php
Normal file
355
inc/Hura8/System/Controller/aExcelUploadController.php
Normal file
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\System\ReadExcel;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
|
||||
|
||||
abstract class aExcelUploadController
|
||||
{
|
||||
|
||||
protected $update_option = [];
|
||||
|
||||
protected static $column_names = [
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
//...
|
||||
];
|
||||
|
||||
protected $file_input_name = "file_excel"; //
|
||||
|
||||
protected $client_excel_col_config = [
|
||||
/*"A" => array(
|
||||
'name' => 'ID Sản phẩm Web',
|
||||
'width' => '10',
|
||||
'data_field_name' => 'id',
|
||||
),
|
||||
"B" => array(
|
||||
'name' => 'Mã kho (SKU)',
|
||||
'width' => '10',
|
||||
'data_field_name' => 'storeSKU',
|
||||
),*/
|
||||
//...
|
||||
];
|
||||
|
||||
protected $field_column_mappings = [
|
||||
//'name' => "A",
|
||||
//'price' => "B",
|
||||
];
|
||||
|
||||
// hold cache for some operations
|
||||
protected $cache = [];
|
||||
|
||||
protected $format_item_middlewares = []; // list of middleware object to format item
|
||||
|
||||
|
||||
public function __construct($client_config_file_name = '', $file_input_name = '', array $update_option = [])
|
||||
{
|
||||
|
||||
if(!$client_config_file_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//this from a config file for each client
|
||||
$client_config_file = "config/client/excel/" . $client_config_file_name;
|
||||
if (!file_exists(ROOT_DIR . '/' . $client_config_file)) {
|
||||
die("Please create config file: " . $client_config_file);
|
||||
}
|
||||
$client_fields_config = include ROOT_DIR . '/' . $client_config_file;
|
||||
|
||||
if($file_input_name) {
|
||||
$this->file_input_name = $file_input_name;
|
||||
}
|
||||
|
||||
// auto add excel column names based on fields' index
|
||||
$this->client_excel_col_config = $this->_make_columns(array_values($client_fields_config));
|
||||
|
||||
// create field-col map
|
||||
$field_column_mappings = [];
|
||||
foreach ($this->client_excel_col_config as $column_name => $_prop) {
|
||||
if(!$_prop['data_field_name']) continue;
|
||||
|
||||
// skip column which is not for upload
|
||||
if(isset($_prop['for_upload']) && !$_prop['for_upload']) continue;
|
||||
|
||||
$field_column_mappings[$_prop['data_field_name']] = $column_name;
|
||||
}
|
||||
|
||||
$this->field_column_mappings = $field_column_mappings;
|
||||
|
||||
$this->update_option = $update_option;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function updateUpdateOptions(array $update_option = []) {
|
||||
foreach ($update_option as $key => $value) {
|
||||
$this->update_option[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getColumnNotForUpload() {
|
||||
$result = [];
|
||||
foreach ($this->client_excel_col_config as $column_name => $_prop) {
|
||||
// skip column which is not for upload
|
||||
if(isset($_prop['for_upload']) && !$_prop['for_upload']) $result[$column_name] = $_prop['name'] ;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function getColumnCanUpdate() {
|
||||
$result = [];
|
||||
foreach ($this->client_excel_col_config as $column_name => $_prop) {
|
||||
// skip column which is not for upload
|
||||
if(isset($_prop['for_upload']) && !$_prop['for_upload']) continue;
|
||||
|
||||
// skip column which is not for update
|
||||
if(isset($_prop['can_update']) && !$_prop['can_update']) continue;
|
||||
|
||||
$result[] = $_prop ;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
protected function getRequiredFields() {
|
||||
$result = [];
|
||||
foreach ($this->client_excel_col_config as $column_name => $_prop) {
|
||||
// skip column which is not for upload
|
||||
if(isset($_prop['required']) && $_prop['required']) $result[] = $_prop['data_field_name'] ;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
protected function _make_columns($fields_config)
|
||||
{
|
||||
$new_array = [];
|
||||
$total_names = sizeof(static::$column_names);
|
||||
foreach ($fields_config as $index => $config) {
|
||||
if ($index >= $total_names) break;
|
||||
$new_array[static::$column_names[$index]] = $config;
|
||||
}
|
||||
|
||||
return $new_array;
|
||||
}
|
||||
|
||||
|
||||
protected function getExcelFileExt($excel_file){
|
||||
$ext = substr(strrchr($excel_file, "."), 1);
|
||||
return (in_array($ext, ["xls", "xlsx"])) ? $ext : false;
|
||||
}
|
||||
|
||||
|
||||
public function start($sheet_start_row = 3, $batch_mode = false, $batch_size=100)
|
||||
{
|
||||
$ext = $this->getExcelFileExt($_FILES[$this->file_input_name]["name"]);
|
||||
if(!$ext) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid excel file',
|
||||
];
|
||||
}
|
||||
|
||||
$objReadExcel = new ReadExcel($ext);
|
||||
|
||||
$all_rows = $objReadExcel->read(
|
||||
$_FILES[$this->file_input_name]["tmp_name"],
|
||||
$sheet_start_row,
|
||||
$this->field_column_mappings,
|
||||
'',
|
||||
true
|
||||
);
|
||||
|
||||
$this->beforeProcessRows($all_rows);
|
||||
|
||||
$success_row_counter = 0;
|
||||
|
||||
if($batch_mode) {
|
||||
// batch mode
|
||||
$small_batch = [];
|
||||
$counter = 0;
|
||||
foreach ($all_rows as $sheet_index => $sheet_content) {
|
||||
foreach ($sheet_content as $row_id => $row_content) {
|
||||
|
||||
$formatted_info = $this->formatItemInfo($this->convertColToField($row_content));
|
||||
if(!$formatted_info) continue;
|
||||
|
||||
$counter += 1;
|
||||
|
||||
$small_batch[] = $formatted_info;
|
||||
if($counter % $batch_size == 0) {
|
||||
$success_row_counter += $this->processBatchItems($small_batch);
|
||||
// reset
|
||||
$counter = 0;
|
||||
$small_batch = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process the remain
|
||||
if(sizeof($small_batch)) {
|
||||
$success_row_counter += $this->processBatchItems($small_batch);
|
||||
}
|
||||
|
||||
} else {
|
||||
// single item mode
|
||||
foreach ($all_rows as $sheet_index => $sheet_content) {
|
||||
foreach ($sheet_content as $row_index => $row_content) {
|
||||
|
||||
$formatted_info = $this->formatItemInfo($this->convertColToField($row_content));
|
||||
if(!$formatted_info) continue;
|
||||
|
||||
if($this->processItem($formatted_info, $row_index)){
|
||||
$success_row_counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//unset($all_rows);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => '',
|
||||
'success_counter' => $success_row_counter,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
protected function convertExcelDateValue($value) {
|
||||
// Date value in Excel's cell can be displayed as text or date value, we need to check for both
|
||||
$format_value = $this->formatExcelUploadDate($value);
|
||||
|
||||
if(!$format_value) {
|
||||
// check if it's excel date
|
||||
try {
|
||||
$format_value = (is_numeric($value)) ? date("d-m-Y", Date::excelToTimestamp($value, date_default_timezone_get())) : '';
|
||||
}catch (\Exception $e) {
|
||||
$format_value = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $format_value;
|
||||
}
|
||||
|
||||
|
||||
protected function formatExcelUploadDate($input_date) {
|
||||
$check_date_pattern = "/\d{1,2}-\d{1,2}-\d{4}/i";
|
||||
$format_date = str_replace("/", "-", $input_date);
|
||||
|
||||
if(preg_match($check_date_pattern, $format_date)) {
|
||||
return date("d-m-Y", strtotime($format_date));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function convertExcelHourMinuteValue($value) {
|
||||
$format_value = $this->formatExcelUploadHourMinute($value);
|
||||
|
||||
if(!$format_value) {
|
||||
// check if it's excel date
|
||||
try {
|
||||
$format_value = (is_numeric($value)) ? date("H:i", Date::excelToTimestamp($value, date_default_timezone_get())) : '';
|
||||
}catch (\Exception $e) {
|
||||
$format_value = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $format_value;
|
||||
}
|
||||
|
||||
|
||||
protected function formatExcelUploadHourMinute($input_date) {
|
||||
$check_date_pattern = "/\d{1,2}:\d{1,2}/i";
|
||||
//$format_date = str_replace("/", "-", $input_date);
|
||||
|
||||
if(preg_match($check_date_pattern, $input_date)) {
|
||||
return date("H:i", strtotime($input_date));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ['A' => '12', 'B' => 'ten sp'] => ['id' => '12', 'product_name' => 'ten sp']
|
||||
protected function convertColToField(array $row_content ) {
|
||||
|
||||
if(!$this->field_column_mappings) {
|
||||
return array_values($row_content);
|
||||
}
|
||||
|
||||
$item_info = [];
|
||||
foreach ($this->field_column_mappings as $field => $col) {
|
||||
//if(!isset($row_content[$col])) continue;
|
||||
$item_info[$field] = $row_content[$col];
|
||||
}
|
||||
|
||||
return $item_info;
|
||||
}
|
||||
|
||||
// abstract methods
|
||||
protected function beforeProcessRows(array &$all_read_rows){
|
||||
// default nothing
|
||||
// derived class should overwrite this method
|
||||
}
|
||||
abstract protected function processItem(array $item_info, $row_index=0);
|
||||
abstract protected function processBatchItems(array $item_list);
|
||||
|
||||
/**
|
||||
* @param array $item_info
|
||||
* @return array|null
|
||||
*/
|
||||
abstract protected function formatItemInfoBeforeProcess(array $item_info);
|
||||
|
||||
public function registerFormatItemInfoMiddleware($middleware_ojb)
|
||||
{
|
||||
$this->format_item_middlewares[] = $middleware_ojb;
|
||||
}
|
||||
|
||||
|
||||
protected $date_fields = [];
|
||||
public function setDateFields(array $date_fields) {
|
||||
$this->date_fields = $date_fields;
|
||||
}
|
||||
|
||||
protected $hour_minute_fields = [];
|
||||
public function setHourMinuteFields(array $hour_minute_fields) {
|
||||
$this->hour_minute_fields = $hour_minute_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $item_info
|
||||
* @return array|null
|
||||
*/
|
||||
protected function formatItemInfo(array $item_info)
|
||||
{
|
||||
$copy = $item_info;
|
||||
|
||||
// apply middleware
|
||||
if (sizeof($this->format_item_middlewares)) {
|
||||
foreach ($this->format_item_middlewares as $_middleware) {
|
||||
$copy = call_user_func($_middleware, $copy);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->date_fields as $field) {
|
||||
$copy[$field] = $this->convertExcelDateValue($item_info[$field]);
|
||||
}
|
||||
|
||||
foreach ($this->hour_minute_fields as $field) {
|
||||
$copy[$field] = $this->convertExcelHourMinuteValue($item_info[$field]);
|
||||
}
|
||||
|
||||
return $this->formatItemInfoBeforeProcess($copy);
|
||||
}
|
||||
|
||||
}
|
||||
81
inc/Hura8/System/Controller/aPublicEntityBaseController.php
Normal file
81
inc/Hura8/System/Controller/aPublicEntityBaseController.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\iPublicEntityController;
|
||||
|
||||
abstract class aPublicEntityBaseController extends aEntityBaseController implements iPublicEntityController
|
||||
{
|
||||
|
||||
public function getListByIds(array $list_id, array $condition = array()) : array
|
||||
{
|
||||
$item_list = array_map(function ($item){
|
||||
return $this->formatItemInList($item);
|
||||
}, $this->iEntityModel->getListByIds($list_id, $condition));
|
||||
|
||||
if($this->iEntityLanguageModel) {
|
||||
$item_list_language_info = $this->iEntityLanguageModel->getListByIds($list_id);
|
||||
|
||||
$final_list = [];
|
||||
foreach ($item_list as $item) {
|
||||
$item_language_info = isset($item_list_language_info[$item['id']]) ? $item_list_language_info[$item['id']] : ["not_translated" => true];
|
||||
$final_list[] = array_merge($item, $item_language_info);
|
||||
}
|
||||
|
||||
return $final_list;
|
||||
}
|
||||
|
||||
return $item_list;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function getList(array $condition) : array
|
||||
{
|
||||
$copy = $condition;
|
||||
|
||||
// public items must have status=1
|
||||
$copy['status'] = 1;
|
||||
|
||||
return parent::getList($copy);
|
||||
}
|
||||
|
||||
|
||||
public function getTotal(array $condition) : int
|
||||
{
|
||||
$copy = $condition;
|
||||
|
||||
// public items must have status=1
|
||||
$copy['status'] = 1;
|
||||
|
||||
return parent::getTotal($copy);
|
||||
}
|
||||
|
||||
|
||||
public function getInfo($id): ?array
|
||||
{
|
||||
if(!$id) return null;
|
||||
|
||||
return self::getCache("getInfo-".$id."-".$this->view_language, function () use ($id){
|
||||
|
||||
$info = $this->iEntityModel->getInfo($id);
|
||||
|
||||
if($this->iEntityLanguageModel && $info) {
|
||||
$item_language_info = $this->iEntityLanguageModel->getInfo($id);
|
||||
if($item_language_info) {
|
||||
return $this->formatItemInfo(array_merge($info, $item_language_info));
|
||||
}else{
|
||||
$info["not_translated"] = true;
|
||||
|
||||
return $this->formatItemInfo($info);
|
||||
}
|
||||
}
|
||||
|
||||
return ($info) ? $this->formatItemInfo($info) : null;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
270
inc/Hura8/System/Controller/bFileHandle.php
Normal file
270
inc/Hura8/System/Controller/bFileHandle.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
namespace Hura8\System\Controller;
|
||||
|
||||
use Hura8\Interfaces\AppResponse;
|
||||
use Hura8\Interfaces\FileHandleInfo;
|
||||
use Hura8\Interfaces\FileHandleResponse;
|
||||
use Hura8\System\FileSystem;
|
||||
use Hura8\System\HuraImage;
|
||||
use Hura8\System\IDGenerator;
|
||||
use League\MimeTypeDetection\FinfoMimeTypeDetector;
|
||||
|
||||
/**
|
||||
* @date 16-Jan-2024
|
||||
* @description based-class to handling files, this is used for :
|
||||
* - FileUpload class
|
||||
* - CopyFileFromUrl class
|
||||
* ...
|
||||
*
|
||||
*/
|
||||
|
||||
abstract class bFileHandle
|
||||
{
|
||||
|
||||
static $image_extensions = array(".jpg",".jpeg",".gif", ".png", ".webp", '.avif', ".ico");
|
||||
|
||||
protected $permit_file_extensions = array(
|
||||
".jpg",".jpeg",".gif", ".png", ".webp", '.avif',
|
||||
".doc", ".docx",".xls",".xlsx", ".ppt", ".pdf",
|
||||
".rar", ".zip",
|
||||
//".avi",".mov",".mpg", ".wmv", ".mpeg",
|
||||
//".mp3",".mp4", ".ogg", ".oga", ".wav", ".wma"
|
||||
);
|
||||
|
||||
protected $permit_mine_types = array(
|
||||
'image/jpeg', 'image/png','image/gif', 'image/webp', 'image/avif',
|
||||
|
||||
// zip files
|
||||
'application/zip', 'application/x-zip-compressed', 'multipart/x-zip', 'application/x-compressed',
|
||||
);
|
||||
|
||||
// full system's path where the file will be finally stored or it's currently there for processing
|
||||
// example: /var/www/html/domain.com/public_html/media/product/
|
||||
protected $target_dir = "";
|
||||
|
||||
// directory appears to the public
|
||||
// example: /media/product/
|
||||
protected $public_dir = "";
|
||||
|
||||
protected $tmp_dir = ROOT_DIR . "/var/tmp_upload/"; // system tmp dir to store uploaded file for security examination before moving to target_dir
|
||||
protected $tmp_folder = ""; // tmp folder per session in the $tmp_dir to hold user's files. This folder will be removed when operation is complete
|
||||
|
||||
protected $setup_success = false;
|
||||
|
||||
/**
|
||||
* @param string $target_dir will be created if not exist i.e. media/product/ or media/user_upload/date('d-m-Y')
|
||||
* @param ?array $permit_file_extensions null to allow all default file extensions
|
||||
*/
|
||||
public function __construct( string $target_dir, ?array $permit_file_extensions = null ) {
|
||||
|
||||
if(is_array($permit_file_extensions)) {
|
||||
$this->permit_file_extensions = $permit_file_extensions;
|
||||
}
|
||||
|
||||
if($target_dir) {
|
||||
$this->target_dir = PUBLIC_DIR . DIRECTORY_SEPARATOR. $target_dir;
|
||||
$this->public_dir = "/".$target_dir;
|
||||
}
|
||||
|
||||
$setup_res = $this->setUp();
|
||||
if($setup_res->getStatus() == AppResponse::SUCCESS) {
|
||||
$this->setup_success = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description (rename if set) and resize file
|
||||
* @param string $name
|
||||
* @param string $new_name
|
||||
* @param array $resized_sizes [] array('small' => ['width' => 100, 'height' => 100], 'large' => ['width' => 200, 'height' => 200] )
|
||||
* @return array|false
|
||||
*/
|
||||
public function resizeFile(string $name, string $new_name = '', array $resized_sizes = []) {
|
||||
|
||||
if($new_name) {
|
||||
$renamed = $this->renameFile($name, $new_name);
|
||||
if(!$renamed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//$file_name = $renamed['file_name'];
|
||||
//$public_path = $renamed['public_path'];
|
||||
$local_path = $renamed['local_path'];
|
||||
}else{
|
||||
//$file_name = $name;
|
||||
//$public_path = $this->public_dir . "/".$name;
|
||||
$local_path = $this->target_dir . "/" . $name;
|
||||
}
|
||||
|
||||
$objHuraImage = new HuraImage();
|
||||
|
||||
list(, $expected_files, ) = $objHuraImage->resize($local_path, $resized_sizes);
|
||||
|
||||
return $expected_files;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description we can rename the uploaded file to new file name, for example: we want product's image have the format [PRODUCT_ID]-name
|
||||
* @param string $name
|
||||
* @param string $new_name
|
||||
* @return array | false
|
||||
*/
|
||||
public function renameFile(string $name, string $new_name) {
|
||||
if(@rename($this->target_dir . "/" . $name, $this->target_dir . "/" . $new_name)){
|
||||
return [
|
||||
"file_name" => $new_name,
|
||||
"public_path" => $this->public_dir . "/".$new_name,
|
||||
"local_path" => $this->target_dir . "/" . $new_name,
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// public utility
|
||||
public static function getFileExtension($file_name) {
|
||||
return strtolower(strrchr($file_name,"."));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description run clean up after finish using the FileHandle instance
|
||||
*/
|
||||
public function cleanUp() {
|
||||
if($this->tmp_folder) {
|
||||
FileSystem::removeDir($this->tmp_folder);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFile(
|
||||
$original_file_name,
|
||||
$original_file_tmp_name, // temporary uploaded file as in case of $_FILES[$input_file_name]["tmp_name"]
|
||||
$fixed_file_name="",
|
||||
$max_file_size=0
|
||||
) : FileHandleResponse {
|
||||
|
||||
if(!$original_file_name) {
|
||||
return new FileHandleResponse(AppResponse::ERROR, 'no file', null);
|
||||
}
|
||||
|
||||
$file_size = filesize($original_file_tmp_name);
|
||||
if($max_file_size > 0 && $max_file_size < $file_size) {
|
||||
return new FileHandleResponse(AppResponse::ERROR, 'Size is too large: '.round($file_size/1000).'KB', null);
|
||||
}
|
||||
|
||||
//validate extension
|
||||
$file_ext = self::getFileExtension($original_file_name);
|
||||
|
||||
if(!in_array($file_ext, $this->permit_file_extensions)) {
|
||||
return new FileHandleResponse(AppResponse::ERROR, "Type ".$file_ext." is not allowed!", null);
|
||||
}
|
||||
|
||||
$file_name = substr($original_file_name, 0, strrpos($original_file_name,"."));
|
||||
$file_name = preg_replace("/[^a-z0-9_-]/i","", $file_name);
|
||||
$file_name = substr($file_name, 0, 50); // max- length
|
||||
$clean_file_name = ($fixed_file_name) ?: $file_name . $file_ext;
|
||||
|
||||
$tmp_file_path = $this->tmp_folder . "/". $clean_file_name;
|
||||
$return_data = null;
|
||||
|
||||
//debug_var([$original_data, $tmp_file_path]);
|
||||
if(@rename($original_file_tmp_name, $tmp_file_path)){
|
||||
|
||||
$is_file_image = (in_array($file_ext, static::$image_extensions ));
|
||||
|
||||
if($is_file_image) {
|
||||
list($width, $height) = getimagesize($tmp_file_path);
|
||||
}else{
|
||||
$width = 0;
|
||||
$height = 0;
|
||||
}
|
||||
|
||||
$detector = new FinfoMimeTypeDetector();
|
||||
$mimeType = $detector->detectMimeTypeFromPath($tmp_file_path);
|
||||
|
||||
// if image, we re-create and optimize it
|
||||
if($is_file_image) {
|
||||
if(in_array($mimeType, $this->permit_mine_types)) {
|
||||
$objHuraImage = new HuraImage();
|
||||
if($objHuraImage->create($tmp_file_path, $this->target_dir . DIRECTORY_SEPARATOR . $clean_file_name)){
|
||||
$return_data = new FileHandleInfo([
|
||||
"file_name" => $clean_file_name,
|
||||
"public_path" => $this->public_dir . "/".$clean_file_name,
|
||||
"local_path" => $this->target_dir . "/" . $clean_file_name,
|
||||
"mime_type" => $mimeType,
|
||||
"file_size" => $file_size,
|
||||
"file_ext" => $file_ext,
|
||||
"width" => $width,
|
||||
"height" => $height,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}elseif(@rename ($tmp_file_path, $this->target_dir . DIRECTORY_SEPARATOR . $clean_file_name )) {
|
||||
$return_data = new FileHandleInfo([
|
||||
"file_name" => $clean_file_name,
|
||||
"public_path" => $this->public_dir . "/".$clean_file_name,
|
||||
"local_path" => $this->target_dir . "/" . $clean_file_name,
|
||||
"mime_type" => $mimeType,
|
||||
"file_size" => $file_size,
|
||||
"file_ext" => $file_ext,
|
||||
"width" => 0,
|
||||
"height" => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// delete tmp file on server
|
||||
if(file_exists($original_file_tmp_name)) {
|
||||
@unlink($original_file_tmp_name);
|
||||
}
|
||||
|
||||
if($return_data) {
|
||||
return new FileHandleResponse(AppResponse::SUCCESS, 'Success', $return_data);
|
||||
}
|
||||
|
||||
return new FileHandleResponse(AppResponse::ERROR, 'Unknown', null);
|
||||
}
|
||||
|
||||
|
||||
protected function setUp(): AppResponse
|
||||
{
|
||||
// check target dir
|
||||
if($this->target_dir && !is_dir($this->target_dir)) {
|
||||
@mkdir($this->target_dir, 0755, true);
|
||||
}
|
||||
|
||||
if(!file_exists($this->target_dir)) {
|
||||
return new AppResponse(AppResponse::ERROR, $this->target_dir.' not exists');
|
||||
}
|
||||
|
||||
// create tmp_folder to upload file to
|
||||
$this->tmp_folder = $this->create_tmp_folder();
|
||||
if(!$this->tmp_folder) {
|
||||
return new AppResponse(AppResponse::ERROR, "Check ".$this->tmp_dir." and make sure it exists and writable");
|
||||
}
|
||||
|
||||
return new AppResponse(AppResponse::SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
protected function create_tmp_folder() : ?string {
|
||||
$tmp_folder = $this->tmp_dir . IDGenerator::createStringId(5);
|
||||
if(!@mkdir($tmp_folder, 0777, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// retest
|
||||
if(!$tmp_folder || !is_dir($tmp_folder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $tmp_folder;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user