From 72170f373ad4f9cd092651c3a6d16daeb45a1d2e Mon Sep 17 00:00:00 2001 From: hieutmd Date: Mon, 29 Jan 2024 10:39:53 +0700 Subject: [PATCH] c --- _shared.php | 13 +- data/product/home.php | 35 + .../AProductAttributeController.php | 165 +++ .../AProductCategoryController.php | 54 + .../AProductCollectionController.php | 33 + .../AdminController/AProductController.php | 73 ++ .../AProductFilterController.php | 130 +++ .../AProductFilterOldController.php | 948 +++++++++++++++ .../AdminController/AProductHotController.php | 40 + .../AProductSpecGroupAttributeController.php | 24 + .../AProductSpecGroupController.php | 74 ++ .../AProductVariantController.php | 85 ++ .../ProductFilterBuilderController.php | 320 +++++ .../Controller/ProductFilterController.php | 797 +++++++++++++ .../ProductFilterOptionsController.php | 104 ++ ...ductFilterOptionsTranslationController.php | 348 ++++++ .../Controller/bProductCategoryController.php | 62 + .../bProductCollectionController.php | 62 + .../Product/Controller/bProductController.php | 210 ++++ .../Model/ProductAttributeLanguageModel.php | 16 + .../Product/Model/ProductAttributeModel.php | 175 +++ .../Model/ProductAttributeValueModel.php | 83 ++ .../Model/ProductCategoryInfoModel.php | 30 + .../Model/ProductCategoryLanguageModel.php | 21 + .../Product/Model/ProductCategoryModel.php | 85 ++ .../Model/ProductCollectionLanguageModel.php | 22 + .../Product/Model/ProductCollectionModel.php | 85 ++ .../Product/Model/ProductFilterModel.php | 452 ++++++++ .../Product/Model/ProductHotModel.php | 95 ++ .../Product/Model/ProductImageModel.php | 78 ++ .../Product/Model/ProductInfoModel.php | 35 + .../Product/Model/ProductLanguageModel.php | 21 + .../Components/Product/Model/ProductModel.php | 410 +++++++ .../Product/Model/ProductSearchModel.php | 33 + .../Model/ProductSpecGroupAttributeModel.php | 65 ++ .../Product/Model/ProductSpecGroupModel.php | 151 +++ .../Product/Model/ProductVariantModel.php | 181 +++ inc/Hura8/Database/ConnectDB.php | 503 ++++++++ inc/Hura8/Database/MysqlValue.php | 16 + inc/Hura8/Database/iConnectDB.php | 55 + inc/Hura8/Interfaces/APIResponse.php | 42 + inc/Hura8/Interfaces/AppResponse.php | 41 + inc/Hura8/Interfaces/EntityType.php | 151 +++ inc/Hura8/Interfaces/FileHandleInfo.php | 43 + inc/Hura8/Interfaces/FileHandleResponse.php | 19 + inc/Hura8/Interfaces/PermissionRole.php | 11 + inc/Hura8/Interfaces/PermissionType.php | 13 + inc/Hura8/Interfaces/TableName.php | 97 ++ inc/Hura8/Interfaces/iClientERP.php | 37 + inc/Hura8/Interfaces/iCustomUrlBuilder.php | 10 + inc/Hura8/Interfaces/iERPProvider.php | 13 + inc/Hura8/Interfaces/iEmail.php | 20 + .../iEntityAdminCategoryController.php | 9 + .../Interfaces/iEntityAdminController.php | 11 + .../Interfaces/iEntityCategoryController.php | 9 + inc/Hura8/Interfaces/iEntityCategoryModel.php | 8 + inc/Hura8/Interfaces/iEntityController.php | 26 + inc/Hura8/Interfaces/iEntityLanguageModel.php | 38 + inc/Hura8/Interfaces/iEntityModel.php | 14 + inc/Hura8/Interfaces/iEntityPermission.php | 12 + inc/Hura8/Interfaces/iEntityStatistic.php | 22 + inc/Hura8/Interfaces/iExcelDownload.php | 20 + inc/Hura8/Interfaces/iPayGate.php | 38 + inc/Hura8/Interfaces/iPricing.php | 22 + .../Interfaces/iProductPromotionProgram.php | 26 + .../Interfaces/iPublicEntityController.php | 12 + inc/Hura8/Interfaces/iSMS.php | 16 + inc/Hura8/Interfaces/iSearch.php | 54 + inc/Hura8/Interfaces/iShippingCost.php | 15 + inc/Hura8/Interfaces/iShippingProvider.php | 13 + inc/Hura8/Interfaces/iSyncWebErpProduct.php | 11 + inc/Hura8/System/APIClient.php | 152 +++ inc/Hura8/System/CDNFileUpload.php | 152 +++ inc/Hura8/System/CDNFileUploadHandle.php | 275 +++++ inc/Hura8/System/Cache.php | 100 ++ inc/Hura8/System/Config.php | 208 ++++ inc/Hura8/System/Constant.php | 106 ++ .../System/Controller/DomainController.php | 124 ++ .../Controller/EntityPermissionController.php | 43 + .../System/Controller/RelationController.php | 82 ++ .../System/Controller/SettingController.php | 144 +++ .../Controller/UrlManagerController.php | 242 ++++ .../Controller/ViewHistoryController.php | 44 + .../System/Controller/WebUserController.php | 27 + .../Controller/aAdminEntityBaseController.php | 78 ++ .../Controller/aCategoryBaseController.php | 304 +++++ .../System/Controller/aERPController.php | 197 ++++ .../Controller/aEntityBaseController.php | 262 +++++ .../Controller/aExcelDownloadController.php | 344 ++++++ .../Controller/aExcelUploadController.php | 355 ++++++ .../aPublicEntityBaseController.php | 81 ++ inc/Hura8/System/Controller/bFileHandle.php | 270 +++++ inc/Hura8/System/CopyFileFromUrl.php | 56 + inc/Hura8/System/CronProcess.php | 66 ++ inc/Hura8/System/Email.php | 251 ++++ inc/Hura8/System/Export.php | 47 + inc/Hura8/System/ExportExcelUseTemplate.php | 158 +++ inc/Hura8/System/FileSystem.php | 212 ++++ inc/Hura8/System/FileUpload.php | 110 ++ inc/Hura8/System/Firewall.php | 190 +++ inc/Hura8/System/HtmlParser.php | 245 ++++ inc/Hura8/System/HuraImage.php | 180 +++ inc/Hura8/System/IDGenerator.php | 56 + inc/Hura8/System/IPFilter.php | 129 +++ inc/Hura8/System/Language.php | 94 ++ inc/Hura8/System/Model/AuthModel.php | 297 +++++ inc/Hura8/System/Model/DomainModel.php | 153 +++ .../System/Model/EntityLanguageModel.php | 456 ++++++++ inc/Hura8/System/Model/RelationModel.php | 341 ++++++ inc/Hura8/System/Model/SettingModel.php | 173 +++ inc/Hura8/System/Model/UrlModel.php | 216 ++++ inc/Hura8/System/Model/UtilityModel.php | 39 + inc/Hura8/System/Model/WebUserModel.php | 92 ++ inc/Hura8/System/Model/aCategoryBaseModel.php | 292 +++++ inc/Hura8/System/Model/aEntityBaseModel.php | 428 +++++++ inc/Hura8/System/Model/aSearchBaseModel.php | 530 +++++++++ inc/Hura8/System/ModuleManager.php | 39 + inc/Hura8/System/Paging.php | 437 +++++++ inc/Hura8/System/Permission.php | 39 + inc/Hura8/System/Plugin.php | 84 ++ inc/Hura8/System/RainTPL.php | 1023 ++++++++++++++++ inc/Hura8/System/RainTPLChecker.php | 1027 +++++++++++++++++ inc/Hura8/System/ReadExcel.php | 98 ++ inc/Hura8/System/Registry.php | 152 +++ inc/Hura8/System/Router.php | 94 ++ inc/Hura8/System/Security/Cookie.php | 606 ++++++++++ inc/Hura8/System/Security/DataClean.php | 126 ++ inc/Hura8/System/Security/DataFormatter.php | 22 + inc/Hura8/System/Security/DataType.php | 17 + inc/Hura8/System/Security/DataValidator.php | 78 ++ inc/Hura8/System/Security/Encryption.php | 38 + inc/Hura8/System/Security/Hash.php | 18 + inc/Hura8/System/Security/Session.php | 224 ++++ inc/Hura8/System/TimeManager.php | 153 +++ inc/Hura8/System/UploadZip.php | 152 +++ inc/Hura8/System/Url.php | 123 ++ inc/Hura8/System/Youtube.php | 86 ++ inc/Hura8/System/_readme.txt.txt | 3 + inc/Hura8/Traits/ClassCacheTrait.php | 33 + inc/config/client/product.hottype.php | 24 + inc/config/db.php | 11 + inc/fun.db.php | 16 + template/product/home.html | 5 +- 143 files changed, 20188 insertions(+), 3 deletions(-) create mode 100644 inc/Hura8/Components/Product/AdminController/AProductAttributeController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductCategoryController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductCollectionController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductFilterController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductHotController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php create mode 100644 inc/Hura8/Components/Product/AdminController/AProductVariantController.php create mode 100644 inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php create mode 100644 inc/Hura8/Components/Product/Controller/ProductFilterController.php create mode 100644 inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php create mode 100644 inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php create mode 100644 inc/Hura8/Components/Product/Controller/bProductCategoryController.php create mode 100644 inc/Hura8/Components/Product/Controller/bProductCollectionController.php create mode 100644 inc/Hura8/Components/Product/Controller/bProductController.php create mode 100644 inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductAttributeModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductCategoryModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductCollectionModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductFilterModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductHotModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductImageModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductInfoModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductLanguageModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductSearchModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php create mode 100644 inc/Hura8/Components/Product/Model/ProductVariantModel.php create mode 100644 inc/Hura8/Database/ConnectDB.php create mode 100644 inc/Hura8/Database/MysqlValue.php create mode 100644 inc/Hura8/Database/iConnectDB.php create mode 100644 inc/Hura8/Interfaces/APIResponse.php create mode 100644 inc/Hura8/Interfaces/AppResponse.php create mode 100644 inc/Hura8/Interfaces/EntityType.php create mode 100644 inc/Hura8/Interfaces/FileHandleInfo.php create mode 100644 inc/Hura8/Interfaces/FileHandleResponse.php create mode 100644 inc/Hura8/Interfaces/PermissionRole.php create mode 100644 inc/Hura8/Interfaces/PermissionType.php create mode 100644 inc/Hura8/Interfaces/TableName.php create mode 100644 inc/Hura8/Interfaces/iClientERP.php create mode 100644 inc/Hura8/Interfaces/iCustomUrlBuilder.php create mode 100644 inc/Hura8/Interfaces/iERPProvider.php create mode 100644 inc/Hura8/Interfaces/iEmail.php create mode 100644 inc/Hura8/Interfaces/iEntityAdminCategoryController.php create mode 100644 inc/Hura8/Interfaces/iEntityAdminController.php create mode 100644 inc/Hura8/Interfaces/iEntityCategoryController.php create mode 100644 inc/Hura8/Interfaces/iEntityCategoryModel.php create mode 100644 inc/Hura8/Interfaces/iEntityController.php create mode 100644 inc/Hura8/Interfaces/iEntityLanguageModel.php create mode 100644 inc/Hura8/Interfaces/iEntityModel.php create mode 100644 inc/Hura8/Interfaces/iEntityPermission.php create mode 100644 inc/Hura8/Interfaces/iEntityStatistic.php create mode 100644 inc/Hura8/Interfaces/iExcelDownload.php create mode 100644 inc/Hura8/Interfaces/iPayGate.php create mode 100644 inc/Hura8/Interfaces/iPricing.php create mode 100644 inc/Hura8/Interfaces/iProductPromotionProgram.php create mode 100644 inc/Hura8/Interfaces/iPublicEntityController.php create mode 100644 inc/Hura8/Interfaces/iSMS.php create mode 100644 inc/Hura8/Interfaces/iSearch.php create mode 100644 inc/Hura8/Interfaces/iShippingCost.php create mode 100644 inc/Hura8/Interfaces/iShippingProvider.php create mode 100644 inc/Hura8/Interfaces/iSyncWebErpProduct.php create mode 100644 inc/Hura8/System/APIClient.php create mode 100644 inc/Hura8/System/CDNFileUpload.php create mode 100644 inc/Hura8/System/CDNFileUploadHandle.php create mode 100644 inc/Hura8/System/Cache.php create mode 100644 inc/Hura8/System/Config.php create mode 100644 inc/Hura8/System/Constant.php create mode 100644 inc/Hura8/System/Controller/DomainController.php create mode 100644 inc/Hura8/System/Controller/EntityPermissionController.php create mode 100644 inc/Hura8/System/Controller/RelationController.php create mode 100644 inc/Hura8/System/Controller/SettingController.php create mode 100644 inc/Hura8/System/Controller/UrlManagerController.php create mode 100644 inc/Hura8/System/Controller/ViewHistoryController.php create mode 100644 inc/Hura8/System/Controller/WebUserController.php create mode 100644 inc/Hura8/System/Controller/aAdminEntityBaseController.php create mode 100644 inc/Hura8/System/Controller/aCategoryBaseController.php create mode 100644 inc/Hura8/System/Controller/aERPController.php create mode 100644 inc/Hura8/System/Controller/aEntityBaseController.php create mode 100644 inc/Hura8/System/Controller/aExcelDownloadController.php create mode 100644 inc/Hura8/System/Controller/aExcelUploadController.php create mode 100644 inc/Hura8/System/Controller/aPublicEntityBaseController.php create mode 100644 inc/Hura8/System/Controller/bFileHandle.php create mode 100644 inc/Hura8/System/CopyFileFromUrl.php create mode 100644 inc/Hura8/System/CronProcess.php create mode 100644 inc/Hura8/System/Email.php create mode 100644 inc/Hura8/System/Export.php create mode 100644 inc/Hura8/System/ExportExcelUseTemplate.php create mode 100644 inc/Hura8/System/FileSystem.php create mode 100644 inc/Hura8/System/FileUpload.php create mode 100644 inc/Hura8/System/Firewall.php create mode 100644 inc/Hura8/System/HtmlParser.php create mode 100644 inc/Hura8/System/HuraImage.php create mode 100644 inc/Hura8/System/IDGenerator.php create mode 100644 inc/Hura8/System/IPFilter.php create mode 100644 inc/Hura8/System/Language.php create mode 100644 inc/Hura8/System/Model/AuthModel.php create mode 100644 inc/Hura8/System/Model/DomainModel.php create mode 100644 inc/Hura8/System/Model/EntityLanguageModel.php create mode 100644 inc/Hura8/System/Model/RelationModel.php create mode 100644 inc/Hura8/System/Model/SettingModel.php create mode 100644 inc/Hura8/System/Model/UrlModel.php create mode 100644 inc/Hura8/System/Model/UtilityModel.php create mode 100644 inc/Hura8/System/Model/WebUserModel.php create mode 100644 inc/Hura8/System/Model/aCategoryBaseModel.php create mode 100644 inc/Hura8/System/Model/aEntityBaseModel.php create mode 100644 inc/Hura8/System/Model/aSearchBaseModel.php create mode 100644 inc/Hura8/System/ModuleManager.php create mode 100644 inc/Hura8/System/Paging.php create mode 100644 inc/Hura8/System/Permission.php create mode 100644 inc/Hura8/System/Plugin.php create mode 100644 inc/Hura8/System/RainTPL.php create mode 100644 inc/Hura8/System/RainTPLChecker.php create mode 100644 inc/Hura8/System/ReadExcel.php create mode 100644 inc/Hura8/System/Registry.php create mode 100644 inc/Hura8/System/Router.php create mode 100644 inc/Hura8/System/Security/Cookie.php create mode 100644 inc/Hura8/System/Security/DataClean.php create mode 100644 inc/Hura8/System/Security/DataFormatter.php create mode 100644 inc/Hura8/System/Security/DataType.php create mode 100644 inc/Hura8/System/Security/DataValidator.php create mode 100644 inc/Hura8/System/Security/Encryption.php create mode 100644 inc/Hura8/System/Security/Hash.php create mode 100644 inc/Hura8/System/Security/Session.php create mode 100644 inc/Hura8/System/TimeManager.php create mode 100644 inc/Hura8/System/UploadZip.php create mode 100644 inc/Hura8/System/Url.php create mode 100644 inc/Hura8/System/Youtube.php create mode 100644 inc/Hura8/System/_readme.txt.txt create mode 100644 inc/Hura8/Traits/ClassCacheTrait.php create mode 100644 inc/config/client/product.hottype.php create mode 100644 inc/config/db.php create mode 100644 inc/fun.db.php diff --git a/_shared.php b/_shared.php index 0cf8078..6f727f1 100644 --- a/_shared.php +++ b/_shared.php @@ -1,8 +1,17 @@ explode("-", getRequest("category", '')), + "brand" => explode("-", getRequest("brand", '')), + "hotType" => explode("-", getRequest("hotType", '')), + "other_filter" => [getRequest("other_filter", '')], + "q" => getRequest("q", ''), + 'numPerPage' => $numPerPage, + 'page' => getPageId(), + 'translated' => getRequestInt('translated', 0), + //... more extended filters +]; +//debug_var($objAProductController->getFilterConditions()); + +$totalResults = $objAProductController->getTotal($conditions); +$item_list = $objAProductController->getList($conditions); + +list($page_collection, $tb_page, $total_pages) = Paging::paging_template($totalResults, $numPerPage); + +return [ + "total" => $totalResults, + "item_list" => $item_list, + "pagination" => [ + 'collection' => $page_collection, + 'html' => $tb_page, + 'total_pages' => $total_pages, + ], +]; diff --git a/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php b/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php new file mode 100644 index 0000000..c4eb804 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductAttributeController.php @@ -0,0 +1,165 @@ +objProductAttributeModel = new ProductAttributeModel(); + + if(!$this->isDefaultLanguage()) { + $this->objProductAttributeLanguageModel = new ProductAttributeLanguageModel($this->view_language); + //$this->objProductAttributeLanguageModel->createTableLang(); + + parent::__construct($this->objProductAttributeModel, $this->objProductAttributeLanguageModel); + + }else{ + parent::__construct($this->objProductAttributeModel); + } + } + + + public function getProductAttributes($product_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + $result = []; + foreach ($objProductAttributeValueModel->getProductAttributes($product_id) as $info) { + $result[$info['attr_id']][] = $info['attr_value_id']; + } + + return $result; + } + + public function updateProductAttributes($product_id, array $current_value, array $new_value) { + /* + $current_value => Array + ( + // attribute_id => [value1, value2] + [2] => Array + ( + [0] => 2 + [1] => 3 + ) + + [1] => Array + ( + [0] => 1 + ) + + ) + + $new_value => Array + ( + // attribute_id => new_str_values by lines + [2] => asdas + [1] => asda + ) + * */ + + // list of values for product + $product_value_ids = []; + + // use current values + foreach ($current_value as $_attr_id => $_value_ids) { + $product_value_ids = array_merge($product_value_ids, $_value_ids); + } + + // create new values for attributes + foreach ($new_value as $_attr_id => $_value_text) { + $new_value_texts = array_filter(explode("\n", $_value_text)); + foreach ($new_value_texts as $new_value) { + // if exist + $check_exist = $this->getValueByTitle($_attr_id, $new_value); + if($check_exist) { + $product_value_ids[] = $check_exist['id']; + }else{ + $try_create_id = $this->addValue($_attr_id, ['title' => $new_value]); + if($try_create_id) $product_value_ids[] = $try_create_id; + } + } + } + + + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + return $objProductAttributeValueModel->updateProductAttributes($product_id, $product_value_ids); + + } + + + public function updateAttributeInSpecGroup($group_id, $attr_id, array $info) { + $this->objProductAttributeModel->updateAttributeInSpecGroup($group_id, $attr_id, $info); + } + + public function removeAttributeFromSpecGroup($group_id, $attr_id) { + $this->objProductAttributeModel->removeAttributeFromSpecGroup($group_id, $attr_id); + } + + public function addAttributeToSpecGroup($group_id, $attr_id, $ordering) { + $this->objProductAttributeModel->addAttributeToSpecGroup($group_id, $attr_id, $ordering); + } + + public function updateAttributeInCategory($cat_id, $attr_id, array $info) { + $this->objProductAttributeModel->updateAttributeInCategory($cat_id, $attr_id, $info); + } + + public function removeAttributeFromCategory($cat_id, $attr_id) { + $this->objProductAttributeModel->removeAttributeFromCategory($cat_id, $attr_id); + } + + public function addAttributeToCategory($cat_id, $attr_id, $ordering) { + $this->objProductAttributeModel->addAttributeToCategory($cat_id, $attr_id, $ordering); + } + + public function getAllAtributes() { + return $this->objProductAttributeModel->getList(["numPerPage" => 2000]); + } + + public function deleteValue($attr_id, $value_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->delete($value_id); + } + + public function updateValue($attr_id, $value_id, array $info) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->updateFields($value_id, $info); + } + + public function getValueByTitle($attr_id, $value_title) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + $filter_code = DataClean::makeInputSafe($value_title, DataType::ID); + return $objProductAttributeValueModel->getInfoByCode($filter_code); + } + + public function addValue($attr_id, array $info) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->create($info); + } + + public function getListAttributeValues($attr_id) { + $objProductAttributeValueModel = new ProductAttributeValueModel($attr_id); + return $objProductAttributeValueModel->getList(['numPerPage' => 200]); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php b/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php new file mode 100644 index 0000000..1d56fe9 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php @@ -0,0 +1,54 @@ +objProductCategoryModel->updateItemCount($id); + } + + + public function getAttributeList($catId) { + return $this->objProductCategoryModel->getAttributeList($catId); + } + + + public function create(array $info) : AppResponse + { + $res = parent::create($info); + + if($res->getStatus() == 'ok') { + $objProductCategoryInfoModel = new ProductCategoryInfoModel(); + $objProductCategoryInfoModel->createInfo($res->getData(), $info); + } + + return $res; + } + + + public function update($id, array $info) : AppResponse + { + if(!$this->isDefaultLanguage()) { + return parent::update($id, $info); + } + + // update info + $objProductCategoryInfoModel = new ProductCategoryInfoModel(); + $objProductCategoryInfoModel->updateInfo($id, $info); + + return parent::update($id, $info); + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php b/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php new file mode 100644 index 0000000..6e727b5 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductCollectionController.php @@ -0,0 +1,33 @@ +objProductCollectionModel->updateProduct($product_id, $collection_id, $info); + } + + + public function removeProduct($product_id, $collection_id) + { + return $this->objProductCollectionModel->removeProduct($product_id, $collection_id); + } + + + public function addProduct($product_id, $collection_id, $ordering=0) + { + return $this->objProductCollectionModel->addProduct($product_id, $collection_id, $ordering); + } + + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductController.php b/inc/Hura8/Components/Product/AdminController/AProductController.php new file mode 100644 index 0000000..7de1ba8 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductController.php @@ -0,0 +1,73 @@ + array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + } + + //get product image list + public function productImageList($proId){ + $objProductImageModel = new ProductImageModel($proId); + $result = array(); + foreach ( $objProductImageModel->getList(["numPerPage" => 100]) as $rs ) { + + $rs['image'] = static::getResizedImageCollection($rs['img_name']); + + $result[] = $rs; + } + + return $result; + } + + // get only from tb_product_info + public function getInfoMore($id) + { + $objProductInfoModel = new ProductInfoModel(); + $info = $objProductInfoModel->getInfo($id); + + if(!$this->isDefaultLanguage() && $info) { + $language_info = $this->iEntityLanguageModel->getInfo($id); + $final_info = []; + foreach ($info as $_k => $_v) { + $final_info[$_k] = $language_info[$_k] ?? $_v; + } + + return $final_info; + } + + return $info; + } + + public function getInfoMoreEmpty($addition_field_value = []) + { + $objProductInfoModel = new ProductInfoModel(); + return $objProductInfoModel->getEmptyInfo($addition_field_value); + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductFilterController.php b/inc/Hura8/Components/Product/AdminController/AProductFilterController.php new file mode 100644 index 0000000..5bb29af --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductFilterController.php @@ -0,0 +1,130 @@ + '', + "name" => "Sắp xếp sản phẩm", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"")), + ), + array( + "selected" => ($order == 'ordering') ? "selected" : "", + "name" => "Thứ tự cửa hàng", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"ordering")), + ), + array( + "selected" => ($order == 'view') ? "selected" : "", + "name" => "Xem nhiều nhất", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"view")), + ), + array( + "selected" => ($order == 'new') ? "selected" : "", + "name" => "Mới nhất", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"new")), + ), + array( + "selected" => ($order == 'last-update') ? "selected" : "", + "name" => "Thời gian cập nhật", + "url" => Url::buildUrl(CURRENT_URL, array("order"=>"last-update")), + ), + ); + } + + + public static function getFilterOptions($other_filter) { + + return array( + array( + "selected" => ($other_filter == 'no-price') ? "selected" : "", + "name" => "Giá bán = 0", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-price")), + ), + array( + "selected" => ($other_filter == 'in-stock') ? "selected" : "", + "name" => "Còn hàng", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"in-stock")), + ), + array( + "selected" => ($other_filter == 'out-stock') ? "selected" : "", + "name" => "Hết hàng", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"out-stock")), + ), + array( + "selected" => ($other_filter == 'has-market-price') ? "selected" : "", + "name" => "Có giá thị trường", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-market-price")), + ), + + array( + "selected" => ($other_filter == 'has-promotion') ? "selected" : "", + "name" => "Có khuyến mại", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-promotion")), + ), + + array( + "selected" => ($other_filter == 'has-config') ? "selected" : "", + "name" => "Có cấu hình", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"has-config")), + ), + + array( + "selected" => ($other_filter == 'no-sku') ? "selected" : "", + "name" => "Chưa có mã kho", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-sku")), + ), + array( + "selected" => ($other_filter == 'no-image') ? "selected" : "", + "name" => "Chưa có ảnh", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-image")), + ), + array( + "selected" => ($other_filter == 'display-off') ? "selected" : "", + "name" => "Chưa hiển thị", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"display-off")), + ), + array( + "selected" => ($other_filter == 'display-on') ? "selected" : "", + "name" => "Đang hiển thị", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"display-on")), + ), + array( + "selected" => ($other_filter == 'no-category') ? "selected" : "", + "name" => "Chưa có danh mục", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-category")), + ), + array( + "selected" => ($other_filter == 'no-brand') ? "selected" : "", + "name" => "Chưa có thương hiệu", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-brand")), + ), + + array( + "selected" => ($other_filter == 'no-warranty') ? "selected" : "", + "name" => "Chưa có bảo hành", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-warranty")), + ), + array( + "selected" => ($other_filter == 'no-description') ? "selected" : "", + "name" => "Chưa có mô tả", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-description")), + ), + array( + "selected" => ($other_filter == 'no-spec-text') ? "selected" : "", + "name" => "Chưa có thông số nhập text", + "url" => Url::buildUrl(CURRENT_URL, array("other_filter"=>"no-spec-text")), + ), + ); + + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php b/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php new file mode 100644 index 0000000..6a4c63c --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductFilterOldController.php @@ -0,0 +1,948 @@ + false, + "brand" => array(), + "collection" => array(), + "supplier" => array(), + "rating" => array(), + "category" => array(), + "status" => "", + "query" => "", + "hotType" => "",//saleoff | not | new + "attribute" => [],//,3793,3794, + "ids" => array(), + 'excluded_ids' => array(), + "promotion" => "", + "storeId" => '', + "other_filter" => array() //in-stock, has-promotion etc... + ]; + + + public function __construct() { + + } + + + public function getDefaultFilter() { + return [ + "price" => self::getPriceFilterRange(), + "brand" => $this->getBrandFilter(), + "collection" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('collection'))), + "supplier" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('supplier'))), + "rating" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('rating'))), + "category" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('category'))), + //"status" => "1", + "query" => getRequest('q', ''), + "hotType" => getRequest('hotType'),//saleoff | not | new + "attribute" => $this->getAttributeFilter([]),//,3793,3794, + "promotion" => getRequest('promo', ''), + "ids" => array_filter(explode(',', str_replace(" ", "", getRequest('ids', '')))), + 'excluded_ids' => [], + "storeId" => getRequest('storeId', ''), + "other_filter" => array_filter(explode(",", getRequest('other_filter', ''))), //in-stock, has-promotion etc... + ]; + } + + + // build the user filters for product query + // we can modify the code to accept various types of url customization + public function getUserFilter(array $category_list_id) { + return [ + "price" => self::getPriceFilterRange(), + "brand" => $this->getBrandFilter(), + "collection" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('collection'))), + "supplier" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('supplier'))), + "rating" => array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('rating'))), + "category" => $category_list_id, + //"status" => "1", + "query" => getRequest('q', ''), + "hotType" => getRequest('hotType'),//saleoff | not | new + "attribute" => $this->getAttributeFilter( $category_list_id),//,3793,3794, + "promotion" => getRequest('promo', ''), + "ids" => array_filter(explode(',', str_replace(" ", "", getRequest('ids', '')))), + "excluded_ids" => array_filter(explode(',', str_replace(" ", "", getRequest('excluded_ids', '')))), + "storeId" => getRequest('storeId', ''), + "other_filter" => array_filter(explode(",", getRequest('other_filter', ''))), //in-stock, has-promotion etc... + ]; + } + + + // allow to update filters + public function setFilters(array $new_filters) { + foreach ($new_filters as $key => $value) { + if(isset($this->filters[$key])) { + $this->filters[$key] = $value; + } + } + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=15trieu-20-trieu + public static function getPriceFilterRange(){ + // default format + if(!defined('PRICE_FILTER_FORMAT') || PRICE_FILTER_FORMAT != 'p') { + return array( + "min" => getRequestInt("min", 0), + "max" => getRequestInt("max", 0), + ); + } + + // custom price range query + $price_range_format = getRequest("p"); // duoi-10trieu , 10ngan-2trieu, 3trieu-6trieu, tren-30trieu + if(strpos($price_range_format, '-') === false) { + return array( + "min" => 0, + "max" => 0, + ); + } + + $unit_translation = [ + 'ngan' => 1000, + 'trieu' => 1000000, + 'ty' => 1000000000, + ]; + + // duoi-10trieu, duoi-10ngan + if(strpos($price_range_format, 'duoi-') !== false) { + $unit_match = self::findPriceUnitMatch(str_replace("duoi-", "", $price_range_format)); + return array( + "min" => 0, + "max" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + ); + } + + // tren-10trieu, tren-10ngan + if(strpos($price_range_format, 'tren-') !== false) { + $unit_match = self::findPriceUnitMatch(str_replace("tren-", "", $price_range_format)); + return array( + "min" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + "max" => 0, + ); + } + + // 10ngan-2trieu, 3trieu-6trieu + $parts = explode('-', $price_range_format); + $min_part = $parts[0]; + $max_part = $parts[1]; + $min_match = self::findPriceUnitMatch($min_part); + $max_match = self::findPriceUnitMatch($max_part); + + return [ + "min" => $min_match['number'] * $unit_translation[$min_match['unit']], + "max" => $max_match['number'] * $unit_translation[$max_match['unit']], + ]; + } + + + //get a list of products that match filtering conditions + public function getProductList( + $start_url, + $sort_by = "new", + $limit = 20, + $page = 1 + ) { + //list of available filters + /* + * - price range + * - brand id or ids + * - collection id or ids + * - supplier id + * - rating: 1-> 5 + * - category id or ids + * - status: 0|1|null + * - detail_page_only: 0 | 1 + * - query: search keyword + * - hotType: saleoff | not | new | or combination of types + * - attribute values + * ... + * */ + + /*$filters = array( + "price" => array("min"=> 1, "max" => 100), + "brand" => array(12,3), + "collection" => array(2,3), + "supplier" => array(2,3), + "rating" => array(2,3), + "category" => array(1,2,), + "status" => "1", + "detail_page_only" => 0, + "query" => "", + "hotType" => "",//saleoff | not | new + "attribute" => "",//,3793,3794, + "ids" => array(12,3), + 'excluded_ids' => array(12,3), + "other_filter" => array(in-stock, has-promotion) + //... + );*/ + + $filterPath = []; + $filter_messages = []; + $where_query = []; + $paging_url = $start_url; + $filters = $this->cleanFilter(); + // debug_var($filters); + + //system controls + if(ENABLE_PRODUCT_EXPIRE) { + //$where_query[] = " AND (from_time =0 OR from_time < '".CURRENT_TIME."') AND (to_time > '".CURRENT_TIME."' OR to_time=0 ) "; + } + + //other filters + if(isset($filters["other_filter"]) && sizeof($filters["other_filter"])) { + foreach ($filters["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $filter_messages[] = [ + 'title' => 'Còn hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `quantity` > 0 "; + break; + case "has-vat"; + $filter_messages[] = [ + 'title' => 'Có thuế VAT', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND has_vat = 1 "; + break; + case "out-stock"; + $filter_messages[] = [ + 'title' => 'Hết hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND quantity = 0 "; + break; + case "has-market-price"; + $filter_messages[] = [ + 'title' => 'Có giá thị trường', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND market_price > 0 "; + break; + case "no-price"; + $filter_messages[] = [ + 'title' => 'Không có giá', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND price = 0 "; + break; + case "no-warranty"; + $filter_messages[] = [ + 'title' => 'Không có bảo hành', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`warranty`) < 2 "; + break; + case "no-sku"; + $filter_messages[] = [ + 'title' => 'Không mã kho hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`sku`) < 2 "; + break; + case "has-sku"; + $filter_messages[] = [ + 'title' => 'Có mã kho hàng', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`sku`) > 2 "; + break; + case "has-config"; + $filter_messages[] = [ + 'title' => 'Không có cấu hình', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $filter_messages[] = [ + 'title' => 'Không có ảnh', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + $where_query[] = " AND `image_count` = 0 "; + break; + case "no-category"; + $where_query[] = " AND `category_ids` = '' "; + break; + case "no-brand"; + $filter_messages[] = [ + 'title' => 'Không có thương hiệu', + 'reset' => Url::buildUrl(CURRENT_URL, ['brand' => '']), + ]; + + $where_query[] = " AND `brand_id` = 0 "; + break; + case "display-off"; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $filter_messages[] = [ + 'title' => 'Có khuyến mại', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query[] = " AND LENGTH(`special_offer`) > 5 "; + break; + + case "no-description"; + $filter_messages[] = [ + 'title' => 'Không có mô tả', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query["no-description"] = " AND `id` IN ( SELECT `id` FROM ".TableName::PRODUCT_INFO." WHERE LENGTH(`description`) < 5 ) "; + break; + + case "no-spec-text"; + $filter_messages[] = [ + 'title' => 'Không có thông số', + 'reset' => Url::buildUrl(CURRENT_URL, ['other_filter' => '']), + ]; + + $where_query["no-spec-text"] = " AND `id` IN ( SELECT `id` FROM ".TableName::PRODUCT_INFO." WHERE LENGTH(`spec`) < 5 ) "; + break; + + } + } + } + + + + //- brand id or ids or brand_indexes + if(isset($filters["brand"]) && sizeof($filters["brand"])) { + global $admin_panel; + + $brand_url_format = (defined('BRAND_FILTER_FORMAT')) ? BRAND_FILTER_FORMAT : ''; + if(isset($admin_panel) && $admin_panel) $brand_url_format = ''; // custom brand format cannot be used in admin panel + + $objBrandModel = new BrandModel(); + $condition = array(); + foreach ($filters["brand"] as $_id) { + if(!$_id) continue; + + $brand_info = $objBrandModel->getInfo($_id); + if(!$brand_info) continue; + + $filterPath["brand"][] = array( + "id" => $brand_info['id'], + "name" => $brand_info["title"], + ); + + $condition[] = " `brand_id` = '".intval($brand_info['id'])."' "; + $filter_messages[] = [ + 'title' => $brand_info['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['brand' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filters["brand"], $_id))]), + ]; + } + + if(sizeof($condition)) { + $paging_url = Url::buildUrl($paging_url, array("brand" => join(FILTER_VALUE_SEPARATOR, $filters['brand']))); + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + } + + //- collection id or ids + if(isset($filters["collection"]) && sizeof($filters["collection"])) { + $condition = array(); + foreach ($filters["collection"] as $_id) { + $filterPath["collection"][] = array( + "id" => $_id, + //"name" => $brand_info["name"], + ); + $condition[] = " `id` IN ( SELECT product_id FROM ".TB_CATEGORY_SPECIAL_PRODUCT." WHERE `special_cat_id`='".intval($_id)."' ) "; + $filter_messages[] = [ + 'title' => 'Bộ sưu tập ', + 'reset' => Url::buildUrl(CURRENT_URL, ['collection' => join(',', remove_item_from_array($filters["collection"], $_id))]), + ]; + } + + $paging_url = Url::buildUrl($paging_url, array("collection" => join(",", $filters['collection']))); + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- category id or ids + if(isset($filters["category"]) && sizeof($filters["category"])) { + + $objCategoryProductModel = new ProductCategoryModel(); + + $condition = array(); + + foreach ($filters["category"] as $cat_id) { + $cat_id = intval($cat_id); + + if(!$cat_id) continue; + + $cat_info = $objCategoryProductModel->getInfo($cat_id); + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ? $cat_info["child_ids"] : '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = '".$cat_id."' "; + } + + $filterPath["category"][] = array( + "id" => $cat_id, + "name" => $cat_info['title'], + ); + + $filter_messages[] = [ + 'title' => $cat_info['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['category' => join(',', remove_item_from_array($filters["category"], $cat_id))]), + ]; + } + + if(sizeof($condition)) { + $paging_url = Url::buildUrl($paging_url, array("category" => join(",", $filters['category']))); + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".TableName::PRODUCT_PER_CATEGORY." WHERE " . join(" OR ", $condition) . " )"; + } + } + + //- status: 0|1|null + if(isset($filters["status"]) && $filters['status']) { + $where_query[] = " AND `status` = 1 "; + /*$filter_messages[] = [ + 'title' => 'Trạng thái: '.($filters['status'] ? 'Đang hiển thị' : 'Đang ẩn'), + 'reset' => Url::buildUrl(CURRENT_URL, ['status' => '']), + ];*/ + } + + + //- query: search keyword + if(isset($filters["query"]) && $filters["query"]) { + $keyword_search = $filters["query"]; + $search_by_product_id = intval(preg_replace('/[^0-9]/i', '', $keyword_search)); + + $objProductSearchModel = new ProductSearchModel(); + + $match_result = $objProductSearchModel->find($keyword_search); + + $filterPath["search"] = array( + "id" => $filters["query"], + "name" => $filters["query"], + ); + $filter_messages[] = [ + 'title' => $filters["query"], + 'reset' => Url::buildUrl(CURRENT_URL, ['q' => '']), + ]; + + $paging_url = Url::buildUrl($paging_url, array("q" => $filters["query"])); + + if(sizeof($match_result) > 0) { + $where_query[] = " AND ( `id` IN (".join(",", $match_result ).") OR `id` = '".$search_by_product_id."' ) "; + }else{ + $where_query[] = " AND `id` = '".$search_by_product_id."' "; + } + + + } + + //- hotType: saleoff | not | new | or combination of types + if(isset($filters["hotType"]) && $filters["hotType"]) { + $hot_type = preg_replace("/[^a-z0-9_\-]/i","", $filters["hotType"]); + $config_hottype = AProductFilterController::getProductHotTypeList(); + if(isset($config_hottype[$hot_type])) { + $filterPath["hotType"] = array( + "id" => $filters["hotType"], + "name" => $filters["hotType"], + ); + $paging_url = Url::buildUrl($paging_url, array("hotType" => $hot_type)); + $where_query[] = " AND `id` IN (SELECT `pro_id` FROM ".TB_PRODUCT_HOT." WHERE hot_type = '".$hot_type."' ) "; + $filter_messages[] = [ + 'title' => $filters["hotType"], + 'reset' => Url::buildUrl(CURRENT_URL, ['hotType' => '']), + ]; + } + } + + //- attribute values + if(isset($filters["attribute"]) && sizeof($filters["attribute"])) { + $filter_attr_value_list = $filters["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + if(ENABLE_FILTER_BY_APIKEY) { + //filter = ,api_key_1,api_key_2,api_key_3, + foreach($this->translate_api_filter($filter_attr_value_list) as $attr_id){ + $query_attr_id[] = $attr_id; + $count_filter ++; + } + + } else { + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + } + + $objProductAttributeModel = new ProductAttributeModel(); + + $product_filter_id_match = $objProductAttributeModel->getProductMatchAttributeValue($query_attr_id); + + + $paging_url = Url::buildUrl($paging_url, array("filter" => join(FILTER_VALUE_SEPARATOR, $filter_attr_value_list))); + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + foreach($filter_attr_value_list as $value_id ){ + $att_name = 'att_name'; //$objCategoryProduct->atrValueName($value_id); + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl(CURRENT_URL, ['attribute' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filters["category"], $value_id))]), + ]; + } + } + + //given products' ids + if(isset($filters["ids"]) && is_array($filters["ids"]) && sizeof($filters["ids"])) { + $where_query[] = " AND `id` IN (". join(",", $filters["ids"]) .") "; + } + + //exclude products' ids + if(isset($filters["excluded_ids"]) && is_array($filters["excluded_ids"]) && sizeof($filters["excluded_ids"])) { + $where_query[] = " AND `id` NOT IN (". join(",", $filters["excluded_ids"]) .") "; + } + + $price_range_query_limit = join(" ",$where_query); + + //- price range + if(isset($filters["price"]) && is_array($filters["price"]) && sizeof($filters["price"]) && ($filters["price"]['min'] > 0 || $filters["price"]['max'] > 0)) { + //limit by price range + $maxPrice = clean_price($filters["price"]['max']); + $minPrice = clean_price($filters["price"]['min']); + $paging_url = Url::buildUrl($paging_url, array("max"=>$maxPrice, "min"=>$minPrice)); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + $where_query[] = " AND ". $price_range_query; + + $filterPath["price"] = array( + "min" => $minPrice, + "max" => $maxPrice, + ); + + $price_format = ProductFilterPrice::buildPriceRangeFormat($minPrice, $maxPrice); + + $filter_messages[] = [ + 'title' => $price_format['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['price' => '']), + ]; + } + + // use location price sorting + $ordering_clause = $this->getOrderingClause($sort_by, $db_condition, 0); + + $db_condition = join(" ", $where_query); + + $total_number = 0; + $list_ids = []; + + /* $query = $this->db->runQuery("SELECT COUNT(`id`) AS total FROM ".TB_PRODUCT_LIGHT." WHERE 1 ".$db_condition." "); + if($rs = $this->db->fetchAssoc($query)) { + $total_number = $rs['total']; + } + + $category_query = " AND idv_product_category.`pro_id` IN (SELECT `id` FROM ".TB_PRODUCT_LIGHT." WHERE 1 ".$db_condition.") "; + + if($location_sorting) { + + $list_ids = array_slice($location_product_ids, ($page-1) * $limit, $limit); + + } else { + + if(in_array($sort_by, ['view', 'name'])) { + $query = $this->db->runQuery(" + SELECT `id`, ".TB_PRODUCT.".proName + FROM ".TB_PRODUCT_LIGHT.", ".TB_PRODUCT." + WHERE `id` = ".TB_PRODUCT.".`id` + ".$db_condition." + ".$ordering_clause." + LIMIT ".($page-1) * $limit.", ".$limit." + "); + }else{ + $query = $this->db->runQuery(" + SELECT `id` FROM ".TB_PRODUCT_LIGHT." + WHERE 1 ".$db_condition." + ".$ordering_clause." + LIMIT ".($page-1) * $limit.", ".$limit." + "); + } + + $list_ids = array_map(function ($rs){ + return $rs['id']; + }, $this->db->fetchAll($query)); + + }*/ + + + return [ + "full_query" => $db_condition, + "price_query" => $price_range_query_limit, + //"category_query" => $category_query, + "filter" => $filterPath, + "filter_messages" => $filter_messages, + "total" => $total_number, + "list_ids" => $list_ids, + "url" => $paging_url, + ]; + + } + + + protected function cleanFilter() { + $clean_filter = []; + foreach ($this->filters as $key => $value) { + $clean_filter[$key] = $this->clearFilterValue($key, $value); + } + return $clean_filter; + } + + + protected function clearFilterValue($key, $value) { + if($key == 'price') { + if(!is_array($value)) return false; + $min = isset($value['min']) ? intval($value['min']) : 0; + $max = isset($value['max']) ? intval($value['max']) : 0; + + return ['min' => $min, 'max' => $max]; // array("min" => getRequestInt("min", 0), "max" => getRequestInt("max", 0)), + } + + if(in_array($key, ['collection', 'supplier', 'rating', 'category', 'ids', 'excluded_ids'])) { + return Security::makeListOfInputSafe($value, 'int'); // int1-int2 => array(int1, int2, ...) + } + + // accept a-z0-9- + if(in_array($key, ['attribute', 'brand'])) { + return Security::makeListOfInputSafe($value, 'string'); + } + + if($key == 'status') { + return (in_array($value, [0, 1])) ? $value : 0; + } + + if($key == 'query') { + return substr(trim(str_replace(['\'', '"'], '', strip_tags($value))), 0, 100); // string + } + + if($key == 'hotType') { + $config_hottype = AProductFilterController::getProductHotTypeList(); + return (isset($config_hottype[$value])) ? $value : ''; //saleoff | not | new + } + + if($key == 'promotion') { + return intval($value); + } + + if($key == 'storeId') { + return intval($value); + } + + if($key == 'detail_page_only') { + return intval($value); // 1|0|'' + } + + if($key == 'other_filter') { + return array_filter( + array_map(function ($item){ + return preg_replace('/[^a-z0-9_\.\-\,]/i', '', $item); + }, $value ) + ); // array() //in-stock, has-promotion + } + + return false; + } + + + protected function filterLocationProductByPrice(array $product_list, $max_price, $min_price) { + if($max_price > 0 && $min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price && $pro['price'] < $max_price); + }); + } + + if($max_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] < $max_price); + }); + } + + if($min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price); + }); + } + + return $product_list; + } + + protected function getProductIds($price_range_query_limit) { + /*$query = $this->db->query(" + SELECT `id` FROM ".TB_PRODUCT_LIGHT." + WHERE 1 ".$price_range_query_limit." + LIMIT 10000 + "); + + return array_map(function ($rs){ + return $rs['id']; + }, $this->db->fetchAll($query));*/ + } + + //10-04-2017 allow ?filter=api_key1,api_key2, ... and translate to id + protected function translate_api_filter($filter_api) { + /*$filter_attr_value_list = array_filter(explode(",", $filter_api)); + + $build_query = array(); + foreach($filter_attr_value_list as $api_key){ + $api_key = \preg_replace('/[^a-z0-9\_\-\.]/i', '', $api_key); + if($api_key) $build_query[] = " `api_key` = '".$this->db->escape($api_key)."' "; + } + + $result = array(); + if(sizeof($build_query)) { + $query = $this->db->runQuery("SELECT id FROM ".TB_ATTRIBUTE_VALUE." WHERE ". join(" OR ", $build_query)) ; + foreach ( $this->db->fetchAll($query) as $info ) { + $result[] = $info['id']; + } + } + + return $result;*/ + return ''; + } + + + // accept url format: + // default: ?brand=1 + // brand_index: ?brand=apple + protected function getBrandFilter() { + $brand_url_format = (defined('BRAND_FILTER_FORMAT')) ? BRAND_FILTER_FORMAT : ''; + + $module = (Registry::getVariable('global')) ? Registry::getVariable('global')['module'] : null; + + if(!$module) { + // possible admin panel + return array_filter(explode(FILTER_VALUE_SEPARATOR, getRequest('brand', ''))); + } + + $current_brand_queries = (isset($module['query']['brand'])) ? array_filter(explode(FILTER_VALUE_SEPARATOR, $module['query']['brand'])) : []; + + // fix for brand-detail page + // example: domain.com/brand/sony + if(isset($module['query']['brandName']) && $module['query']['brandName']) { + $objBrandModel = new BrandModel(); + $brand_info = $objBrandModel->getInfoByUrl($module['query']['brandName']); + + if($brand_url_format == 'brand_index') { + $current_brand_queries = array($brand_info['brand_index']); + }else{ + $current_brand_queries = array($brand_info['id']); + } + } + + if($brand_url_format == 'brand_index') { + //i.e ?brand=apple-canon + return $current_brand_queries; + } + + $selected_brand = $current_brand_queries; + if(isset($module['query']['brand_id']) && $module['query']['brand_id']) { + $selected_brand[] = intval($module['query']['brand_id']); + } + + return $selected_brand; + } + + // accept url format: + // default: ?filter=3793-3794, + // custom: ?ram=8gb&hdd=500GB + protected function getAttributeFilter(array $category_list_id){ + + $request_filter_list = getRequest('filter', ''); + + // custom1 + if( !$request_filter_list && defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT == 'custom1' ) { + // iUCategoryProduct $objCategoryProduct, array $category_list_id + return $this->getAttributeFilterPara($category_list_id); //join(FILTER_VALUE_SEPARATOR, $this->getAttributeFilterPara($category_list_id)); + } + + if(!$request_filter_list) { + return []; + } + + // default + $clean_list = Security::makeListOfInputSafe( + explode(FILTER_VALUE_SEPARATOR, str_replace([',', '-', '_'], FILTER_VALUE_SEPARATOR, $request_filter_list) ), + DataType::INTEGER + ) ; //preg_replace("/[^0-9,_\-]/", "", getRequest('filter', ''));//3793-3794, + + return array_filter($clean_list); + } + + + protected function getAttributeFilterPara( array $category_list_id){ + + $excluded_keys = ['p', 'min', 'max', 'brand', 'page', 'request_path']; + $attr_filter_params = []; + + $query_parameters = Url::parse(CURRENT_URL)['query']; + + + foreach ( $query_parameters as $key => $value) { + if(in_array($key, $excluded_keys) || !$value) continue; + + $attr_filter_params[$key] = $value; + } + + + if(sizeof($attr_filter_params)) { + /*$objCategoryProduct = new UCategoryProduct(); + $category_attributes = $objCategoryProduct->getAttributesForCategory($category_list_id); + + $match_attr_value_ids = []; + foreach ($attr_filter_params as $attr_filter_code => $attr_value_api_key) { + foreach ($category_attributes as $attribute) { + if($attribute['filter_code'] != $attr_filter_code) continue; + + foreach ($attribute['value_list'] as $_value_id => $_value_info) { + if($_value_info['info']['api_key'] == $attr_value_api_key) { + $match_attr_value_ids[] = $_value_id; + } + } + } + } + + return $match_attr_value_ids;*/ + return []; + } + + return []; + } + + + public static function findPriceUnitMatch($str){ + $match = []; + $pattern = "/^([\d]+)(ngan|trieu|ty)/i"; + if(preg_match($pattern, $str, $match)){ + return [ + "number" => $match[1], + "unit" => $match[2], + ]; + } + return [ + "number" => 0, + "unit" => 'trieu', + ]; + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=tu-15-20-trieu + protected static function getPriceRange(){ + // default + $min_price = getRequest("min"); + $max_price = getRequest("max"); + + // custom price range query + $price_range = getRequest("p"); // duoi-10-trieu , tu-15-20-trieu, tren-30-trieu + + // duoi-10-trieu + $match = []; + if(preg_match("/^duoi-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = ''; + $max_price = $match[1] * 1000000; + } + elseif (preg_match("/^tu-([0-9]+)-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = $match[1] * 1000000; + $max_price = $match[2] * 1000000; + } + elseif (preg_match("/^tren-([0-9]+)-trieu$/i", $price_range, $match)) { + $min_price = $match[1] * 1000000; + $max_price = ''; + } + + return [ + "min" => intval($min_price), + "max" => intval($max_price), + ]; + } + + + protected function getOrderingClause($sort_by, &$where_query, $current_viewed_category = 0) { + + // show by global order + $ordering = "ORDER BY ordering DESC, id DESC"; + + switch($sort_by) { + case "order-new"; + $ordering = "ORDER BY `ordering` DESC, `id` DESC"; + break; + case "order-last-update"; + $ordering = "ORDER BY `ordering` DESC, last_update DESC"; + break; + case "last-update"; + $ordering = "ORDER BY last_update DESC"; + break; + case "order"; + $ordering = "ORDER BY `ordering` DESC"; + break; + case "new"; + $ordering = "ORDER BY `id` DESC"; + break; + case "price-asc"; + $where_query .= " AND `price` > 100 "; + $ordering = "ORDER BY `price` ASC"; + break; + case "price-desc"; + $ordering = " ORDER BY `price` DESC "; + break; + case "view"; + $ordering = "ORDER BY `visit` desc"; + break; + } + + + return $ordering; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductHotController.php b/inc/Hura8/Components/Product/AdminController/AProductHotController.php new file mode 100644 index 0000000..7af96fc --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductHotController.php @@ -0,0 +1,40 @@ +objProductHotModel = new ProductHotModel(); + } + + + public function getProductHot(array $product_id_array) { + $hot_script = $this->objProductHotModel->getProductHot($product_id_array); + + //add empty for other products + foreach ($product_id_array as $pro_id) { + if(!isset($hot_script[$pro_id])) $hot_script[$pro_id] = []; + } + + return $hot_script; + } + + + public function updateProductHot($pro_id, array $new_types) { + return $this->objProductHotModel->updateProductHot($pro_id, $new_types); + } + + + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php new file mode 100644 index 0000000..db48431 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupAttributeController.php @@ -0,0 +1,24 @@ +objProductSpecGroupAttributeModel = new ProductSpecGroupAttributeModel(); + parent::__construct($this->objProductSpecGroupAttributeModel); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php new file mode 100644 index 0000000..4b27829 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductSpecGroupController.php @@ -0,0 +1,74 @@ +objProductSpecGroupModel = new ProductSpecGroupModel(); + parent::__construct($this->objProductSpecGroupModel); + } + + public function getSpecGroupAttributeWithValues($group_id) + { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getSpecGroupAttributeWithValues($group_id); + } + + public function getSpecGroupAttribute($group_id) + { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getSpecGroupAttribute($group_id); + } + + + public function clearProductSpecGroup($product_id) + { + return $this->objProductSpecGroupModel->clearProductSpecGroup($product_id); + } + + + public function setProductSpecGroup($product_id, $group_id) + { + return $this->objProductSpecGroupModel->setProductSpecGroup($product_id, $group_id); + } + + + public function getProductSpec($product_id) + { + return $this->objProductSpecGroupModel->getProductSpec($product_id); + } + + + public function getProductSpecGroupInfo($product_id) + { + return $this->objProductSpecGroupModel->getProductSpecGroupInfo($product_id); + } + + + public function getProductSpecGroupId($product_id) + { + return $this->objProductSpecGroupModel->getProductSpecGroupId($product_id); + } + + + public function getSpecGroupInfo($group_id) + { + return $this->objProductSpecGroupModel->getSpecGroupInfo($group_id); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + +} diff --git a/inc/Hura8/Components/Product/AdminController/AProductVariantController.php b/inc/Hura8/Components/Product/AdminController/AProductVariantController.php new file mode 100644 index 0000000..2d6d132 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductVariantController.php @@ -0,0 +1,85 @@ +objProductVariantModel = new ProductVariantModel($product_id); + } + + + public function delete($id) { + return $this->objProductVariantModel->delete($id); + } + + + public function updateImage($variant_id, $image_name) { + return $this->objProductVariantModel->updateFields($variant_id, [ + "thumbnail" => $image_name + ]); + } + + public function removeImage($variant_id) { + return $this->objProductVariantModel->updateFields($variant_id, [ + "thumbnail" => '' + ]); + } + + + public function getInfo($id) { + return $this->objProductVariantModel->getInfo($id); + } + + public function getProductVariantOption($product_id){ + return $this->objProductVariantModel->getProductVariantOption($product_id); + } + + + //update product's variant options + public function updateVariantOption($attribute) { + return $this->objProductVariantModel->updateVariantOption($attribute); + } + + public function useVariantOptionSample($select_id) { + $sample = $this->objProductVariantModel->useVariantOptionSample($select_id); + + return ($sample) ? \json_decode($sample,true) : []; + } + + + //use a product's variant-option to create a choice, so next product can select without recreate from beginning + public function createVariantOptionSample($use_from_pro_id, $sample_title) { + return $this->objProductVariantModel->createVariantOptionSample($use_from_pro_id, $sample_title); + } + + + public function getVariantOptionSample() { + return $this->objProductVariantModel->getVariantOptionSample(); + } + + public function updateVariant($variant_id, array $variant_info) { + if($variant_id) { + return $this->objProductVariantModel->update($variant_id, $variant_info); + }else{ + return $this->objProductVariantModel->create($variant_info); + } + } + + public function getProductVariantPriceRange(){ + return $this->objProductVariantModel->getProductVariantPriceRange(); + } + + public function getProductVariantList(){ + return $this->objProductVariantModel->getList([]); + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php b/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php new file mode 100644 index 0000000..b6ff89c --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterBuilderController.php @@ -0,0 +1,320 @@ + '', + 'host' => '', + 'port' => '', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => [], + 'fragment' => '', + ]; + + protected $_price_format = ''; + protected $_brand_format = ''; + protected $_filter_format = ''; + + + public function __construct($request_url) { + + if(defined('PRICE_FILTER_FORMAT') && PRICE_FILTER_FORMAT) { + $this->_price_format = PRICE_FILTER_FORMAT; + } + + if(defined('BRAND_FILTER_FORMAT') && BRAND_FILTER_FORMAT) { + $this->_brand_format = BRAND_FILTER_FORMAT; + } + + if(defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT ) { + $this->_filter_format = ATTRIBUTE_FILTER_FORMAT; + } + + $this->_request_url = $request_url; + $this->_url_elements = Url::parse($request_url); + } + + + public function buildProductFilterFromUrl(array $existing_filters = []) { + $filters = [ + "price" => $this->getPriceFilterRange(), + "brand" => $this->getBrandFilter(), // array(1,2,3,) + "collection" => $this->getGenericFilter('collection'), // array(1,2,3,) + "supplier" => $this->getGenericFilter('supplier'), // array(1,2,3,) + "rating" => $this->getGenericFilter('rating'), // array(1,2,3,) + "category" => $this->getGenericFilter('category'), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => (isset($this->_url_elements['query']['q'])) ? $this->_url_elements['query']['q'] : '', // string search keyword + "hotType" => $this->getGenericFilter('hotType', 'string'),// array(saleoff | not | new) + "attribute" => $this->getAttributeFilter(), // array(1,2,3,) + "ids" => $this->getGenericFilter('ids'), // array(1,2,3,) + "promotion" => "", + "storeId" => $this->getGenericFilter('store'), // array(1,2,3,) + "other_filter" => $this->getGenericFilter('other_filter', 'string'), // array(in-stock, has-promotion etc...) + "spec_group_id" => $this->getGenericFilter('spec_group_id'), + ]; + + + // debug_var(array_merge($filters, $existing_filters)); + if(sizeof($existing_filters)) { + foreach ($existing_filters as $key => $values) { + if(!isset($filters[$key])) continue; + if(!$values) continue; + + if(is_array($values) && is_array($filters[$key])) { + $filters[$key] = array_merge($filters[$key], $values); + }else{ + $filters[$key] = $values; + } + } + } + + return $filters ; + } + + + protected function getGenericFilter($url_para, $data_type='int') { + $value = (isset($this->_url_elements['query'][$url_para])) ? $this->_url_elements['query'][$url_para] : ''; + + if($data_type == 'int') { + $safe_value = trim(preg_replace("/[^0-9_\-\,]/i", '', $value)); + }else{ + $safe_value = trim(preg_replace("/[^a-z0-9_\-\.\,]/i", '', $value)); + } + + if(!$safe_value) return []; + + $safe_value_list = explode(FILTER_VALUE_SEPARATOR, $safe_value); + + return array_values(array_filter($safe_value_list)); + } + + + protected function getBrandFilter() + { + if($this->_brand_format == 'brand_index'){ + + $objBrandModel = new BrandModel(); + + $result = []; + $brand_filters = $this->getGenericFilter('brand', 'string'); + + foreach ($brand_filters as $_brand) { + if(is_int($_brand)) { + $result[] = $_brand; + } + + $brand_info = $objBrandModel->getInfoByUrl( $_brand); + if(!$brand_info) continue; + + $result[] = $brand_info['id']; + } + + return $result; + } + + + // default + return $this->getGenericFilter('brand'); + } + + + protected function getAttributeFilter(array $category_list_id = []) { + + if($this->_filter_format == 'filter_code') { + + $excluded_keys = [ + 'q', + 'p', + 'min', 'max', + 'brand', + 'page', + 'request_path', + 'category', + 'supplier', 'collection', + 'ids', + 'filter',' hotType', + 'other_filter', + 'rating', 'price', + // special ajax key + 'action','action_type', '_auto' + ]; + $attr_filter_params = []; + + foreach ( $this->_url_elements['query'] as $key => $value) { + if(in_array($key, $excluded_keys) || !$value) continue; + + $attr_filter_params[$key] = explode(',', urldecode(urldecode($value))); + } + + if(sizeof($attr_filter_params)) { + + $category_attributes = $this->getAttributesForCategory($category_list_id); + //debug_var($category_attributes); + + $match_attr_value_ids = []; + foreach ($attr_filter_params as $attr_filter_code => $attr_value_api_key_list) { + + foreach ($category_attributes as $attribute) { + if($attribute['filter_code'] != $attr_filter_code) continue; + + foreach ($attribute['value_list'] as $_index => $_value_info) { + + foreach ($attr_value_api_key_list as $attr_value_api_key) { + if($_value_info['api_key'] == $attr_value_api_key) { + $match_attr_value_ids[] = $_value_info['id']; + } + } + } + } + } + + + return $match_attr_value_ids; + } + } + + // default + return $this->getGenericFilter('filter'); + } + + + + protected function getAttributesForCategory(array $category_ids){ + + $category_att_id_list = array(); + $attr_query_cond = (sizeof($category_ids)) ? " AND category_id IN (".join(',', $category_ids).") " : ""; + + $query = $this->db->runQuery(" SELECT `attr_id` FROM `idv_attribute_category` WHERE 1 ". $attr_query_cond); + foreach ($this->db->fetchAll($query) as $rs ){ + $category_att_id_list[] = $rs['attr_id']; + } + + if(!sizeof($category_att_id_list)) return array(); + + $query = $this->db->query(" + SELECT + attval.id , + attval.attributeId , + attval.value , + attval.api_key , + att.attribute_code , + att.filter_code + FROM ".TB_ATTRIBUTE_VALUE." attval + LEFT JOIN ".TB_ATTRIBUTE." att ON attval.attributeId = att.id + WHERE att.id IN (".join(',', $category_att_id_list).") AND att.isSearch=1 + ORDER BY attval.ordering DESC "); + + $attribute_list = []; + + foreach ($this->db->fetchAll($query) as $rs ){ + $att_value_id = $rs['id']; + $att_id = $rs['attributeId']; + + $value_info = array( + "id" => $att_value_id, + "api_key" => $rs['api_key'], + ); + + if(!array_key_exists($att_id, $attribute_list)) { + $attribute_list[$att_id] = [ + "id" => $att_id, + "code" => $rs['attribute_code'], + "filter_code" => $rs['filter_code'], + 'value_list' => [$value_info], + ]; + }else{ + $attribute_list[$att_id]['value_list'][] = $value_info; + } + } + + + return array_values($attribute_list); + } + + + // accept url format: + // default: ?min=1000&max=20000 + // custom: ?p=15trieu-20-trieu + protected function getPriceFilterRange(){ + // default format + if($this->_price_format != 'p') { + return array( + "min" => isset($this->_url_elements['query']['min']) ? intval($this->_url_elements['query']['min']) : 0 , + "max" => isset($this->_url_elements['query']['max']) ? intval($this->_url_elements['query']['max']) : 0 , + ); + } + + // custom price range query + $price_range_format = (isset($this->_url_elements['query']['p'])) ? $this->_url_elements['query']['p'] : ''; // duoi-10trieu , 10ngan-2trieu, 3trieu-6trieu, tren-30trieu + + if(strpos($price_range_format, '-') === false) { + return array( + "min" => 0, + "max" => 0, + ); + } + + $unit_translation = [ + 'ngan' => 1000, + 'trieu' => 1000000, + 'ty' => 1000000000, + ]; + + // duoi-10trieu, duoi-10ngan + if(strpos($price_range_format, 'duoi-') !== false) { + $unit_match = $this->findPriceUnitMatch(str_replace("duoi-", "", $price_range_format)); + return array( + "min" => 0, + "max" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + ); + } + + // tren-10trieu, tren-10ngan + if(strpos($price_range_format, 'tren-') !== false) { + $unit_match = $this->findPriceUnitMatch(str_replace("tren-", "", $price_range_format)); + return array( + "min" => $unit_match['number'] * $unit_translation[$unit_match['unit']], + "max" => 0, + ); + } + + // 10ngan-2trieu, 3trieu-6trieu + $parts = explode('-', $price_range_format); + $min_part = $parts[0]; + $max_part = $parts[1]; + $min_match = $this->findPriceUnitMatch($min_part); + $max_match = $this->findPriceUnitMatch($max_part); + + return [ + "min" => $min_match['number'] * $unit_translation[$min_match['unit']], + "max" => $max_match['number'] * $unit_translation[$max_match['unit']], + ]; + } + + + protected function findPriceUnitMatch($str){ + $match = []; + $pattern = "/^([\d]+)(ngan|trieu|ty)/i"; + if(preg_match($pattern, $str, $match)){ + return [ + "number" => $match[1], + "unit" => $match[2], + ]; + } + return [ + "number" => 0, + "unit" => 'trieu', + ]; + } +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterController.php b/inc/Hura8/Components/Product/Controller/ProductFilterController.php new file mode 100644 index 0000000..979ca41 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterController.php @@ -0,0 +1,797 @@ + translated to brand=>array(2,), + - ?monitor_size=12inch&color=red => translated to attribute=> array(123, 23121,) + */ + protected $filters = [ + "price" => array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => "", // string search keyword + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "ids" => array(), // array(1,2,3,) + "excluded_ids" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + + + public function __construct($request_url = '') { + $this->_request_url = $request_url; + } + + + public function getResult( + $current_page_id =1, + $number_per_page=10, + $sort_by = 'new', + array $existing_filters=[], + $current_category_ids = [], + $price_range = [] + ) { + + $objProductFilterBuilder = new ProductFilterBuilderController($this->_request_url); + $built_filters = $objProductFilterBuilder->buildProductFilterFromUrl($existing_filters); + $this->setFilters($built_filters); + + $product_result = $this->findProductListIdMatchFilters($current_page_id, $number_per_page, $sort_by); + //debug_var($product_result); + + $objProductFilterOptions = new ProductFilterOptionsController( + $this->getFilters(), + $current_category_ids, + $product_result['item_list'], + $price_range + ); + $objProductFilterOptionsTranslation = new ProductFilterOptionsTranslationController($this->_request_url); + + list ( $filter_options, $filter_messages ) = $objProductFilterOptionsTranslation->getAllFilterOptions( + $objProductFilterOptions->categoryList(), + $objProductFilterOptions->attributeList(), + $objProductFilterOptions->brandList(), + $objProductFilterOptions->priceList() + ); + + return [ + 'total' => $product_result['total'], + 'paged_product_list' => $product_result['item_list'], + 'filter_options' => $filter_options, + 'filter_messages' => $filter_messages, + ]; + } + + + public function getFilters() { + return $this->filters; + } + + + // allow to update filters + public function setFilters(array $new_filters) { + foreach ($new_filters as $key => $value) { + if(isset($this->filters[$key])) { + $this->filters[$key] = $value; + } + } + } + + + protected function createFilterCacheKey($sort_by) { + return md5(\json_encode($this->filters) . $sort_by); + } + + + // find a list of products that match filtering conditions + protected function findProductListIdMatchFilters($current_page_id, $number_per_page, $sort_by = "new" ) { + + $cache_key = $this->createFilterCacheKey($sort_by); + + return self::getCache($cache_key, function () use ($current_page_id, $number_per_page, $sort_by) { + return $this->findProductListIdMatchFilters_raw($current_page_id, $number_per_page, $sort_by ); + }); + } + + + protected function findProductListIdMatchFilters_raw($current_page_id=1, $number_per_page=10, $sort_by = "new" ) { + + list( , , , $where_query ) = $this->buildSQLConditionFromFilters($sort_by); + + $ordering_clause = $this->getOrderingClause($sort_by); + + $objProductFilterModel = new ProductFilterModel(); + + list($total_number, $item_list) = $objProductFilterModel->findProductListIdMatchFilters([ + $where_query, + $current_page_id, + $number_per_page, + $ordering_clause + ]); + + + $final_result = [ + "total" => $total_number, + "item_list" => $item_list, + ]; + + return $final_result; + } + + + protected function buildSQLConditionFromFilters( $sort_by = "new" ) { + //list of available filters + $filterPath = []; + $filter_messages = []; + $where_query = []; + + // system controls + if( ENABLE_PRODUCT_EXPIRE ) { + $where_query[] = " AND (from_time =0 OR from_time < '".CURRENT_TIME."') + AND (to_time > '".CURRENT_TIME."' OR to_time=0 ) "; + } + + //other filters + if( array_key_exists("other_filter", $this->filters) && sizeof($this->filters["other_filter"])) { + foreach ($this->filters["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $filter_messages[] = [ + 'title' => 'Còn hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND quantity > 0 "; + break; + case "has-vat"; + $filter_messages[] = [ + 'title' => 'Có thuế VAT', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `has_vat` = 1 "; + break; + case "out-stock"; + $filter_messages[] = [ + 'title' => 'Hết hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `quantity` = 0 "; + break; + case "has-market-price"; + $filter_messages[] = [ + 'title' => 'Có giá thị trường', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND market_price > 0 "; + break; + case "no-price"; + $filter_messages[] = [ + 'title' => 'Không có giá', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `price` = 0 "; + break; + case "no-warranty"; + $filter_messages[] = [ + 'title' => 'Không có bảo hành', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(warranty) < 2 "; + break; + case "no-sku"; + $filter_messages[] = [ + 'title' => 'Không mã kho hàng', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(sku) < 2 "; + break; + case "has-config"; + $filter_messages[] = [ + 'title' => 'Không có cấu hình', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $filter_messages[] = [ + 'title' => 'Không có ảnh', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND image_count = 0 "; + break; + case "no-category"; + $filter_messages[] = [ + 'title' => 'Không có danh mục', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `category` = '0' "; + break; + case "display-off"; + $filter_messages[] = [ + 'title' => 'Đang ẩn', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $filter_messages[] = [ + 'title' => 'Đang hiển thị', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $filter_messages[] = [ + 'title' => 'Có khuyến mại', + 'reset' => Url::buildUrl($this->_request_url, ['other_filter' => '']), + ]; + $where_query[] = " AND LENGTH(`special_offer`) < 5 "; + break; + //...add more + } + } + } + + /*// limit by admin permission + //1-1-2014 gioi han theo san pham danh muc ma quan tri nay quan ly + global $admin_permission; + $restricted_category_per_admin = (isset($admin_permission['permit'])) ? $admin_permission['permit']['product:category']['ids'] : []; + if(sizeof($restricted_category_per_admin)) { + $cat_query = $this->db->runQuery(" + SELECT DISTINCT pro_id + FROM ".TB_PRODUCT_CATEGORY." + WHERE `category_id` IN (".join(",", $restricted_category_per_admin).") + LIMIT 5000 + "); + + //lay danh sach danh muc cua san pham + $product_in_category = []; + foreach( $this->db->fetchAll($cat_query) as $cat_info ){ + $product_in_category[] = $cat_info["pro_id"]; + } + + $where_query[] = (sizeof($product_in_category)) ? " AND id IN (". join(',', $product_in_category) .") " : " AND id = 0 "; + }*/ + + + //- brand id or ids or brand_indexes + if( array_key_exists("brand", $this->filters) && sizeof($this->filters["brand"])) { + + $objBrandModel = new BrandModel(); + + $condition = array(); + foreach ($this->filters["brand"] as $brand_id) { + if(!$brand_id) continue; + + $brand_info = $objBrandModel->getInfo( $brand_id); + if(!$brand_info) continue; + + $filterPath["brand"][] = array( + "id" => $brand_info['id'], + "name" => $brand_info["title"], + ); + + $condition[] = " brandId = '".intval($brand_info['id'])."' "; + $filter_messages[] = [ + 'title' => $brand_info['title'], + 'reset' => Url::buildUrl($this->_request_url, ['brand' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["brand"], $brand_id ))] ), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + } + + + //- collection id or ids + if( array_key_exists("collection", $this->filters) && sizeof($this->filters["collection"])) { + $condition = array(); + + $objProductCollectionModel = new ProductCollectionModel(); + + foreach ($this->filters["collection"] as $_id) { + + $collection_info = $objProductCollectionModel->getInfo($_id); + if(!$collection_info) continue; + + $filterPath["collection"][] = array( + "id" => $_id, + "name" => $collection_info["title"], + ); + $condition[] = " `id` IN ( SELECT product_id FROM ".TableName::PRODUCT_PER_COLLECTION." WHERE `collection_id`='".intval($collection_info['id'])."' ) "; + $filter_messages[] = [ + 'title' => 'Bộ sưu tập ', + 'reset' => Url::buildUrl($this->_request_url, ['collection' => join(',', remove_item_from_array($this->filters["collection"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + //- supplier id + if( array_key_exists("supplier", $this->filters) && sizeof($this->filters["supplier"])) { + /*$condition = array(); + + if(!$this->objSupplier) $this->objSupplier = new Supplier(); + + foreach ($this->filters["supplier"] as $_id) { + $_id = intval($_id); + + $supplier_info = $this->objSupplier->getInfo($_id); + + $filterPath["supplier"][] = array( + "id" => $_id, + "name" => $supplier_info["name"], + ); + $condition[] = " supplier_id = '".$_id."' "; + $filter_messages[] = [ + 'title' => $supplier_info["name"], + 'reset' => Url::buildUrl($this->_request_url, ['supplier' => join(',', remove_item_from_array($this->filters["supplier"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )";*/ + } + + //- rating: 1-> 5 + if( array_key_exists("rating", $this->filters) && sizeof($this->filters["rating"])) { + $condition = array(); + foreach ($this->filters["rating"] as $_id) { + + $condition[] = " `rating` = '".intval($_id)."' "; + $filterPath["rating"][] = array( + "id" => $_id, + "name" => $_id, + ); + $filter_messages[] = [ + 'title' => 'Đánh giá '.$_id, + 'reset' => Url::buildUrl($this->_request_url, ['rating' => join(',', remove_item_from_array($this->filters["rating"], $_id))] ), + ]; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + //- category id or ids + if( array_key_exists("category", $this->filters) && sizeof($this->filters["category"])) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $condition = array(); + foreach ($this->filters["category"] as $cat_id) { + + $cat_info = $objProductCategoryModel->getInfo($cat_id); + if(!$cat_info) continue; + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ?: '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = '".$cat_id."' "; + } + + $filterPath["category"][] = array( + "id" => $cat_id, + "name" => $cat_info['title'], + ); + + $filter_messages[] = [ + 'title' => $cat_info['title'], + 'reset' => Url::buildUrl($this->_request_url, ['category' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["category"], $cat_id))] ), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".TableName::PRODUCT_PER_CATEGORY." WHERE " . join(" OR ", $condition) . " )"; + } + } + + //- status: 0|1|null + if( array_key_exists("status", $this->filters) && sizeof($this->filters["status"])) { + $where_query[] = " AND `status` IN (".join(',', $this->filters["status"]).") "; + } + + // spec_group_id + if( array_key_exists("spec_group_id", $this->filters) && sizeof($this->filters["spec_group_id"])) { + $where_query[] = " AND `spec_group_id` IN (".join(',', $this->filters["spec_group_id"]).") "; + } + + // detail_page_only: 0 | 1 + if( array_key_exists("detail_page_only", $this->filters) && $this->filters["detail_page_only"]) { + $where_query[] = " AND `detail_page_only` = '".intval($this->filters['detail_page_only'])."' "; + } + + + //- query: search keyword + if( array_key_exists("query", $this->filters) && $this->filters["query"]) { + $keyword_search = $this->filters["query"]; + $search_by_product_id = intval(preg_replace('/[^0-9]/i', '', $keyword_search)); + + // todo: add more + $filter_search = []; + if(isset($this->filters["brand"]) && $this->filters["brand"]) { + $filter_search['brand'] = ["IN", $this->filters["brand"]]; + } + + if(isset($this->filters["status"]) && $this->filters["status"]) { + $filter_search['status'] = ["IN", $this->filters["status"]]; + } + + $objProductSearchModel = new ProductSearchModel(); + $match_result = $objProductSearchModel->find($keyword_search, $filter_search); + + //debug_var($match_result); + + $filterPath["search"] = array( + "id" => $keyword_search, + "name" => $keyword_search, + ); + $filter_messages[] = [ + 'title' => $keyword_search, + 'reset' => Url::buildUrl($this->_request_url, ['q' => ''] ), + ]; + + if($search_by_product_id > 0) { + $where_query[] = (sizeof($match_result) > 0) ? " AND ( `id` IN (".join(',', $match_result).") OR `id` = '".$search_by_product_id."' ) " : " AND `id` = '".$search_by_product_id."' "; + }else{ + $where_query[] = (sizeof($match_result) > 0) ? " AND `id` IN (".join(',', $match_result).") " : " AND `id` = -1 "; + } + + } + + + //- hotType: saleoff | not | new | or combination of types + if( array_key_exists("hotType", $this->filters) && sizeof($this->filters["hotType"])) { + $config_hottype = AProductHotController::getProductHotTypeList(); + $condition = array(); + foreach ($this->filters["hotType"] as $_id) { + if(!array_key_exists($_id, $config_hottype)) continue; + + $condition[] = " `hot_type` = '".$_id."' "; + + $filter_messages[] = [ + 'title' => $config_hottype[$_id], + 'reset' => Url::buildUrl($this->_request_url, ['hotType' => '']), + ]; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN (SELECT pro_id FROM ".TB_PRODUCT_HOT." WHERE 1 AND ( ".join(" OR ", $condition)." ) ) "; + } + + } + + + //- attribute values + /* + if( array_key_exists("attribute", $this->filters) && sizeof($this->filters["attribute"])) { + $filter_attr_value_list = $this->filters["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + + $product_filter_id_match = array(); + if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + if(!$this->objCategoryProduct) $this->objCategoryProduct = new CategoryProduct(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $this->objCategoryProduct->atrValueName($value_id); + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["attribute"], $value_id))] ), + ]; + } + + } + + */ + + //- attribute values + if( array_key_exists("attribute", $this->filters) && sizeof($this->filters["attribute"])) { + + $filter_attr_value_list = $this->filters["attribute"]; + + $attr_values_per_attribute = $this->groupAttributeValuesByAttributeId($filter_attr_value_list); + + $having_match_count = sizeof(array_keys($attr_values_per_attribute)); + + $attribute_info_list = $this->groupAttributeListInfo(array_keys($attr_values_per_attribute)); + + $attr_where_query = []; + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + // if same attribute : find products that match either ONE of these values (not match ALL values) + $_att_info = $attribute_info_list[$_att_id]; + if (!$_att_info) continue; + + // this attribute requires all products must have ALL selected values + if ($_att_info['value_match_all'] && sizeof($_att_value_list) > 1) { + + $_product_id_match_all = $this->getProductMatchAllAttributeValueIds($_att_value_list); + + if (sizeof($_product_id_match_all) > 0) { + $attr_where_query[] = " ( `id` IN (" . join(",", $_product_id_match_all) . ") ) "; + } else { + $attr_where_query[] = " ( `id` = -1 ) "; + } + + } else { + $attr_where_query[] = " ( SELECT DISTINCT `pro_id` FROM " . TB_PRODUCT_ATTRIBUTE . " WHERE (" . + join(" OR ", array_map(function ($_item) { return " `attr_value_id` = '" . intval($_item['id']) . "' "; }, $_att_value_list)) + . " ) ) "; + } + + } + + $product_filter_id_match = array(); + + if (sizeof($attr_where_query)) { + $query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ( " . join(" UNION ALL ", $attr_where_query) . " ) AS tpm_tb + GROUP BY pro_id + HAVING num_pro = " . $having_match_count . " + LIMIT 10000 + "); + foreach ($this->db->fetchAll($query) as $rs) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND " . TB_PRODUCT_LIGHT . ".`id` IN (" . join(', ', $product_filter_id_match) . ") " : " AND " . TB_PRODUCT_LIGHT . ".`id` = 0 "; + + //xay lai url de back + $objProductAttributeModel = new ProductAttributeModel(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $objProductAttributeModel->getInfo($value_id)['title']; + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($this->filters["attribute"], $value_id))] ), + ]; + } + + /*//xay lai url de back + $filterPath["attribute"] = $filter_attr_value_list; + + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + foreach ($_att_value_list as $_item) { + $filter_messages[] = [ + 'title' => $_item['title'], + 'reset' => Url::buildUrl(CURRENT_URL, ['filter' => join('-', remove_item_from_array($filter_attr_value_list, $_item['id']))]), + ]; + } + }*/ + + } + + + //given products' ids + if( array_key_exists("ids", $this->filters) && sizeof($this->filters["ids"])) { + + $filter_messages[] = [ + 'title' => "IDs", + 'reset' => Url::buildUrl($this->_request_url, ['ids' => '']), + ]; + + $where_query[] = " AND `id` IN (". join(",", $this->filters["ids"]) .") "; + } + + // exclude some ids + if( array_key_exists("excluded_ids", $this->filters) && sizeof($this->filters["excluded_ids"])) { + + $filter_messages[] = [ + 'title' => "Excluded IDs", + 'reset' => Url::buildUrl($this->_request_url, ['excluded_ids' => '']), + ]; + + $where_query[] = " AND `id` NOT IN (". join(",", $this->filters["excluded_ids"]) .") "; + } + + $price_range_query_limit = join(" ",$where_query); + + //- price range + if(isset($this->filters["price"]) && sizeof($this->filters["price"]) && ($this->filters["price"]['min'] > 0 || $this->filters["price"]['max'] > 0)) { + //limit by price range + $maxPrice = clean_price($this->filters["price"]['max']); + $minPrice = clean_price($this->filters["price"]['min']); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + /*if(ENABLE_INVENTORY_PER_STORE && USER_LOCATION) { + + $list_ids = $this->getProductIds($price_range_query_limit); + $this->objCache->set("all_product_ids_location", $list_ids); + + $objUStoreLocation = new UStoreLocation(); + $location_products = $objUStoreLocation->getAllProductForLocation(USER_LOCATION, $list_ids, $sort_by); + $location_products = $this->filterLocationProductByPrice($location_products, $maxPrice, $minPrice); + + $this->objCache->set("location_products", $location_products); + + $location_product_ids = array_keys($location_products); + $where_query[] = (sizeof($location_product_ids)) ? " AND `id` IN (".join(',', $location_product_ids).") " : " AND `id` = 0"; + + } else { + $where_query[] = " AND ". $price_range_query; + }*/ + + $where_query[] = " AND ". $price_range_query; + + $filterPath["price"] = array( + "min" => $minPrice, + "max" => $maxPrice, + ); + + $price_format = ProductFilterPrice::buildPriceRangeFormat($minPrice, $maxPrice); + + $filter_messages[] = [ + 'title' => $price_format['title'], + 'reset' => Url::buildUrl($this->_request_url, ['p' => '', 'min' => '', 'max' => '']) + ]; + } + + return [ + $price_range_query_limit, + $filterPath, + $filter_messages, + $where_query + ]; + } + + + protected function getProductMatchAllAttributeValueIds(array $_att_value_list) { + $objProductAttributeModel = new ProductAttributeModel(); + return $objProductAttributeModel->getProductMatchAllAttributeValueIds($_att_value_list); + } + + + protected function groupAttributeListInfo(array $attr_ids) { + + $objProductAttributeModel = new ProductAttributeModel(); + $attr_list_info = $objProductAttributeModel->getListByIds($attr_ids); + + $final_list = []; + foreach ($attr_ids as $_id) { + $final_list[$_id] = (isset($attr_list_info[$_id])) ? $attr_list_info[$_id] : null; + } + + return $final_list; + } + + + protected function groupAttributeValuesByAttributeId(array $attr_value_ids) { + + $objProductAttributeValueModel = new ProductAttributeValueModel(0); + + $result = array(); + foreach ( $objProductAttributeValueModel->getListByIds($attr_value_ids) as $info ) { + $result[$info['attributeId']][] = [ + 'id' => $info['id'], + 'title' => $info['value'], + ]; + } + + return $result; + } + + + protected function filterLocationProductByPrice(array $product_list, $max_price, $min_price) { + if($max_price > 0 && $min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price && $pro['price'] < $max_price); + }); + } + + if($max_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] < $max_price); + }); + } + + if($min_price > 0){ + return array_filter($product_list, function ($pro) use ($max_price, $min_price) { + return ($pro['price'] >= $min_price); + }); + } + + return $product_list; + } + + + protected function getOrderingClause($sort_by) { + $mappings = [ + 'order-new' => "ORDER BY `ordering` DESC, `id` DESC", + 'order-last-update' => "ORDER BY `ordering` DESC, last_update DESC", + 'last-update' => "ORDER BY `last_update` DESC", + 'order' => "ORDER BY `ordering` DESC", + 'new' => "ORDER BY `id` DESC", + 'price-asc' => "ORDER BY `price` asc", + 'price-desc' => "ORDER BY `price` DESC ", + 'view' => "ORDER BY `visit` desc", + 'ranking' => "ORDER BY `ranking` DESC", + 'name' => "ORDER BY `title` asc", + ]; + + if(array_key_exists($sort_by, $mappings)) { + return $mappings[$sort_by]; + } + + return "ORDER BY `ordering` DESC, `id` DESC"; + } +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php new file mode 100644 index 0000000..c293a94 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsController.php @@ -0,0 +1,104 @@ + array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "query" => "", // string search keyword + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "ids" => array(), // array(1,2,3,) + "promotion" => "", + "storeId" => array(), // array(1,2,3,) + "other_filter" => array() // array(in-stock, has-promotion etc...) + ]; + + protected $objProductFilterModel; + + public function __construct($filters, $current_category_ids = [], $list_product_ids = [], $price_range = []) { + + $this->objProductFilterModel = new ProductFilterModel(); + + $this->_filters = $filters; + $this->_current_category_ids = $current_category_ids; + $this->_list_product_ids = $list_product_ids; + $this->_price_range = $price_range; + } + + + public function ratingList() { + + $neededList = array(); + + foreach ( $this->objProductFilterModel->ratingList($this->_list_product_ids) as $result ) { + $neededList[] = array( + "rating" => $result['review_rate'] , + "count" => $result['num'] , + 'is_selected' => in_array($result['review_rate'], $this->_filters['rating']), + ); + } + + return $neededList; + } + + + public function categoryList() { + + if(!sizeof($this->_list_product_ids)) { + return []; + } + + return $this->objProductFilterModel->categoryList($this->_filters['category'], $this->_list_product_ids); + } + + + public function brandList() { + + if(!sizeof($this->_list_product_ids)) { + return []; + } + + return $this->objProductFilterModel->brandList($this->_filters['brand'], $this->_list_product_ids); + } + + + public function attributeList() { + + return $this->objProductFilterModel->attributeList( + $this->_filters['attribute'], + $this->_current_category_ids, + $this->_list_product_ids + ); + } + + + public function priceList() { + + return $this->objProductFilterModel->priceList( + $this->_filters['price']['max'] , + $this->_filters['price']['min'], + $this->_list_product_ids, + $this->_price_range + ); + + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php new file mode 100644 index 0000000..a0926db --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/ProductFilterOptionsTranslationController.php @@ -0,0 +1,348 @@ + '', + 'host' => '', + 'port' => '', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => [], + 'fragment' => '', + ]; + + protected $_price_format = ''; + protected $_brand_format = ''; + protected $_filter_format = ''; + + + public function __construct($request_url) { + if(defined('PRICE_FILTER_FORMAT') && PRICE_FILTER_FORMAT) { + $this->_price_format = PRICE_FILTER_FORMAT; + } + + if(defined('BRAND_FILTER_FORMAT') && BRAND_FILTER_FORMAT) { + $this->_brand_format = BRAND_FILTER_FORMAT; + } + + if(defined('ATTRIBUTE_FILTER_FORMAT') && ATTRIBUTE_FILTER_FORMAT ) { + $this->_filter_format = ATTRIBUTE_FILTER_FORMAT; + } + + $this->_request_url = $request_url; + $this->_url_elements = Url::parse($request_url); + } + + + public function getAllFilterOptions(array $category_list = [], array $attribute_list = [], array $brand_list = [], array $price_list_options = []) { + + $result = [ + 'category' => $category_list, + 'price' => $this->translatePriceList($price_list_options), + 'brand' => $this->translateBrandList($brand_list), + 'attribute' => $this->translateAttributeList($attribute_list), + ]; + + $selected_categories = array_filter($category_list, function ($item){ return $item['is_selected'];}); + $selected_prices = array_filter($result['price'], function ($item){ return $item['is_selected'];}); + $selected_brands = array_filter($result['brand'], function ($item){ return $item['is_selected'];}); + $selected_attributes = array_filter($result['attribute'], function ($item){ return $item['is_selected'];}); + + $filter_messages = []; + + foreach ($selected_categories as $selected_category) { + $filter_messages[] = [ + 'name' => $selected_category['name'], // 'Danh mục: '. + 'reset_url' => Url::buildUrl($this->_request_url, ['category' => ''] ), + ]; + } + + foreach ($selected_prices as $selected_price) { + $filter_messages[] = [ + 'name' => $selected_price['name'], // 'Giá: '. + 'reset_url' => $selected_price['url'], + ]; + } + + foreach ($selected_brands as $selected_brand) { + $filter_messages[] = [ + 'name' => $selected_brand['name'], // 'Thương hiệu: '. + 'reset_url' => $selected_brand['url'], + ]; + } + + foreach ($selected_attributes as $selected_attribute) { + + $selected_attribute_values = array_filter($selected_attribute['value_list'], function ($item){ return $item['is_selected'];}); + + foreach ($selected_attribute_values as $selected_attribute_value) { + $filter_messages[] = [ + 'name' => $selected_attribute_value['name'], //$selected_attribute['name'].': '.$selected_attribute_value['name'], + 'reset_url' => $selected_attribute_value['url'], + ]; + } + } + + + return [$result, $filter_messages]; + } + + + public function translateAttributeList(array $attribute_list = []) { + $result = []; + + foreach ($attribute_list as $attr_info) { + $copy = $attr_info; + $copy['value_list'] = array_map(function ($value_info) use ($attr_info) { + return $this->translateAttributeValue($attr_info['filter_code'], $value_info); + }, $attr_info['value_list']); + + $result[] = $copy; + } + + return $result; + } + + + protected function translateAttributeValue($attribute_filter_code, array $value_info = []) { + $copy = $value_info; + + if($copy['is_selected']) { + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlResetParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + }else{ + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + } + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildAttributeValueUrlResetParameters($attribute_filter_code, $value_info['id'], $value_info['api_key']) ); + + return $copy; + } + + + protected function buildAttributeValueUrlParameters($attribute_filter_code, $value_id, $value_api_key) { + if($this->_filter_format == 'filter_code') { + $filter_query = (isset($this->_url_elements['query'][$attribute_filter_code])) ? $this->_url_elements['query'][$attribute_filter_code] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list[] = $value_api_key; + + $result = []; + $result[$attribute_filter_code] = join(FILTER_VALUE_SEPARATOR, $filter_value_list ); + + return $result; + } + + // default + $filter_query = (isset($this->_url_elements['query']['filter'])) ? $this->_url_elements['query']['filter'] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list[] = $value_id; + sort($filter_value_list); + + return [ + 'filter' => join(FILTER_VALUE_SEPARATOR, $filter_value_list ), + ]; + } + + + protected function buildAttributeValueUrlResetParameters($attribute_filter_code, $value_id, $value_api_key) { + if($this->_filter_format == 'filter_code') { + $filter_query = (isset($this->_url_elements['query'][$attribute_filter_code])) ? $this->_url_elements['query'][$attribute_filter_code] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list = remove_item_from_array($filter_value_list, $value_api_key); + + $result = []; + $result[$attribute_filter_code] = join(FILTER_VALUE_SEPARATOR, $filter_value_list ); + + return $result; + } + + + // default + $filter_query = (isset($this->_url_elements['query']['filter'])) ? $this->_url_elements['query']['filter'] : ''; + $filter_value_list = ($filter_query) ? explode(FILTER_VALUE_SEPARATOR, $filter_query) : []; + + $filter_value_list = remove_item_from_array($filter_value_list, $value_id); + + return [ + 'filter' => join(FILTER_VALUE_SEPARATOR, $filter_value_list ), + ]; + } + + + public function translateBrandList(array $brand_list = []) { + + return array_map(function ($item){ + $copy = $item; + + if(($copy['is_selected'])) { + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlResetParameters($item['id'], $item['brand_index']) ) ; + }else{ + $copy['url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlParameters($item['id'], $item['brand_index']) ); + } + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildBrandUrlResetParameters($item['id'], $item['brand_index']) ); + //unset($copy['brand_index']); + + return $copy; + + }, $brand_list); + + } + + + protected function buildBrandUrlResetParameters($brand_id, $brand_index){ + $brand_query = (isset($this->_url_elements['query']['brand'])) ? $this->_url_elements['query']['brand'] : ''; + $brand_list = ($brand_query) ? explode(FILTER_VALUE_SEPARATOR, $brand_query) : []; + + if($this->_brand_format == 'brand_index') { + $brand_list = remove_item_from_array($brand_list, $brand_index); + }else{ + $brand_list = remove_item_from_array($brand_list, $brand_id); + } + + return [ + 'brand' => join(FILTER_VALUE_SEPARATOR, $brand_list ), + ]; + } + + + protected function buildBrandUrlParameters($brand_id, $brand_index){ + $brand_query = (isset($this->_url_elements['query']['brand'])) ? $this->_url_elements['query']['brand'] : ''; + $brand_list = ($brand_query) ? explode(FILTER_VALUE_SEPARATOR, $brand_query) : []; + + if($this->_brand_format == 'brand_index') { + if(!in_array($brand_index, $brand_list)) $brand_list[] = $brand_index; + }else{ + if(!in_array($brand_id, $brand_list)) $brand_list[] = $brand_id; + } + + return [ + 'brand' => join(FILTER_VALUE_SEPARATOR, $brand_list ), + ]; + } + + + public function translatePriceList(array $price_list_options = []) { + + return array_map(function ($item){ + $copy = $item; + + $format = $this->buildPriceRangeFormat($item['min'], $item['max']); + + $copy['name'] = $format['name']; + $copy['url'] = ($copy['is_selected']) ? Url::buildUrl($this->_request_url, $this->buildPriceUrlResetParameters()) : Url::buildUrl($this->_request_url, $format['url_para']); + + //$copy['reset_url'] = Url::buildUrl($this->_request_url, $this->buildPriceUrlResetParameters()); + //unset($copy['min'], $copy['max']); + + return $copy; + + }, $price_list_options); + + } + + + protected function buildPriceRangeFormat($min_price, $max_price) { + if($min_price > 0 && !$max_price) { + $title = "Trên ".$this->format_search_price($min_price); + }elseif(!$min_price && $max_price > 0) { + $title = "Dưới ".$this->format_search_price($max_price); + }else{ + $title = $this->format_search_price($min_price)." - ".$this->format_search_price($max_price); + } + + return [ + 'url_para' => $this->buildPriceUrlParameters($min_price, $max_price), + 'name' => $title, + ]; + } + + + protected function format_search_price($p_price){ + + $price_len = strlen($p_price); + + if($price_len < 7) { + return round($p_price/1000,1)." ngàn"; + } + else if($price_len < 10) { + return round($p_price/1000000,1)." triệu"; + } + + return round($p_price/1000000000,1)." tỷ"; + } + + + protected function buildPriceUrlResetParameters() { + if( $this->_price_format == 'p') { + return [ + "p" => '', + ]; + } + + return [ + "min" => '' , + "max" => '' , + ]; + } + + + protected function buildPriceUrlParameters($min_price = 0, $max_price = 0) { + + if($min_price > 0 && !$max_price) { + if( $this->_price_format == 'p') { + return [ + "p" => "tren-".self::convertPriceFormatToFilter($this->format_search_price($min_price)), + ]; + } + + return [ + "min" => $min_price , + ]; + } + + if(!$min_price && $max_price > 0) { + + if( $this->_price_format == 'p') { + return [ + "p" => "duoi-" . self::convertPriceFormatToFilter($this->format_search_price($max_price)), + ]; + } + + return [ + "max" => $max_price, + ]; + } + + if( $this->_price_format == 'p') { + return [ + "p" => self::convertPriceFormatToFilter($this->format_search_price($min_price)) . "-" . self::convertPriceFormatToFilter($this->format_search_price($max_price)), + ]; + } + + return [ + "max" => $max_price , + "min" => $min_price, + ]; + } + + + // convert: 10 ngàn -> 10ngan, 10 triệu => 10trieu + protected static function convertPriceFormatToFilter($price_format){ + $price_format = str_replace(['ngàn', 'triệu'], ['ngan', 'trieu'], $price_format); + // remove whitespace + return str_replace(" ",'', $price_format); + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductCategoryController.php b/inc/Hura8/Components/Product/Controller/bProductCategoryController.php new file mode 100644 index 0000000..12720e3 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductCategoryController.php @@ -0,0 +1,62 @@ +objProductCategoryModel = new ProductCategoryModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objProductCategoryLanguageModel->createTableLang(); + parent::__construct( + $this->objProductCategoryModel, + new ProductCategoryLanguageModel() + ); + + }else{ + parent::__construct( + $this->objProductCategoryModel + ); + } + } + + + protected function formatItemInfo(?array $info) : ?array + { + if(!$info) return null; + + if($info['icon']) $info['icon'] = STATIC_DOMAIN . "/" . static::$image_folder . "/" . $info['icon']; + + return $info; + } + + // get full info- basic with description + public function getFullInfo($id): ?array + { + $info = $this->objProductCategoryModel->getFullInfo($id); + + if(!$info) return null; + + if($this->iEntityLanguageModel) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id); + if($item_language_info) { + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + } + + return $this->formatItemInfo($info); + } + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductCollectionController.php b/inc/Hura8/Components/Product/Controller/bProductCollectionController.php new file mode 100644 index 0000000..d4f6fae --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductCollectionController.php @@ -0,0 +1,62 @@ +objProductCollectionModel = new ProductCollectionModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objProductCategoryLanguageModel->createTableLang(); + parent::__construct( + $this->objProductCollectionModel, + new ProductCollectionLanguageModel() + ); + + }else{ + parent::__construct( + $this->objProductCollectionModel + ); + } + } + + + public function getTotalProduct($collection_id, array $condition = []) + { + return $this->objProductCollectionModel->getTotalProduct($collection_id, $condition); + } + + + public function getListProduct($collection_id, array $condition = []) { + return $this->objProductCollectionModel->getListProduct($collection_id, $condition); + } + + + protected function formatItemInList(array $info) : array + { + return $this->formatItemInfo($info); + } + + + protected function formatItemInfo(?array $info) : ?array + { + $info['url'] = "/collection/".$info['url_index']; + return $info; + } + + +} diff --git a/inc/Hura8/Components/Product/Controller/bProductController.php b/inc/Hura8/Components/Product/Controller/bProductController.php new file mode 100644 index 0000000..d37f093 --- /dev/null +++ b/inc/Hura8/Components/Product/Controller/bProductController.php @@ -0,0 +1,210 @@ + ['width' => 100,] , + 's' => ['width' => 300,] , + 'l' => ['width' => 650,] + ); + + protected $objProductModel; + + public function __construct() + { + $this->objProductModel = new ProductModel(); + + if(!$this->isDefaultLanguage()) { + $objProductLanguageModel = new ProductLanguageModel(); + // $this->objProductLanguageModel->createTableLang(); + parent::__construct($this->objProductModel, $objProductLanguageModel); + }else{ + parent::__construct($this->objProductModel); + } + } + + + //get product image list + public function productImageList($proId){ + $objProductImageModel = new ProductImageModel($proId); + $result = array(); + foreach ( $objProductImageModel->getList(["numPerPage" => 100]) as $rs ) { + $result[] = [ + "size" => static::getResizedImageCollection($rs['img_name']), + "alt" => $rs['alt'], + "folder" => $rs['folder'], + ]; + } + + return $result; + } + + + public function getProductDisplayOptions($current_url) { + $allowed_options = array( + "list" => "Danh sách", + "grid" => "Xem nhóm", + "detail" => "Chi tiết" + ) ; + + $collection = array(); + foreach($allowed_options as $option => $name){ + $url = Url::buildUrl($current_url, array("display" => "")); + $collection[] = array( + "url" => Url::buildUrl($url, array("display" => $option)) , + "key" => $option , + "name" => $name , + ); + } + return $collection; + } + + + public function getProductOtherFilterOptions($current_url) { + $allowed_options = array( + //"order" => $global_language['sort_by_order'] , + "in-stock" => 'Còn hàng' , + ) ; + + $_collection = array(); + foreach($allowed_options as $option => $_name){ + $_collection[] = array( + "url" => Url::buildUrl($current_url, array("other_filter" => $option, 'page' => '')) , + "key" => $option , + "name" => $_name , + ); + } + return $_collection; + } + + + public function getProductSortOptions($current_url, array $options = []) { + //global $global_language; + + if(!sizeof($options)) $options = array( + //"order" => $global_language['sort_by_order'] , + "new" => "Mới nhất" , + "price-asc" => "Giá tăng dần", //$global_language['sort_by_price_asc'] , + "price-desc" => "Giá giảm dần", //$global_language['sort_by_price_desc'] , + "view" => "Lượt xem", //$global_language['sort_by_view'], + "comment" => "Trao đổi", //$global_language['sort_by_comment'] , + "rating" => "Đánh giá", //$global_language['sort_by_rating'] , + "name" => "Tên A->Z" , + ) ; + + $sort_by_collection = array(); + foreach($options as $sort_option=>$sort_name){ + // $url = build_url($current_url, array("sort" => "")); + $sort_by_collection[] = array( + "url" => Url::buildUrl($current_url, array("sort" => $sort_option)) , + "key" => $sort_option , + "name" => $sort_name , + ); + } + return $sort_by_collection; + } + + + public function getProductInfoBySKU($sku) { + + return self::getCache(md5("getFullInfo-".$sku), function () use ($sku) { + + $info = $this->objProductModel->getProductInfoBySKU($sku); + + return ($info) ? $this->formatItemInfo($info) : null; + }); + } + + + // get full info- basic with description + public function getFullInfo($id) + { + return self::getCache("getFullInfo-".$id, function () use ($id) { + + $info = $this->objProductModel->getFullInfo($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)); + } + } + + return ($info) ? $this->formatItemInfo($info) : null; + }); + } + + + protected function validateAndCleanFilterCondition(array $raw_filter_condition): array + { + $clean_values = parent::validateAndCleanFilterCondition($raw_filter_condition); + + // special cases for 'price' which is in range + if(array_key_exists('price', $raw_filter_condition)) { + $value = $raw_filter_condition['price']; + // expect $value = array('max' => 0, 'min'=> 0) + if(isset($value['max']) && isset($value['min'])) { + $clean_values['price'] = array( + 'max' => DataClean::makeInputSafe($value['max'], DataType::INTEGER), + 'min' => DataClean::makeInputSafe($value['min'], DataType::INTEGER), + ); + } + } + + return $clean_values; + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(?array $item_info): ?array + { + if(!$item_info) return null; + + $info = $item_info; + + $info['image'] = static::getResizedImageCollection($info['thumbnail']); + + return $info; + } + + + public static function getResizedImageCollection($image_name) { + + $image = []; + + $size_in_full = [ + 't' => 'thumb' , + 's' => 'small' , + 'l' => 'large' , + ]; + + foreach (static::$resized_sizes as $size => $value) { + $image[$size_in_full[$size]] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $image_name : ''; + } + + $image['original'] = ($image_name) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $image_name : ''; + + return $image; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php new file mode 100644 index 0000000..8f2f0b5 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductAttributeLanguageModel.php @@ -0,0 +1,16 @@ +db->runQuery( + "SELECT + a.title, + a.summary, + a.is_filter, + a.value_match_all, + a.filter_code, + a.is_display, + a.is_header, + a.is_multi, a.in_summary, + a.value_count, + g.attr_id, + g.ordering, + g.status + FROM ".$this->tb_attribute_per_spec_group." g, ".$this->tb_entity." a + WHERE g.`group_id`= ? AND g.`attr_id`= a.`id` + ORDER BY g.`ordering` DESC, a.`ordering` DESC ", + ['d'], + [$group_id] + ) ; + + $attribute_ids = []; + $value_per_attribute_ids = []; + $result = []; + foreach ($this->db->fetchAll($query) as $item) { + $attribute_ids[] = $item['attr_id']; + $value_per_attribute_ids[$item['attr_id']] = []; + + $result[$item['attr_id']] = [ + "attribute_info" => $item, + "attribute_values" => &$value_per_attribute_ids[$item['attr_id']], + ]; + } + + + if(!sizeof($attribute_ids)) { + return []; + } + + // now get all values for each attribute + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($attribute_ids, "int"); + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_attribute_value." + WHERE `attribute_id` IN (".$parameterized_ids.") + ORDER BY `ordering` DESC, `title` ASC ", + $bind_types, + $attribute_ids + ) ; + + foreach ($this->db->fetchAll($query) as $item) { + $value_per_attribute_ids[$item['attribute_id']][] = $item; + } + + return $result; + } + + + public function getSpecGroupAttribute($group_id) { + + $query = $this->db->runQuery( + "SELECT + a.title, + a.summary, + a.is_filter, + a.value_match_all, + a.filter_code, + a.is_display, + a.is_header, a.is_multi, a.value_count, + g.attr_id, + g.ordering, + g.status + FROM ".$this->tb_attribute_per_spec_group." g, ".$this->tb_entity." a + WHERE g.`group_id`= ? AND g.`attr_id`= a.`id` + ORDER BY g.`ordering` DESC, a.`ordering` DESC ", + ['d'], + [$group_id] + ) ; + + return $this->db->fetchAll($query); + } + + + public function getProductMatchAllAttributeValueIds(array $_att_value_list) { + /*$query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE " . join(" OR ", array_map(function ($_item){ return " `attr_value_id` = '".intval($_item['id'])."' "; }, $_att_value_list )) ." + GROUP BY pro_id + HAVING num_pro = ".sizeof($_att_value_list)." + LIMIT 10000 + "); + + return array_map(function ($item){ + return $item["pro_id"]; + }, $this->db->fetchAll($query));*/ + + return []; + } + + public function getProductMatchAttributeValue(array $value_ids) { + $product_filter_id_match = array(); + // todo: + /*if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + }*/ + + return $product_filter_id_match; + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "letter" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php b/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php new file mode 100644 index 0000000..4efdb01 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductAttributeValueModel.php @@ -0,0 +1,83 @@ +attribute_id = $attribute_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getProductAttributes($product_id) { + + $query = $this->db->runQuery( + "SELECT `attr_id`, `attr_value_id` FROM `".$this->tb_product_attribute."` WHERE `pro_id` = ? ", + ['d'], [ $product_id] + ); + + return $this->db->fetchAll($query); + } + + + protected function getAttributeValueCount() { + $query = $this->db->runQuery( + "SELECT `value_count` FROM `".$this->tb_attribute."` WHERE `id` = ? LIMIT 1 ", + ['d'], [$this->attribute_id] + ); + + if($info = $this->db->fetchAssoc($query)) { + return $info['value_count']; + } + + return 0; + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = [" AND `attribute_id` = ? "]; + $bind_types = ["d"]; + $bind_values = [$this->attribute_id]; + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + public function getInfoByCode($filter_code) + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` WHERE `attribute_id` = ? AND `filter_code` = ? LIMIT 1", + ['d', 's'], [$this->attribute_id, $filter_code ] + ); + + return $this->db->fetchAssoc($query); + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php new file mode 100644 index 0000000..6af77c4 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryInfoModel.php @@ -0,0 +1,30 @@ +db = get_db(); + } + + public function getInfo($id) { + return $this->db->select( + $this->tb_category_info, + [], + [ + 'id' => ['=', $id], + ] + ); + } + + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php new file mode 100644 index 0000000..f8dd0e5 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCategoryModel.php b/inc/Hura8/Components/Product/Model/ProductCategoryModel.php new file mode 100644 index 0000000..794a625 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryModel.php @@ -0,0 +1,85 @@ +db->runQuery("SELECT + a.id , + a.filter_code , + a.title , + a.value_count , + a.is_filter , + c.ordering , + c.status , + a.is_header + FROM ".$this->tb_attribute." a + LEFT JOIN ".$this->tb_attribute_per_category." c ON a.id = c.attr_id + WHERE c.category_id = ? + ORDER BY c.ordering desc + ", ['d'], [ $catId ]) ; // AND isSearch = 1 + + return $this->db->fetchAll($query); + } + + + public function getFullInfo($id) : ?array { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_category_info."` info + WHERE basic.`id` = info.`id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + $item_info['settings'] = ($item_info['settings']) ? \json_decode($item_info['settings'], true) : []; + return $item_info; + } + + return null; + } + + + public function getEmptyInfo($addition_field_value = []) : array + { + // basic table + $basic_empty = parent::getEmptyInfo($addition_field_value); + + // info table + foreach ($this->db->getTableInfo($this->tb_category_info) as $field => $field_info) { + $basic_empty[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float']) ) ? 0 : '' ; + } + + if(sizeof($addition_field_value)) { + return array_merge($basic_empty, $addition_field_value); + } + + return $basic_empty; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php new file mode 100644 index 0000000..30c85a8 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCollectionLanguageModel.php @@ -0,0 +1,22 @@ +richtext_fields); + + //$this->createTableLang(); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductCollectionModel.php b/inc/Hura8/Components/Product/Model/ProductCollectionModel.php new file mode 100644 index 0000000..cda3041 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCollectionModel.php @@ -0,0 +1,85 @@ +db->runQuery( + " SELECT * FROM `".$this->tb_entity."` WHERE `url_index` = ? LIMIT 1", + ['s'], [$url] + ); + + return $this->db->fetchAssoc($query); + } + + public function getTotalProduct($collection_id, array $condition = []) + { + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_collection_product."` WHERE `collection_id` = ? ", + ['d'], [$collection_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListProduct($collection_id, array $condition = []) + { + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + $order_by = " `ordering` DESC, `id` DESC"; + + $query = $this->db->runQuery( + "SELECT `product_id`, `ordering` FROM ".$this->tb_collection_product." WHERE `collection_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$collection_id] + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $item ) { + $counter += 1; + + $item_list[$item['product_id']] = [ + 'counter' => $counter, + 'ordering' => $item['ordering'], + ]; + } + + $objProductModel = new ProductModel(); + $product_list_info = $objProductModel->getListByIds(array_keys($item_list)); + + // final list + $final_list = []; + foreach ($item_list as $_pro_id => $_pro_info_in_collection) { + $pro_basic = $product_list_info[$_pro_id] ?? null; + if($pro_basic) { + $final_list[] = array_merge($pro_basic, $_pro_info_in_collection); + } + } + + return $final_list; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductFilterModel.php b/inc/Hura8/Components/Product/Model/ProductFilterModel.php new file mode 100644 index 0000000..2aca558 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductFilterModel.php @@ -0,0 +1,452 @@ +db = get_db('', ENABLE_DB_DEBUG); + } + + + public function findProductListIdMatchFilters(array $SQLConditionFromFilters) { + list( + $where_query, + $current_page_id, + $number_per_page, + $ordering_clause + ) = $SQLConditionFromFilters; + + $db_condition = join(" ", $where_query); + + $total_number = 0; + $query = $this->db->runQuery("SELECT COUNT(`id`) AS total FROM ".TableName::PRODUCT." WHERE 1 ".$db_condition." "); + if($rs = $this->db->fetchAssoc($query)) { + $total_number = $rs['total']; + } + + $query = $this->db->runQuery(" + SELECT * FROM ".TableName::PRODUCT." + WHERE 1 ".$db_condition." + ".$ordering_clause." + LIMIT ". ($current_page_id - 1) * $number_per_page .", ".$number_per_page." + "); + + $item_list = $this->db->fetchAll($query); + + return [$total_number, $item_list]; + } + + + public function ratingList(array $list_product_ids) { + + if(!sizeof($list_product_ids)) return []; + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " + SELECT `review_rate`, COUNT(`id`) as num + FROM ".TableName::PRODUCT." + WHERE `id` IN (".$parameterized_ids.") + GROUP BY `review_rate` + ORDER BY review_rate ASC + ", + $bind_types, + $list_product_ids + ); + + return $this->db->fetchAll($query); + } + + + public function categoryList(array $current_category_ids, array $list_product_ids) { + if(!sizeof($list_product_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " + SELECT `category_id`, COUNT(`item_id`) AS numPro + FROM ".TableName::PRODUCT_PER_CATEGORY." + WHERE `item_id` IN (".$parameterized_ids.") + GROUP BY `category_id` ", + $bind_types, + $list_product_ids + ); + + //lay ket qua dem + $result_count = []; + foreach ($this->db->fetchAll($query) as $rs ){ + $result_count[$rs['category_id']] = $rs['numPro']; + } + + if(sizeof($result_count)) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $final_result = []; + foreach ($objProductCategoryModel->getListByIds(array_keys($result_count)) as $rs ){ + + $rs['count'] = $result_count[$rs['id']]; + $rs['is_selected'] = in_array($rs['id'], $current_category_ids); + + $final_result[] = $rs; + } + + return $final_result; + } + + return []; + } + + + public function brandList(array $current_brand_ids, array $list_product_ids) { + + if(!sizeof($list_product_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_product_ids, 'int'); + + $query = $this->db->runQuery( + " SELECT `brand_id`, COUNT(`id`) AS total + FROM ".TableName::PRODUCT." + WHERE `id` IN (".$parameterized_ids.") + GROUP BY `brand_id` + ORDER BY total DESC + LIMIT 100", + $bind_types, + $list_product_ids + ); + + + $neededList = array(); + foreach ( $this->db->fetchAll($query) as $result ) { + $neededList[$result['brand_id']] = array( + "id" => $result['brand_id'] , + "count" => $result['total'] , + ); + } + + $brand_list_ids[] = array_keys($neededList); + $result_list = array(); + + if(sizeof($brand_list_ids)) { + + $objBrandModel = new BrandModel(); + + foreach ( $objBrandModel->getListByIds($brand_list_ids) as $_brand ) { + + $item_count = (isset($neededList[$_brand['id']])) ? $neededList[$_brand['id']]['count'] : 0; + + $_brand['count'] = $item_count; + $_brand["is_selected"] = in_array($_brand['id'], $current_brand_ids); + + + $result_list[$_brand['id']] = $_brand; + } + } + + return array_values($result_list); + } + + + public function attributeList(array $current_filter_attribute_value_ids, array $current_category_ids, array $list_product_ids) { + if(!sizeof($list_product_ids)) { + return []; + } + + $category_attribute_filter_array = $this->countNumProPerAttributeForCategory($current_category_ids, $list_product_ids); + + if(!sizeof($category_attribute_filter_array)) { + return []; + } + + $filter_box = array(); + + foreach($category_attribute_filter_array as $attributeIdGroup){ + + $attribute_value_array = $attributeIdGroup['value_list']; + $attribute_name = $attributeIdGroup['name']; + $attribute_selected = 0; + $filterList = array(); + + $count_num = 0; + + foreach($attribute_value_array as $att_value_id => $rs_value){ + $att_value_name = $rs_value["info"]["name"]; + $att_value_description = $rs_value["info"]["description"]; + $count_pro = $rs_value["pro_count"]; + $is_selected = in_array($att_value_id, $current_filter_attribute_value_ids); + if($is_selected) { + $attribute_selected = 1; + } + + unset($this_filter_query, $this_filter_col_value); + + //gioi han danh sach + if($count_num > 50) { + break; + } + + $count_num++; + + $filterList[] = array( + "id" => $att_value_id , + "name" => $att_value_name , + 'api_key' => $rs_value["info"]["api_key"], + "description" => $att_value_description, + "count" => $count_pro , + "is_selected" => $is_selected, + ); + } + + if($count_num > 0){ + $filter_box[] = [ + "name" => $attribute_name, + 'filter_code' => $attributeIdGroup['filter_code'], + "is_selected" => $attribute_selected, + "value_list" => $filterList , + ]; + } + + } + + return $filter_box; + } + + + protected function countNumProPerAttributeForCategory(array $current_category_ids, array $list_product_ids){ + + if(!sizeof($list_product_ids)) { + return []; + } + + $category_attributes = $this->getAttributesForCategory($current_category_ids); + $att_value_id_list = []; + foreach ($category_attributes as $attribute) { + $att_value_id_list = array_merge($att_value_id_list, array_keys($attribute['value_list'])); + } + + // count product per attribute-value-id + $result_count = array(); + if(sizeof($att_value_id_list)) { + $query = $this->db->runQuery(" + SELECT + pro_att.`attr_value_id`, + COUNT(DISTINCT ".TB_PRODUCT_CATEGORY.".pro_id) AS numPro + FROM ".TB_PRODUCT_ATTRIBUTE." pro_att + LEFT JOIN ".TB_PRODUCT_CATEGORY." ON pro_att.pro_id = ".TB_PRODUCT_CATEGORY.".pro_id + WHERE pro_att.pro_id IN (".join(',', $list_product_ids ).") + AND pro_att.`attr_value_id` IN (".join(",", $att_value_id_list).") + GROUP BY pro_att.`attr_value_id` "); + + //lay ket qua dem + foreach ($this->db->fetchAll($query) as $rs ){ + $result_count[$rs['attr_value_id']] = $rs['numPro']; + } + } + + $final_result = []; + foreach ($category_attributes as $attribute) { + $new_value_list = []; + foreach ($attribute['value_list'] as $_value_id => $_value_info) { + $pro_count = isset($result_count[$_value_id]) ? $result_count[$_value_id] : 0; + if($pro_count) { + $_value_info['pro_count'] = $pro_count; + $new_value_list[$_value_id] = $_value_info; + } + } + $attribute['value_list'] = $new_value_list; + $final_result[] = $attribute; + } + + return $final_result; + } + + + // 15-07-2020 get all attributes and values for a list of category + protected function getAttributesForCategory(array $category_ids){ + + if(!sizeof($category_ids)) { + return array(); + } + + + $final_result = array();//array to return + + $category_att_id_list = array(); + $attribute_array = array(); //keep & re-order attribute + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($category_ids, 'int'); + + $query = $this->db->runQuery(" + SELECT `attr_id`, `ordering` FROM `idv_attribute_category` + WHERE `category_id` IN (". $parameterized_ids .") AND `status` = 1 ", $bind_types, $category_ids); + + foreach ($this->db->fetchAll($query) as $rs ){ + $category_att_id_list[] = $rs['attr_id']; + $attribute_array[$rs['attr_id']] = $rs['ordering']; + } + + if(!sizeof($category_att_id_list)) { + return array(); + } + + + $att_value_id_list = array(); + $attribute_info = array(); + $attribute_att_value_item = array(); + $attribute_value_info = array(); + $query = $this->db->runQuery(" + SELECT + attval.id , + attval.attributeId , + attval.value , + attval.description , + attval.api_key , + attval.value_en , + att.attribute_code , + att.filter_code, + att.name , + att.ordering + FROM ".TB_ATTRIBUTE_VALUE." attval + LEFT JOIN ".TB_ATTRIBUTE." att ON attval.attributeId = att.id + WHERE att.id IN (".join(',', $category_att_id_list).") AND att.isSearch = 1 + ORDER BY attval.ordering DESC "); + + foreach ($this->db->fetchAll($query) as $rs ){ + $att_value_id = $rs['id']; + $att_id = $rs['attributeId']; + + $att_value_id_list[] = $att_value_id; + + $attribute_att_value_item[$att_id][] = $att_value_id; //in ordering + + $attribute_info[$att_id] = array( + "id" => $att_id, + "name" => $rs['name'], + "code" => $rs['attribute_code'], + "filter_code" => $rs['filter_code'], + ); + $attribute_value_info[$att_value_id] = array( + "id" => $att_value_id, + "name" => $rs['value'], + "description" => $rs['description'], + "api_key" => $rs['api_key'], + ); + } + + //cho ra ket qua + arsort($attribute_array); //sap xep thu tu cua attribute theo ordering + + foreach($attribute_array as $att_id => $att_order){ + $this_attribute_atr_value_item = array(); + if(isset($attribute_att_value_item[$att_id])) { + foreach($attribute_att_value_item[$att_id] as $att_value_id){ + $this_attribute_atr_value_item[$att_value_id] = array( + "id" => $att_value_id, + "info" => $attribute_value_info[$att_value_id], + ); + } + } + + if(isset($attribute_info[$att_id])) { + $info = $attribute_info[$att_id]; + $info['value_list'] = $this_attribute_atr_value_item; + + $final_result[] = $info; + } + } + + return $final_result; + } + + + public function priceList($current_max_price, $current_min_price, array $list_product_ids, array $category_price_range) { + + if(!sizeof($list_product_ids)) { + return []; + } + + $total_prices = sizeof($category_price_range); + if( ! $total_prices ){ + return []; + } + + //pad head and tail with 0 + array_unshift($category_price_range, 0); + $category_price_range[] = 0; + + $result = array(); + $queries = []; + + for($i = 0; $i <= $total_prices ; $i++){ + + $range_key = md5($category_price_range[$i] .'-'. $category_price_range[$i+1] ); + $queries[] = $this->buildRangeQuery($list_product_ids, $range_key, $category_price_range[$i], $category_price_range[$i+1]); + + $min_price = intval($category_price_range[$i]); + $max_price = intval($category_price_range[$i+1]); + $is_selected = ($max_price == $current_max_price && $min_price == $current_min_price ); + + $result[$range_key] = array( + "min" => $min_price , + "max" => $max_price, + "count" => 0 , + "is_selected" => ($is_selected) ? 1 : 0 , + ); + + } + + $query = $this->db->runQuery(" SELECT `range_key`, `total` FROM ( ".join(' UNION ALL ', $queries)." ) AS tmp"); + foreach ($this->db->fetchAll($query) as $rs) { + $result[$rs['range_key']]['count'] = $rs['total']; + } + + // get only range with product-count > 0 + return array_values(array_filter( $result , function ($item){ + return $item['count'] > 0; + })); + } + + + private function buildRangeQuery(array $list_product_ids, $range_key, $minPrice, $maxPrice){ + + $minPrice = (int) $minPrice; + $maxPrice = (int) $maxPrice; + if($minPrice < 10) $minPrice = 10; + + $new_price_range = " AND `id` IN (".join(',', $list_product_ids ).") "; + + if($maxPrice > 0 && $minPrice > 0){ + $new_price_range .= " AND ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + }elseif($maxPrice > 0){ + $new_price_range .= " AND `price` < '".$maxPrice."' "; + }elseif($minPrice > 0 && $maxPrice == 0){ + $new_price_range .= " AND `price` >='".$minPrice."' "; + } + + return " SELECT '".$range_key."' AS `range_key`, COUNT(`id`) AS total + FROM ".TableName::PRODUCT." + WHERE 1 ".$new_price_range; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductHotModel.php b/inc/Hura8/Components/Product/Model/ProductHotModel.php new file mode 100644 index 0000000..3984cc0 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductHotModel.php @@ -0,0 +1,95 @@ +db = get_db(); + } + + public function updateProductHot($pro_id, array $new_types) { + + $this->db->runQuery("DELETE FROM " . $this->tb_product_hot . " WHERE `pro_id` = ? ", ['d'], [ $pro_id ]); + $this->db->update( + $this->tb_product, + [ + "hot_type" => '', + ], + [ + 'id' => $pro_id, + ] + ); + + + $config_hottype = Config::getProductHotTypeList(); + + //insert what good + $batch_insert = array(); + $accepted_types = array(); + foreach ($new_types as $hot_type) { + if (!isset($config_hottype[$hot_type])) continue; + + $batch_insert[] = [ + "pro_id" => $pro_id, + "hot_type" => $hot_type, + ]; + + $accepted_types[] = $hot_type; + } + + if (sizeof($batch_insert)) { + + $this->db->bulk_insert($this->tb_product_hot, $batch_insert ); + + $this->db->update( + $this->tb_product, + [ + "hot_type" => join(",", $accepted_types), + ], + [ + "id" => $pro_id, + ] + ); + } + + return true; + } + + + public function getProductHot(array $product_ids) { + + if(!sizeof($product_ids)){ + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($product_ids, 'int'); + + $query = $this->db->runQuery( + "SELECT `pro_id`, `hot_type` FROM ".$this->tb_product_hot." WHERE `pro_id` IN (".$parameterized_ids.") ", + $bind_types, + $product_ids + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ) { + $result[$rs['pro_id']][] = $rs['hot_type']; + } + + return $result; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductImageModel.php b/inc/Hura8/Components/Product/Model/ProductImageModel.php new file mode 100644 index 0000000..cc12988 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductImageModel.php @@ -0,0 +1,78 @@ +product_id = $product_id; + $this->objProductModel = new ProductModel(); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function setProduct($product_id) { + $this->product_id = $product_id; + } + + public function countProductImage($product_id) { + $query = $this->db->runQuery("SELECT COUNT(*) AS total FROM `".$this->tb_entity."` WHERE `pro_id` = ? ", ['d'], [ $product_id]); + if($info = $this->db->fetchAssoc($query)) { + return $info['total']; + } + + return 0; + } + + + protected function _buildQueryOrderBy($sort_by = "new") + { + return " `ordering` DESC, `id` DESC "; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "letter" => "", + "status" => 0, + );*/ + + $catCondition = [" AND `pro_id` = ? "]; + $bind_types = ['d']; + $bind_values = [$this->product_id]; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function getProductMainImage() { + $query = $this->db->runQuery( + "SELECT `id` FROM `".$this->tb_entity."` WHERE `pro_id` = ? AND `is_main` = 1 LIMIT 1", + ['d'], [ $this->product_id] + ); + + return $this->db->fetchAssoc($query); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductInfoModel.php b/inc/Hura8/Components/Product/Model/ProductInfoModel.php new file mode 100644 index 0000000..0c8092a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductInfoModel.php @@ -0,0 +1,35 @@ +richtext_fields); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + return null; + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductLanguageModel.php b/inc/Hura8/Components/Product/Model/ProductLanguageModel.php new file mode 100644 index 0000000..ce8701a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductModel.php b/inc/Hura8/Components/Product/Model/ProductModel.php new file mode 100644 index 0000000..0fc53d7 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductModel.php @@ -0,0 +1,410 @@ +richtext_fields + ); + } + + + protected function extendedFilterOptions() : array + { + return [ + "price" => array('max' => 0, 'min'=> 0), + "brand" => array(), // array(1,2,3,) + "collection" => array(), // array(1,2,3,) + "supplier" => array(), // array(1,2,3,) + "rating" => array(), // array(1,2,3,) + "category" => array(), // array(1,2,3,) + "status" => array(), // array(1,2,3,) + "hotType" => array(),// array(saleoff | not | new) + "attribute" => array(), // array(1,2,3,) + "promotion" => "", + "other_filter" => array(), // array(in-stock, has-promotion etc...) + "spec_group_id" => array(), // array(1,2,3,) + ]; + } + + + public function getProductInfoBySKU(string $sku) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_product_info."` info + WHERE basic.`id` = info.`id` AND basic.sku = ? + LIMIT 1 ", + ['d'], [$sku] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return false; + } + + + public function getFullInfo($id) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_product_info."` info + WHERE basic.`id` = info.`id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + + public function getProductCategoryList($pro_id) { + $query = $this->db->runQuery( + "SELECT `category_id` FROM ".$this->tb_product_per_category." WHERE `item_id` = ? ", + ['d'], [ $pro_id ] + ) ; + + $pro_cat = []; + foreach ( $this->db->fetchAll($query) as $rs ) { + $pro_cat[] = $rs['category_id']; + } + + return $pro_cat; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_query = []; + $bind_types = []; + $bind_values = []; + + //------------------- + + //other filters + if( array_key_exists("other_filter", $filter_condition) && sizeof($filter_condition["other_filter"])) { + foreach ($filter_condition["other_filter"] as $_filter) { + switch ($_filter) { + case "in-stock"; + $where_query[] = " AND quantity > 0 "; + break; + case "has-vat"; + $where_query[] = " AND `has_vat` = 1 "; + break; + case "out-stock"; + $where_query[] = " AND `quantity` = 0 "; + break; + case "has-market-price"; + $where_query[] = " AND `market_price` > 0 "; + break; + case "no-price"; + $where_query[] = " AND `price` = 0 "; + break; + case "no-warranty"; + $where_query[] = " AND LENGTH(`warranty`) < 2 "; + break; + case "no-sku"; + $where_query[] = " AND LENGTH(`sku`) < 2 "; + break; + case "has-config"; + $where_query[] = " AND `config_count` > 0 "; + break; + case "no-image"; + $where_query[] = " AND `image_count` = 0 "; + break; + case "no-category"; + $where_query[] = " AND `category` = '0' "; + break; + case "display-off"; + $where_query[] = " AND `status`=0 "; + break; + case "display-on"; + $where_query[] = " AND `status`=1 "; + break; + case "has-promotion": + $where_query[] = " AND LENGTH(`special_offer`) < 5 "; + break; + //...add more + } + } + } + + + //- brand id or ids or brand_indexes + if( array_key_exists("brand", $filter_condition) && sizeof($filter_condition["brand"])) { + + $condition = array(); + foreach ($filter_condition["brand"] as $brand_id) { + if(!intval($brand_id)) continue; + $condition[] = " brand_id = ? "; + $bind_types[] = 'd'; + $bind_values[] = $brand_id; + } + + if(sizeof($condition)) { + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + } + + + //- collection id or ids + if( array_key_exists("collection", $filter_condition) && sizeof($filter_condition["collection"])) { + $condition = array(); + + foreach ($filter_condition["collection"] as $_id) { + if(!intval($_id)) continue; + + $condition[] = " `id` IN ( SELECT `product_id` FROM ".$this->tb_collection_product." WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- rating: 1-> 5 + if( array_key_exists("rating", $filter_condition) && sizeof($filter_condition["rating"])) { + $condition = array(); + foreach ($filter_condition["rating"] as $_id) { + $condition[] = " `rating` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition)." )"; + } + + + //- category id or ids + if( array_key_exists("category", $filter_condition) && sizeof($filter_condition["category"])) { + + $objProductCategoryModel = new ProductCategoryModel(); + + $condition = array(); + foreach ($filter_condition["category"] as $cat_id) { + + $cat_info = $objProductCategoryModel->getInfo($cat_id); + if(!$cat_info) continue; + + if($cat_info["is_parent"]) { + $childListId = ($cat_info["child_ids"]) ?: '0'; + $condition[] = " `category_id` IN (".$childListId .") "; + }else{ + $condition[] = " `category_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $cat_id; + } + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN ( SELECT DISTINCT `item_id` FROM ".$this->tb_product_per_category." WHERE " . join(" OR ", $condition) . " )"; + } + } + + + // spec_group_id + if( array_key_exists("spec_group_id", $filter_condition) && sizeof($filter_condition["spec_group_id"])) { + + $condition = array(); + foreach ($filter_condition["spec_group_id"] as $_id) { + if(!$_id) continue; + + $condition[] = " `spec_group_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $_id; + } + + $where_query[] = " AND ( ".join(" OR ", $condition )." ) "; + } + + + //- hotType: saleoff | not | new | or combination of types + if( array_key_exists("hotType", $filter_condition) && sizeof($filter_condition["hotType"])) { + $config_hottype = Config::getProductHotTypeList(); + $condition = array(); + foreach ($filter_condition["hotType"] as $_id) { + if(!array_key_exists($_id, $config_hottype)) continue; + + $condition[] = " `hot_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $_id; + } + + if(sizeof($condition)) { + $where_query[] = " AND `id` IN (SELECT `pro_id` FROM ".$this->tb_product_hot." WHERE 1 AND ( ".join(" OR ", $condition)." ) ) "; + } + + } + + + //- attribute values + /* + if( array_key_exists("attribute", $filter_condition) && sizeof($filter_condition["attribute"])) { + $filter_attr_value_list = $filter_condition["attribute"]; + //filter = attr_value_1-attr_value_2-attr_value_3, + $query_attr_id = []; + $count_filter = 0; + + foreach($filter_attr_value_list as $attr_id){ + $attr_id = (int) $attr_id; + if($attr_id) { + $query_attr_id[] = $attr_id; + $count_filter ++; + } + } + + $product_filter_id_match = array(); + if(sizeof($query_attr_id)) { + $query = $this->db->runQuery(" + SELECT DISTINCT pro_id , COUNT(*) as num_pro + FROM ".TB_PRODUCT_ATTRIBUTE." + WHERE attr_value_id IN (".join(',', $query_attr_id).") + GROUP BY pro_id + HAVING num_pro = ".$count_filter." + LIMIT 10000 + "); + foreach ( $this->db->fetchAll($query) as $rs ) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND `id` IN (".join(', ', $product_filter_id_match).") " : " AND `id` = 0 " ; + + //xay lai url de back + if(!$this->objCategoryProduct) $this->objCategoryProduct = new CategoryProduct(); + + foreach($filter_attr_value_list as $value_id ){ + $att_name = $this->objCategoryProduct->atrValueName($value_id); + + $filterPath["attribute"][] = array( + "id" => $value_id, + "name" => $att_name, + ); + $filter_messages[] = [ + 'title' => $att_name, + 'reset' => Url::buildUrl($this->_request_url, ['filter' => join(FILTER_VALUE_SEPARATOR, remove_item_from_array($filter_condition["attribute"], $value_id))] ), + ]; + } + + } + + */ + + //- attribute values + if( array_key_exists("attribute", $filter_condition) && sizeof($filter_condition["attribute"])) { + + $filter_attr_value_list = $filter_condition["attribute"]; + + $attr_values_per_attribute = $this->groupAttributeValuesByAttributeId($filter_attr_value_list); + + $having_match_count = sizeof(array_keys($attr_values_per_attribute)); + + $attribute_info_list = $this->groupAttributeListInfo(array_keys($attr_values_per_attribute)); + + $attr_where_query = []; + foreach ($attr_values_per_attribute as $_att_id => $_att_value_list) { + // if same attribute : find products that match either ONE of these values (not match ALL values) + $_att_info = $attribute_info_list[$_att_id]; + if (!$_att_info) continue; + + // this attribute requires all products must have ALL selected values + if ($_att_info['value_match_all'] && sizeof($_att_value_list) > 1) { + + $_product_id_match_all = $this->getProductMatchAllAttributeValueIds($_att_value_list); + + if (sizeof($_product_id_match_all) > 0) { + $attr_where_query[] = " ( `id` IN (" . join(",", $_product_id_match_all) . ") ) "; + } else { + $attr_where_query[] = " ( `id` = -1 ) "; + } + + } else { + $attr_where_query[] = " ( SELECT DISTINCT `pro_id` FROM " . TB_PRODUCT_ATTRIBUTE . " WHERE (" . + join(" OR ", array_map(function ($_item) { return " `attr_value_id` = '" . intval($_item['id']) . "' "; }, $_att_value_list)) + . " ) ) "; + } + + } + + $product_filter_id_match = array(); + + if (sizeof($attr_where_query)) { + $query = $this->db->runQuery(" + SELECT `pro_id` , COUNT(*) AS num_pro + FROM ( " . join(" UNION ALL ", $attr_where_query) . " ) AS tpm_tb + GROUP BY pro_id + HAVING num_pro = " . $having_match_count . " + LIMIT 10000 + "); + foreach ($this->db->fetchAll($query) as $rs) { + $product_filter_id_match[] = $rs["pro_id"]; + } + } + + $where_query[] = (sizeof($product_filter_id_match)) ? " AND " . TB_PRODUCT_LIGHT . ".`id` IN (" . join(', ', $product_filter_id_match) . ") " : " AND " . TB_PRODUCT_LIGHT . ".`id` = 0 "; + + } + + + //- price range + if( + isset($filter_condition["price"]) + && sizeof($filter_condition["price"]) + && ($filter_condition["price"]['min'] > 0 || $filter_condition["price"]['max'] > 0) + ) { + + //limit by price range + $maxPrice = DataClean::makeInputSafe($filter_condition["price"]['max'], DataType::INTEGER); + $minPrice = DataClean::makeInputSafe($filter_condition["price"]['min'], DataType::INTEGER); + + $price_range_query = ''; + if($maxPrice > 0 && $minPrice > 0){ + $price_range_query = " ( `price` BETWEEN '".$minPrice."' AND '".$maxPrice."' ) "; + + }else if($maxPrice > 0){ + $price_range_query = " `price` < '".$maxPrice."' "; + + }else if($minPrice > 0){ + $price_range_query = " `price` >='".$minPrice."' "; + } + + $where_query[] = " AND ". $price_range_query; + } + + // ------------ + + return array( join(" ", $where_query), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSearchModel.php b/inc/Hura8/Components/Product/Model/ProductSearchModel.php new file mode 100644 index 0000000..4e8a30e --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSearchModel.php @@ -0,0 +1,33 @@ + "tb_product.price", + "ranking" => "tb_product.ranking", + "status" => "tb_product.status", + "brand" => "tb_product.brand_id", + ]; + + private $fulltext_fields = [ + "category_keywords" => ["tb_product_category.title", ], + "product_keywords" => ["tb_product.sku", "tb_product.title", "tb_product.model", "tb_product.related_keywords"], + ]; + + + public function __construct() + { + parent::__construct( + "tb_product", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php b/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php new file mode 100644 index 0000000..7cf1ae1 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSpecGroupAttributeModel.php @@ -0,0 +1,65 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_product_spec_group_record."` WHERE `group_id` = ? ORDER BY `ordering` ASC ", + ['d'], [$group_id] + ); + + return $this->db->fetchAll($query); + } + + + + protected function updateGroupAttributeCount($group_id) { + $this->db->runQuery("UPDATE `".$this->tb_product_spec_group."` SET + `attribute_count` = (SELECT COUNT(*) FROM `".$this->tb_product_spec_group_record."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", ['d', 'd' ], [ $group_id, $group_id ]); + } + + protected function buildQueryCondition(array $condition) + { + // TODO: Implement buildQueryCondition() method. + } + + protected function buildOrderByCondition($order_by) + { + // TODO: Implement buildOrderByCondition() method. + } + + + protected function _buildQueryConditionExtend(array $extend_filter_conditions) : ?array + { + return null; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php b/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php new file mode 100644 index 0000000..5502134 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductSpecGroupModel.php @@ -0,0 +1,151 @@ +getSpecGroupAttribute($group_id); + } + + + protected function clearProductAttributes($product_id) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_product_attribute."` WHERE `pro_id` = ? ", + [ 'd' ], [ $product_id ] + ); + } + + + public function clearProductSpecGroup($product_id) { + + $this->clearProductAttributes($product_id); + + $product_spec_group_id = $this->getProductSpecGroupId($product_id); + + // update + $this->db->runQuery( + "UPDATE `".$this->tb_product."` SET + `spec_group_id` = 0 + WHERE `id` = ? LIMIT 1 ", + [ 'd' ], [ $product_id ] + ); + + $this->updateSpecGroupProductCount($product_spec_group_id); + + return true; + } + + + public function setProductSpecGroup($product_id, $new_group_id) + { + $current_group_id = $this->getProductSpecGroupId($product_id); + + if($current_group_id != $new_group_id) { + // update + $this->db->runQuery( + "UPDATE `".$this->tb_product."` SET + `spec_group_id` = ? + WHERE `id` = ? LIMIT 1 ", + [ 'd', 'd' ], [ $new_group_id, $product_id ] + ); + + $this->updateSpecGroupProductCount($new_group_id); + + $this->clearProductAttributes($product_id); + + return true; + } + + return false; + } + + + protected function updateSpecGroupProductCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_product_spec_group."` SET + `product_count` = (SELECT COUNT(*) FROM ".$this->tb_product." WHERE `spec_group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + [ 'd', 'd' ], [ $group_id, $group_id ] + ); + } + + + public function getProductSpecGroupInfo($product_id) + { + $product_spec_group_id = $this->getProductSpecGroupId($product_id); + + return ($product_spec_group_id) ? $this->getSpecGroupInfo($product_spec_group_id) : false ; + } + + + public function getProductSpecGroupId($product_id) + { + $query = $this->db->runQuery("SELECT `spec_group_id` FROM `".$this->tb_product."` WHERE `id` = ? LIMIT 1 ", ['d'], [$product_id]); + + if ($info = $this->db->fetchAssoc($query) ) { + return $info['spec_group_id'] ; + } + + return 0; + } + + + public function getSpecGroupInfo($group_id) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_product_spec_group."` WHERE `id` = ? ", ['d'], [$group_id]); + + return $this->db->fetchAssoc($query); + } + + + protected function buildQueryCondition(array $condition) + { + $catCondition = []; + + //Tim kiem theo tu khoa + if(isset($condition["q"]) && $condition["q"]){ + $catCondition[] = " AND `title` LIKE '%".$this->db->escape($condition["q"])."%' "; + } + + return join(" ", $catCondition); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_condition = ""; + $bind_types = []; + $bind_values = []; + + return [ + $where_condition, + $bind_types, + $bind_values + ]; + } + + +} diff --git a/inc/Hura8/Components/Product/Model/ProductVariantModel.php b/inc/Hura8/Components/Product/Model/ProductVariantModel.php new file mode 100644 index 0000000..022a9c0 --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductVariantModel.php @@ -0,0 +1,181 @@ +product_id = $product_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getProductVariantOption($product_id){ + $query = $this->db->runQuery("SELECT `variant_option` FROM `".$this->tb_product."` WHERE `id` = ? LIMIT 1", ['d'], [ $product_id ]) ; + + if($rs = $this->db->fetchAssoc($query)) { + return ($rs['variant_option']) ? \json_decode($rs['variant_option'], true) : null; + } + + return null; + } + + + //use a product's variant-option to create a sample, so next product can select without recreate from beginning + public function createVariantOptionSample($use_from_pro_id, $sample_title) { + + if( !$use_from_pro_id || strlen($sample_title) < 3 ) return false; + + $pro_variant_option = $this->getProductVariantOption($use_from_pro_id); + if(!$pro_variant_option) { + return false; + } + + $pro_variant_option_index = md5(\json_encode($pro_variant_option)); + + $check_duplicate = $this->db->runQuery( + "SELECT `id` FROM ".$this->tb_variant_option_sample." WHERE `variant_option_index` = ? LIMIT 1 ", + ['s'], [ $pro_variant_option_index ] + ); + + if($this->db->fetchAssoc($check_duplicate)) { + return false; + } + + // ok to save + $this->db->insert( + $this->tb_variant_option_sample, + [ + "title" => $sample_title, + "variant_option" => \json_encode($pro_variant_option), + "variant_option_index" => $pro_variant_option_index, + "create_time" => CURRENT_TIME, + ] + ); + + return true; + } + + public function getVariantOptionSample() { + $query = $this->db->runQuery("SELECT * FROM ".$this->tb_variant_option_sample." ORDER BY `id` DESC LIMIT 500 "); + + return $this->db->fetchAll($query); + } + + public function getProductVariantPriceRange(){ + $result = [ + "sale_price" => [ + "min" => 0, + "max" => 0, + ], + "market_price" => [ + "min" => 0, + "max" => 0, + ], + ]; + + $query = $this->db->runQuery( + " SELECT `sale_price`, `market_price`, `extend` FROM `".$this->tb_entity."` WHERE `product_id` = ? ", + ['d'], [$this->product_id] + ); + + foreach ( $this->db->fetchAll($query) as $info) { + + // find min + if($info["sale_price"] > 0 && ( $info["sale_price"] < $result["sale_price"]["min"] || $result["sale_price"]["min"] == 0 ) ) { + $result["sale_price"]["min"] = $info["sale_price"]; + } + + // find max + if($info["sale_price"] > 0 && $info["sale_price"] > $result["sale_price"]["max"] ) { + $result["sale_price"]["max"] = $info["sale_price"]; + } + + // market_price + $market_price = $info["market_price"]; + if($info['extend']) { + $extend = unserialize($info['extend']); + if(isset($extend['market_price'])) $market_price = clean_price($extend['market_price'], "vnd"); + } + + if($market_price > 0 && ( $market_price < $result["market_price"]["min"] || $result["market_price"]["min"] == 0 ) ) { + $result["market_price"]["min"] = $market_price; + } + + if($market_price > 0 && $market_price > $result["market_price"]["max"] ) { + $result["market_price"]["max"] = $market_price; + } + } + + return $result; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + $where_condition = " AND `product_id` = ? "; + $bind_types = ["d"]; + $bind_values = [$this->product_id]; + + return [ + $where_condition, + $bind_types, + $bind_values + ]; + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return " `ordering` DESC, `id` DESC "; + } + + protected function formatItemInList(array $item_info): array + { + $info = $item_info; + + if($item_info['attribute']) $info['attribute'] = \json_decode($item_info['attribute'], true); + + return $info; + } + + protected function updateProductVariantCount() { + $this->db->runQuery( + "UPDATE ".$this->tb_product." SET + `config_count` = ( SELECT COUNT(*) AS total FROM `".$this->tb_entity."` WHERE `product_id` = ? AND `status` = 1 ) + WHERE `id` = ? ", + ['d', 'd'], [ $this->product_id, $this->product_id] + ) ; + } + + protected function formatItemInfo(array $item_info) : ?array + { + $info = $item_info; + + if($info['attribute']) $info['attribute'] = \json_decode($info['attribute'], true); + + return $info; + } + + +} diff --git a/inc/Hura8/Database/ConnectDB.php b/inc/Hura8/Database/ConnectDB.php new file mode 100644 index 0000000..457cbdc --- /dev/null +++ b/inc/Hura8/Database/ConnectDB.php @@ -0,0 +1,503 @@ +debug = $debug; // (defined('ENABLE_DB_DEBUG') && ENABLE_DB_DEBUG); + if($db_id) $this->db_id = $db_id; + + if(!sizeof(self::$cnn_props)) { + self::$cnn_props = self::setConnectionSettings(); + } + } + + private static function setConnectionSettings() { + return static::getCache("getConnectionSettings", function (){ + $db_file = CONFIG_DIR.'/db.php'; + if(file_exists($db_file)) { + return include $db_file; + } + + return []; + }); + } + + /** + * @param string $table + * @return array + */ + public function getTableDefaultItemInfo($table) { + $column_info = $this->getTableInfo($table); + $default_info = []; + foreach ($column_info as $field => $info) { + $default_info[$field] = $info['COLUMN_DEFAULT']; + } + + return $default_info; + } + + + //28-07-2015 get all columns of a table + public function getTableInfo($table) { + + return self::getCache('getTableInfo-'.$table, function () use ($table) { + + $db_props = $this->getConnectionProps(); + if(!$db_props) { + return false; + } + + $database = $db_props['db'] ?? null; + if(!$database) { + return false; + } + + $query = $this->runQuery( + "SELECT + `COLUMN_NAME` , + COLUMN_DEFAULT, + ORDINAL_POSITION, + COLUMN_DEFAULT, + DATA_TYPE, + CHARACTER_MAXIMUM_LENGTH, + COLUMN_TYPE, + COLUMN_KEY, + EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE `TABLE_SCHEMA` = ? AND`TABLE_NAME` = ? ", + ['s', 's'], [$database, $table] + ); + + $output = []; + foreach( $this->fetchAll($query) as $row){ + $output[$row['COLUMN_NAME']] = $row; + } + + return $output; + }); + } + + + protected function setTrace($query, $start_time, $msg = ''){ + static::$traces[] = [ + "msg" => $msg, + "query" => $query, + "start_time" => $start_time, + "total_time" => round($this->getCurrentTime() - $start_time, 5), + ]; + } + + + public static function getTraces() { + $query_count = sizeof(static::$traces); + $query_time = array_sum(array_map(function ($item){ return $item['total_time'];}, static::$traces)) ; + return [ + 'query_count' => $query_count, + 'query_time' => $query_time, + 'list' => static::$traces, + ]; + } + + + private function __clone(){ + // + } + + + /** + * @param string $db_id + * @param bool $debug + * @return iConnectDB + */ + public static function getInstance(string $db_id = '', bool $debug = false) { + if( ! $db_id ) $db_id = 'main'; + + if(!isset(ConnectDB::$instance[$db_id])) { + ConnectDB::$instance[$db_id] = new self($db_id, $debug); + } + + return ConnectDB::$instance[$db_id]; + } + + + //close all connections + public static function close() { + foreach (ConnectDB::$instance as $db_id => $cnn) { + $cnn->disconnect(); + } + // reset + ConnectDB::$instance = []; + } + + + public function isConnected(): bool { + return ($this->connection instanceof \mysqli); + } + + + private function getConnectionProps() { + return self::$cnn_props[$this->db_id] ?? null; + } + + + private function connect() + { + // already connect + if($this->connection instanceof \mysqli) { + return true; + } + + // connection props not set + $db_props = $this->getConnectionProps(); + if(!$db_props) { + return false; + } + + $host = $db_props['host'] ?? null; + $user = $db_props['user'] ?? null; + $pass = $db_props['pass'] ?? null; + $database = $db_props['db'] ?? null; + $db_charset = $db_props['charset'] ?? 'latin1'; + + if(!$host || !$user || !$pass || !$database) { + return false; + } + + try { + + //create the object + $cnn = \mysqli_init(); + $cnn->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5); + //specify the read timeout + if (!defined('MYSQLI_OPT_READ_TIMEOUT')) { + define ('MYSQLI_OPT_READ_TIMEOUT', 11); + } + $cnn->options(MYSQLI_OPT_READ_TIMEOUT, 10); + + if($cnn->real_connect($host, $user, $pass)){ + $cnn->select_db($database); + $cnn->query("SET NAMES ".$db_charset); // UTF8|latin1 + $cnn->query("SET sql_mode=''"); //for old version to work with mysql 5.7 + //mysqli_select_db($this->connection, $database); + //mysqli_query($this->connection, "SET NAMES UTF8") ;//set utf8 if database using utf-8 encode + //mysqli_query($this->connection, "SET NAMES latin1") ;//set back to latin1 + //mysqli_query($this->connection, "SET sql_mode=''") ;//for old version to work with mysql 5.7 + $this->connection = $cnn; + + return true; + } + + //throw new \Exception('Unable to connect'); + return false; + + }catch (\Exception $e) { + //die($e->getMessage()); + echo $e->getMessage(); + + return false; + } + } + + + public function disconnect() + { + if($this->connection instanceof \mysqli) { + $this->connection->close(); + $this->connection = null; + } + } + + + public function ping() { + if($this->connection) { + $this->connection->ping(); + } + } + + + /** + * @param string $query + * @param array $bind_types + * @param array $bind_values + * @param bool $get_affected_row_or_id get the affected row or newly insert-id from the query, default return the mysqli_result + * @return \mysqli_result | false | int + */ + public function runQuery($query, array $bind_types=[], array $bind_values=[], $get_affected_row_or_id = false) + { + $start_time = $this->getCurrentTime(); + + // connect on demand + if(!$this->connect()) { + if($this->debug) $this->setTrace($query, $start_time, 'runQuery: Connection fails'); + return false; + } + + $stmt = $this->connection->prepare( $query ); + if(!$stmt) { + //throw new \Exception($stmt->error); + //throw new \Exception($this->connection->error); + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: '.$this->connection->error); + die("runQuery error: ".$this->connection->error.". Query: ".$query); + } + return false; + } + + if(sizeof($bind_types) && sizeof($bind_types) == sizeof($bind_values)) { + if(!$stmt->bind_param(join('', $bind_types), ...$bind_values)) { + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: bind_param '.$this->connection->error); + die('runQuery: bind_param '.$this->connection->error); + } + return false; + } + } + + + if(!$stmt->execute()) { + if($this->debug) { + $this->setTrace($query, $start_time, 'runQuery: execute '.$this->connection->error); + die("runQuery error: ".$this->connection->error.". Query: ".$query); + } + return false; + } + + if($this->debug) $this->setTrace($query, $start_time, ''); + + if($get_affected_row_or_id) { + return $stmt->insert_id ?: $stmt->affected_rows; + } + + // default + return $stmt->get_result(); + } + + + private function getCurrentTime(){ + return microtime(true); + } + + + /** + * @deprecated Unsafe, make sure all variables are properly escaped + */ + public function multi_query(array $array_query){ + + if(!sizeof($array_query)) return false; + + $start_time = $this->getCurrentTime(); + + // late connect + if(!$this->connect()) { + if($this->debug) $this->setTrace(join("; ", $array_query), $start_time, 'multi_query: Connection fails'); + return false; + } + + $multi_query = join(";", $array_query); + $multi_query = str_replace("\n", " ", $multi_query); + //remove double ; if exist + $multi_query = preg_replace("/;(\s+)?;/i", ";", $multi_query); + + $set = 0; + $list = []; + if(mysqli_multi_query($this->connection, $multi_query)){ + // flush multi_queries, so any query after can run + do { + $set ++; + /* store first result set */ + if ($result = mysqli_store_result($this->connection)) { + /*while ($row = mysqli_fetch_assoc($result)) { + $list[$set][] = $row; + }*/ + $list[$set] = $this->fetchAll($result); + mysqli_free_result($result); + } + } while (mysqli_more_results($this->connection) && mysqli_next_result($this->connection)); + } + + if($this->debug) $this->setTrace(join(";", $array_query), $start_time); + + return $list; + } + + + // a simple utitily method which we use very frequently + public function getItemInfo($table_name, $id_value, $id_field = 'id'){ + return $this->select( + $table_name, + [], + [ + $id_field => ["=", $id_value], + ], + '', + 1 + ); + } + + + //$table_name = 'table_abc' + /*$fields = array( + "field_name_1", + );*/ + /*$where_condition = array( + "field_name_1" => ["=", "value_1"], + "field_name_2" => ["LIKE", "value_1"], + );*/ + public function select($table_name, $fields =[], $where_condition=[], $order_by = '', $limit='1'){ + + // connect on demand + if(!$this->connect()) { + if($this->debug) $this->setTrace($table_name, 0, 'select: Connection fails'); + return false; + } + + $start_time = $this->getCurrentTime(); + + $check_fields = []; + if(sizeof($fields) && $fields[0] != '*') $check_fields = $fields; + $check_fields = array_merge($check_fields, array_keys($where_condition)); + + $cols_in_tables = $this->filterColumnInTable($table_name, $check_fields); + + $bind_types = []; + $bind_values = []; + $condition_list = []; + $permitted_operator = [">", ">=", "<", "<=", "!=", "=", "BETWEEN", "LIKE", "IS"]; // https://dev.mysql.com/doc/refman/8.0/en/non-typed-operators.html + foreach ($where_condition as $field_name => $operator_value) { + + // if any field invalid, stop the query + if(!in_array($field_name, $cols_in_tables)) { + echo "Invalid field_name ".$field_name; + return false; + }; + + // invalidate + if(!is_array($operator_value) || sizeof($operator_value) != 2) continue; + + $operator = $operator_value[0]; + if(!in_array($operator, $permitted_operator)) continue; + + $value = $operator_value[1]; + + $condition_list[] = " `".$field_name."` ".$operator." ? "; + $bind_types[] = (is_int($value)) ? 'd' : 's'; + $bind_values[] = $value; + } + + $select_fields = (sizeof($fields) > 0) ? array_map(function ($field){ return "`".$field."`";}, $fields) : ["*"]; + $order_by_query = ($order_by) ? "ORDER BY ".preg_replace("/[^a-z0-9\s_]/i", "", $order_by) : ""; + $safe_limit = ($limit) ? preg_replace("/[^0-9\,\s]/i", "", $limit) : "1"; + $where_condition = (sizeof($condition_list)) ? " AND " . join(" AND ", $condition_list) : ""; + + $query = "SELECT ".join(',', $select_fields)." + FROM `".$table_name."` WHERE 1 ".$where_condition." + ".$order_by_query." + LIMIT ".$safe_limit; + + $stmt = $this->connection->prepare( $query ); + if(!$stmt) return false; + + if(sizeof($bind_types)) { + $stmt->bind_param(join('', $bind_types), ...$bind_values); + } + $stmt->execute(); + $result = $stmt->get_result(); + + if($this->debug) $this->setTrace($query, $start_time); + + return ($safe_limit == '1') ? $result->fetch_assoc() : $result->fetch_all(); + } + + + protected function filterColumnInTable($table_name, array $list_check_fields) { + $table_columns = $this->getTableInfo($table_name); + + $safe_fields = []; + foreach ($list_check_fields as $field_name) { + // make sure the field exists + if(!array_key_exists($field_name, $table_columns)) continue; + + $safe_fields[] = $field_name; + } + + return $safe_fields; + } + + + //fetch array + /** + * @deprecated + */ + public function fetchArray(\mysqli_result $resource){ + return $resource->fetch_array(); + } + + /** + * @param \mysqli_result | false $resource + * @return array|null + */ + public function fetchAssoc( $resource){ + if(!$resource) { + //die("Resource failed: code fetchAssoc 122"); + return null; + } + + return $resource->fetch_assoc(); + } + + /** + * @param \mysqli_result | false $resource + * @return array|mixed + */ + public function fetchAll($resource){ + + if(!$resource) return []; + + //some hosting does not enable this function, so this is the work-around + if( ! function_exists("mysqli_fetch_all")) { + $item_list = array(); + while ($item = $this->fetchAssoc($resource)) { + $item_list[] = $item; + } + + return $item_list; + } + + return ($resource) ? $resource->fetch_all(MYSQLI_ASSOC) : []; + } + + //affected rows from last query + public function get_affected_rows(){ + return $this->connection->affected_rows; + } + + + public function getErrorNo(){ + return mysqli_errno($this->connection); + } + + + public function __destruct() { + $this->disconnect(); + } +} diff --git a/inc/Hura8/Database/MysqlValue.php b/inc/Hura8/Database/MysqlValue.php new file mode 100644 index 0000000..58a5762 --- /dev/null +++ b/inc/Hura8/Database/MysqlValue.php @@ -0,0 +1,16 @@ +value = $value; + } + + public function getValue() { + return $this->value; + } +} diff --git a/inc/Hura8/Database/iConnectDB.php b/inc/Hura8/Database/iConnectDB.php new file mode 100644 index 0000000..ee3151e --- /dev/null +++ b/inc/Hura8/Database/iConnectDB.php @@ -0,0 +1,55 @@ + ["=", "value_1"], + "field_name_2" => ["LIKE", "value_1"], + );*/ + public function select($table_name, $fields =[], $where_condition=[], $order_by = '', $limit='1'); + + + /** + * @param \mysqli_result | false $resource + * @return array|null + */ + public function fetchAssoc( $resource); + + /** + * @param \mysqli_result | false $resource + * @return array|mixed + */ + public function fetchAll($resource); + + public function getErrorNo(); + +} diff --git a/inc/Hura8/Interfaces/APIResponse.php b/inc/Hura8/Interfaces/APIResponse.php new file mode 100644 index 0000000..a5a2f21 --- /dev/null +++ b/inc/Hura8/Interfaces/APIResponse.php @@ -0,0 +1,42 @@ +errCode = $errCode; + $this->msg = $msg; + $this->data = $data; + } + + /** + * @return int + */ + public function getCode() { + return $this->errCode; + } + + /** + * @return mixed if status = 'error' + */ + public function getMsg() { + return $this->msg; + } + + /** + * @return mixed if status = 'ok' + */ + public function getData() { + return $this->data; + } + +} diff --git a/inc/Hura8/Interfaces/AppResponse.php b/inc/Hura8/Interfaces/AppResponse.php new file mode 100644 index 0000000..e48eac4 --- /dev/null +++ b/inc/Hura8/Interfaces/AppResponse.php @@ -0,0 +1,41 @@ +status = $status; + $this->msg = $msg; + $this->data = $data; + } + + /** + * @return string which is 'ok' or 'error' + */ + public function getStatus() { + return $this->status; + } + + /** + * @return mixed if status = 'error' + */ + public function getMsg() { + return $this->msg; + } + + /** + * @return mixed if status = 'ok' + */ + public function getData() { + return $this->data; + } + +} diff --git a/inc/Hura8/Interfaces/EntityType.php b/inc/Hura8/Interfaces/EntityType.php new file mode 100644 index 0000000..237f93b --- /dev/null +++ b/inc/Hura8/Interfaces/EntityType.php @@ -0,0 +1,151 @@ + $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, + ];*/ + + $this->file_name = $file_info['file_name'] ?? '' ; + $this->public_path = $file_info['public_path'] ?? '' ; + $this->local_path = $file_info['local_path'] ?? '' ; + $this->mime_type = $file_info['mime_type'] ?? '' ; + $this->file_size = $file_info['file_size'] ?? 0 ; + $this->file_ext = $file_info['file_ext'] ?? '' ; + $this->width = $file_info['width'] ?? 0 ; + $this->height = $file_info['height'] ?? 0 ; + + } + + +} diff --git a/inc/Hura8/Interfaces/FileHandleResponse.php b/inc/Hura8/Interfaces/FileHandleResponse.php new file mode 100644 index 0000000..360cf06 --- /dev/null +++ b/inc/Hura8/Interfaces/FileHandleResponse.php @@ -0,0 +1,19 @@ + 2000000, + "tb_product.title" => "Máy tính ABC", + "tb_category.price" => "Máy tính", + ]; + * @return bool + */ + public function updateItem($item_id, array $table_field_values = []): AppResponse; + + /** + * @description delete item from index + * @param $item_id + */ + public function deleteItem($item_id): AppResponse; + + /** + * @description delete items from index + * @param $item_list_ids + */ + public function deleteItems(array $item_list_ids): AppResponse; + + /** + * @param string $keyword + * @param array $field_filters + * @param array $fulltext_fields + * @param int $limit_result + * @return array[int] + */ + public function find(string $keyword, array $field_filters = [], array $fulltext_fields = [], int $limit_result = 2000) : array; + +} diff --git a/inc/Hura8/Interfaces/iShippingCost.php b/inc/Hura8/Interfaces/iShippingCost.php new file mode 100644 index 0000000..87d7c76 --- /dev/null +++ b/inc/Hura8/Interfaces/iShippingCost.php @@ -0,0 +1,15 @@ +client = new Client([ + // Base URI is used with relative requests + 'base_uri' => $endpoint, + // You can set any number of default request options. + 'timeout' => $timeout, + ]); + } + + + public function post($path, array $parameters) { + return $this->call_service("post", $path, $parameters); + } + + + public function get($path, array $parameters) { + return $this->call_service("get", $path, $parameters); + } + + + public function setHeaders(array $headers) { + // Authorization: + /*$headers = [ + 'Authorization' => "Basic ". base64_encode($this->api_key), + ];*/ + $this->headers = $headers; + } + + //ref: http://docs.guzzlephp.org/en/stable/quickstart.html#query-string-parameters + // make a consistent call with the same $payload format: ["key" => "value", ...] + /* + return json { + "errCode" => 1|0, (1=error, 0=success) + "msg" => "error_message" | "", + }*/ + protected function call_service($http_method, $path, array $payload) { + + try { + + // get + if( $http_method == 'get' ) { + + $response = $this->client->request('GET', $path, [ + 'headers' => $this->headers, + 'query' => $payload + ]); + } + + // post + else { + + $request_options = $this->buildPostRequestOptions($this->headers, $payload); + $response = $this->client->request('POST', $path, $request_options); + } + + return [ + "errCode" => "0", + "msg" => $response->getBody()->getContents(), + ]; + + } + catch (ServerException $e) { + $response = $e->getResponse(); + //$errors = \json_decode($response->getBody()->getContents(), true); + return [ + "errCode" => 1, + "msg" => $response->getBody()->getContents(), + ]; + } + catch (ClientException $e) { + $response = $e->getResponse(); + //$errors = \json_decode($response->getBody()->getContents(), true); + return [ + "errCode" => 2, + "msg" => $response->getBody()->getContents(), + ]; + } + catch (\Exception $e) { + return [ + "errCode" => 3, + "msg" => "Exception: ".$e->getMessage(), + ]; + } + } + + + // ref: http://docs.guzzlephp.org/en/stable/request-options.html + // form_params cannot be used with the multipart option. You will need to use one or the other. + // Use form_params for application/x-www-form-urlencoded requests, and multipart for multipart/form-data requests. + protected function buildPostRequestOptions(array $headers, array $payload) { + $content_type = isset($headers["Content-Type"]) ? $headers["Content-Type"] : ""; + + if($content_type == "application/x-www-form-urlencoded") { + return [ + 'headers' => $headers, + 'form_params' => $payload, + //'debug' => true + ]; + } + + if($content_type == "application/json") { + return [ + 'headers' => $headers, + 'json' => $payload, + //'body' => json_encode($payload), + ]; + } + + + // reformat the payload for multipart + $multipart_request = []; + foreach ($payload as $key => $value) { + if( ! $key) continue; + + $multipart_request[] = [ + 'name' => $key, + 'contents' => (is_array($value)) ? json_encode($value) : $value, + ]; + } + + // multipart/form-data + return [ + 'headers' => $headers, + 'multipart' => $multipart_request, + ]; + + } +} diff --git a/inc/Hura8/System/CDNFileUpload.php b/inc/Hura8/System/CDNFileUpload.php new file mode 100644 index 0000000..421c2eb --- /dev/null +++ b/inc/Hura8/System/CDNFileUpload.php @@ -0,0 +1,152 @@ + 'media/product', // media/product + 'article' => 'media/news', // media/news + 'brand' => 'media/brand', + 'category' => 'media/category', + 'banner' => 'media/banner', + 'lib' => 'media/lib/', + 'user_upload' => 'media/user_upload', + ]; + + public function __construct($user_id) { + $this->user_id = $user_id; + } + + /** + * @description: an implementation of start method to upload an item's image + * @param $item_type string + * @param $content string + * @param $file_name string + * @return array | boolean + */ + public function uploadFile($item_type, $file_name, $content, $set_target_path = '') + { + //$file_name = substr(strrchr($img_url, "/"), 1); + //$content = get_url_content($img_url); + + $header_setting = [ + // 'Authorization' => "Basic ". base64_encode(API_KEY), + ]; + + if($set_target_path) { + $target_path = $set_target_path; + }else{ + $target_path = (isset($this->target_path_item_type_mapping[$item_type])) ? $this->target_path_item_type_mapping[$item_type] : ''; + } + + $multipart_settings = [ + [ + 'name' => 'upload_method', + 'contents' => 'content', + ], + [ + 'name' => 'file_name', + 'contents' => $file_name, + ], + [ + 'name' => 'target_path', + 'contents' => $target_path, + ], + [ + 'name' => 'file_content', + 'contents' => $content, + ], + + //for upload server + [ + 'name' => 'uid', + 'contents' => $this->user_id, + ], + [ + 'name' => 'time', + 'contents' => CURRENT_TIME, + ], + [ + 'name' => 'token', + 'contents' => $this->createToken($this->user_id, CURRENT_TIME), + ], + [ + 'name' => 'item_type', + 'contents' => $item_type , + ], + ]; + + return $this->start($header_setting, $multipart_settings); + } + + /* + let settings = { + element : '',//element id to click on + holder : '', //id file holder + form_name : '', + item_type : '', + item_id : '', + file_type : '', //what is the file type: doc, photo, video ? + file_field : '', //what field in the item table this file is used for ? + resize : '200,400,600' //sizes to resize + square: '200,400', //sizes to be square + keep_width : '400', //sizes to keep the width + max_file: 1 + } + */ + + /** + * @description: a general method for upload + * @param array $header_setting + * @param array $multipart_settings + * @return array | boolean + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function start(array $header_setting = [], array $multipart_settings = []) { + $client = new \GuzzleHttp\Client(); + + $request = $client->request('POST', $this->upload_handle, [ + 'headers' => $header_setting, + 'multipart' => $multipart_settings + ]); + + return $request->getBody()->getContents(); + } + + + // return string | boolean + protected function getAssetType($file_name) { + $ext = strtolower(strrchr($file_name, ".")); + + if( $ext == '.css') return 'css'; + + if( $ext == '.js') return 'js'; + + if( \in_array($ext, ['.jpg', '.jpeg', '.gif', '.png', '.ico'])) return 'image'; + + return false; + } + + + // this salt and createToken must be the same ones as in Hura8/Base/CDNFileUploadHandle + // and different per project (or else others will use it to upload forbidden files on our clients' hosting) + const SECURITY_SALT = 'ahss@3asdaaSDFSD'; + + protected function createToken($id, $time) { + return sha1(join("-", [$id , $time , static::SECURITY_SALT])); + } + +} + diff --git a/inc/Hura8/System/CDNFileUploadHandle.php b/inc/Hura8/System/CDNFileUploadHandle.php new file mode 100644 index 0000000..c4844e6 --- /dev/null +++ b/inc/Hura8/System/CDNFileUploadHandle.php @@ -0,0 +1,275 @@ + 2000000,//bytes ~ 1MB + 'allowed_file_types' => [ + 'image' => ['.jpeg', '.jpg', '.gif', '.png'], + 'script' => ['.css', '.js'], + ], + + //2. uploaded by client + 'uid' => '', + 'token' => '', + 'time' => '', + 'item_type' => 'product', // product|article|media + 'target_path' => '', + 'upload_method' => 'content', //file || content + "file_content" => "", //only needed if upload_method = content + "file_name" => "",//only needed if upload_method = content + ]; + private $tmp_file_prop = null; //array, temporary file's props in tmp folder + private $accepted_file_input_names = [ + "file", // + //"qqfile", //name use for files uploaded through FineUploader library + ]; + private $user_tmp_dir = ''; + + public function __construct(){ + + } + + protected function createToken($id, $time) { + return sha1(join("-", [$id , $time , static::SECURITY_SALT])); + } + + public function start() { + + $this->_get_post_info(); + + if(!$this->validateUser()) { + return $this->set_return_result('error', 'User failed to verify', []); + } + + //receive files + $file = $this->receive_file(); + + //return file-location to upload API to return to client application + return $this->set_return_result('success', 'Upload succeeded', $file ); + } + + protected function validateUser() { + return ($this->config['token'] == $this->createToken($this->config['uid'], $this->config['time'])); + } + + protected function _get_post_info() { + $expected_keys = [ + 'upload_method', + 'file_name', + 'target_path', + 'file_content', + 'uid', + 'time', + 'token', + 'item_type', + ]; + + foreach ($expected_keys as $key) { + $this->config[$key] = isset($_POST[$key]) ? $_POST[$key] : null; + } + } + + + //return boolean + protected function remove_tmp_file() { + $this->deleteDirectory($this->user_tmp_dir); + + return true; + } + + //return boolean + protected function validate_file() { + //$this->tmp_file_prop + //validate file size + if($this->config['max_file_size'] && $this->config['max_file_size'] < $this->tmp_file_prop['size']) { + return 'Size too large'; + } + + //validate allowed extension: allow image but upload .docx files + $has_extension = false; + $file_type = ''; + foreach ( $this->config['allowed_file_types'] as $group => $group_ext ) { + if(in_array( $this->tmp_file_prop['ext'], $group_ext )) { + $has_extension = true; + $file_type = $group; + break ; + } + } + if( ! $has_extension ) return "File type not allowed"; + + //validate claimed extension: claim image but not actual image + $full_file_path = $this->tmp_file_prop['tmp_location'] . DIRECTORY_SEPARATOR . $this->tmp_file_prop['name']; + if( $file_type == 'image' && ! v::image()->validate( $full_file_path )) { + return "Not actual image"; + } + + return ''; + } + + + //return mixed : array $tmp_file_prop or false + protected function receive_file() { + + //check upload method + if( $this->config['upload_method'] == 'content' ) { + //upload by content - file created on server + $file_ext = $this->get_ext($this->config['file_name']); + $upload_folder = $this->create_upload_folder() ; + + if( ! $this->move_file($upload_folder)) { + return false; + } + + return [ + "ext" => $file_ext, + "name" => $this->config['file_name'], + "folder" => $upload_folder, + "size" => strlen($this->config['file_content']), + ]; + + } else { + + //upload by file + //get list of files to be uploaded + $file_uploaded = null; + foreach ($this->accepted_file_input_names as $_name) { + if(isset($_FILES[$_name])) { + $file_uploaded = $_FILES[$_name]; + break; + } + } + + if ( ! $file_uploaded ) return false; + + //if file name too long, then error + if ( strlen($file_uploaded['name']) > 225 ) return false; + + //move file to tmp folder + $file_ext = $this->get_ext($file_uploaded['name']); + $upload_folder = $this->create_upload_folder() ; + + if( ! $this->move_file($upload_folder) ){ + return false; + } + + return [ + "ext" => $file_ext, + "name" => $this->config['file_name'], + "folder" => $upload_folder, + "size" => $file_uploaded['size'], + ]; + } + } + + + protected function set_return_result($status = 'success', $message = 'Upload success', $content = []) { + return [ + "status" => $status, + "message" => $message, + "files" => $content, + ]; + } + + //create upload folder for user: use current date, user id or app id based on $this->config + private function create_upload_folder() { + //$build_folder = [$this->tmp_dir]; + /*$user_id = (isset($this->config['user_id'])) ? intval($this->config['user_id']) : null; + if($user_id) { + $build_folder[] = $user_id; + } + $build_folder[] = date("Ymd");*/ + + //rebuild upload folder + $build_folder = array_filter(explode("/", $this->config['target_path'])); + $folder = join("/", $build_folder); + $this->createDir($folder); + + return $folder; + } + + //get a file's extension + private function get_ext($fileName){ + return strtolower(strrchr($fileName, '.')); + } + + // check if find is image + private function isImage($fileName) { + return (in_array( $this->get_ext($fileName), ['.jpeg', '.jpg', '.gif', '.png'] )); + } + + + //for security reason: only allow a-z0-9_- in file name (no other dot . except for the extension) + //example: bad-file.php.js -> bad-filephp.js + private function rename_file($uploaded_file_name, $ext) { + $new_name = preg_replace("/[^a-z0-9_\-]/i", "", str_replace( strrchr($uploaded_file_name, '.'), "", $uploaded_file_name ) ) . $ext; + + return strtolower($new_name); + } + + + private function move_file($folder) { + $new_file = $folder . '/' . $this->config['file_name']; + + if( $this->config['upload_method'] == 'content' ) { + // image upload by content + if( $this->isImage($new_file) ) { + //Store in the filesystem. + $fp = fopen($new_file, "w+"); + $status = fwrite($fp, $this->config['file_content']); + fclose($fp); + + return ($status !== false); + } + // file upload + else if( file_put_contents($new_file, $this->config['file_content'])){ + return true; + } + } + + return false; + } + + + protected function createDir($path, $folder_permission = 0750){ + if(file_exists($path)) { + return true; + } + + return mkdir($path, $folder_permission, true); + } + + protected function deleteDirectory($dirPath) { + if (!is_dir($dirPath)) { + return false; + } + + $objects = scandir($dirPath); + foreach ($objects as $object) { + if ($object != "." && $object !="..") { + if (filetype($dirPath . DIRECTORY_SEPARATOR . $object) == "dir") { + $this->deleteDirectory($dirPath . DIRECTORY_SEPARATOR . $object); + } else { + unlink($dirPath . DIRECTORY_SEPARATOR . $object); + } + } + } + + return rmdir($dirPath); + } + +} diff --git a/inc/Hura8/System/Cache.php b/inc/Hura8/System/Cache.php new file mode 100644 index 0000000..a9b3b89 --- /dev/null +++ b/inc/Hura8/System/Cache.php @@ -0,0 +1,100 @@ + 10, + //'retry_interval' => 2, + /* + 'persistent' => 1, + 'persistent_id' => null, + 'timeout' => 10, + + 'tcp_keepalive' => 0, + 'lazy' => null, + 'redis_cluster' => false, + 'redis_sentinel' => null,*/ + ] ); + + //var_dump($redisConnection->isConnected()); + + }catch (\Exception $exception) { + //echo $exception->getMessage(); + $redisConnection = false; + } + + if(!$redisConnection) { + static::$redisCache = false; + return false; + } + + static::$redisCache = new RedisAdapter( + // the object that stores a valid connection to your Redis system + $redisConnection, + + // the string prefixed to the keys of the items stored in this cache + $namespace = '', + + // the default lifetime (in seconds) for cache items that do not define their + // own lifetime, with a value 0 causing items to be stored indefinitely (i.e. + // until RedisAdapter::clear() is invoked or the server(s) are purged) + $defaultLifetime = 10 + ); + + return static::$redisCache; + + } + + +} diff --git a/inc/Hura8/System/Config.php b/inc/Hura8/System/Config.php new file mode 100644 index 0000000..a9e8a79 --- /dev/null +++ b/inc/Hura8/System/Config.php @@ -0,0 +1,208 @@ + 'Hình sản phẩm', + ]; + } + + return include $config_image_type_file; + } + + + public static function getProductUnitList() { + return static::getCache("getProductUnitList", function (){ + $config_file = CONFIG_DIR . "/client/product.unit.php"; + if(file_exists($config_file)) { + return include $config_file; + } + + return []; + }); + } + + + public static function getLanguageCount(){ + $language_list = static::getLanguageConfig(); + $count = sizeof($language_list); + return ($count > 1) ? $count : 1; //always have at least 1 language + } + + + public static function getLanguageConfig(){ + return static::getCache('getLanguageConfig', function (){ + $config_file = CONFIG_DIR . "/client/language_enable.php"; + if(!file_exists($config_file)) { + return []; + } + + return include $config_file; + }); + } + + + public static function getClientBuildConfig() { + return static::getCache("getClientBuildConfig", function (){ + $config_file = CONFIG_DIR . '/build/store.config.php'; + if(file_exists($config_file)) { + return include $config_file; + } + + return []; + }); + } + + +} diff --git a/inc/Hura8/System/Constant.php b/inc/Hura8/System/Constant.php new file mode 100644 index 0000000..67d8d2d --- /dev/null +++ b/inc/Hura8/System/Constant.php @@ -0,0 +1,106 @@ + $title) { + $extended_types[] = preg_replace("/[^a-z0-9_\-]/i", "", $key); + } + } + + $list = []; + try { + $reflectionClass = new \ReflectionClass(new \Hura8\Interfaces\EntityType()); + foreach ($reflectionClass->getConstants() as $handle => $value) { + $list[] = $value; + } + + // add extend + foreach ($extended_types as $type) { + if(!in_array($type, $list)) { + $list[] = $type; + } + } + + } catch (\ReflectionException $e) { + // + } + + return $list; + }); + } + + + // list of languages can be enabled + public static function languagePermitList() { + return [ + "vi" => "Tiếng Việt", // default + "en" => "English", + ]; + } + + + public static function mobileProviderPrefixList() { + return static::getCache('mobileProviderPrefixList', function () { + return include static::$constant_dir. '/mobile_provider.php'; + }); + } + + + public static function customerStageList() { + return static::getCache('customerStageList', function () { + return include static::$constant_dir. '/customer_stage_list.php'; + }); + } + + + public static function saluteList() { + return static::getCache('genderList', function () { + return include static::$constant_dir. '/salute_list.php'; + }); + } + + + public static function genderList() { + return static::getCache('genderList', function () { + return include static::$constant_dir. '/gender_list.php'; + }); + } + + + public static function industryList() { + return static::getCache('industryList', function () { + return include static::$constant_dir. '/industry_list.php'; + }); + } + + + public static function maritalStatusList() { + return static::getCache('maritalStatusList', function () { + return include static::$constant_dir. '/marital_status_list.php'; + }); + } + +} diff --git a/inc/Hura8/System/Controller/DomainController.php b/inc/Hura8/System/Controller/DomainController.php new file mode 100644 index 0000000..b239898 --- /dev/null +++ b/inc/Hura8/System/Controller/DomainController.php @@ -0,0 +1,124 @@ +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(); + } +} diff --git a/inc/Hura8/System/Controller/EntityPermissionController.php b/inc/Hura8/System/Controller/EntityPermissionController.php new file mode 100644 index 0000000..1dd7721 --- /dev/null +++ b/inc/Hura8/System/Controller/EntityPermissionController.php @@ -0,0 +1,43 @@ +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; + } + + +} diff --git a/inc/Hura8/System/Controller/SettingController.php b/inc/Hura8/System/Controller/SettingController.php new file mode 100644 index 0000000..d1d4e12 --- /dev/null +++ b/inc/Hura8/System/Controller/SettingController.php @@ -0,0 +1,144 @@ +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 = "_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); + } + +} diff --git a/inc/Hura8/System/Controller/UrlManagerController.php b/inc/Hura8/System/Controller/UrlManagerController.php new file mode 100644 index 0000000..dc406b6 --- /dev/null +++ b/inc/Hura8/System/Controller/UrlManagerController.php @@ -0,0 +1,242 @@ +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); + } + + +} diff --git a/inc/Hura8/System/Controller/ViewHistoryController.php b/inc/Hura8/System/Controller/ViewHistoryController.php new file mode 100644 index 0000000..9b5368b --- /dev/null +++ b/inc/Hura8/System/Controller/ViewHistoryController.php @@ -0,0 +1,44 @@ + [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] : []; + } + +} diff --git a/inc/Hura8/System/Controller/WebUserController.php b/inc/Hura8/System/Controller/WebUserController.php new file mode 100644 index 0000000..fffe2f6 --- /dev/null +++ b/inc/Hura8/System/Controller/WebUserController.php @@ -0,0 +1,27 @@ +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; + } + + +} diff --git a/inc/Hura8/System/Controller/aAdminEntityBaseController.php b/inc/Hura8/System/Controller/aAdminEntityBaseController.php new file mode 100644 index 0000000..d87b6ee --- /dev/null +++ b/inc/Hura8/System/Controller/aAdminEntityBaseController.php @@ -0,0 +1,78 @@ +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); + } + + +} diff --git a/inc/Hura8/System/Controller/aCategoryBaseController.php b/inc/Hura8/System/Controller/aCategoryBaseController.php new file mode 100644 index 0000000..333b7d2 --- /dev/null +++ b/inc/Hura8/System/Controller/aCategoryBaseController.php @@ -0,0 +1,304 @@ +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[] = "".$_cat_info['title']." "; + + $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; + } + + +} diff --git a/inc/Hura8/System/Controller/aERPController.php b/inc/Hura8/System/Controller/aERPController.php new file mode 100644 index 0000000..3575b90 --- /dev/null +++ b/inc/Hura8/System/Controller/aERPController.php @@ -0,0 +1,197 @@ +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()); + } + } + +} diff --git a/inc/Hura8/System/Controller/aEntityBaseController.php b/inc/Hura8/System/Controller/aEntityBaseController.php new file mode 100644 index 0000000..44ad562 --- /dev/null +++ b/inc/Hura8/System/Controller/aEntityBaseController.php @@ -0,0 +1,262 @@ +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; + + }); + + } + + +} diff --git a/inc/Hura8/System/Controller/aExcelDownloadController.php b/inc/Hura8/System/Controller/aExcelDownloadController.php new file mode 100644 index 0000000..d462c03 --- /dev/null +++ b/inc/Hura8/System/Controller/aExcelDownloadController.php @@ -0,0 +1,344 @@ + 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(); + } + +} diff --git a/inc/Hura8/System/Controller/aExcelUploadController.php b/inc/Hura8/System/Controller/aExcelUploadController.php new file mode 100644 index 0000000..5f545e2 --- /dev/null +++ b/inc/Hura8/System/Controller/aExcelUploadController.php @@ -0,0 +1,355 @@ + 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); + } + +} diff --git a/inc/Hura8/System/Controller/aPublicEntityBaseController.php b/inc/Hura8/System/Controller/aPublicEntityBaseController.php new file mode 100644 index 0000000..c305542 --- /dev/null +++ b/inc/Hura8/System/Controller/aPublicEntityBaseController.php @@ -0,0 +1,81 @@ +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; + + }); + + } + + +} diff --git a/inc/Hura8/System/Controller/bFileHandle.php b/inc/Hura8/System/Controller/bFileHandle.php new file mode 100644 index 0000000..7de9d70 --- /dev/null +++ b/inc/Hura8/System/Controller/bFileHandle.php @@ -0,0 +1,270 @@ +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; + } + + +} diff --git a/inc/Hura8/System/CopyFileFromUrl.php b/inc/Hura8/System/CopyFileFromUrl.php new file mode 100644 index 0000000..a19daaf --- /dev/null +++ b/inc/Hura8/System/CopyFileFromUrl.php @@ -0,0 +1,56 @@ +setup_success) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to setup"); + } + + $original_file_name = substr($file_url, strrpos($file_url, "/")); + $original_file_path = $this->tmp_folder . "/". $original_file_name; + + $url_content = Url::getUrlContent($file_url); + if(!$url_content || !@file_put_contents($original_file_path, $url_content)) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to copy"); + } + + /*if(!@copy($file_url, $original_file_path)) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to copy"); + }*/ + + $result = $this->processFile( + $original_file_name, + $original_file_path, + $fixed_file_name, + $max_file_size + ); + + unset($url_content); + + return $result; + } + + +} diff --git a/inc/Hura8/System/CronProcess.php b/inc/Hura8/System/CronProcess.php new file mode 100644 index 0000000..600c7df --- /dev/null +++ b/inc/Hura8/System/CronProcess.php @@ -0,0 +1,66 @@ +pid = $pid; + $this->objFileSystem = new FileSystem(self::LOG_DIR); + $this->max_allow_run_time = $max_allow_run_time; + } + + public function getPidFile() { + return $this->objFileSystem->getFile($this->pid); + } + + /** + * Check if the process is running. True if: + * - has pid file + * - the file's create-time within the time() and time() - $this->max_allow_run_time + * + * Else: False and remove pid file if exist + * @return bool + */ + public function isRunning() : bool { + + $file_exist = ($this->objFileSystem->getFile($this->pid)); + if(!$file_exist) { + return false; + } + + $last_modified_time = $this->objFileSystem->getFileLastModifiedTime($this->pid); + + if($last_modified_time > 0 && $last_modified_time < time() - $this->max_allow_run_time) { + + echo "Pid File exists but too old. Trying to auto-delete now". PHP_EOL; + + if($this->objFileSystem->delete($this->pid)){ + echo "File is deleted successfully". PHP_EOL; + }else{ + echo "Auto-delete fails. File needs to be removed manually. Path: ". $this->objFileSystem->getFilename($this->pid). PHP_EOL; + } + + return false; + } + + return true; + } + + public function start() { + return $this->objFileSystem->writeFile($this->pid, time()); + } + + public function finish() { + return $this->objFileSystem->delete($this->pid); + } + +} diff --git a/inc/Hura8/System/Email.php b/inc/Hura8/System/Email.php new file mode 100644 index 0000000..b1d3602 --- /dev/null +++ b/inc/Hura8/System/Email.php @@ -0,0 +1,251 @@ + "", + "from_name" => "", + "from_email" => "", + "reply_to" => "", + "subject" => "", + "content" => "", + ]; + + /* @var SettingController $objSettingController */ + protected $objSettingController; + + //construct + public function __construct() { + // default + if(defined("MAIL_METHOD")) $this->config["mail_method"] = MAIL_METHOD; + if(defined("MAIL_NAME")) $this->config["from_name"] = MAIL_NAME; + if(defined("MAIL_ADDRESS")) $this->config["from_email"] = MAIL_ADDRESS; + + if(!$this->config["from_name"]) $this->config["from_name"] = str_replace('www.', '', $_SERVER['HTTP_HOST']); + + $this->objSettingController = new SettingController(); + + $email_settings = $this->objSettingController->getList([ + 'email_from_default', + ]); + + $email_from = (isset($email_settings['email_from_default']) && $email_settings['email_from_default']) ? $email_settings['email_from_default'] : '' ; + if($email_from && self::validate_email($email_from) && $email_from != MAIL_ADDRESS) { + $this->setUp([ + "from_email" => $email_from, + ]); + } + } + + public function setUp(array $config) + { + // over-ride default config + if(sizeof($config)) { + foreach ($config as $key => $value) { + $this->config[$key] = $value; + } + } + } + + public function send(array $to_emails, $subject, $content, array $key_values = []) + { + if(!$this->checkConfig()) return [ + 'status' => 'error', + 'message' => "Config errors: ".\json_encode($this->config), + ]; + + // send email using a template in email/ + $this->config['subject'] = TemplateController::parse(null, $subject, $key_values); + $this->config['content'] = TemplateController::parse(null, $content, $key_values); + + try { + switch ($this->config["mail_method"]){ + case "huraserver"; + $result = $this->sendEmailFromHura($to_emails); + break; + case "queue"; + $result = $this->addQueue($to_emails); + break; + case "adman"; + $result = $this->sendFromAdman($to_emails); + break; + case "smtp"; + $result = $this->sendFromSmtp($to_emails); + break; + default; + $result = null; + } + }catch (\Exception $exception) { + $result = "Error: ".$exception->getMessage(); + } + + if($result) { + $this->log(array_merge($this->config, [ + 'to_emails' => $to_emails, + 'result' => $result, + ])); + } + + return $result; + } + + protected function checkConfig() { + return ( + $this->config["mail_method"] + && $this->config["from_name"] + && $this->config["from_email"] + && self::validate_email($this->config["from_email"]) + ); + } + + protected function addQueue(array $to_emails){ + // todo: + /*foreach($to_email_list as $single_email){ + $this->createQueue(array( + "message_id" => 0, + "from_email" => $from_mail, + "from_name" => $from_name, + "reply_to" => $reply_to_email, + "to_email" => $single_email, + "to_name" => $to_name, + "subject" => $title, + "content" => $content, + "source" => "", + )); + }*/ + return true; + } + + protected function sendFromAdman(array $to_emails){ + if(!defined("ADMAN_API_KEY") || ADMAN_API_KEY == '') return 'ADMAN_API_KEY not set'; + if(!defined("ADMAN_USER_DOMAIN") || ADMAN_USER_DOMAIN == '') return 'ADMAN_USER_DOMAIN not set'; + + $recipients = array_map(function ($email){ + return [ + 'to_email' => $email, + 'to_name' => '', + 'personal_settings' => array(), + ]; + }, $to_emails); + + $payload = array( + 'message' => array( + 'from_email' => $this->config['from_email'], + 'from_name' => $this->config['from_name'], + 'reply_to_email' => $this->config['reply_to'], + 'subject' => $this->config['subject'], + 'settings' => array(), + 'schedule_go_time_utc' => 0, + 'content_html' => $this->config['content'], + 'content_plain' => '', + ), + 'recipients' => $recipients + ); + + return Adman::send(ADMAN_USER_DOMAIN, ADMAN_API_KEY, $payload); + } + + + protected function sendFromSmtp(array $to_emails){ + + if(!SMTP_HOST || !SMTP_USERNAME || !SMTP_PASSWORD) { + return 'Smtp credentials not set'; + } + + // https://swiftmailer.symfony.com/docs/introduction.html + // Create the Transport + $transport = (new Swift_SmtpTransport(SMTP_HOST, SMTP_PORT, SMTP_SECURE)) + ->setUsername(SMTP_USERNAME) + ->setPassword(SMTP_PASSWORD) + ; + $transport->setTimeout(10); + + // Create the Mailer using your created Transport + $mailer = new Swift_Mailer($transport); + + // Create a message + $message = (new Swift_Message()) + ->setSubject($this->config['subject']) + ->setFrom($this->config['from_email'], $this->config['from_name']) + ->setTo($to_emails) + ->setBody($this->config['content'], 'text/html') + ; + + // Send the message + return $mailer->send($message); + } + + + protected function sendEmailFromHura(array $to_emails){ + $rebuild_to_emails = []; + $max_email_allow = 3; + $counter = 0; + foreach ($to_emails as $email) { + if(self::validate_email($email)) { + $counter += 1; + if($counter >= $max_email_allow) break; + $rebuild_to_emails[] = ['email' => $email, 'name' => '']; + } + } + + if(!sizeof($rebuild_to_emails)) { + return [ + 'status' => 'error', + 'message' => 'Error. No valid emails to be sent', + ]; + } + + $data = [ + 'title' => $this->config['subject'], + 'content' => $this->config['content'], + 'to_emails' => $rebuild_to_emails, + 'from_email' => $this->config['from_email'], + 'from_name' => $this->config['from_name'], + 'reply_to_email' => $this->config['reply_to'], + 'reply_to_name' => '', + 'client_id' => CLIENT_ID, + 'domain' => CURRENT_DOMAIN, + ]; + + // protect + $msg_hash = sha1(serialize($data).'asdas32'); + $data['msg_hash'] = $msg_hash; + + $headers = []; + + return curl_post(HURAMAIL_RECEIVE_ENDPOINT, $data, $headers); + } + + protected function createQueue(array $config) { + // todo: + } + + //email log after sending an email and remove from queue + protected function log(array $log_content){ + /*$db = ConnectDB::getInstance(''); + $db->insert( + 'email_log', + [ + 'payload' => \json_encode($log_content), + 'create_time' => CURRENT_TIME, + ] + );*/ + } + + //validate email + public static function validate_email($email){ + return filter_var($email, FILTER_VALIDATE_EMAIL); + } + +} diff --git a/inc/Hura8/System/Export.php b/inc/Hura8/System/Export.php new file mode 100644 index 0000000..dd4d2c4 --- /dev/null +++ b/inc/Hura8/System/Export.php @@ -0,0 +1,47 @@ +setHeaderXLS($file_name); + } + else if($type=='doc') { + $this->setHeaderDoc($file_name); + } + else if($type=='csv') { + $this->setHeaderCSV($file_name); + } + + echo $content; + } + + // method for Excel file + protected function setHeaderXLS($file_name) + { + header("Content-type: application/ms-excel"); + header("Content-Disposition: attachment; filename=$file_name"); + header("Pragma: no-cache"); + header("Expires: 0"); + } + + // method for Doc file + protected function setHeaderDoc($file_name) + { + header("Content-type: application/x-ms-download"); + header("Content-Disposition: attachment; filename=$file_name"); + header('Cache-Control: public'); + } + + // method for CSV file + protected function setHeaderCSV($file_name) + { + header("Content-type: application/csv"); + header("Content-Disposition: inline; filename=$file_name"); + } +} diff --git a/inc/Hura8/System/ExportExcelUseTemplate.php b/inc/Hura8/System/ExportExcelUseTemplate.php new file mode 100644 index 0000000..82efbbf --- /dev/null +++ b/inc/Hura8/System/ExportExcelUseTemplate.php @@ -0,0 +1,158 @@ +_createActiveSheet($tpl_file); + } + } + + + protected function _createActiveSheet($tpl_file) { + $this->spreadsheet = IOFactory::load($tpl_file); + + $this->spreadsheet->setActiveSheetIndex($this->active_sheet_index); + + $this->activeSheet = $this->spreadsheet->getActiveSheet(); + } + + + public function setRowHeight($row_number, $height_pt) { + $this->activeSheet->getRowDimension($row_number)->setRowHeight($height_pt, 'pt'); + } + + + public function removeRow($row_id) { + if(!$this->activeSheet) { + return false; + } + + $this->activeSheet->removeRow($row_id); + + return true; + } + + + /** + * Set a cell value. + * + * @param string $cell Coordinate of the cell, eg: 'A1' + * @param mixed $data Value of the cell + * @param string $pDataType Explicit data type, see DataType::TYPE_* + * + */ + public function writeToCell($cell, $data, $pDataType) { + if(!$this->activeSheet) { + return false; + } + + $this->activeSheet->setCellValueExplicit($cell, $data, $pDataType); + + return true; + } + + + /** + * @param int $start_row + * @param array $row_list array( ['field-1' => value, 'field-2' => value, 'field-3' => value], ['field-1' => value, 'field-2' => value,],) + * @param array $map_column_fields map excel column to row field . Example: array('A' => 'name', 'B' => 'price', ...) + * @param array $number_fields list of data fields that their values will be write to cell as number. Example ['price', 'qty', ] + * @param array $hyperlink_fields list of data fields that their values will be added hyperlink + * @return false|int|mixed|string + */ + public function writeRowList($start_row = 1, array $row_list =[], array $map_column_fields = [], array $number_fields = [], array $hyperlink_fields = []) { + + if(!$this->activeSheet) { + return false; + } + + // add new rows to hold new data + for($i = 0; $i< sizeof($row_list); $i++) { + $this->activeSheet->insertNewRowBefore($start_row + 1); + } + + $last_insert_row_index = 0; + + foreach ($row_list as $index => $item) { + + $last_insert_row_index = $start_row + $index; + + foreach ($map_column_fields as $column => $field) { + + $cell_cor = $column . $last_insert_row_index; + + if(array_key_exists($field, $number_fields)) { + $this->activeSheet->setCellValueExplicit($cell_cor, $item[$field], DataType::TYPE_NUMERIC); + }else{ + $this->activeSheet->setCellValueExplicit($cell_cor, $item[$field], DataType::TYPE_STRING); + } + + // set hyperlink + if(array_key_exists($field, $hyperlink_fields)) { + + // link can be a field of the item or a fixed value + $hyperlink_url = (isset($item[$hyperlink_fields[$field]])) ? $item[$hyperlink_fields[$field]] : $hyperlink_fields[$field]; + + $this->activeSheet->setHyperlink($cell_cor, new Hyperlink($hyperlink_url, 'Mở link để xem tại website')); + } + + // format number + if(array_key_exists($field, $number_fields)) { + + // check format type + if($number_fields[$field]) { + $this->activeSheet->getStyle($cell_cor)->getNumberFormat()->setFormatCode($number_fields[$field]); + } + + }else{ + $this->activeSheet->getStyle($cell_cor)->getAlignment()->setWrapText(true); + } + + } + } + + return $last_insert_row_index; + } + + + public function exportToBrowser($file_name='export') { + if(!$this->spreadsheet) { + die("No file"); + } + + // export + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment;filename="'.$file_name.'-'.time().'.xlsx"'); + header('Cache-Control: max-age=0'); + + $writer = new Xlsx($this->spreadsheet); + ob_end_clean(); + $writer->save('php://output'); + + // clean up + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + +} diff --git a/inc/Hura8/System/FileSystem.php b/inc/Hura8/System/FileSystem.php new file mode 100644 index 0000000..38f07b8 --- /dev/null +++ b/inc/Hura8/System/FileSystem.php @@ -0,0 +1,212 @@ +umask = $umask; + + if (! $this->createPathIfNeeded($directory)) { + throw new \Exception(sprintf( + 'The directory "%s" does not exist and could not be created.', + $directory + )); + } + + if (! is_writable($directory)) { + throw new \Exception(sprintf( + 'The directory "%s" is not writable.', + $directory + )); + } + + // YES, this needs to be *after* createPathIfNeeded() + $this->directory = realpath($directory); + + //$this->directoryStringLength = strlen($this->directory); + //$this->isRunningOnWindows = defined('PHP_WINDOWS_VERSION_BUILD'); + } + + + /** + * @description extract a zip file after it has been uploaded to the server + * @param string $filename name of the zip file (abc.zip) + * @param string $uploaded_zip_path path where it's uploaded to i.e /usr/.../tmp_upload/ + * @param string $extract_target_path path where the zip files will be extracted to , if not provided the $uploaded_zip_path will be used + * @return array [extracted_folder, test_folder_exist] + */ + public static function unzip(string $filename, string $uploaded_zip_path, string $extract_target_path = '') { + $extract_path = $extract_target_path ?: $uploaded_zip_path; + + $zip = new \ZipArchive(); + $x = $zip->open($uploaded_zip_path . DIRECTORY_SEPARATOR . $filename); + if ($x === true) { + $zip->extractTo($extract_path); // change this to the correct site path + $zip->close(); + } + @unlink($uploaded_zip_path . DIRECTORY_SEPARATOR . $filename); + + $expected_result = $extract_path . DIRECTORY_SEPARATOR . str_replace(".zip", "", $filename); + + return array($expected_result, file_exists($expected_result)); + } + + + /** + * @description scan a folder recursively and return all files with sub-path + * @param string $folder full path /usr/local/.../ + * @param array $file_list + */ + public static function scanDirRecursive(string $folder, array &$file_list = []) { + $dir = opendir($folder); + while(( $file = readdir($dir)) ) { + if (( $file != '.' ) && ( $file != '..' )) { + if ( is_dir($folder. '/' . $file) ) { + FileSystem::scanDirRecursive($folder. '/' . $file, $file_list); + } + else { + //$file_list[] = str_replace(PUBLIC_DIR . "/", "", $folder .'/'. $file); + $file_list[] = $folder .'/'. $file; + } + } + } + closedir($dir); + } + + //remove directory recursively + public static function removeDir($dir) { + if (is_dir($dir)) { + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (is_dir($dir."/".$object)) { + self::removeDir($dir."/".$object); + } + else { + @unlink($dir."/".$object); + } + } + } + @rmdir($dir); + } + } + + public function getFile(string $filename) + { + $full_filepath = $this->getFilename($filename); + if(file_exists($full_filepath)) { + return file_get_contents($full_filepath); + } + + return false; + } + + public function getFileLastModifiedTime(string $filename) + { + $full_filepath = $this->getFilename($filename); + if(file_exists($full_filepath)) { + return filemtime($full_filepath); + } + + return 0; + } + + /** + * Writes a string content to file in an atomic way. + * + * @param string $filename Path to the file where to write the data. + * @param string $content The content to write + * + * @return bool TRUE on success, FALSE if path cannot be created, if path is not writable or an any other error. + */ + public function writeFile(string $filename, string $content) + { + $full_filepath = $this->getFilename($filename); + $filepath = pathinfo($full_filepath, PATHINFO_DIRNAME); + + if (! $this->createPathIfNeeded($filepath)) { + return false; + } + + if (! is_writable($filepath)) { + return false; + } + + $tmpFile = tempnam($filepath, 'swap'); + @chmod($tmpFile, 0666 & (~$this->umask)); + + if (file_put_contents($tmpFile, $content) !== false) { + + @chmod($tmpFile, 0666 & (~$this->umask)); + if (@rename($tmpFile, $full_filepath)) { + return true; + } + + @unlink($tmpFile); + } + + return false; + } + + public function delete($filename) + { + $full_filepath = $this->getFilename($filename); + + return @unlink($full_filepath) || ! file_exists($full_filepath); + } + + /** + * @param string $filename + * + * @return string + */ + public function getFilename($filename) + { + return $this->directory + . DIRECTORY_SEPARATOR + . $filename ; + } + + /** + * Create path if needed. + * + * @return bool TRUE on success or if path already exists, FALSE if path cannot be created. + */ + private function createPathIfNeeded(string $path) + { + if (! is_dir($path)) { + if (@mkdir($path, 0755 & (~$this->umask), true) === false && ! is_dir($path)) { + return false; + } + } + + return true; + } + +} diff --git a/inc/Hura8/System/FileUpload.php b/inc/Hura8/System/FileUpload.php new file mode 100644 index 0000000..a25bbde --- /dev/null +++ b/inc/Hura8/System/FileUpload.php @@ -0,0 +1,110 @@ +handleUpload("file"); + + if($upload_result->getStatus() == 'ok') { + $file_info = $upload_result->getData(); + + $file_name = $file_info->file_name; + $file_ext = $file_info->file_ext; + $public_path = $file_info->public_path; + + $renamed = $objFileUpload->renameUploadedFile($file_name, $item_id . $file_ext); + + if($renamed) { + $new_file_name = $renamed['file_name']; + $new_public_path = $renamed['public_path']; + } + + } + * + */ +class FileUpload extends bFileHandle +{ + + /** + * @description version of handleUpload to support multiple files using array input + * example: instead of + * + * @param string $input_file_name name of => use: multiple_file + * @param int $max_file_size max file size in bytes accepts, default = 1MB + * @return FileHandleResponse[] + */ + public function handleMultipleUpload(string $input_file_name, int $max_file_size = 1000000) : array + { + $upload_result = []; + for($i = 0; $i < sizeof($_FILES[$input_file_name]['name']); $i++){ + $original_uploaded_file_name = $_FILES[$input_file_name]["name"][$i]; + $original_uploaded_data = $_FILES[$input_file_name]["tmp_name"][$i]; + + $upload_result[] = $this->processFile( + $original_uploaded_file_name, + $original_uploaded_data, + '', + $max_file_size + ); + } + + // clean up + $this->cleanUp(); + + return $upload_result; + } + + + /** + * @param string $input_file_name name of (which is upload_file) + * @param int $max_file_size max file size in bytes accepts, default = 1MB + * @param string $fixed_file_name set uploaded name to this fixed name (ie. [item_id].jpg) so old file will be replaced + * @return FileHandleResponse + */ + public function handleUpload(string $input_file_name, string $fixed_file_name='', int $max_file_size = 1000000) : FileHandleResponse + { + if(!$this->setup_success) { + return new FileHandleResponse(AppResponse::ERROR, "Fail to setup"); + } + + if(!$input_file_name || !isset($_FILES[$input_file_name])) { + return new FileHandleResponse('error', 'no file', null); + } + + $original_uploaded_file_name = $_FILES[$input_file_name]["name"] ?? ""; + $original_uploaded_data = $_FILES[$input_file_name]["tmp_name"]; + + $upload_result = $this->processFile( + $original_uploaded_file_name, + $original_uploaded_data, + $fixed_file_name, + $max_file_size + ); + + // clean up + $this->cleanUp(); + + return $upload_result; + } + + +} diff --git a/inc/Hura8/System/Firewall.php b/inc/Hura8/System/Firewall.php new file mode 100644 index 0000000..b148831 --- /dev/null +++ b/inc/Hura8/System/Firewall.php @@ -0,0 +1,190 @@ +log_dir = ROOT_DIR . '/cache/firewall/log/'; + $this->ban_dir = ROOT_DIR . '/cache/firewall/ban/'; + if(!file_exists($this->log_dir)) { + mkdir($this->log_dir, 0755, true); + } + if(!file_exists($this->ban_dir)) { + mkdir($this->ban_dir, 0755, true); + }*/ + $this->user_ip = getIpAddress(); + + if(file_exists(self::WHITE_LIST_IP)) { + $this->white_list = array_filter(explode("\n", file_get_contents(self::WHITE_LIST_IP))); + } + + } + + + public static function getInstance() { + if(!static::$instance) { + static::$instance = new self(); + } + + return static::$instance; + } + + // check if current user agent is an crawler + public function isCrawler() : bool { + $objCrawlerDetect = new CrawlerDetect(); + //echo $objCrawlerDetect->getUserAgent(); + // https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers + return $objCrawlerDetect->isCrawler(); + } + + public function monitor() { + // todo: + return false; + + $total_concurrent_visit = $this->logIp($this->user_ip); + if($total_concurrent_visit > self::MAX_CONCURRENT_VISIT_ALLOW) { + // ban if not in whitelist + if(!$this->isWhiteListed()) { + $this->banTemporary($this->user_ip, self::TEMPORARY_BAN_DURATION); + } + + die("System overload"); + } + + return true; + } + + public function stopIfBanned() { + if($this->isIPBanned()) { + die("Perhaps, this website is not for you."); + } + + return false; + } + + protected function isWhiteListed() { + $filter = new IPFilter($this->white_list); + return $filter->check($this->user_ip); + } + + protected function isIPBanned() { + + // check whitelist + if($this->isWhiteListed()) { + return false; + } + + // check temporary ban + /*$file_id = $this->ban_dir . $this->user_ip.'.data'; + if(file_exists($file_id)) { + $lift_time = file_get_contents($file_id); + // lift_time is not over yet + if($lift_time > time()) { + return true; + } + }*/ + + // permanent ban + $config_file = static::BAN_IP_LIST; + if(@file_exists( $config_file )) { + $list_ips = @file_get_contents( $config_file ); + $list_ip_arr = array_filter(explode("\n", $list_ips)); + $filter = new IPFilter($list_ip_arr); + + return $filter->check($this->user_ip); + } + + return false; + } + + // ban an IP temporarily + protected function banTemporary($ip, $time) { + $file_id = $this->ban_dir . $ip.'.data'; + @file_put_contents($file_id, time() + $time); + } + + + + // only allow 1 of these formats: + // - Full IP: 127.0.0.1 + // - Wildcard: 172.0.0.* + // - Mask: 125.0.0.1/24 + // - Range: 125.0.0.1-125.0.0.9 + public static function validateIP($ip) { + // - Full IP: 127.0.0.1 + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return true; + } + + // - Wildcard: 172.0.0.* + if (strpos($ip, "*") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.(([0-9]+)|\*)\.(([0-9]+)|\*)$/i", $ip)) { + return true; + } + + // - Mask: 125.0.0.1/24 + if (strpos($ip, "/") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\/([0-9]+)$/i", $ip)) { + return true; + } + + // - Range: 125.0.0.1-125.0.0.9 + if (strpos($ip, "-") !== false ) { + list($begin, $end) = explode('-', $ip); + return filter_var($begin, FILTER_VALIDATE_IP) && filter_var($end, FILTER_VALIDATE_IP); + } + + return false; + } + + + private function logIp($ip) { + $file_id = $this->log_dir . $ip.'.data'; + $time_track = CURRENT_TIME - CURRENT_TIME % 5; // track within 5 seconds, + $data = []; + if(file_exists($file_id)) { + $data = \json_decode(file_get_contents($file_id), true); + } + + if(isset($data[$time_track])) { + $data[$time_track] = $data[$time_track] + 1; + }else{ + $data[$time_track] = 1; + } + + // only log for current time to prevent large $data's size + // our purpose is only to detect if this IP is brutally forceing the system (too many visits per time) + + @file_put_contents($file_id, \json_encode($data)); + + return $data[$time_track]; + } + +} diff --git a/inc/Hura8/System/HtmlParser.php b/inc/Hura8/System/HtmlParser.php new file mode 100644 index 0000000..61175a0 --- /dev/null +++ b/inc/Hura8/System/HtmlParser.php @@ -0,0 +1,245 @@ +source_html = str_replace(array("\n", "\r", "\t")," ", $source_html); + } + + //@$boundary_pattern: pattern to find a smaller boundary-block within source_html so that our content does not wander around + /** + * @param $pattern_arr_or_str array|string ['pattern1', 'pattern2'] or 'pattern' + * @param bool $match_once + * @param bool $find_image + * @param string $boundary_pattern + * @return array|bool + */ + public function extract($pattern_arr_or_str, $match_once = true, $find_image = false, $boundary_pattern = "") { + if(is_array($pattern_arr_or_str)) { + foreach ($pattern_arr_or_str as $pattern){ + $result = $this->extractSinglePattern($pattern, $match_once, $find_image, $boundary_pattern); + if(is_array($result) && isset($result['result'])) { + return $result; + } + } + + return false; + } + + // default is string + return $this->extractSinglePattern($pattern_arr_or_str, $match_once, $find_image, $boundary_pattern); + } + + /** + * @param $pattern string + * @param bool $match_once + * @param bool $find_image + * @param string $boundary_pattern + * @return array|bool + */ + protected function extractSinglePattern($pattern, $match_once = true, $find_image = false, $boundary_pattern = "") { + + $elements = $this->getCodeElement($pattern); + if(!$elements["code"]) return false; + + $match = array(); + $source_html = $this->source_html; + //found boundary if pattern exist + if($boundary_pattern && preg_match("@".$boundary_pattern."@i", $source_html, $match)) { + $source_html = $match[1]; + } + + if($match_once) { + if(preg_match("@".$elements["code"]."@i", $source_html, $match)){ + //echo $match[1]; + return array( + "result" => $this->cleanHtmlBlock($match[1], $elements), + "images" => ($find_image) ? $this->extractImages($match[1]) : null + ); + } + } else { + $results = array(); + if(preg_match_all("@".$elements["code"]."@", $source_html, $match)){ + foreach ($match[1] as $html_block ) { + $results[] = array( + "result" => $this->cleanHtmlBlock($html_block, $elements), + "images" => ($find_image) ? $this->extractImages($html_block) : null + ); + } + } + + return $results; + } + + return false; + } + + private function cleanHtmlBlock($html_block, $elements) { + + if($elements["removed"]){ + $arrayRemover = array_filter(explode(";",$elements["removed"])); + foreach($arrayRemover as $char_removed){ + $char_removed = stripslashes(trim($char_removed)); + $html_block = str_replace($char_removed.";","", $html_block); //sometimes   + $html_block = str_replace($char_removed,"", $html_block); + $html_block = preg_replace("{".addslashes($char_removed)."}","", $html_block); + } + } + + if($elements["sepa"]){ + $html_block = strrchr($html_block, $elements["sepa"]); + $html_block = str_replace($elements["sepa"],"", $html_block); + } + + if($elements["extra_url"] && $html_block) $html_block = $elements["extra_url"] . trim($html_block); + + //thay the cum tu source bang cum tu tuy chon + if($elements["invalid"]){ + $arrayReplace = array_filter(explode(";", $elements["invalid"])); + foreach($arrayReplace as $replace_group){ + $replace_group = stripslashes($replace_group); + //echo $replace_group; + $replace_group_a = explode("#",$replace_group); + $html_block = str_replace(trim($replace_group_a[0]),trim($replace_group_a[1]), $html_block); + $html_block = preg_replace("{".addslashes(trim($replace_group_a[0]))."}",trim($replace_group_a[1]), $html_block); + } + } + + return trim($html_block); + } + + + private function extractImages($source_html){ + $img_match = array(); + if(preg_match_all("/img(.*?)?src\s*=\s*\\\\?[\'\"]?([+:%\/\?~=&;\(\),|!._a-zA-Z0-9-]*)[\'\"]?/i", $source_html, $img_match)){ + return array_unique($img_match[2]); + } + return array(); + } + + //$code_line = + // singlePro | chandau(.*?)chancuoi | remove_word'];word2 | sepa | prepend_after | invalid_list + // pagePro | chandau(.*?)chancuoi | remove_word'];word2 | sepa | prepend_after | invalid_list + /** + * @param $code_line string + * @return array|false + */ + private function getCodeElement($code_line){ + if(!$code_line) return false; + + $element = explode("|", trim($code_line)); + $result = array(); + if(array_key_exists(0, $element)){ + $result["type"] = trim($element[0]); //pagePro, singlePro + }else $result["type"] = ""; + + if(array_key_exists(1, $element)){ + $result["code"] = trim($element[1]); + }else $result["code"] = ""; + + if(array_key_exists(2,$element)){ + $result["removed"] = trim($element[2]); + }else $result["removed"] = ""; + + if(array_key_exists(3,$element)){ + $result["sepa"] = trim($element[3]); + }else $result["sepa"] = ""; + + if(array_key_exists(4,$element)){ + $result["extra_url"] = trim($element[4]); + }else $result["extra_url"] = ""; + + if(array_key_exists(5,$element)){ + $result["invalid"] = trim($element[5]); //for images + }else $result["invalid"] = ""; + + return $result; + } + + + private function buildFullUrl($url, $base_url){ + if(!$base_url) return $url; + if(strlen($url) < 2) return ""; + + if(preg_match("/(http|www.|javascript|mailto|ymsgr)/i",$url)){ + return $url; + }else{ + return $this->convert_to_absolute( $base_url, $url ); + } + } + + private function convert_to_absolute($absolute, $relative) { + $p = parse_url($relative); + $first_letter = $relative[0]; + $last_letter = substr($relative, strlen($relative) -1, 1); + + if(array_key_exists("scheme",$p) || strpos($relative,"www.") !== false || substr($relative, 0, 2) == '//') return $relative; //it's absolute + + if(in_array($first_letter,array('?',';'))) return str_replace(strrchr($absolute,$first_letter),"",$absolute) . $relative; + + if($first_letter == "#") return $absolute;//already crawled this page + + extract(parse_url($absolute)); + + $path = (isset($path)) ? $path : ""; + $path = (strrchr($absolute,"/")!="/") ? ((dirname($path) != "\\") ? dirname($path) : "") : $path; + + if($first_letter == '/') { + $cparts = array_filter(explode("/", $relative)); + } + else { + + $aparts = array_filter(explode("/", $path)); + //print_r($aparts); + $rparts = array_filter(explode("/", $relative)); + //print_r($rparts); + $cparts = array_merge($aparts, $rparts); + //print_r($cparts); + if(!preg_match("/[a-z0-9]/i",$first_letter)){ + foreach($cparts as $i => $part) { + if($part == '.') { + $cparts[$i] = null; + } + if($part == '..') { + $cparts[$i] = ''; + if(array_key_exists($i - 1,$cparts)){ + if($cparts[$i - 1] != null) $cparts[$i - 1] = null; + else if(array_key_exists($i - 3,$cparts)) $cparts[$i - 3] = null; // in case ../../ + } + } + + } + } + $cparts = array_filter($cparts); + } + + $path = implode("/", $cparts); + if($last_letter == '/') $path .= "/"; + + + $url = ""; + if($scheme) { + $url = "$scheme://"; + } + + if($host) { + $url .= "$host/"; + } + $url .= $path; + return $url; + } + +} diff --git a/inc/Hura8/System/HuraImage.php b/inc/Hura8/System/HuraImage.php new file mode 100644 index 0000000..55bc3b3 --- /dev/null +++ b/inc/Hura8/System/HuraImage.php @@ -0,0 +1,180 @@ +objInterventionImage = new InterventionImage(); + } + + + /** + * optimize file size of an image and resize its width to the max_width it + * + * @param string $file_dir __DIR__."/media/product/' + * @param string $file_name file.jpg + * @param int $max_width + * @return bool + */ + public function optimizeFile(string $file_dir, string $file_name, int $max_width = 1200): bool + { + $file_path = $file_dir . DIRECTORY_SEPARATOR . $file_name; + $random_str = IDGenerator::createStringId(10); + $clone_file = $file_dir . DIRECTORY_SEPARATOR . $random_str. '-' . $file_name; + + if(!copy($file_path, $clone_file)) { + return false; + } + + $img = $this->objInterventionImage->make($clone_file); + + list ( $img_width ) = getimagesize($clone_file); + if($img_width > $max_width) { + $img->resize($max_width, null, function ($constraint) { + $constraint->aspectRatio(); + })->save($file_path, 90); + }else{ + $img->save($file_path, 90); + } + + // then delete the clone + unlink($clone_file); + + return file_exists($file_path); + } + + + /** + * create image to local system + * copy from https://image.intervention.io/v2/api/make + * @param mixed $source + * Source to create an image from. The method responds to the following input types: + string - Path of the image in filesystem. + string - URL of an image (allow_url_fopen must be enabled). + string - Binary image data. + string - Data-URL encoded image data. + string - Base64 encoded image data. + resource - PHP resource of type gd. (when using GD driver) + object - Imagick instance (when using Imagick driver) + object - Intervention\Image\Image instance + object - SplFileInfo instance (To handle Laravel file uploads via Symfony\Component\HttpFoundation\File\UploadedFile) + * + * @param string $saved_file_path __DIR__."/media/product/file.jpg' + * @return bool + */ + public function create($source, $saved_file_path) { + + try { + $this->objInterventionImage->make($source)->save($saved_file_path, 90); + }catch (\Exception $e) { + // NotReadableException: Unsupported image type. GD/PHP installation does not support WebP format + @copy($source, $saved_file_path); + } + + return file_exists($saved_file_path); + } + + + /** + * https://image.intervention.io/v2/api/encode + * @param $local_file_path + * @param string $format jpg | png | gif | webp | data-url + * @return bool + */ + public function convertFileFormat($local_file_path, $format ) { + + // if same format as local file, stop + $file_ext = strtolower(substr(strrchr($local_file_path, '.'), 1)); + if($file_ext == $format) { + return true; + } + + $saved_file_path = substr($local_file_path, 0, strrpos($local_file_path,".") ) . ".". $format; + + $this->objInterventionImage->make($local_file_path) + ->encode($format, 100) + ->save($saved_file_path, 90); + + return file_exists($saved_file_path); + } + + + /** + * guide: https://image.intervention.io/v2/api/resize + * @param $local_file_path + * @param array $size_key_dimension array('small' => ['width' => 100, 'height' => 100], 'large' => ['width' => 200, 'height' => 200] ) + * @param string $resized_file_directory + * @return array + */ + public function resize($local_file_path, array $size_key_dimension, $resized_file_directory = '') { + + $stored_directory = ($resized_file_directory) ?: substr($local_file_path, 0, strrpos($local_file_path,"/") ) ; + //echo "

stored_directory: ".$stored_directory; + + $file_name = substr(strrchr($local_file_path,"/"), 1); + //echo "

file_name: ".$file_name; + + $expected_files = []; + foreach ($size_key_dimension as $key => $dimension) { + + $resized_file_name = $key. IMAGE_FILE_SEPARATOR . $file_name; + $expected_files[] = $resized_file_name; + + $saved_file_path = $stored_directory . "/".$resized_file_name; + $new_width = $dimension['width'] ?? null; + $new_height = $dimension['height'] ?? null; + + $img = $this->objInterventionImage->make($local_file_path); + + if(!$new_width || !$new_height) { + $img->resize($new_width, $new_height, function ($constraint) { + $constraint->aspectRatio(); + })->save($saved_file_path, 90); + }else{ + $img->resize($new_width, $new_height)->save($saved_file_path, 90); + } + + $img->destroy(); + } + + //echo "

Expected files: ".\json_encode($expected_files); + $exist_all = true; + foreach ($expected_files as $_file) { + if(!file_exists($stored_directory . "/". $_file)) { + $exist_all = false; + break; + } + } + + return array($exist_all, $expected_files, $stored_directory); + } + + + /** + * @param $original_file_path + * @param $img_watermark_path + * @param string $position + */ + public function watermark($original_file_path, $img_watermark_path, $position='bottom-right') { + $img = $this->objInterventionImage->make($original_file_path); + $watermark = $this->objInterventionImage->make($img_watermark_path); + $img->insert($watermark, $position, 10, 10); + //$img->bac ($destinationPath.'/'.$fileName); + $img->save($original_file_path, 100); + } + +} diff --git a/inc/Hura8/System/IDGenerator.php b/inc/Hura8/System/IDGenerator.php new file mode 100644 index 0000000..eadaf26 --- /dev/null +++ b/inc/Hura8/System/IDGenerator.php @@ -0,0 +1,56 @@ + final id = P000020 + * @return string + */ + public static function createItemId(string $prefix, int $increment_id, $length = 6): string + { + $num_zeros = $length - strlen($increment_id); + if($num_zeros > 0) { + return join("", [$prefix, str_repeat("0", $num_zeros), $increment_id]); + } + + return join("", [$prefix, $increment_id]); + } + + + private static $nanoClientCache; + + + /** + * @description create string id using Nano ID + * Collision Calculator: https://zelark.github.io/nano-id-cc/ + * @param int $length + * @param bool $lower_case + * @return string + */ + public static function createStringId(int $length = 15, bool $lower_case = false) : string { + + if($lower_case) { + $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; + } else { + $alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + } + + if(isset(static::$nanoClientCache)) { + return static::$nanoClientCache->formattedId($alphabet, $length); + } + + // create one + $client = new Client(); + static::$nanoClientCache = $client; + + return $client->formattedId($alphabet, $length); + } + +} diff --git a/inc/Hura8/System/IPFilter.php b/inc/Hura8/System/IPFilter.php new file mode 100644 index 0000000..bd7174d --- /dev/null +++ b/inc/Hura8/System/IPFilter.php @@ -0,0 +1,129 @@ + check('126.1.0.2');*/ + +class IPFilter +{ + private static $_IP_TYPE_SINGLE = 'single'; + private static $_IP_TYPE_WILDCARD = 'wildcard'; + private static $_IP_TYPE_MASK = 'mask'; + private static $_IP_TYPE_SECTION = 'section'; + private $_allowed_ips = array(); + + public function __construct($allowed_ips) + { + $this -> _allowed_ips = $allowed_ips; + } + + + public function check($ip, $allowed_ips = null) + { + $allowed_ips = $allowed_ips ? $allowed_ips : $this->_allowed_ips; + + foreach($allowed_ips as $allowed_ip) + { + $type = $this->_judge_ip_type($allowed_ip); + $sub_rst = call_user_func(array($this,'_sub_checker_' . $type), $allowed_ip, $ip); + + if ($sub_rst) + { + return true; + } + } + + return false; + } + + + private function _judge_ip_type($ip) + { + if (strpos($ip, '*')) + { + return self::$_IP_TYPE_WILDCARD; + } + + if (strpos($ip, '/')) + { + return self::$_IP_TYPE_MASK; + } + + if (strpos($ip, '-')) + { + return self::$_IP_TYPE_SECTION; + } + + return self::$_IP_TYPE_SINGLE; + + /*if (ip2long($ip)) + { + return self::$_IP_TYPE_SINGLE; + } + + return false;*/ + } + + // can use for IPV4&6 + private function _sub_checker_single($allowed_ip, $ip) + { + return strval($allowed_ip) == strval($ip); + //return (ip2long($allowed_ip) == ip2long($ip)); + } + + // currently only IPV4: 172.0.0.* + private function _sub_checker_wildcard($allowed_ip, $ip) + { + $allowed_ip_arr = explode('.', $allowed_ip); + $ip_arr = explode('.', $ip); + for($i = 0;$i < count($allowed_ip_arr);$i++) + { + if ($allowed_ip_arr[$i] == '*') + { + return true; + } + else + { + if (false == ($allowed_ip_arr[$i] == $ip_arr[$i])) + { + return false; + } + } + } + + return true; + } + + // currently only IPV4: 125.0.0.1/24 + private function _sub_checker_mask($banned_range, $ip) + { + // $allowed_ip is in IP/CIDR format eg 127.0.0.1/24 + list( $range, $netmask ) = explode( '/', $banned_range, 2 ); + $range_decimal = ip2long( $range ); + $ip_decimal = ip2long( $ip ); + $wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1; + $netmask_decimal = ~ $wildcard_decimal; + return !( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); + } + + // currently only IPV4: 125.0.0.1-125.0.0.9 + private function _sub_checker_section($allowed_ip, $ip) + { + list($begin, $end) = explode('-', $allowed_ip); + $begin = ip2long(trim($begin)); + $end = ip2long(trim($end)); + $ip = ip2long($ip); + + return ($ip >= $begin && $ip <= $end); + } +} diff --git a/inc/Hura8/System/Language.php b/inc/Hura8/System/Language.php new file mode 100644 index 0000000..fe4f164 --- /dev/null +++ b/inc/Hura8/System/Language.php @@ -0,0 +1,94 @@ + nguyeexn minh hieesu + public static function convertText($vietnamese_txt){ + + $vietnamese_char = array( + "đ", + "ó","ỏ","ò","ọ","õ","ô","ỗ","ổ","ồ","ố","ộ","ơ","ỡ","ớ","ờ","ở","ợ", + "ì","í","ỉ","ì","ĩ","ị", + "ê","ệ","ế","ể","ễ","ề","é","ẹ","ẽ","è","ẻ", + "ả","á","ạ","ã","à","â","ẩ","ấ","ầ","ậ","ẫ","ă","ẳ","ắ","ằ","ặ","ẵ", + "ũ","ụ","ú","ủ","ù","ư","ữ","ự","ứ","ử","ừ", + "ỹ","ỵ","ý","ỷ","ỳ", + ); + + $equivalent_char = array( + "dd", + "os","or","of","oj","ox","oo","oox","oor","oof","oos","ooj","ow","owx","ows","owf","owr","owj", + "if","is","ir","if","ix","ij", + "ee","eej","ees","eef","eex","eer","es","ej","ex","ef","or", + "ar","as","aj","ax","af","aa","aar","aas","aaf","aaj","aax","aw","awr","aws","awf","awj","aax", + "ux","uj","us","ur","uf","uw","uwx","uwj","uws","uwr","uwf", + "yx","yj","ys","yr","yf", + ); + + return str_replace($vietnamese_char, $equivalent_char, static::convert_lower($vietnamese_txt)); + + } +} diff --git a/inc/Hura8/System/Model/AuthModel.php b/inc/Hura8/System/Model/AuthModel.php new file mode 100644 index 0000000..8c38af1 --- /dev/null +++ b/inc/Hura8/System/Model/AuthModel.php @@ -0,0 +1,297 @@ +tb_login = $tb_login; + $this->tb_access_code = $tb_access_code; + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + private const ACCESS_CODE_LENGTH = 30; + const ONE_TIME_KEY_LENGTH = 15; + + + public function checkOneTimeKey($auth_key) { + $db_response = $this->db->select( + $this->tb_onetime_key, + ['user_id', 'user_name', 'client_id', 'create_time'], + [ + 'auth_key' => ["=", $auth_key], + ], + '', + 1 + ); + + if($db_response->getCode()) { + return null; + } + + $info = $db_response->getData(); + + if($info) { + + // used ONCE and delete the key + $this->db->runQuery("DELETE FROM `".$this->tb_onetime_key."` WHERE `user_id` = ? ", ['s'], [$info['user_id']]); + + return $info; + } + + return false; + } + + + // auth key allows users to export data (i.e. export to excel) for offline use + public function createNewOneTimeKey($user_id) { + + // and make a new one + $auth_key = IDGenerator::createStringId(self::ONE_TIME_KEY_LENGTH); + + $this->db->insert($this->tb_onetime_key, [ + 'user_id' => $user_id, + 'auth_key' => $auth_key, + 'create_time' => CURRENT_TIME, + ]); + + return $auth_key; + } + + + // for all subsequent requests, API need to provide access-code in the request's header + // the server will verify the code + public function checkAccessCode($access_code) { + return $this->db->select( + $this->tb_access_code, + ['user_id', 'create_time'], + [ + 'access_code' => ["=", $access_code], + ], + '', + 1 + ); + } + + + public function deleteAllAccessCode($user_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_access_code."` WHERE `user_id` = ? ", ['s'], [$user_id]); + } + + + protected function createNewAccessCode($user_id) { + + // when use login here, delete all other access code + $this->deleteAllAccessCode($user_id); + + // and make a new one + $access_code = IDGenerator::createStringId(self::ACCESS_CODE_LENGTH); + $user_device = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; + + $db_response = $this->db->insert($this->tb_access_code, [ + 'user_id' => $user_id, + 'access_code' => $access_code, + 'user_device' => substr($user_device, 0, 150), + 'create_time' => CURRENT_TIME, + ]); + + return $access_code; + } + + /** + * allow to login via mobile + * - step 1: sms an OTP code to mobile number + * - step 2: verify OTP code (mobile number and otp code sent along in the form) + */ + + public function checkLoginViaMobile($mobile, $otp) { + // todo + } + + /** + * @description An OTP code is sent to user's email. This method helps user need not remember the password + * @param $email string + * @param $otp string + */ + + public function checkLoginByOTP($user_id, $otp) { + $info = $this->db->select( + $this->tb_login, + [], + [ + 'user_id' => ["=", $user_id], + 'login_otp' => ["=", $otp], + ], + '', + 1 + ); + + if($info) { + // return to browser + return array( + 'user_id' => $user_id, + 'access_code' => $this->createNewAccessCode($user_id), + ); + } + + return false; + } + + + public function createLoginOTP($user_id) { + // check email exist + $info = $this->db->select( + $this->tb_login, + [], + [ + 'user_id' => ["=", $user_id], + ], + '', + 1 + ); + + if($info) { + $otp = IDGenerator::createStringId(6); + $this->db->update( + $this->tb_login, + [ + 'login_otp' => $otp + ], + [ + 'user_id' => $info['user_id'], + ] + ); + + return $otp; + } + + return false; + } + + + public function createOrUpdatePassword($user_id, $new_password) { + $query = $this->db->runQuery( + "SELECT `user_id` FROM `".$this->tb_login."` WHERE `user_id` = ? LIMIT 1 ", + ['d'], [ $user_id ] + ); + + if($this->db->fetchAssoc($query)) { + return $this->updatePassword($user_id, $new_password); + } + + return $this->createPassword($user_id, $new_password); + } + + + protected function createPassword($user_id, $new_password) { + return $this->db->insert( + $this->tb_login, + [ + 'user_id' => $user_id, + 'password_hash' => $this->hashPassword($new_password), + 'create_time' => CURRENT_TIME, + 'create_by' => '', + ] + ); + } + + + protected function updatePassword($user_id, $new_password) { + return $this->db->update( + $this->tb_login, + [ + 'password_hash' => $this->hashPassword($new_password), + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ], + [ + 'user_id' => $user_id, + ] + ); + } + + + /** + * @param $user_id int + * @param $password string + * @return array|false + */ + public function checkLogin($user_id, $password) { + + $info = $this->db->select( + $this->tb_login, + [ 'password_hash'], + [ + 'user_id' => ["=", $user_id], + ], + '', + 1 + ); + + //test password + if($info && $this->verifyHash($password, $info['password_hash'])) { + + $this->updateUserLogin($user_id); + + // return to browser + return array( + 'user_id' => $user_id, + 'access_code' => $this->createNewAccessCode($user_id), + ); + } + + return false; + } + + + private function updateUserLogin($user_id){ + return $this->db->update( + $this->tb_login, + [ + 'last_login_time' => CURRENT_TIME, + 'last_login_ip' => USER_IP, + 'last_login_device' => '', + 'last_login_session_id' => \Hura8\System\Security\Session::id() ?: '', + 'last_login_browser' => USER_AGENT, + ], + [ + 'user_id' => $user_id, + ] + ); + } + + /** + * @param $str string + * @return string + */ + private function hashPassword($str) { + return password_hash($str, PASSWORD_BCRYPT, array('cost' => 12 )); + } + + /** + * 15-04-2016 verify string with given hash + * @param $str_to_verify string + * @param $hash string + * @return boolean + */ + private function verifyHash($str_to_verify, $hash) { + return (password_verify($str_to_verify, $hash)); + } + + +} diff --git a/inc/Hura8/System/Model/DomainModel.php b/inc/Hura8/System/Model/DomainModel.php new file mode 100644 index 0000000..11f01f4 --- /dev/null +++ b/inc/Hura8/System/Model/DomainModel.php @@ -0,0 +1,153 @@ +create([ + 'domain' => $clean_domain, + 'lang' => $language, + ]); + } + + + protected function getInfoByDomain($clean_domain){ + $query = $this->db->runQuery( + "SELECT * from ".$this->tb_entity." WHERE `domain` = ? LIMIT 1 ", + ['s'], [ $clean_domain ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function deleteDomain($clean_domain){ + $domain_info = $this->getInfoByDomain($clean_domain); + if($domain_info) { + $this->delete($domain_info['id']); + } + + return true; + } + + + public function setDomainMain($domain, $language){ + + $this->db->update( + $this->tb_entity, + [ "is_main" => 0, ], + [ + "lang" => $language, + "is_main" => 1 + ] + ); + + $this->db->update( + $this->tb_entity, + [ + "is_main" => 1, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ], + [ + "domain" => $domain, + ] + ); + + return true; + } + + + public function setDomainLayout($domain, $layout = 'pc'){ + $this->db->update( + $this->tb_entity, + [ "layout" => $layout, ], + [ "domain" => $domain, ] + ); + + return true; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $where_condition = ""; + $bind_types = []; + $bind_values = []; + + if(isset($condition['language']) && $condition['language']) { + $where_condition = " AND `lang` = ? "; + $bind_types[] = 's'; + $bind_values[] = $condition['language']; + } + + return [ + $where_condition, + $bind_types, + $bind_values, + ]; + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + // if domain exist + if($this->getInfoByDomain($info['domain'])) { + return new AppResponse('error', "Domain exist"); + } + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } +} diff --git a/inc/Hura8/System/Model/EntityLanguageModel.php b/inc/Hura8/System/Model/EntityLanguageModel.php new file mode 100644 index 0000000..edcd454 --- /dev/null +++ b/inc/Hura8/System/Model/EntityLanguageModel.php @@ -0,0 +1,456 @@ +entity_type = $entity_type; + $this->tb_entity = $this->inferTbEntity($tb_entity); + $this->tb_entity_lang = $this->tb_entity."_lang"; + + $this->language_fields = (Config::getEntityLanguageFields())[$this->tb_entity] ?? []; + if(!sizeof($this->language_fields)) { + die("No language_fields found for ".$this->tb_entity." on file: config/client/language_fields.php"); + } + + $this->allow_richtext_fields = $allow_richtext_fields; + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + protected function inferTbEntity(string $tb_entity = '') : string + { + if(!$tb_entity) { + // auto infer + $tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $this->entity_type)); + return "tb_".strtolower($tb_name); + } + + return $tb_entity; + } + + + // set language to be used by model + public function setLanguage($language) : bool + { + $this->language = $language; + return true; + } + + + public function createTableLang(): AppResponse { + $result = $this->db->checkTableExist($this->tb_entity_lang); + + // check if table exist + if(!$result) { + $this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + + // recheck + $result = $this->db->checkTableExist($this->tb_entity_lang); + } + + return new AppResponse($result ? 'ok': 'error'); + } + + + public function recreateTableLang() { + if($this->db->checkTableExist($this->tb_entity_lang)) { + $this->db->runQuery("DROP TABLE `".$this->tb_entity_lang."` "); + } + $this->db->runQuery("CREATE TABLE `".$this->tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + } + + + // any fields to have language: title, summary, description, price, etc... + public function setLanguageFields(array $language_fields) { + $this->language_fields = $language_fields; + } + + + public function getLanguageFields() : array { + return $this->language_fields; + } + + + protected function beforeCreateItem(array $input_info) { + $info = []; + foreach ($input_info as $key => $value) { + + if(!in_array($key, $this->language_fields)) continue; + + $info[$key] = $value; + } + + return (sizeof($info) > 0) ? $info : false; + } + + protected function beforeUpdateItem($current_item_info, $new_input_info) { + $info = []; + $has_change = false; + foreach ($new_input_info as $key => $value) { + if(!in_array($key, $this->language_fields)) continue; + + if(!isset($current_item_info[$key]) || $current_item_info[$key] != $value) { + $has_change = true; + } + + $info[$key] = $value; + } + + + return ($has_change) ? $info : false; + } + + public function getEntityType() : string + { + return $this->entity_type; + } + + public function getTranslatedIds(): array + { + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `lang` = ? LIMIT 50000", + ['s', 's'], + [ $this->entity_type, $this->language] + ); + + return array_map(function ($item) { + return $item['item_id']; + }, $this->db->fetchAll($query)); + + } + + /** + * @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are) + * - only fields in ::allow_richtext_fields can keep html tags. Example: description + * - if value is array (example data=['title' => '', 'description' => html tags ]) ... + then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields + because this method checks on array value recursively + * @param array $input_info + * @return array + */ + protected function cleanRichtextFields(array $input_info) : array { + $cleaned_info = []; + foreach ($input_info as $key => $value) { + if(in_array($key, $this->allow_richtext_fields)) { + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value; + }else{ + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value); + } + } + + return $cleaned_info; + } + + public function update($id, array $new_input_info, $search_keyword = "") : AppResponse + { + + $new_input_info = $this->cleanRichtextFields($new_input_info); + + // clean url_index if any + if(isset($new_input_info['url_index']) && $new_input_info['url_index']) { + $new_input_info['url_index'] = UrlManagerController::create_url_index($new_input_info['url_index']); + + // create url for this language if url_index setup + if(in_array('url_index', $this->language_fields) && isset($new_input_info['url_index'])) { + $request_path = $this->updateUrl($id, $new_input_info['url_index']); + $new_input_info['request_path'] = $request_path; + + // request_path must be auto allowed in language_fields if url_index in there. If not, update the language_fields + // so the beforeCreateItem method can accept it + // request_path is needed for PublicEntityBaseControllerTraits to build public urls for items in when view the language + if(!in_array('request_path', $this->language_fields)){ + $this->setLanguageFields(array_merge($this->language_fields, ['request_path'])); + } + } + } + + + $current_info = $this->getInfo($id); + + if(!$current_info) { + + $data = $this->beforeCreateItem($new_input_info); + if(!$data) { + //debug_var($new_input_info); + //die("EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data"); + return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": beforeCreateItem no data"); + } + + // create + $this->db->insert( + $this->tb_entity_lang, + array( + "id" => $id, + "lang" => $this->language, + "data" => $data, + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ) + ); + + // track item + $this->db->insert( + $this->tb_track_entity_lang, + array( + "item_type" => $this->entity_type, + "item_id" => $id, + "lang" => $this->language, + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ) + ); + + return new AppResponse("ok"); + } + + // update + $new_change_info = $this->beforeUpdateItem($current_info, $new_input_info); + if(!$new_change_info) { + return new AppResponse("error", "EntityLanguageModel ".$this->entity_type.": Nothing changed"); + } + + + $updated_info = [ + "data" => array_merge($current_info, $new_change_info), // make sure update 1 key won't affect the other keys in the language + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ]; + + $result = $this->db->update( + $this->tb_entity_lang, + $updated_info, + array( + "id" => $id, + "lang" => $this->language, + ) + ); + + return new AppResponse("ok", null, $result); + } + + + protected function updateUrl($id, $lang_url_index) { + + /* + Steps: + - infer entity model -> get default url setting (url_type, request_path, ...) + - find RequestPathConfig for entity + - create RequestPath for this language + - insert/update in tb-url + + 'url_type' => $url_type, + 'request_path' => $request_path , + 'id_path' => $id_path, + 'redirect_code' => $redirect_code, + */ + + $baseModel = $this->createEntityBaseModelInstance(); + $base_info = $baseModel->getInfo($id); + + if(!$base_info || !$base_info['request_path']) { + return false; + } + + //debug_var($base_info['request_path']); + + $objUrlManager = new UrlManagerController(); + $item_url_info = $objUrlManager->getUrlInfoByRequestPath($base_info['request_path']); + + //debug_var($item_url_info); + + $url_module = $item_url_info['module'] ; + $url_view = $item_url_info['view'] ; + + $module_routing = ModuleManager::getModuleRouting($url_module); + $request_path_config = isset($module_routing[$url_view]) ? $module_routing[$url_view]['url_manager']['request_path'] : ''; + + if(!$request_path_config) { + return false; + } + + $request_path = UrlManagerController::translateRequestPathConfig($request_path_config, $id, $lang_url_index); + + if($request_path == $item_url_info['request_path']) { + return false; + } + + $id_path = $item_url_info['id_path']."/_l:".$this->language; + + $objUrlManager->createUrl($item_url_info['url_type'], $request_path, $id_path, 0); + + return $request_path; + } + + + protected function createEntityBaseModelInstance() : iEntityModel + { + $class = EntityType::getModelClass($this->entity_type); + if(class_exists($class)) { + return _init_class($class); + } + + die($class." not exist!"); + } + + + // delete all languages for items + public function deleteAll($id): AppResponse + { + + $this->db->runQuery( + "DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? ", + ['d'], + [$id] + ); + + $this->db->runQuery( + "DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? ", + ['s', 's'], + [ $this->entity_type, $id] + ); + + return new AppResponse("ok"); + } + + + public function delete($id): AppResponse + { + + $result = $this->db->runQuery( + "DELETE FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ", + ['d', 's'], + [$id, $this->language], true + ); + + if($result) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_track_entity_lang."` WHERE `item_type` = ? AND `item_id` = ? AND `lang` = ? LIMIT 1 ", + ['s', 's', 's'], + [ $this->entity_type ,$id, $this->language], + true + ); + } + + return new AppResponse("ok", null, $result); + } + + + public function getListByIds(array $list_id): array + { + + if(!sizeof($list_id)) return array(); + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int'); + + $bind_types[] = "s"; + $bind_values = $list_id; + $bind_values[] = $this->language; + + $query = $this->db->runQuery( + " SELECT * FROM ".$this->tb_entity_lang." WHERE `id` IN (".$parameterized_ids.") AND `lang` = ? ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['id']] = $this->formatItemInfo($item); + } + + return $item_list; + } + + + public function getInfo($id) : ?array + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity_lang."` WHERE `id` = ? AND `lang` = ? LIMIT 1 ", + ['d', 's'], + [$id, $this->language] + ) ; + + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + protected function formatItemInfo(array $item_info) : array + { + return ($item_info['data']) ? \json_decode($item_info['data'], true) : []; + } + + + public function getTotal(array $condition): int + { + $query = $this->db->runQuery( + " SELECT COUNT(*) AS total FROM `".$this->tb_entity_lang."` WHERE `lang` = ? " , + ['s'], [ $this->language ] + ); + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + // get empty/default item for form + public function getEmptyInfo() : array { + $empty_info = [ + "id" => 0, + "lang" => $this->language, + "create_time" => 0, + "create_by" => "", + "last_update" => 0, + "last_update_by" => "", + ]; + foreach ($this->language_fields as $field) { + $empty_info[$field] = ''; + } + + return $empty_info; + } + + +} diff --git a/inc/Hura8/System/Model/RelationModel.php b/inc/Hura8/System/Model/RelationModel.php new file mode 100644 index 0000000..9e7ccbb --- /dev/null +++ b/inc/Hura8/System/Model/RelationModel.php @@ -0,0 +1,341 @@ +db = get_db('', ENABLE_DB_DEBUG); + $this->main_item_type = $main_item_type; + $this->main_item_id = $main_item_id; + } + + + //given related_type and type_id, get all the main item ids + public function getMainItems($related_item_type, $related_item_id = 0, array $condition = [], $return_type = 'list') { + + $where_clause = " AND `main_item_type` = '" . $this->db->escape($this->main_item_type) . "' + AND `main_item_id` = '".intval($this->main_item_id)."' "; + + $where_clause .= " AND `related_item_type` = '" . $this->db->escape($related_item_type) . "' "; + + if($related_item_id) $where_clause .= " AND `related_item_id` = '".intval($related_item_id)."' "; + + //excluded_ids + if(isset($condition["excluded_ids"]) && $condition["excluded_ids"] ){ + $list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["excluded_ids"]), DataType::INTEGER); + + if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` NOT IN (".join(',', $list_ids ).") "; + } + + //included_ids + if(isset($condition["included_ids"]) && $condition["included_ids"] ){ + $list_ids = DataClean::makeListOfInputSafe(explode(",", $condition["included_ids"]), DataType::INTEGER); + if(sizeof($list_ids)) $where_clause .= " AND `related_item_id` IN (".join(',', $list_ids ).") "; + } + + + if($return_type == 'total') { + + $query = $this->db->query(" + SELECT COUNT(*) as total FROM `".$this->tb_relation."` + WHERE 1 " . $where_clause . " "); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + + } else { + + $page = isset($condition['page']) ? intval($condition['page']) : 1; + $numPerPage = isset($condition['numPerPage']) ? intval($condition['numPerPage']) : 10; + + $query = $this->db->query(" + SELECT `related_item_id` FROM `". $this->tb_relation ."` + WHERE 1 " . $where_clause . " + ORDER BY `ordering` DESC, `id` DESC + LIMIT " . ($page - 1) * $numPerPage . ", ". $numPerPage ." + "); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[] = $info['related_item_id']; + } + + return $result; + } + } + + public function updateOrdering($related_item_id, $new_order) { + + $this->db->runQuery( + "UPDATE `".$this->tb_relation."` SET + `ordering` = ? + WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_id` = ? ", + ['d', 's', 'd', 'd'], + [ $new_order, $this->main_item_type, $this->main_item_id, $related_item_id ] + ); + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + //@warn: this does not check if records exist. + public function create(array $related_items, $both_way_relation = true) { + + $build_insert = array(); + foreach ($related_items as $item) { + if(!$this->checkExist($this->main_item_type, $this->main_item_id, $item['type'], $item['id'])) { + $build_insert[] = [ + "main_item_type" => $this->main_item_type, + "main_item_id" => intval($this->main_item_id), + "related_item_type" => $item['type'], + "related_item_id" => intval($item['id']), + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ]; + } + + //if 2-way relation, create item->main + if($both_way_relation) { + if(!$this->checkExist($item['type'], $item['id'], $this->main_item_type, $this->main_item_id)) { + + $build_insert[] = [ + "main_item_type" => $item['type'], + "main_item_id" => intval($item['id']), + "related_item_type" => $this->main_item_type, + "related_item_id" => intval($this->main_item_id), + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + ]; + + } + } + } + + if(sizeof($build_insert)) { + $this->db->bulk_insert($this->tb_relation, $build_insert); + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + return [ + 'status' => 'error', + 'message' => 'Đã tồn tại' + ]; + } + + + public function checkExist($main_item_type, $main_item_id, $related_item_type, $related_item_id) { + + // itself + if($main_item_type == $related_item_type && $main_item_id == $related_item_id) { + return true; + } + + $query = $this->db->runQuery( + "SELECT id FROM `".$this->tb_relation."` + WHERE `main_item_type` = ? AND `main_item_id` = ? AND `related_item_type` = ? AND `related_item_id` = ? + LIMIT 1" , + [ + 's', 'd', 's', 'd' + ], + [ + $main_item_type, + intval($main_item_id), + $related_item_type, + intval($related_item_id) + ] + ); + + if($rs = $this->db->fetchAssoc($query)) { + return $rs['id']; + } + + return false; + } + + //remove a related-item + public function remove($related_item_type, $related_item_id, $remove_both_way = true) { + + $main_item_query = ( $this->main_item_id ) ? " `main_item_id` = '".intval($this->main_item_id)."' AND " : ""; + + $this->db->query("DELETE FROM `".$this->tb_relation."` WHERE + `main_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND + ". $main_item_query ." + `related_item_type` = '".$this->db->escape($related_item_type)."' AND + `related_item_id` = '".intval($related_item_id)."' "); + + if($remove_both_way) { + + $related_item_query = ( $this->main_item_id ) ? " `related_item_id` = '".intval($this->main_item_id)."' AND " : ""; + + $this->db->query("DELETE FROM `".$this->tb_relation."` WHERE + `related_item_type` = '" . $this->db->escape($this->main_item_type) . "' AND + " . $related_item_query . " + `main_item_type` = '".$this->db->escape($related_item_type)."' AND + `main_item_id` = '".intval($related_item_id)."' "); + } + + return [ + 'status' => 'success', + 'message' => '' + ]; + } + + + //remove all relate items + public function truncate() { + $this->db->runQuery( + "DELETE FROM `".$this->tb_relation."` WHERE `main_item_type` = ? AND `main_item_id` = ? ", + [ 's', 's' ], + [ $this->main_item_type, $this->main_item_id ] + ); + } + + + //count related items + public function getRelatedItemCount() { + + $query = $this->db->runQuery( + " + SELECT `related_item_type`, COUNT(`related_item_id`) AS total FROM `". $this->tb_relation ."` + WHERE `main_item_type` = ? AND `main_item_id` = ? + GROUP BY `related_item_type` + ", + [ 's', 's' ], [ $this->main_item_type, $this->main_item_id ] + ); + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['related_item_type']] = $info['total']; + } + + return $result; + } + + + //extend getRelatedItems to get for list of main_item_ids + public function getRelatedItemsForList(array $main_item_list_ids, array $related_item_types = []) { + + if(!sizeof($main_item_list_ids)) return []; + + $bind_values = $main_item_list_ids; + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($main_item_list_ids, 'int'); + $condition = " AND `main_item_id` IN (". $parameterized_ids .") "; + + $bind_types[] = 's'; + $bind_values[] = $this->main_item_type; + $condition .= " AND `main_item_type` = ? "; + + if(sizeof($related_item_types)) { + $type_condition = []; + foreach ($related_item_types as $_type) { + $type_condition[] = " `related_item_type` = ? "; + + $bind_types[] = 's'; + $bind_values[] = $_type; + } + + $condition .= " AND ( ".join(' OR ', $type_condition )." ) "; + } + + $query = $this->db->runQuery(" + SELECT + `main_item_id` , + `related_item_type`, + `related_item_id`, + `ordering` + FROM `". $this->tb_relation ."` + WHERE 1 ". $condition ." + ORDER BY `ordering` DESC, `id` DESC + LIMIT 5000 + ", $bind_types, $bind_values); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['main_item_id']][$info['related_item_type']][$info['related_item_id']] = [ + "item_id" => $info['related_item_id'], + "ordering" => $info['ordering'], + ]; + } + + //final result + $final_result = []; + foreach ($main_item_list_ids as $_id) { + $final_result[$_id] = (isset($result[$_id])) ? $result[$_id] : false; + } + + return $final_result; + } + + //get all related items and group them by type + public function getRelatedItems(array $related_item_types = []) { + + $bind_types = ['s', 's']; + $bind_values = [$this->main_item_type, $this->main_item_id]; + + $condition = ""; + if(sizeof($related_item_types)) { + $type_condition = []; + foreach ($related_item_types as $_type) { + $type_condition[] = " `related_item_type` = ? "; + + $bind_types[] = 's'; + $bind_values[] = $_type; + } + + $condition .= " AND ( ".join(' OR ', $type_condition )." ) "; + } + + + $query = $this->db->runQuery(" + SELECT + `related_item_type`, + `related_item_id`, + `ordering` + FROM `". $this->tb_relation ."` + WHERE `main_item_type` = ? AND `main_item_id` = ? ". $condition ." + ORDER BY `ordering` DESC, `id` DESC + LIMIT 5000 + ", $bind_types, $bind_values ); + $result = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $result[$info['related_item_type']][$info['related_item_id']] = [ + "item_id" => $info['related_item_id'], + "ordering" => $info['ordering'], + ]; + } + + //check if we get only single type, then return all the results + if(sizeof($related_item_types) == 1) { + $related_item_type = $related_item_types[0]; + return isset($result[$related_item_type]) ? $result[$related_item_type] : array(); + } + + return $result; + } + + +} diff --git a/inc/Hura8/System/Model/SettingModel.php b/inc/Hura8/System/Model/SettingModel.php new file mode 100644 index 0000000..c03cb42 --- /dev/null +++ b/inc/Hura8/System/Model/SettingModel.php @@ -0,0 +1,173 @@ + 'pcbuilder_config', + ]; + + + public function __construct(){ + $this->db = get_db('', ENABLE_DB_DEBUG); + } + + + public function getAll(){ + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_setting."` WHERE 1 ORDER BY `id` DESC LIMIT 100 " + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ){ + if($rs['setting_value']) { + $rs['setting_value'] = \unserialize($rs['setting_value']); + } + + $result[] = $rs; + } + + return $result; + } + + + // get a list of keys + public function getList(array $keys){ + $conditions = []; + $bind_types = []; + $bind_values = []; + + foreach ($keys as $key) { + $conditions[] = " `setting_key`= ? "; + $bind_types[] = 's'; + $bind_values[] = $key; + } + + if(sizeof($conditions)) { + $query = $this->db->runQuery( + " SELECT `setting_key`, `setting_value` FROM `".$this->tb_setting."` WHERE ".join(" OR ", $conditions)." LIMIT 100 ", + $bind_types, $bind_values + ); + + $result = []; + foreach ( $this->db->fetchAll($query) as $rs ){ + $result[$rs['setting_key']] = \unserialize($rs['setting_value']); + } + + return $result; + } + + return []; + } + + + // get a single key, = false if not found and no default is set + public function get($key, $default = null){ + $key = $this->cleanKey($key); + if(!$key) return null; + + $query = $this->db->runQuery( + "SELECT `setting_value` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ", + ['s'], [ $key ] + ); + + if($rs = $this->db->fetchAssoc($query)){ + return \unserialize($rs['setting_value']); + } + + return ($default) ?: null; + } + + + public function delete($key){ + return $this->db->runQuery( + "DELETE FROM `" . $this->tb_setting . "` WHERE `setting_key` = ? LIMIT 1 ", + ['s'], [ $key ] + ); + } + + + // update or create + public function updateOrCreate($key, $value = null, $comment = ''){ + $key = $this->cleanKey($key); + if(!$key) return false; + + if($this->checkKeyExist($key)) { + return $this->db->update( + $this->tb_setting , + [ + 'setting_value' => ($value) ? \serialize($value) : null, + 'comment' => subString($comment, 80), + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ], + [ + 'setting_key' => $key, + ] + ); + } + + return $this->db->insert( + $this->tb_setting , + [ + 'setting_key' => $key, + 'setting_value' => ($value) ? \serialize($value) : null, + 'comment' => subString($comment, 80), + 'create_time' => CURRENT_TIME, + 'create_by' => ADMIN_NAME, + 'last_update' => CURRENT_TIME, + 'last_update_by' => ADMIN_NAME, + ] + ); + } + + + public function populateKeys(array $key_list) { + $build_inserts = []; + foreach ($key_list as $key) { + if( ! $this->checkKeyExist($key)) { + $build_inserts[] = [ + "setting_key" => $key, + "create_time" => CURRENT_TIME, + "create_by" => 'system', + ]; + } + } + + if(sizeof($build_inserts)) { + $this->db->insert($this->tb_setting, $build_inserts); + } + + return sizeof($build_inserts); + } + + + protected function checkKeyExist($key) : bool { + $key = $this->cleanKey($key); + if(!$key) return false; + + $query = $this->db->runQuery( + " SELECT `id` FROM `".$this->tb_setting."` WHERE `setting_key`= ? LIMIT 1 ", ['s'], [ $key ] + ); + + return (bool) $this->db->fetchAssoc($query); + } + + + // only allow some characters + protected function cleanKey($key) { + return DataClean::makeInputSafe($key, DataType::ID); + } + +} diff --git a/inc/Hura8/System/Model/UrlModel.php b/inc/Hura8/System/Model/UrlModel.php new file mode 100644 index 0000000..058c679 --- /dev/null +++ b/inc/Hura8/System/Model/UrlModel.php @@ -0,0 +1,216 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_url."` WHERE `id_path` = ? LIMIT 1 ", + ['s'], [ $id_path ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function deleteByType(string $url_type) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_url."` WHERE `url_type` = ? ", + ['s'], [ $url_type ] + ); + } + + + public function deleteByRequestPath($request_path) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ", + ['s'], [ md5($request_path) ] + ); + } + + + public function getUrlByRequestPath($request_path) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_url."` WHERE `request_path_index` = ? LIMIT 1 ", + ['s'], [ md5($request_path) ] + ); + + return $this->db->fetchAssoc($query); + } + + + //02-12-2015 + public function getUrlMetaInfo($request_path){ + return $this->db->getItemInfo($this->tb_url_meta, md5($request_path), 'request_path_index'); + } + + + public function getUrlMetaInfoById($id){ + return $this->db->getItemInfo($this->tb_url_meta, $id, 'id'); + } + + + public function getEmptyUrlMetaInfo() { + return array( + 'id' => 0, + 'request_path' => '', + 'h1' => '', + 'meta_title' => '', + 'meta_keyword' => '', + 'meta_description' => '', + 'og_image' => '', + 'summary' => '', + 'description' => '', + ); + } + + + //02-12-2015 + public function createUrlMeta(array $info){ + + //prevent duplication + if($this->getUrlMetaInfo($info['request_path'])) return false; + + if(!defined("ADMIN_NAME")) define("ADMIN_NAME", "system"); + + $copy = $info; + $copy['request_path_index'] = md5($info['request_path']); + $copy['last_update'] = CURRENT_TIME; + $copy['last_update_by'] = ADMIN_NAME; + + return $this->db->insert( + $this->tb_url_meta , + $copy + ); + } + + + public function updateUrlMeta($id, array $info){ + $copy = $info; + $copy['last_update'] = CURRENT_TIME; + $copy['last_update_by'] = ADMIN_NAME; + + return $this->db->update( + $this->tb_url_meta , + $copy, + [ + 'id' => $id, + ] + ); + } + + + protected function _buildQueryConditionExtend(array $condition): ?array + { + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //loc theo is_redirect + if(isset($condition["request_path"]) && $condition["request_path"]) { + $path = preg_replace("/[^a-z0-9_\.\/\-]/i", '', $condition["request_path"]); + if($path) $catCondition[] = " AND `request_path` LIKE '%".$path."%' "; + } + + //loc theo is_redirect + if(isset($condition["is_redirect"]) && $condition["is_redirect"]) { + if($condition["is_redirect"] == 1) $catCondition[] = " AND `url_type` = 'redirect' "; + else if($condition["is_redirect"] == -1) $catCondition[] = " AND `url_type` != 'redirect' "; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info): AppResponse + { + $request_path = $input_info['request_path'] ?? ''; + $id_path = $input_info['id_path'] ?? ''; + + if(!$request_path || $this->getUrlByRequestPath($request_path)) { + return new AppResponse('error', "request_path exist"); + } + + if($id_path && $this->getUrlByIdPath($id_path)) { + return new AppResponse('error', "id path exist"); + } + + $admin_name = (defined("ADMIN_NAME")) ? ADMIN_NAME : "system"; + + $info = $input_info; + + $info['request_path_index'] = md5($info['request_path']); + + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = $admin_name; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = $admin_name; + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // TODO: Implement afterCreateItem() method. + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info): AppResponse + { + $info = $new_input_info; + unset($info['id_path']); + + if(isset($info['request_path']) && $info['request_path']) { + $info['request_path_index'] = md5($info['request_path']); + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + // TODO: Implement afterUpdateItem() method. + } + + + protected function beforeDeleteItem($item_id, $item_info): AppResponse + { + return new AppResponse('ok'); + } + + + protected function afterDeleteItem($item_id, $item_info) + { + // TODO: Implement afterDeleteItem() method. + } + + + protected function extendedFilterOptions(): array + { + return []; + } +} diff --git a/inc/Hura8/System/Model/UtilityModel.php b/inc/Hura8/System/Model/UtilityModel.php new file mode 100644 index 0000000..6470a66 --- /dev/null +++ b/inc/Hura8/System/Model/UtilityModel.php @@ -0,0 +1,39 @@ +db = get_db($db_id, ENABLE_DB_DEBUG); + } + + + public function createTableLang($tb_entity_lang): AppResponse { + $result = $this->db->checkTableExist($tb_entity_lang); + + // check if table exist + if(!$result) { + $this->db->runQuery("CREATE TABLE `".$tb_entity_lang."` LIKE `".$this->tb_entity_lang_template."` "); + + // recheck + $result = $this->db->checkTableExist($tb_entity_lang); + } + + return new AppResponse($result ? 'ok': 'error'); + } + + +} diff --git a/inc/Hura8/System/Model/WebUserModel.php b/inc/Hura8/System/Model/WebUserModel.php new file mode 100644 index 0000000..97c6c6e --- /dev/null +++ b/inc/Hura8/System/Model/WebUserModel.php @@ -0,0 +1,92 @@ +db = get_db('', ENABLE_DB_DEBUG); + $this->user_browser_id = preg_replace("/[^a-z0-9]/i", "", $user_browser_id); + $this->user_db_id = (defined("USER_ID")) ? USER_ID : 0; + } + + public function getValue($key) { + $key = $this->cleanKey($key); + + $query = $this->db->runQuery("SELECT `content` FROM `".$this->tb_user."` + WHERE `user_id` = ? AND `key` = ? + LIMIT 1 ", ['s', 's'], [ $this->user_browser_id , $key ]) ; + if($rs = $this->db->fetchAssoc($query )){ + return \unserialize($rs['content']); + } + + return null; + } + + public function setValue($key, $value) { + $key = $this->cleanKey($key); + + if($row_id = $this->checkKey($key)) { + return $this->db->update( + $this->tb_user , + [ + 'content' => \serialize($value) , + 'user_db_id' => $this->user_db_id , + 'last_update' => CURRENT_TIME , + ], + [ + 'id' => $row_id, + ] + ); + + } + + return $this->db->insert( + $this->tb_user , + [ + 'user_id' => $this->user_browser_id , + 'user_db_id' => $this->user_db_id, + 'key' => $key, + 'content' => \serialize($value), + 'create_time' => CURRENT_TIME , + 'last_update' => CURRENT_TIME, + ] + ); + } + + //delete all user history + public function deleteUser($key) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_user."` WHERE `user_id` = ? LIMIT 1 ", + ['s'], + [ $this->user_browser_id ] + ) ; + } + + protected function cleanKey($key) { + return preg_replace("/[^a-z0-9]/i", "", $key); + } + + protected function checkKey($key) { + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_user."` + WHERE `user_id` = ? AND `key` = ? + LIMIT 1", ['s', 's'], [ $this->user_browser_id , $key ] ) ; + if($rs = $this->db->fetchAssoc($query )){ + return $rs['id']; + } + + return false; + } +} diff --git a/inc/Hura8/System/Model/aCategoryBaseModel.php b/inc/Hura8/System/Model/aCategoryBaseModel.php new file mode 100644 index 0000000..5540e59 --- /dev/null +++ b/inc/Hura8/System/Model/aCategoryBaseModel.php @@ -0,0 +1,292 @@ +array(childId) + foreach ( $this->getAll($condition) as $item ) { + $item_list[$item['parent_id']][$item['id']] = $item; + } + + return $item_list; + } + + + // get all categories + public function getAll(array $condition = array()){ + /*$condition = [ + "parent_id" => 1, + "status" => 1, + ];*/ + + $bind_types = []; + $bind_values = []; + $where_clause = ''; + + if(isset($condition['status'])) { + $where_clause .= " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = intval($condition['status']); + } + + $query = $this->db->runQuery( + " SELECT * FROM `". $this->tb_entity ."` WHERE 1 ".$where_clause." ORDER BY `ordering` DESC LIMIT 5000 ", + $bind_types, $bind_values + ); + + return $this->db->fetchAll($query); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $parent_id = isset($input_info['parent_id']) ? $input_info['parent_id'] : 0; + $api_key = (isset($input_info['api_key']) && $input_info['api_key']) ? $input_info['api_key'] : $input_info['title']; + $api_key = Security\DataClean::makeInputSafe($api_key, DataType::ID); + + $api_key = $this->createUniqueAPIKey(0, $api_key); + + if(!isset($input_info['url_index']) || !$input_info['url_index']) { + $input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['title']); + }else{ + $input_info['url_index'] = $this->createUniqueUrlIndex(0, $input_info['url_index']); + } + + $info = array_merge($input_info, array( + "parent_id" => $parent_id, + "api_key" => $api_key, + "create_by" => ADMIN_NAME, + "create_time" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + ) ); + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) { + //update path&child + $this->updatePath($new_item_id); + $this->updateChild($new_item_id); + $this->updateChild($new_item_info['parent_id']); + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + if(isset($info['url_index'])) { + if(!$info['url_index']) { + $info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['title']); + }else{ + $info['url_index'] = $this->createUniqueUrlIndex($item_id, $info['url_index']); + } + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) { + //update cat-path for category + $this->updatePath($item_id); + $this->updateChild($item_id); + + //update child_ids for new/old parents and parents of parent + if($new_item_info['parent_id'] != $old_item_info['parent_id']) { + $this->updateChild($new_item_info['parent_id']); + $this->updateChild($old_item_info['parent_id']); + } + } + + + protected function afterDeleteItem($item_id, $item_info){ + $this->updateChild($item_info["parent_id"]); + } + + + //create an unique request-path + protected function createUniqueAPIKey($id, $api_key){ + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `api_key` = ? LIMIT 1 ", ['s'], [$api_key]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $id) { + $new_api_key = $api_key."-1"; + return $this->createUniqueAPIKey($id, $new_api_key); + } + } + + return $api_key; + } + + + protected function updatePathAndChildAll($id, $child_ids, $parent_id, $old_parent_id) { + $this->updatePathAndChild($id); + + //update for childs + $list_child_to_update = array_filter(explode(",", $child_ids)); + foreach($list_child_to_update as $_id) { + if($_id != $id) $this->updatePathAndChild($_id); + } + + //cap nhat lai child list cua danh muc old_parent and new parent id, and parent of these parents + $query = $this->db->runQuery( + "SELECT cat_path FROM `". $this->tb_entity ."` WHERE `id`= ? OR `id`= ? ", + ['d', 'd'], [$parent_id, $old_parent_id] + ); + + $cat_path_all = join(":", array_map(function ($item){ + return $item['cat_path']; + } , $this->db->fetchAll($query))); + + + $list_parent_to_update = array_unique(array_filter(explode(":", $cat_path_all))); + + foreach($list_parent_to_update as $_id) { + if($_id > 0) $this->updatePathAndChild($_id); + } + } + + + public function updatePathAndChild($id) { + if(!$id) return false; + + $new_cat_path = $this->findCatPath($id); + $new_child_list = $this->findChildList($id); + $is_parent = ( $new_child_list === $id) ? 0 : 1; + + return $this->db->update( + $this->tb_entity , + [ + 'cat_path' => $new_cat_path, + 'child_ids' => $new_child_list, + 'is_parent' => $is_parent, + ], + [ + 'id' => $id, + ] + ); + } + + + protected function updatePath($id) { + if(!$id) return false; + + $new_cat_path = $this->findCatPath($id); + + return $this->db->update( + $this->tb_entity , + [ + 'cat_path' => $new_cat_path, + ], + [ + 'id' => $id, + ] + ); + } + + //update childs for current id and its parents and its parents' parents.... + protected function updateChild($id) { + if(!$id) return false; + + $query = $this->db->runQuery("SELECT `cat_path` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ; + + if($item_info = $this->db->fetchAssoc($query)){ + $cat_id_list = array_filter(explode(":", $item_info['cat_path'])); + + foreach ($cat_id_list as $_id) { + $new_child_list = $this->findChildList($_id); + $is_parent = ( $new_child_list === $_id) ? 0 : 1; + + $this->db->update( + $this->tb_entity , + [ + 'child_ids' => $new_child_list, + 'is_parent' => $is_parent, + ], + [ + 'id' => $_id, + ] + ); + } + } + + return true; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array(join(" ", $catCondition), $bind_types, $bind_values); + } + + + //build category path 0:parent:categoryId + protected function findCatPath($categoryId){ + $path = ":".$categoryId; + $query = $this->db->runQuery("SELECT `parent_id` FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1", ['d'], [$categoryId]); + if($rs = $this->db->fetchAssoc($query)){ + if($rs['parent_id']) $path .= $this->findCatPath($rs['parent_id']); + } + + return $path; + } + + //lay toan bo cac muc la con + protected function findChildList($p_category_id){ + $all_categories = $this->getAllByParent(); + + $list = $p_category_id; + if(isset($all_categories[$p_category_id])) { + foreach ( $all_categories[$p_category_id] as $rs ) { + $list .= ",". $this->findChildList($rs['id']); + } + } + + return $list; + } + + + protected function getAllParent(array $condition = []) { + $cache_key = 'all-category-by-parent-'.$this->getEntityType(); + + return self::getCache($cache_key, function () use ($condition){ + return $this->getAllByParent($condition); + }); + } + +} diff --git a/inc/Hura8/System/Model/aEntityBaseModel.php b/inc/Hura8/System/Model/aEntityBaseModel.php new file mode 100644 index 0000000..26bd886 --- /dev/null +++ b/inc/Hura8/System/Model/aEntityBaseModel.php @@ -0,0 +1,428 @@ + '', // keyword search + 'q_options' => [ + 'field_filters' => [], + 'fulltext_fields' => ['keywords'], + 'limit_result' => 2000 + ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000) + 'featured' => '', // 1|-1 + 'status' => '', // 1|-1 + 'excluded_ids' => null, // [id1, id2, ...] or null + 'included_ids' => null, // [id1, id2, ...] or null + ]; + + /* @var ?iSearch $iSearchModel */ + protected $iSearchModel; + + public function __construct( + $entity_type, + $tb_entity = "", + $iSearchModel = null, + $allow_richtext_fields = [] + ) { + + $this->entity_type = $entity_type; + + $this->db = get_db('', ENABLE_DB_DEBUG); + $this->iSearchModel = $iSearchModel; + $this->allow_richtext_fields = $allow_richtext_fields; + + if($tb_entity) { + $this->tb_entity = $tb_entity; + }else{ + //auto infer + $tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $entity_type)); + $this->tb_entity = "tb_".strtolower($tb_name); + } + } + + + /** + * @description to be extended by extending class + * besides the $base_filter_condition_keys which allows to find items in ::getList($condition) + * example: + [ + 'category' => array[], + 'brand' => array[], + ] + * @return array + */ + abstract protected function extendedFilterOptions() : array ; + + + /** + * @description utility to get all possible entity filter during module development + * @return array + */ + public function getAllowedFilterConditionKeys(): array + { + return array_merge( + $this->extendedFilterOptions(), + $this->base_filter_condition_keys + ); + } + + + public function setRichtextFields(array $allow_richtext_fields = []) { + $this->allow_richtext_fields = $allow_richtext_fields; + } + + + + + /** + * @description + * @param array $filter_condition + * @return array|null array($string_where, $bind_types, $bind_values ); + */ + protected function _buildQueryConditionExtend(array $filter_condition): ?array { + return []; + } + + + /** + * @param string $sort_by + * @return string + */ + protected function _buildQueryOrderBy(string $sort_by = "new") { + return " `id` DESC "; + } + + + /** + * @param array $item_info + * @return array + */ + protected function formatItemInList(array $item_info) : array { + return $item_info; + } + + + /** + * @param array $item_info + * @return array|null + */ + protected function formatItemInfo(array $item_info) : ?array { + return $item_info; + } + + + + public function getEntityType() : string { + return $this->entity_type; + } + + + /** + * @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are) + * - only fields in ::allow_richtext_fields can keep html tags. Example: description + * - if value is array (example data=['title' => '', 'description' => html tags ]) ... + then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields + because this method checks on array value recursively + * @param array $input_info + * @return array + */ + protected function cleanRichtextFields(array $input_info) : array { + $cleaned_info = []; + foreach ($input_info as $key => $value) { + if($value instanceof MysqlValue) { + $cleaned_info[$key] = $value; + } else { + if(in_array($key, $this->allow_richtext_fields)) { + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value; + }else{ + $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value); + } + } + } + + return $cleaned_info; + } + + + + public function getListByIds(array $list_id, array $condition = array()) : array + { + if(!sizeof($list_id)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int'); + + $where_clause = ""; + $bind_values = $list_id; + if(isset($condition['status'])) { + $where_clause .= " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = intval($condition['status']); + } + + $query = $this->db->runQuery( + " SELECT * FROM ".$this->tb_entity." WHERE `id` IN (".$parameterized_ids.") ".$where_clause, + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['id']] = $this->formatItemInList($item); + } + + return $item_list; + } + + + public function getInfo($id) : ?array + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + /** + * @description utility to inspect the actual filters which will be used in getList by Model + * @param array $condition + * @return array + */ + public function getQueryCondition(array $condition) : array + { + return $this->_buildQueryCondition($condition); + } + + + public function getTotal(array $condition) : int + { + list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition); + + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_entity."` WHERE 1 " . $where_clause, + $bind_types, $bind_values + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getList(array $condition) : array + { + list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition); + + //debug_var([$where_clause, $bind_types, $bind_values]); + + $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ; + $page = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ; + + $sort_by = (isset($condition['sort_by']) && $condition['sort_by']) ? $condition['sort_by'] : 'new' ; + $order_by = $this->_buildQueryOrderBy($sort_by); + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_entity." WHERE 1 ". $where_clause . " + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + $bind_types, + $bind_values + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + + foreach ( $this->db->fetchAll($query) as $item_info ) { + $counter += 1; + + $item = $item_info; + $item['counter'] = $counter; + + $item_list[] = $this->formatItemInList($item); + } + + return $item_list; + } + + + //to avoid duplicate codes, extended class should implement this method for extra filter keys + // which are not in the base filter ::_buildQueryConditionBase + protected function _buildQueryCondition(array $filter_condition) + { + // these keys are for pagination and ordering list, which are commonly included in the $filter_condition + $excluded_keys = [ + 'numPerPage', 'page', 'sort_by' + ]; + + + $base_filter_conditions = []; + $extend_filter_conditions = []; + foreach ($filter_condition as $key => $value) { + if(in_array($key, $excluded_keys)) { + continue; + } + + if(array_key_exists($key, $this->base_filter_condition_keys) ) { + $base_filter_conditions[$key] = $value; + }else{ + $extend_filter_conditions[$key] = $value; + } + } + + list($base_where, $base_bind_types, $base_bind_values) = $this->_buildQueryConditionBase($base_filter_conditions); + + list( + $extend_where, + $extend_bind_types, + $extend_bind_values + ) = $this->_buildQueryConditionExtend($extend_filter_conditions) ?: [ '', null, null]; + + + if($extend_where) { + return array( + join(" ", [$base_where, $extend_where]), + array_merge($base_bind_types, $extend_bind_types), + array_merge($base_bind_values, $extend_bind_values), + ); + } + + return array( + $base_where, + $base_bind_types, + $base_bind_values + ); + + } + + + protected function _buildQueryConditionBase(array $filter_conditions) + { + /* + $filter_conditions = [ + 'q' => '', // keyword search + 'q_options' => [ + 'field_filters' => [], + 'fulltext_fields' => ['keywords'], + 'limit_result' => 2000 + ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000) + + 'featured' => '', // 1|-1 + 'status' => '', // 1|-1 + 'excluded_ids' => [], // [id1, id2, ...] or null + 'included_ids' => [], // [id1, id2, ...] or null + ]; + */ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //Tim kiem theo tu khoa + if(isset($filter_conditions["q"]) && $filter_conditions["q"] && $this->iSearchModel ){ + + $q_options = $filter_conditions["q_options"] ?? []; + $field_filters = $q_options['field_filters'] ?? []; + $fulltext_fields = $q_options['fulltext_fields'] ?? []; + $limit_result = $q_options['limit_result'] ?? 2000; + + $match_result = $this->iSearchModel->find($filter_conditions["q"], $field_filters, $fulltext_fields, $limit_result); + + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($match_result, 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $match_result); + }else{ + $catCondition[] = " AND `id` = -1 "; + } + } + + //loc theo featured + if(isset($filter_conditions["featured"]) && $filter_conditions["featured"]) { + $catCondition[] = " AND `is_featured` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($filter_conditions["featured"] == 1) ? 1 : 0; + } + + //loc theo status + if(isset($filter_conditions["status"]) && $filter_conditions["status"]) { + $catCondition[] = " AND `status` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($filter_conditions["status"] == 1) ? 1 : 0; + } + + // excluded_ids + if(isset($filter_conditions['excluded_ids']) && is_array($filter_conditions['excluded_ids']) ) { + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['excluded_ids'], 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` NOT IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $filter_conditions['excluded_ids']); + } + } + + // included_ids + if(isset($filter_conditions['included_ids']) && is_array($filter_conditions['included_ids']) ) { + list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['included_ids'], 'int'); + + if($parameterized_ids) { + $catCondition[] = " AND `id` IN (".$parameterized_ids.") "; + $bind_types = array_merge($bind_types, $sub_bind_types); + $bind_values = array_merge($bind_values, $filter_conditions['included_ids']); + }else{ + $catCondition[] = " AND `id` = -1 "; + } + } + + return array( + join(" ", $catCondition), + $bind_types, + $bind_values + ); + } + + + // get empty/default item for form + public function getEmptyInfo($addition_field_value = []) : array + { + $table_info = $this->db->getTableInfo($this->tb_entity); + $empty_info = []; + foreach ($table_info as $field => $field_info) { + $empty_info[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float', 'mediumint', 'smallint', 'tinyint']) ) ? 0 : '' ; + } + + if(sizeof($addition_field_value)) { + return array_merge($empty_info, $addition_field_value); + } + + return $empty_info; + } + + +} diff --git a/inc/Hura8/System/Model/aSearchBaseModel.php b/inc/Hura8/System/Model/aSearchBaseModel.php new file mode 100644 index 0000000..33ab607 --- /dev/null +++ b/inc/Hura8/System/Model/aSearchBaseModel.php @@ -0,0 +1,530 @@ + matches: abcd, abc, abcde + + // require rebuilding search if change this value + protected $min_star_search_length = 2; //min star word length: result: abcd -> keywords: ab, abc, abcd NOT a + + // define list of fields to be the filters + protected $config_filter_fields = [ + //format: field_name => map table_name.field + //'price' => "tb_product.price", + //'quantity' => "tb_product.quantity", + ]; + + protected $config_fulltext_fields = [ + //format: field_name => map [table_name.field] + //"product_keywords" => ["tb_product.title", "tb_product.model", "tb_product.sku"], + //"category_keywords" => ["tb_category.title"], + ]; + + + public function __construct( + $tb_main, + array $config_fulltext_fields , + array $config_filter_fields = [] + ) { + + // ovewrite default + if(defined('CONFIG_STAR_SEARCH')) $this->star_search = CONFIG_STAR_SEARCH; + if(defined('CONFIG_STAR_SEARCH_MIN_LENGTH')) $this->min_star_search_length = CONFIG_STAR_SEARCH_MIN_LENGTH; + if(defined('CONFIG_SEARCH_VIETNAMESE')) $this->search_vietnamese = CONFIG_SEARCH_VIETNAMESE; + + $this->db = get_db('', ENABLE_DB_DEBUG); + $this->config_fulltext_fields = $config_fulltext_fields; + $this->config_filter_fields = $config_filter_fields; + + $this->tb_main = $tb_main; + $part_name = str_replace("tb_", "", preg_replace("/[^a-z0-9_]/i", "", $tb_main)); + $this->tb_fulltext = "tb_search_".strtolower($part_name); + } + + /** + * @description get filter fields + * @param array[string] + */ + public function getFilterFields(): array { + return $this->config_filter_fields; + } + + + /** + * @description get fulltext fields + * @param array[string] + */ + public function getFulltextFields(): array { + return $this->config_fulltext_fields; + } + + + public function getSampleData() : array { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_fulltext."` ORDER BY `item_id` DESC LIMIT 10" + ); + + return $this->db->fetchAll($query); + } + + + public function getTableName() : string { + return $this->tb_fulltext; + } + + + /** + * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]] + * @return array + */ + protected function _buildFieldFilters(array $field_filters = []) { + $permit_operations = [">","=","<",">=","<=", "BETWEEN", "IN"]; + $where_clause = []; + $bind_types = []; + $bind_values = []; + + foreach ($field_filters as $field => $info) { + list($operation, $value) = $info; + + $operation = strtoupper($operation); + + if(!in_array(strtolower($operation), $permit_operations)) continue; + + if($operation == 'BETWEEN') { + // $value must be array(value_int_1, value_int_2) + if(is_array($value) && sizeof($value) == 2) { + list($value_1, $value_2) = $value; + + if(is_int($value_1) && is_int($value_2) && $value_1 < $value_2) { + $where_clause[] = " AND `".$field."` BETWEEN (?, ?) "; + + $bind_types[] = "d"; + $bind_types[] = "d"; + + $bind_values[] = $value_1; + $bind_values[] = $value_2; + } + } + + continue; + } + + if($operation == 'IN') { + // $value must be array(value1, value2) + if(is_array($value) && sizeof($value) > 0) { + $parameterized = []; + foreach ($value as $_v) { + $parameterized[] = "?"; + $bind_types[] = "s"; + $bind_values[] = $_v; + } + + $where_clause[] = " AND `".$field."` IN (".join(",", $parameterized).") "; + } + + continue; + } + + + // default operation, comparison requires value to be a digit + $where_clause[] = " AND `".$field."` ".$operation." ? "; + $bind_types[] = "d"; + $bind_values[] = $value; + } + + return array(join(" ", $where_clause), $bind_types, $bind_values); + } + + + /** + * @param $keyword + * @param array $field_filters ["price" => [">", 100], "brand" => ["=", 1]] + * @param array $fulltext_fields ['title_keywords', "category_keywords"] + * @param int $limit_result + * @return int[] + */ + public function find($keyword, array $field_filters = [], array $fulltext_fields = [], $limit_result = 2000) : array + { + + if(!sizeof($fulltext_fields)) $fulltext_fields = array_keys($this->config_fulltext_fields); + + $cache_key = md5("find-".$keyword); + + return static::getCache($cache_key, function () use ($keyword, $field_filters, $fulltext_fields, $limit_result){ + + $keyword_clean = ($this->search_vietnamese) ? $this->make_text_search_vn($keyword) : $this->make_text_clean($keyword) ; //make_text_search_vn + + $keyword_clean_ele = array_filter(explode(" ", $keyword_clean)); + $build_boolean_search = "+".join(" +", $keyword_clean_ele); + + if(!$limit_result || $limit_result > 5000) $limit_result = 5000; + $limit_condition = ($limit_result > 0) ? " LIMIT ".$limit_result : ""; + + list($where_clause, $bind_types, $bind_values) = $this->_buildFieldFilters($field_filters); + + //validate search fields + $validated_fulltext_fields = array_filter($fulltext_fields, function ($item) { return array_key_exists($item, $this->config_fulltext_fields); }); + + if(!sizeof($validated_fulltext_fields)) { + return []; + } + + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_fulltext."` + WHERE MATCH(".join(',', $validated_fulltext_fields).") AGAINST('".$build_boolean_search."' IN BOOLEAN MODE) + ".$where_clause." + ORDER BY `item_id` DESC + ".$limit_condition." + ", + $bind_types, $bind_values + ); + + return array_map(function ($item){ return $item['item_id']; }, $this->db->fetchAll($query) ); + + }); + + } + + + protected function getItemInfo($item_id) + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return $this->db->fetchAssoc($query); + } + + + protected function checkExist($item_id) : bool + { + $query = $this->db->runQuery( + "SELECT `item_id` FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return (bool) $this->db->fetchAssoc($query); + } + + + // get all table_fields to watch which will affect the search + protected function getWatchTableFields() { + $result = []; + foreach ($this->config_filter_fields as $field => $table_field ) { + $result[] = $table_field; + } + + foreach ($this->config_fulltext_fields as $field => $table_fields ) { + $result = array_merge($result, $table_fields); + } + + return $result; + } + + + /** + * @description update or create item if not exist + * @param $item_id + * @param array $table_field_values + $table_field_values = [ + "tb_product.price" => 2000000, + "tb_product.title" => "Máy tính ABC", + "tb_category.price" => "Máy tính", + + "tb_product.field_not_be_update" => "Mmodel", + ]; + * @return AppResponse + */ + public function updateItem($item_id, array $table_field_values = []) : AppResponse + { + + $fulltext_fields = []; + foreach ($this->config_fulltext_fields as $fulltext_field => $fulltext_map_fields ) { + foreach ($fulltext_map_fields as $_table_field) { + if(array_key_exists($_table_field, $table_field_values)) { + $fulltext_fields[$fulltext_field][$_table_field] = $table_field_values[$_table_field]; + } + } + } + + //echo "fulltext_fields"; + //debug_var($fulltext_fields); + //echo "
"; + + $field_filters = []; + foreach ($this->config_filter_fields as $filter_field => $filter_map_field ) { + if(array_key_exists($filter_map_field, $table_field_values)) { + $field_filters[$filter_field] = $table_field_values[$filter_map_field]; + } + } + + //echo "field_filters"; + //debug_var($field_filters); + + // nothing to update + if(!sizeof($fulltext_fields) && !sizeof($field_filters)) { + return new AppResponse('error', "nothing to update" ); + } + + + // create entry if not exist + if(!$this->checkExist($item_id)) { + $this->db->insert($this->tb_fulltext, ['item_id' => $item_id, 'create_time' => CURRENT_TIME]); + } + + $current_info = $this->getItemInfo($item_id); + if(!$current_info) { + return new AppResponse('error', "Cannot find record for ".$item_id ); + } + + // update + $updated_fulltext = []; + + foreach ($this->config_fulltext_fields as $_filter_field => $_n ) { + if(!isset($fulltext_fields[$_filter_field])) continue; + + $current_filter_field_base = ($current_info[$_filter_field."_base"]) ? \json_decode($current_info[$_filter_field."_base"], true) : []; + + $new_filter_field_base = $current_filter_field_base; + + foreach ($fulltext_fields[$_filter_field] as $_table_field => $_new_value) { + $new_filter_field_base[$_table_field] = $_new_value; + } + + if(json_encode($new_filter_field_base) != json_encode($current_filter_field_base)) { + $updated_fulltext[$_filter_field."_base"] = $new_filter_field_base; + $updated_fulltext[$_filter_field] = ($this->search_vietnamese) ? $this->make_text_search_vn(join(" ", array_values($new_filter_field_base))) : $this->make_text_clean(join(" ", array_values($new_filter_field_base))); + + } + } + + + $updated_filters = []; + foreach ($this->config_filter_fields as $_filter_field => $_table_field ) { + if(!isset($field_filters[$_filter_field])) continue; + + if($field_filters[$_filter_field] != $current_info[$_filter_field] ) { + $updated_filters[$_filter_field] = $field_filters[$_filter_field]; + } + } + + + // nothing to update + if(!sizeof($updated_filters) && !sizeof($updated_fulltext)) { + return new AppResponse('error', "nothing to update" ); + } + + $updated_info = array_merge($updated_filters, $updated_fulltext); + $updated_info['last_update'] = CURRENT_TIME; + + //echo "updated_info =
"; + //debug_var($updated_info); + + $this->db->update( + $this->tb_fulltext, + $updated_info, + ['item_id' => $item_id] + ); + + return new AppResponse('ok'); + } + + public function deleteItem($item_id) : AppResponse + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` = ? LIMIT 1", + ['d'], [$item_id] + ); + + return new AppResponse('ok'); + } + + public function deleteItems(array $item_list_ids) : AppResponse + { + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($item_list_ids, "int"); + + $this->db->runQuery( + "DELETE FROM `".$this->tb_fulltext."` WHERE `item_id` IN (".$parameterized_ids.") ", + $bind_types, + $item_list_ids + ); + + return new AppResponse('ok'); + } + + public function recreateTableSearch() { + if($this->db->checkTableExist($this->tb_fulltext)) { + $this->db->runQuery( + "DROP TABLE `".$this->tb_fulltext."` " + ); + } + + $this->createTableSearch(); + } + + public function createTableSearch() { + // check if table exist + if($this->db->checkTableExist($this->tb_fulltext)) { + return true; + } + + if(!$this->db->checkTableExist($this->tb_main)) { + return false; + } + + $sql = "CREATE TABLE `".$this->tb_fulltext."` ( `item_id` INT(11) UNSIGNED NOT NULL DEFAULT '0', "; + + // add fields + + foreach ($this->config_filter_fields as $_filter_field => $_map_table_field ) { + list($_table, $_f) = explode(".", $_map_table_field); + + $_table_info = $this->db->getTableInfo($_table); + + $column_default = $_table_info[$_f]['COLUMN_DEFAULT']; + + $default_value = (in_array($_table_info[$_f]['DATA_TYPE'], ['mediumint', 'int', 'tinyint', 'double', 'float'])) ? 0 : $column_default; + + $sql .= " `".$_filter_field."` ".$_table_info[$_f]['COLUMN_TYPE']." NOT NULL DEFAULT '".$default_value."', "; + } + + foreach ($this->config_fulltext_fields as $_filter_field => $_t ) { + $sql .= " `".$_filter_field."` TEXT NULL , "; + + // create field-data to compare new values before re-indexing + $sql .= " `".$_filter_field."_base` TEXT NULL , "; + } + + $sql .= " `create_time` int(11) NOT NULL DEFAULT '0', "; + $sql .= " `last_update` int(11) NOT NULL DEFAULT '0', "; + + // index + + foreach ($this->config_filter_fields as $_filter_field => $_data_type ) { + $sql .= " INDEX (`".$_filter_field."`), "; + } + + // fulltext on separate columns + foreach ($this->config_fulltext_fields as $_filter_field => $_t ) { + $sql .= " FULLTEXT INDEX (`".$_filter_field."` ) , "; + } + + // create full text for all columns combined, so we can do query match(col_1, col_2) against keyword + $_filter_fields = array_keys($this->config_fulltext_fields); + if(sizeof($_filter_fields)) $sql .= " FULLTEXT INDEX (".join(", ", $_filter_fields).") , "; + + $sql .= " PRIMARY KEY ( `item_id` ) "; + $sql .= " ) ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + //return $sql; + $this->db->runQuery($sql); + + // add foreign key constraint + $this->db->runQuery(" + ALTER TABLE `".$this->tb_fulltext."` ADD FOREIGN KEY (`item_id`) REFERENCES `".$this->tb_main."`(`id`) ON DELETE CASCADE; + "); + + return true; + } + + //11-11-2012 chuyen text thanh tieng viet voi dau chuyen doi: vd. nhà = nhas + //because mysql allow fulltext search with minimum 4 characters, we have a trick: add hura to everyone what has length < 4 + protected function make_text_search_vn($text, $enable_star = false){ + $text = Language::convertText($text); + $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text)); + //$text = str_replace(" "," ",$text); + $text_ele = array_filter(explode(" ", $text)); + $new_text = ""; + foreach($text_ele as $ele) { + if($ele == '/' ) continue; //skip this character if it stands alone + + if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele; + else $new_text .= " ".$ele; + } + return trim($new_text); + } + + //11-11-2012 chuyen text thanh tieng viet khong dau: vd. nhà = nha + /* + @variable: + - $text: text to be index + + //16-11-2012 + - $enable_star: + true: allow search abcd by word: ab or abc + false: not enabled + - $min_len: if $enable_star = true, abcd -> tao thanh: ab, abd, abcd (if $min_len = 2) + */ + protected function make_text_clean($_text, $enable_star = false){ + //$text = Language::chuyenKhongdau(Language::convert_lower($text)); + $text = Language::chuyenKhongdau($_text); + + $text = preg_replace("@[^a-z0-9\s]@si", " ", strtolower($text)); + + $text_ele = array_filter(explode(" ", $text)); + $new_text = ""; + foreach($text_ele as $ele) { + + if($this->star_search && $enable_star) { + + $word_list = static::create_star_search($ele, $this->min_star_search_length); + foreach($word_list as $new_ele) { + if(strlen($new_ele) < 4 || $ele == 'plus') $new_text .= " hura".$new_ele; + else $new_text .= " ".$new_ele; + } + + }else{ + if(strlen($ele) < 4 || $ele == 'plus') $new_text .= " hura".$ele; + else $new_text .= " ".$ele; + } + } + return trim($new_text); + } + + //16-11-2012 + //create all possible combination for a word for star search: abcd -> tao thanh: ab, abd, abcd, bc, bcd, cd (if $min_len = 2) + protected function create_star_search($word, $min_len = 2){ + if($min_len < 2) $min_len = 2; //in case missing value for CONFIG_STAR_SEARCH_MIN_LENGTH + $word_len = strlen($word); + $result = array(); + if($word_len <= $min_len) return array($word); + for($i = $min_len; $i < $word_len; $i ++ ){ + $result[] = substr($word, 0, $i); + } + $result[] = $word; + + //09-01-2012 + //reduce $word 1 character to create new word and create combination from there + $new_word = substr($word, 1); + $new_result = self::create_star_search($new_word, $min_len = 2); + foreach($new_result as $el){ + $result[] = $el; + } + + return array_unique($result); + } + +} diff --git a/inc/Hura8/System/ModuleManager.php b/inc/Hura8/System/ModuleManager.php new file mode 100644 index 0000000..80c8a90 --- /dev/null +++ b/inc/Hura8/System/ModuleManager.php @@ -0,0 +1,39 @@ + $maxPage]); + } + + public static function pageSizeSelectBox($default_size = 15) { + $html = [""; + + return join("", $html); + } + + //allow to select pageSize + public static function getPageSizes() { + $sizes = [15, 30, 50]; + $final_list = []; + foreach ($sizes as $size) { + $final_list[] = [ + "size" => $size, + "name" => $size . "/ trang", + "url" => Url::buildUrl($_SERVER['REQUEST_URI'], ["page" => '', "pageSize" => $size]) + ]; + } + + return $final_list; + } + + //Function nhay trang paging + public static function pagingAjax($totalResults,$numPerRow,$currentURL,$maxPageDisplay,$holder_id){ + $currentPage = isset($_GET["page"]) ? (int) $_GET["page"] : 1; + $currentURL = str_replace(array("?page=".$currentPage, "&page=".$currentPage),"",$currentURL); + if(strpos($currentURL, "?") !== false) $currentURL .= "&"; + else $currentURL .= "?"; + + $maxPage = ceil($totalResults/$numPerRow); + //max page to see : 20 + $maxPage = ($maxPage > 50) ? 50 : $maxPage; + + if($maxPage==1) return ""; + $farLeft ="..."; + $farRight ="..."; + $link =""; + //$maxPageDisplay la so page se hien ra, thuong lay 7 + if($currentPage > 1) $link .= ""; + else $link .= ""; + $link .= ""; + + if($maxPage<=$maxPageDisplay){ + + for($i=1;$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + + if($currentPage < $maxPage) $link .= ""; + + }else{ + //Vay la tong so trang nhieu hon so trang muon hien ra + if($currentPage<=(ceil($maxPageDisplay/2))){ + + //Neu trang dang xem dang o muc 1,2,3,4 + for($i=1;$i<=$maxPageDisplay;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + + }elseif($currentPage<=($maxPage-round($maxPageDisplay/2,0))){ + //Neu trang dang xem o muc cao hon 4 + $link .= "";//the 1st page + $link .=$farLeft; + for($i=($currentPage-round($maxPageDisplay/2,0)+1);$i<($currentPage+round($maxPageDisplay/2,0));$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + }else{ + //May trang cuoi cung + $link .= "";//the 1st page + $link .=$farLeft; + for($i=($maxPage-$maxPageDisplay+1);$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + } + } + $link .= "
<<Xem$i$i>>$i$i".$maxPage.">>1$i$i".$maxPage.">>1$i$i
"; + //Lua chon hien thi, phai it nhat tu 2 trang tro len moi hien ra + + if($maxPage>1){ + return $link; + } + + return false; + } + + // 04-Sept-2020 + public static function paging_template($totalResults, $numPerRow, $currentURL = '', $maxPageDisplay = 7){ + // parameter variables + if(!$currentURL) $currentURL = $_SERVER['REQUEST_URI']; + $url_parts = self::destructUrlWithCustomPageParam($currentURL); + /*[ + 'path' => $url_path, + 'query' => $query_params, + 'page' => $current_page, + ];*/ + + $query_without_page = $url_parts['query']; + $current_page = (isset($url_parts['page']) && $url_parts['page']) ? intval($url_parts['page']) : 1; + $total_pages = ceil($totalResults / $numPerRow); + $page_space = ceil(($maxPageDisplay - 1) / 2); + if($current_page > $total_pages) $current_page = $total_pages; + + /* + * $current_page is expected to be always in the middle: + * 3 = $page_space = ( $maxPageDisplay = 7-1/2) + * if current_page = 4 -> expect start = 4-3 = 1, expect end = 4+3 = 7 + * if current_page = 2 -> expect start = 2-3 = -1, expect end = 2+3 = 5. Since -1 is not valid, we need to add 2 to make it at least 1, so the end need to add 2 as well, so the result is: start = 1, end = 7 + * if current_page = 6 -> expect start = 6-3 = 3, expect end = 6+3 = 9. if max page =8, the end need to offset 9-8 = 1, so the start also need to offset 1, the result: start=3-1 = 2, end=9-1=8 + * */ + + // expect result + $start_index = $current_page - $page_space; + $end_index = $current_page + $page_space; + + //calculate offset + if($start_index < 1) { + $offset = 1 - $start_index; + $start_index = 1; + $end_index += $offset; + if($end_index > $total_pages) $end_index = $total_pages; + }else if($end_index > $total_pages) { + $offset = $end_index - $total_pages; + $end_index = $total_pages; + $start_index -= $offset; + if($start_index < 1) $start_index = 1; + } + + $page_collection = []; + $build_table = []; + + // add prev + if($start_index > 1) { + $query_without_page['page'] = $current_page-1; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + + $build_table[] = " << "; + $page_collection[] = [ + "name" => "prev", + "url" => $url, + "is_active" => 0, + ]; + } + + for($i = $start_index; $i <= $end_index ; $i++) { + $query_without_page['page'] = $i; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + $is_active = ($i == $current_page) ? 1 : 0; + + $page_collection[] = [ + "name" => $i, + "url" => $url, + "is_active" => $is_active, + ]; + + if($is_active) { + $build_table[] = "".$i.""; + }else{ + $build_table[] = "".$i.""; + } + } + + // add forward + if($end_index < $total_pages) { + $query_without_page['page'] = $current_page + 1; + $url = self::buildUrlWithCustomPageParam($url_parts['path'], $query_without_page); + $build_table[] = " >> "; + $page_collection[] = [ + "name" => "next", + "url" => $url, + "is_active" => 0, + ]; + } + + //------------------- + if($total_pages > 1 ){ + $tb_page = join("", [ + '', + join('', $build_table) , + "
" + ]) ; + + return array($page_collection, $tb_page, $total_pages); + } + + return array([], '', 1); + } + + // Url with page parameter format + // default: domain/path?key1=value1&key2=value2&page=1|2|3... + // custom1: domain/path/1|2|3/?key1=value1&key2=value2 + public static function buildUrlWithCustomPageParam($url_path, array $params){ + $url_format_type = self::getUrlWithCustomPageFormat(); + + $params_without_page = $params; + $page_number = 1; + if(isset($params['page'])) { + $page_number = intval($params['page']); + unset($params_without_page['page']); + } + + // custom1 (use case: hanoicomputer.vn) + if($url_format_type == 'custom1') { + if($page_number > 1){ + $url_path = $url_path."/".$page_number."/"; + } + }else{ + // default + if($page_number > 1){ + // push page to the end, always + $params_without_page['page'] = $page_number; + } + } + + + if(sizeof($params_without_page)) { + return join("", [$url_path, "?", http_build_query($params_without_page)]); + } + + return $url_path; + } + + // destruct Url to get url_path and query params + public static function destructUrlWithCustomPageParam($full_url){ + $cache_key = md5($full_url); + $cached_value = self::getCache($cache_key); + // if in cache, get right away + if($cached_value) return $cached_value; + + // not in cache, build and set cache + $url_format_type = self::getUrlWithCustomPageFormat(); + + // destruct + $parsed_url = explode("?", $full_url); + $url_path = isset($parsed_url[0]) ? $parsed_url[0] : ''; + $query_part = isset($parsed_url[1]) ? $parsed_url[1] : ''; + $query_params = []; + $current_page = 1; + + if($query_part) { + parse_str($query_part, $query_params); + } + + // custom1: url format= domain/path/1|2|3/?key1=value1&key2=value2 + if($url_format_type == 'custom1') { + $match = []; + if(preg_match("{/(.*?)/([0-9]+)/$}", $url_path, $match)){ + if(isset($match[2]) && $match[2] > 1) $current_page = $match[2]; + $url_path = "/".$match[1]; + } + + } else { + // default + if(isset($query_params['page'])) { + $current_page = $query_params['page']; + unset($query_params['page']); + } + } + + $cached_value = [ + 'path' => $url_path, + 'query' => $query_params, + 'page' => $current_page, + ]; + + self::setCache($cache_key, $cached_value); + + return $cached_value; + } + + + // check if user in in the admin panel + protected static function is_in_admin(){ + return substr($_SERVER['REQUEST_URI'], 0, 7) == '/admin/'; + } + + // Url with page parameter format + // default: domain/path?key1=value1&key2=value2&page=1|2|3... + // custom1: domain/path/1|2|3/?key1=value1&key2=value2 + protected static function getUrlWithCustomPageFormat() { + $url_format_type = "default"; + + // keep default for admin panel + if(self::is_in_admin()) { + return $url_format_type; + } + + // or search page + if(substr($_SERVER['REQUEST_URI'], 0, 4) == '/tim') { + return $url_format_type; + } + + if(defined("URL_PAGING_FORMAT") && URL_PAGING_FORMAT) { + $url_format_type = URL_PAGING_FORMAT; + } + + return $url_format_type; + } + + protected static function getCache($key) { + return isset(self::$cache[$key]) ? unserialize(self::$cache[$key]) : null; + } + + protected static function setCache($key, $value) { + self::$cache[$key] = serialize($value); + } + + //Function nhay trang + public static function paging($totalResults, $numPerRow, $currentURL, $maxPageDisplay, $limitPage = 50){ + $currentPage = isset($_GET["page"]) ? (int) $_GET["page"] : 1; + $currentURL = str_replace(array("?page=".$currentPage, "&page=".$currentPage),"",$currentURL); + //build_url($currentURL, array("page"=>'')); + + $first_url = $currentURL; //when $page=1 + + if(strpos($currentURL, "?") !== false) $currentURL .= "&"; + else $currentURL .= "?"; + + $maxPage = ceil($totalResults/$numPerRow); + //max page to see : 20 + if($limitPage > 0){ + $maxPage = ($maxPage > $limitPage) ? $limitPage : $maxPage; + } + + if($maxPage==1) return ""; + $farLeft ="..."; + $farRight ="..."; + $link =""; + //$maxPageDisplay la so page se hien ra, thuong lay 7 + if($currentPage == 2) $link .= ""; + else if($currentPage > 2) $link .= ""; + else $link .= ""; + + $link .= ""; + + if($maxPage<=$maxPageDisplay){ + + for($i=1;$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else { + if($i== 1) $link .= ""; + else $link .= ""; + } + } + + if($currentPage < $maxPage) $link .= ""; + + }else{ + + //Vay la tong so trang nhieu hon so trang muon hien ra + if($currentPage<=(ceil($maxPageDisplay/2))){ + + //Neu trang dang xem dang o muc 1,2,3,4 + for($i=1;$i<=$maxPageDisplay;$i++){ + if($i==$currentPage) $link .= ""; + else { + if($i== 1) $link .= ""; + else $link .= ""; + } + } + $link .=$farRight; + //$link .= "";//the last page + $link .= ""; + + }elseif($currentPage<=($maxPage-round($maxPageDisplay/2,0))){ + //Neu trang dang xem o muc cao hon 4 + $link .= "";//the 1st page + $link .= $farLeft; + for($i=($currentPage-round($maxPageDisplay/2,0)+1);$i<($currentPage+round($maxPageDisplay/2,0));$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + $link .= $farRight; + //$link .= "";//the last page + $link .= ""; + }else{ + //May trang cuoi cung + $link .= "";//the 1st page + $link .= $farLeft; + for($i=($maxPage-$maxPageDisplay+1);$i<=$maxPage;$i++){ + if($i==$currentPage) $link .= ""; + else $link .= ""; + } + } + } + + $link .= "
Quay lạiQuay lạiXem trang$i$i$iTiếp theo$i$i$i".$maxPage."Tiếp theo1$i$i".$maxPage."Tiếp theo1$i$i
"; + + //Lua chon hien thi, phai it nhat tu 2 trang tro len moi hien ra + if($maxPage > 1){ + return $link; + } + + return false; + } +} diff --git a/inc/Hura8/System/Permission.php b/inc/Hura8/System/Permission.php new file mode 100644 index 0000000..4e8ccbb --- /dev/null +++ b/inc/Hura8/System/Permission.php @@ -0,0 +1,39 @@ + $is_enabled) { + if($is_enabled && isset($plugin_mapping[$key]) && $plugin_mapping[$key]) { + $file = "display_plugin/". $plugin_mapping[$key]; + if(file_exists($file)) { + $page_plugin_data[$key] = include $file; + } + } + } + + return $page_plugin_data; + + } else { + + die($plugin_mapping_file." not exist"); + + } + } + + + protected static function getPlugins() { + + $config_file = ROOT_DIR . "/config/client/config_plugin.php"; + + if(file_exists($config_file)) { + + $config_plugins = include $config_file; + //global plugin should over-ride local one + $allowed_plugins = $config_plugins; + + foreach ($config_plugins as $module => $content) { + if($module == "global") continue; //skip + + foreach ($content as $option => $option_pages) { + foreach ($option_pages as $page => $precate) { + if($precate && array_key_exists($page, $allowed_plugins['global']) && $allowed_plugins['global'][$page] ) { + $allowed_plugins[$module][$option][$page] = false; + } + } + } + } + + return $allowed_plugins; + + } else { + + die($config_file." not exist"); + + } + } + +} diff --git a/inc/Hura8/System/RainTPL.php b/inc/Hura8/System/RainTPL.php new file mode 100644 index 0000000..18c5231 --- /dev/null +++ b/inc/Hura8/System/RainTPL.php @@ -0,0 +1,1023 @@ +), stylesheet (), script (