commit 97427d7cffa26add58f9fab71c1a53bd834e7409 Author: Tieptk Date: Sat Oct 4 11:46:59 2025 +0700 update diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..fbca9d3 --- /dev/null +++ b/.htaccess @@ -0,0 +1,7 @@ +RewriteEngine on +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} !^/index.php +# prevent rewrite non-existent files +RewriteCond %{REQUEST_URI} !\.(jpg|png|gif|css|js|php|tiff|jpeg|ico)$ +RewriteRule ^(.*)$ /index.php [QSA,L] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d21748 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +

Hướng dẫn

+ +

Link thiết kế: Giao diện Xstore

+ +

Repo: https://repo.hurasoft.com/tieptk/xstore

+ +

Theo dõi tiến độ: https://docs.google.com/spreadsheets/d/1Po3ANsG00pm_Y3dnrwuV81cidTCCqepYmR3yPDlIq6c/edit#gid=0

+ +

Cài đặt hệ thống

+ +

Test và làm việc chính tại web: http://local.xstore/

+ + + +

Cấu trúc thư mục

+ + + +

Cài đặt Composer (dùng tải package của PHP)

+ +

Xem hướng dẫn: https://getcomposer.org/doc/00-intro.md#installation-windows

+ +

Sau khi cài đặt xong. Mở cmd của Windows và thao tác lệnh sau để cài các thư viện code PHP cần cho dự án này.

+ +

+    > cd /thuc-muc-check-out/xstore/package
+    > composer i 
+
+ +

Sử dụng Tailwind để style giao diện.

+ + diff --git a/_shared.php b/_shared.php new file mode 100644 index 0000000..6f727f1 --- /dev/null +++ b/_shared.php @@ -0,0 +1,17 @@ + -1) { + item_name.find($('.note-error')).html("Họ tên chứa các ký tự không hợp lệ, bạn vui lòng kiểm tra lại"); + error = true; + } else { + item_name.find($('.note-error')).html(""); + } + return error +} + +function checkInputPhone() { + var number_regex1 = /^[0]\d{9}$/i; + var number_regex2 = /^[0]\d{10}$/i; + + var check_tel = document.getElementById('phone').value; + var $tel = $("#phone"); + var item_tel = $tel.parents(".check-form"); + if (check_tel.length < 4) { + item_tel.addClass('error') + item_tel.find($('.note-error')).html("Bạn chưa nhập SĐT"); + error = true; + } else if (!check_tel.match(number_regex1) && !check_tel.match(number_regex2)) { + item_tel.addClass('error') + item_tel.find($('.note-error')).html("Số điện thoại chưa chính xác"); + error = true; + } else { + item_tel.removeClass('error'); + item_tel.find($('.note-error')).html(""); + } + + return error +} + + +function checkInputEmail() { + var check_email = document.getElementById('email').value; + var $email = $("#email"); + var item_email = $email.parents(".check-form"); + if (check_email.length < 4) { + item_email.addClass('error') + item_email.find($('.note-error')).html("Bạn chưa nhập Email"); + error = true; + } else if (!validateEmail(check_email)) { + item_email.addClass('error') + item_email.find($('.note-error')).html("Địa chỉ email chưa chính xác"); + error = true; + } else { + item_email.removeClass('error'); + item_email.find($('.note-error')).html(""); + } + + return error +} + + +function checkProvince() { + var check_province = document.getElementById('province').value; + var $check_province = $('#province'); + var item_province = $check_province.parents('.check-form'); + if (check_province == 0) { + item_province.addClass('error'); + item_province.find($('.note-error')).html("Bạn chưa chọn Tỉnh/Thành phố"); + error = true; + } else { + item_province.removeClass('error'); + item_province.find($('.note-error')).html(""); + } + + return error; +} + + +function checkInputShop() { + var error = false; + var check_shop = document.getElementById('shop').value; + var $shop = $('#shop'); + var item_shop = $shop.parents(".check-form"); + if (check_shop.length < 4) { + item_shop.find($('.note-error')).html("Tên quá ngắn"); + error = true; + } else if (check_shop.indexOf(' -1) { + item_shop.find($('.note-error')).html("Tên chứa các ký tự không hợp lệ, bạn vui lòng kiểm tra lại"); + error = true; + } else { + item_shop.find($('.note-error')).html(""); + } + return error +} + + +function checkformTrial() { + + var error = false; + + checkInputName(); + + checkInputPhone(); + + checkInputEmail(); + + checkInputShop(); + + checkProvince(); + + + if (error) { + alert('Vui lòng kiểm tra lại thông tin'); + return false; + } else { + $(".button-send").css("pointer-events", "none"); + $(".button-send").html("ĐANG XỬ LÝ..."); + localStorage.getItem("Key_voucher"); + return true; + } +} + + +function validateEmail(sEmail) { + var filter = /^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/; + if (filter.test(sEmail)) { + return true; + } + else { + return false; + } +} + + +function initPasswordStrength() { + const inputPassword = document.getElementById('password'); + const toggleBtn = document.getElementById('togglePassword'); + const strengthBar = document.getElementById('strengthBar'); + const strengthText = document.getElementById('strengthText'); + const submitBtn = document.getElementById('submitBtn'); + + // Đánh giá mật khẩu + function evaluateStrength(password) { + let score = 0; + if (password.length >= 8) score++; + if (/[A-Z]/.test(password)) score++; + if (/\d/.test(password)) score++; + if (/[^A-Za-z0-9]/.test(password)) score++; + return score; // 0..4 + } + + // Render UI + function renderStrength(score) { + let label = "—", color = "bg-gray-300", width = "0%"; + if (score === 1) { label = "yếu"; color = "bg-red-500"; width = "25%"; } + if (score === 2) { label = "trung bình"; color = "bg-yellow-500"; width = "50%"; } + if (score === 3) { label = "khá"; color = "bg-lime-500"; width = "75%"; } + if (score === 4) { label = "mạnh"; color = "bg-green-500"; width = "100%"; } + + const levels = [ + { label: "—", color: "bg-gray-300", width: "0%" }, + { label: "yếu", color: "bg-red-500", width: "25%" }, + { label: "trung bình", color: "bg-yellow-500", width: "50%" }, + { label: "khá", color: "bg-lime-500", width: "75%" }, + { label: "mạnh", color: "bg-green-500", width: "100%" } + ]; + + const level = levels[score]; + strengthBar.className = "h-1 rounded transition-all " + level.color; + strengthBar.style.width = width; + strengthText.textContent = "Độ mạnh của mật khẩu: " + level.label; + submitBtn.disabled = score < 3; + } + + // Event nhập mật khẩu + inputPassword.addEventListener('input', e => { + $('#check-pass').removeClass('hidden'); + const score = evaluateStrength(e.target.value); + renderStrength(score); + }); + + // Toggle hiển thị/ẩn mật khẩu + toggleBtn.addEventListener('click', () => { + inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password'; + }); + + renderStrength(0); // khởi tạo +} + + +initPasswordStrength(); \ No newline at end of file diff --git a/assets/script/style.css b/assets/script/style.css new file mode 100644 index 0000000..aefc1aa --- /dev/null +++ b/assets/script/style.css @@ -0,0 +1,202 @@ +:root { + --color-blue: #0f5edd; + --text-green: #00c75d; +} + +::-webkit-scrollbar-track { + background-color: #f5f5f5; + border-radius: 10px; +} + +::-webkit-scrollbar { + width: 7px; + background-color: #fff; +} + +::-webkit-scrollbar-thumb { + background: #00112b; + border-radius: 20px; + background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(0.7, #00112b), color-stop(0.5, transparent), to(transparent)); +} + +body { + font-family: "Roboto", sans-serif; + font-size: 14px; + background: #fcfcfc; + max-width: 1920px; + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; +} + +html { + font-family: "Roboto", sans-serif; +} + +.clearfix::after { + content: ""; + clear: both; + display: table; +} + +.container { + max-width: 1200px !important; + margin: 0 auto; +} + +.icon_2025 { + display: block; + background: url(../images/icon_2025.png) no-repeat; + background-size: 92px 82px; +} +.icon_2025.checkbox { + width: 18px; + height: 23px; + background-position: 0 0; +} +.icon_2025.boxreview { + width: 18px; + height: 23px; + background-position: -34px 0; +} +.icon_2025.cart { + width: 23px; + height: 23px; + background-position: -68px 0; +} +.icon_2025.up { + width: 22px; + height: 22px; + background-position: 1px -29px; +} +.icon_2025.thuhut { + width: 22px; + height: 22px; + background-position: -34px -29px; +} +.icon_2025.tietkiem { + width: 22px; + height: 22px; + background-position: -69px -29px; +} +.icon_2025.up-sale { + width: 22px; + height: 22px; + background-position: 1px -58px; +} +.icon_2025.setting { + width: 22px; + height: 22px; + background-position: -33px -58px; +} +.icon_2025.support { + width: 24px; + height: 24px; + background-position: -68px -60px; +} + +.homepage .banner { + background: url(../images/background-banner.jpg) no-repeat; + background-size: 100% 100%; + min-height: 920px; +} +.homepage .background-tamnhin { + background: url(../images/background-tamnhin.jpg) no-repeat; + background-size: 100% 100%; + background-position: center; +} + +.effect-image { + position: relative; + overflow: hidden; +} + +.effect-image:before { + position: absolute; + top: 0; + left: -100%; + z-index: 2; + display: block; + content: ""; + width: 50%; + height: 100%; + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.3) 100%); + transform: skewX(-25deg); + cursor: pointer; +} + +.effect-image:hover:before { + animation: image 1.1s; +} + +@keyframes image { + 100% { + left: 125%; + } +} +.breadcrumb li:last-child i { + display: none; +} + +.breadcrumb li a span:hover { + color: var(--color-blue); + font-weight: bold; +} + +.background-pricing { + background: url(../images/background-pricing.png) no-repeat; + background-size: 100% 100%; + background-position: center center; +} + +.content-compare li { + margin-bottom: 10px; + position: relative; + padding-left: 15px; + color: #5d5d69; +} +.content-compare li::before { + position: absolute; + content: ""; + left: 0; + top: 7px; + width: 5px; + height: 5px; + background: #000; + clip-path: polygon(100% 0, 0% 100%, 100% 100%); +} + +.background-free { + background: url(../images/background-banner.jpg) no-repeat; + background-size: 100% 100%; + min-height: 640px; +} + +.form-input label::before { + position: absolute; + content: "*"; + right: -10px; + top: -3px; + color: red; +} + +.form-select select:focus { + outline: none; +} +.form-select option { + padding: 0 10px; +} +.form-select label::before { + position: absolute; + content: "*"; + right: -10px; + top: -3px; + color: red; +} + +.page-login { + background: url(../images/background-login.jpg) no-repeat; + background-size: 100% 100%; + background-position: center; +}/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/assets/script/style.css.map b/assets/script/style.css.map new file mode 100644 index 0000000..bc16889 --- /dev/null +++ b/assets/script/style.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA;EACI,qBAAA;EACA,qBAAA;ACCJ;;ADCA;EACI,yBAAA;EACA,mBAAA;ACEJ;;ADAA;EACI,UAAA;EACA,sBAAA;ACGJ;;ADDA;EACI,mBAAA;EACA,mBAAA;EACA,gIAAA;ACIJ;;ADKA;EACI,iCAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;EACA,aAAA;EACA,sBAAA;EACA,iBAAA;EACA,8BAAA;ACFJ;;ADIA;EACI,iCAAA;ACDJ;;ADGA;EACI,WAAA;EACA,WAAA;EACA,cAAA;ACAJ;;ADEA;EACI,4BAAA;EACA,cAAA;ACCJ;;ADEA;EACI,cAAA;EACA,kDAAA;EACA,0BAAA;ACCJ;ADAI;EACI,WAAA;EACA,YAAA;EACA,wBAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,4BAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,4BAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,8BAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,gCAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,gCAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,8BAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,gCAAA;ACER;ADAI;EACI,WAAA;EACA,YAAA;EACA,gCAAA;ACER;;ADGI;EACI,0DAAA;EACA,0BAAA;EACA,iBAAA;ACAR;ADEI;EACI,2DAAA;EACA,0BAAA;EACA,2BAAA;ACAR;;ADIA;EACI,kBAAA;EACA,gBAAA;ACDJ;;ADIA;EACI,kBAAA;EACA,MAAA;EACA,WAAA;EACA,UAAA;EACA,cAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,+FAAA;EACA,wBAAA;EACA,eAAA;ACDJ;;ADIA;EACI,qBAAA;ACDJ;;ADIA;EACI;IACI,UAAA;ECDN;AACF;ADIA;EACI,aAAA;ACFJ;;ADKA;EACI,wBAAA;EACA,iBAAA;ACFJ;;ADKA;EACI,2DAAA;EACA,0BAAA;EACA,kCAAA;ACFJ;;ADMI;EACI,mBAAA;EACA,kBAAA;EACA,kBAAA;EACA,cAAA;ACHR;ADIQ;EACI,kBAAA;EACA,WAAA;EACA,OAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,gBAAA;EACA,8CAAA;ACFZ;;ADMA;EACI,0DAAA;EACA,0BAAA;EACA,iBAAA;ACHJ;;ADQQ;EACI,kBAAA;EACA,YAAA;EACA,YAAA;EACA,SAAA;EACA,UAAA;ACLZ;;ADWI;EACI,aAAA;ACRR;ADUI;EACI,eAAA;ACRR;ADWQ;EACI,kBAAA;EACA,YAAA;EACA,YAAA;EACA,SAAA;EACA,UAAA;ACTZ;;ADcA;EACI,yDAAA;EACA,0BAAA;EACA,2BAAA;ACXJ","file":"style.css"} \ No newline at end of file diff --git a/assets/script/style.scss b/assets/script/style.scss new file mode 100644 index 0000000..237c012 --- /dev/null +++ b/assets/script/style.scss @@ -0,0 +1,214 @@ +:root { + --color-blue: #0f5edd; + --text-green: #00c75d; +} +::-webkit-scrollbar-track { + background-color: #f5f5f5; + border-radius: 10px; +} +::-webkit-scrollbar { + width: 7px; + background-color: #fff; +} +::-webkit-scrollbar-thumb { + background: #00112b; + border-radius: 20px; + background-image: -webkit-gradient( + linear, + 0 0, + 0 100%, + color-stop(0.7, #00112b), + color-stop(0.5, transparent), + to(transparent) + ); +} +body { + font-family: "Roboto", sans-serif; + font-size: 14px; + background: #fcfcfc; + max-width: 1920px; + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; +} +html { + font-family: "Roboto", sans-serif; +} +.clearfix::after { + content: ""; + clear: both; + display: table; +} +.container { + max-width: 1200px !important; + margin: 0 auto; +} + +.icon_2025 { + display: block; + background: url(../images/icon_2025.png) no-repeat; + background-size: 92px 82px; + &.checkbox { + width: 18px; + height: 23px; + background-position: 0 0; + } + &.boxreview { + width: 18px; + height: 23px; + background-position: -34px 0; + } + &.cart { + width: 23px; + height: 23px; + background-position: -68px 0; + } + &.up { + width: 22px; + height: 22px; + background-position: 1px -29px; + } + &.thuhut { + width: 22px; + height: 22px; + background-position: -34px -29px; + } + &.tietkiem { + width: 22px; + height: 22px; + background-position: -69px -29px; + } + &.up-sale { + width: 22px; + height: 22px; + background-position: 1px -58px; + } + &.setting { + width: 22px; + height: 22px; + background-position: -33px -58px; + } + &.support { + width: 24px; + height: 24px; + background-position: -68px -60px; + } +} + +.homepage { + .banner { + background: url(../images/background-banner.jpg) no-repeat; + background-size: 100% 100%; + min-height: 920px; + } + .background-tamnhin { + background: url(../images/background-tamnhin.jpg) no-repeat; + background-size: 100% 100%; + background-position: center; + } +} + +.effect-image { + position: relative; + overflow: hidden; +} + +.effect-image:before { + position: absolute; + top: 0; + left: -100%; + z-index: 2; + display: block; + content: ""; + width: 50%; + height: 100%; + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.3) 100%); + transform: skewX(-25deg); + cursor: pointer; +} + +.effect-image:hover:before { + animation: image 1.1s; +} + +@keyframes image { + 100% { + left: 125%; + } +} + +.breadcrumb li:last-child i { + display: none; +} + +.breadcrumb li a span:hover { + color: var(--color-blue); + font-weight: bold; +} + +.background-pricing { + background: url(../images/background-pricing.png) no-repeat; + background-size: 100% 100%; + background-position: center center; +} + +.content-compare { + li { + margin-bottom: 10px; + position: relative; + padding-left: 15px; + color: #5d5d69; + &::before { + position: absolute; + content: ""; + left: 0; + top: 7px; + width: 5px; + height: 5px; + background: #000; + clip-path: polygon(100% 0, 0% 100%, 100% 100%); + } + } +} +.background-free { + background: url(../images/background-banner.jpg) no-repeat; + background-size: 100% 100%; + min-height: 640px; +} + +.form-input { + label { + &::before { + position: absolute; + content: "*"; + right: -10px; + top: -3px; + color: red; + } + } +} + +.form-select { + select:focus { + outline: none; + } + option { + padding: 0 10px; + } + label { + &::before { + position: absolute; + content: "*"; + right: -10px; + top: -3px; + color: red; + } + } +} + +.page-login { + background: url(../images/background-login.jpg) no-repeat; + background-size: 100% 100%; + background-position: center; +} diff --git a/data/features/conversion.php b/data/features/conversion.php new file mode 100644 index 0000000..a814366 --- /dev/null +++ b/data/features/conversion.php @@ -0,0 +1 @@ + '', 'value' => ] from [key1 => value1, key2=>value2, ...] + * + * @param array $key_values [key1 => value1, key2=>value2] + * + * @return array [['key' => 'key1', 'value' => value1], ['key' => 'key2', 'value' => value2]] + */ + public static function to_array(array $key_values) { + $result = []; + foreach ($key_values as $key => $value) { + $result[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + + return $result; + } + + + /** + * split a s by line to create array + * + * @param string $txt + * + * @return array + */ + public static function get_line($txt) { + + if(is_array($txt)) { + return $txt; + } + + $txt = trim($txt); + if( ! $txt ) return []; + + return preg_split("/\n/", $txt); + } + + /** + * Implement strlen + * + * @param string $str + * + * @return int + */ + public static function length($str) { + return strlen(trim($str)); + } + + + /** + * Make number easier to read: 1000 -> 1.000 + * + * @param string $number + * + * @return string + */ + public static function format_number($number) { + if(!$number) return ''; + $number = floatval($number); + + $number = number_format($number, 0, ",", "."); //Vietnamese format with decimals by a coma + + return $number; + } + + public static function format_price($p_price, $currency = ''){ + if(!$p_price) return ''; + if(!$currency) $currency = (defined("DEFAULT_CURRENCY")) ? DEFAULT_CURRENCY : "vnd"; + //if(is_string($p_price)) return 0; + if($currency == 'usd') { + return number_format($p_price,2,".",","); + }else { + return number_format($p_price,0,",","."); + } + } + + public static function global_asset_url($file_name = '') + { + return GLOBAL_ASSETS_PATH . $file_name; + } + + /** + * + * Description: get the shop's full asset url for template's images/js/css + * + * //Returns the URL of a file in the "assets" folder of a theme. + // {{ 'shop.css' | asset_url : 'arg1', 'arg2' ...}} -> //cdn.shopify.com/s/files/1/0087/0462/t/394/assets/shop.css?28253 + * + * @param string $file_name + * + * @return string + */ + public static function asset_url($file_name = '') + { + if( !$file_name ) return ''; + + $file_ext = strtolower(strrchr($file_name, ".")); + + // script tags + if(in_array($file_ext, ['.js', '.css'])) return TEMPLATE_ASSET . "/script/" . $file_name; + + // default image + return TEMPLATE_ASSET . "/images/" . $file_name; + } + + + /** + * + * Description: construct a full html tag for images/js/css file + * + * @param string $file_path domain.com/static/style.css?v=3.1.1 + * + * @return string + */ + public static function script_tag($file_path) { + if( ! $file_path ) return ''; + + //check for ? + if(strpos($file_path, "?") !== false) { + $file_ext = str_replace(strrchr($file_path, "?"), "", $file_path); + $file_ext = strtolower(strrchr($file_ext, ".")); + } else { + $file_ext = strtolower(strrchr($file_path, ".")); + } + + $tag_config = [ + ".css" => "", + ".js" => "", + ".jpg" => "\"n\"/", + ".jpeg" => "\"\"/", + ".gif" => "\"\"/", + ".png" => "\"\"/", + ]; + + return (isset($tag_config[$file_ext])) ? $tag_config[$file_ext] : ''; + } + + /** + * {{ product_info.main_image | img_url: '300x300' }} => https://cdn.shopify.com/s/files/1/1183/1048/products/boat-shoes_300x300.jpeg?1459175177 + * @param string $full_path + * @param string $modifier + * $modifier: + * - must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + * - or be one of these: small | medium | large + * @return string + */ + public static function img_url($full_path, $modifier) + { + $clean_modifier = ($modifier) ? trim($modifier) : ""; + + // verify $modifier + // must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + if($clean_modifier + && !preg_match("/^[0-9]{2,4}x([0-9]{2,4})?$/i", $clean_modifier) + && !in_array($clean_modifier, ["small", "medium", "large"]) + ) { + $clean_modifier = ""; + } + + // return if no valid modifier + if( ! $clean_modifier ) { + return $full_path; + } + + $last_dot_position = strrpos($full_path, "."); + if( ! $last_dot_position ) return $full_path . $clean_modifier; + + return join("", [ + substr($full_path, 0, $last_dot_position), + "_", + $clean_modifier, + substr($full_path, $last_dot_position) + ]); + } + + /** + * //Returns the URL of a file in the Files page of the admin. + //{{ 'size-chart.pdf' | file_url }} -> //cdn.shopify.com/s/files/1/0087/0462/files/size-chart.pdf?28261 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_url($input, $string) + { + return strtoupper($input) . " = " . $string; + } + + /** + * //Returns the asset URL of an image in the Files page of the admin. file_img_url accepts an image size parameter. + //{{ 'logo.png' | file_img_url: '1024x768' }} -> //cdn.shopify.com/s/files/1/0246/0527/files/logo_1024x768.png?42 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_img_url($input, $string) + { + return ''; + } + + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function print_r($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function show_var($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } +} diff --git a/inc/Hura8/AppAdmin.php b/inc/Hura8/AppAdmin.php new file mode 100644 index 0000000..af5d5de --- /dev/null +++ b/inc/Hura8/AppAdmin.php @@ -0,0 +1,212 @@ + 'home', + "view" => 'home', + "url" => '/admin/product' + ]; + + protected $data = []; + + + public function __construct() + { + + } + + // start the app + public function start() { + $this->getRouter(); + $this->getData(); + echo $this->renderModule(); + } + + + protected function getRouter() { + /*$route = [ + "module" => (isset($_REQUEST['module'])) ? $_REQUEST['module'] : 'home', + "view" => (isset($_REQUEST['view'])) ? $_REQUEST['view'] : 'home', + ];*/ + + $objRouter = new Router(); + $this->current_route_info = $objRouter->getRouting(); + } + + + protected function getData() { + $module_file = $this->getModuleFile(); + + if(file_exists($module_file)) { + // print_r($this->current_route_info); + // die('Page '. $module_file .' not found!'); + $data = include_once $module_file; + }else{ + $data = ['file data '. $module_file .' not found!']; + } + + $global_data = [ + "module" => $this->current_route_info['module'], + "view" => $this->current_route_info['view'], + "url" => $this->current_route_info['url'], + "main_menu" => include_once ROOT_DIR."/data/menu.php", + ]; + + $this->data = array( + 'global' => $global_data, + // module-specific data, just print {{ page }} to see all available data for the current page!!! + 'page' => (is_array($data)) ? $data : [], + ); + } + + + protected function getModuleFile() { + return join(DIRECTORY_SEPARATOR, [ + "data", + $this->current_route_info["module"], + str_replace("-", "_", $this->current_route_info["view"]).".php" + ]) ; + } + + + protected function renderModule() { + + if(!$this->current_route_info['module'] || !$this->current_route_info['view']) { + die("Module not exist"); + } + + $template_file_path = $this->tpl_path ."/". $this->current_route_info['module']; + $template_file_name = str_replace("-", '_', $this->current_route_info['view']).".html"; + $template_file_full_path = $template_file_path."/".$template_file_name; + + //check exist + if(!@file_exists( $template_file_full_path)) { + // attempt to auto create first + // todo: this MUST BE TURNED OFF IN PRODUCTION, else many files will be created unintentionally + $module_file = $this->getModuleFile(); + // only create if module file exist + if(file_exists($module_file) && !$this->autoCreateTplFile( $template_file_path, $template_file_name )) { + die("Please manually create template file at: ". $template_file_full_path); + } + } + + $theme_file_path = $this->tpl_path ."/theme.html"; + if( ! @file_exists( $theme_file_path)) { + die("Theme not exist (please create): " . $theme_file_path); + } + + $theme_content = @file_get_contents( $theme_file_path ); + $module_content = @file_get_contents( $template_file_full_path ); + + $page_content_to_parse = preg_replace([ + "/{{(\s+)?page_content(\s+)?}}/" + ], [ + $module_content, + ] , $theme_content ); + + + return $this->parse( + $page_content_to_parse, + $template_file_path + ); + } + + + protected function autoCreateTplFile($file_path, $file_name) : bool { + // create dir if not exist + if(!file_exists($file_path)) { + if(!mkdir($file_path, 0755, true)) { + return false; + } + + if(!file_exists($file_path)) { + return false; + } + } + + //create file + $file_full_path = $file_path . "/". $file_name; + @file_put_contents($file_full_path, $file_full_path); + + return file_exists($file_full_path); + } + + + /* + * 2 ways to render a html template + * 1. Use $html_to_parse, which requires no dependencies + * Example: + * Template::parse(null, 'Age = {{age}}', ['age' => 21], ''); + * + * 2. Use $template_file_path, which requires dependency $path + * Template::parse(Template::$setting_template_path, null, ['age' => 21], 'email/test'); + * */ + protected function parse($html_to_parse = null, $template_file_path = '') { + + if(!$html_to_parse && !$template_file_path) { + return 'Nothing to parse'; + } + + //output to html + Liquid::set('INCLUDE_SUFFIX', 'html'); + Liquid::set('INCLUDE_PREFIX', ''); + //Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', false); + + $enable_cache = false; // default = true, turn this on-off to disable cache while working on local mode + //$enable_cache = true; + + //catch exception and print friendly notice + try { + + $objLiquidTemplate = new LiquidTemplate( $this->tpl_path ); + $objLiquidTemplate->registerFilter( AdminTemplateFilter::class ); + if($enable_cache) { + /*$objLiquidTemplate->setCache(new File([ + 'cache_dir' => self::$cache_dir + ]));*/ + } + + if($html_to_parse) { + $objLiquidTemplate->parse($html_to_parse); + }elseif ($template_file_path) { + $objLiquidTemplate->parseFile($template_file_path); + } + + return $objLiquidTemplate->render($this->data); + + } catch (\Exception $e) { + $result = []; + do { + //printf("%s:%d %s (%d) [%s]\n", $e->getFile(), $e->getLine(), $e->getMessage(), $e->getCode(), get_class($e)); + //echo $e->getTraceAsString(); + //$code = $e->getTrace()[0]['args'][0]; + //if(is_array($code)) $code = serialize($code); + $result[] = sprintf( + " +Lỗi code trong file template html:
+- Chi tiết lỗi: %s
+- File template: %s
+- Hướng dẫn xử lý: Tách từng phần html để kiểm tra và nhấn F5 mỗi lần. Nếu không xuất hiện thông báo này nghĩa là phần đó không tạo lỗi +", + $e->getMessage(), + substr($template_file_path, strrpos($template_file_path, DIRECTORY_SEPARATOR) + 1 ), + //static::$cache_dir + ); + + } while($e = $e->getPrevious()); + + return join(" - ", $result); + } + } + +} diff --git a/inc/Hura8/Components/Analytics/Controller/bTrackingController.php b/inc/Hura8/Components/Analytics/Controller/bTrackingController.php new file mode 100644 index 0000000..b52f119 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Controller/bTrackingController.php @@ -0,0 +1,17 @@ +objTrackingModel = new TrackingModel(); + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php b/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php new file mode 100644 index 0000000..a0df313 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackDeviceInfo.php @@ -0,0 +1,19 @@ +ip_address = $ip_address; + $this->user_agent = $user_agent; + $this->referrer = $referrer; + $this->is_mobile = $is_mobile; + } +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php b/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php new file mode 100644 index 0000000..e68b8bc --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackRouteInfo.php @@ -0,0 +1,28 @@ +url = $url; + $this->module = $module; + $this->view = $view; + $this->view_id = $view_id; + $this->query = $query; + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php b/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php new file mode 100644 index 0000000..21243a1 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackUserInfo.php @@ -0,0 +1,19 @@ +web_user_id = $web_user_id; + $this->customer_id = $customer_id; + $this->is_crawler = $is_crawler ? 1 : 0; + } + +} diff --git a/inc/Hura8/Components/Analytics/Model/TrackingModel.php b/inc/Hura8/Components/Analytics/Model/TrackingModel.php new file mode 100644 index 0000000..5ec20d0 --- /dev/null +++ b/inc/Hura8/Components/Analytics/Model/TrackingModel.php @@ -0,0 +1,32 @@ +isDefaultLanguage()) { + return $this->iEntityLanguageModel->update($item_id, $new_item_info); + } + + return $this->objArticleModel->updateTableInfo($item_id, $new_item_info); + } + +} diff --git a/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php b/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php new file mode 100644 index 0000000..dbd2202 --- /dev/null +++ b/inc/Hura8/Components/Article/Controller/bArticleCategoryController.php @@ -0,0 +1,35 @@ +objArticleCategoryModel = new ArticleCategoryModel(); + + if(!$this->isDefaultLanguage()) { + + parent::__construct( + $this->objArticleCategoryModel, + new ArticleCategoryLanguageModel() + ); + + } else { + + parent::__construct($this->objArticleCategoryModel); + + } + } + + +} diff --git a/inc/Hura8/Components/Article/Controller/bArticleController.php b/inc/Hura8/Components/Article/Controller/bArticleController.php new file mode 100644 index 0000000..b3ed6be --- /dev/null +++ b/inc/Hura8/Components/Article/Controller/bArticleController.php @@ -0,0 +1,91 @@ + ['width' => 200,] , + 'l' => ['width' => 600,] , + ); + + /* @var ArticleModel $objArticleModel */ + protected $objArticleModel; + + public function __construct() + { + $this->objArticleModel = new ArticleModel(); + + if(!$this->isDefaultLanguage()) { + parent::__construct( + $this->objArticleModel, + new ArticleLanguageModel() + ); + + } else { + parent::__construct($this->objArticleModel); + } + } + + + public function getFullInfo($id) + { + + if(!$id) return null; + + return self::getCache("getFullInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->objArticleModel->getFullInfo($id); + + if($this->iEntityLanguageModel && $info ) { + $item_language_info = $this->iEntityLanguageModel->getInfo($id) ?? ["not_translated" => true]; + return $this->formatItemInfo(array_merge($info, $item_language_info)); + } + + return ($info) ? $this->formatItemInfo($info) : null; + + }); + } + + + protected function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) + { + if(!$item_info) return null; + + $info = $item_info; + $info['image'] = self::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 : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php b/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php new file mode 100644 index 0000000..8b37216 --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleCategoryLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php b/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php new file mode 100644 index 0000000..c367bed --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleCategoryModel.php @@ -0,0 +1,33 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleModel.php b/inc/Hura8/Components/Article/Model/ArticleModel.php new file mode 100644 index 0000000..1c5771f --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleModel.php @@ -0,0 +1,150 @@ +db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_article_info."` info + WHERE basic.`id` = info.`article_id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "category" => getRequestInt("category"), + "no_image" => 0,//1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + //Tim danh muc + if(isset($filter_condition["category"]) && $filter_condition["category"]) { + + $objArticleCategoryModel = new ArticleCategoryModel(); + $category_info = $objArticleCategoryModel->getInfo($filter_condition["category"]); + + if($category_info) { + if($category_info['is_parent']) { + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_article_per_category."` WHERE `category_id` IN (".$category_info['child_ids'].") ) "; + //$bind_types[] = 'd'; + //$bind_values[] = $filter_condition["category"]; + }else{ + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_article_per_category."` WHERE `category_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["category"]; + } + } + + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function addArticleToCategory($item_id, array $category_list_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_article_per_category."` WHERE `item_id` = ? ", ['d'], [$item_id]); + + $bulk_inserts = []; + foreach($category_list_id as $cat_id) { + if (! $cat_id) continue; + + $bulk_inserts[] = [ + 'category_id' => $cat_id, + 'item_id' => $item_id, + 'status' => 1, + 'create_time' => CURRENT_TIME, + ]; + } + + if(sizeof($bulk_inserts)) { + $this->db->bulk_insert($this->tb_article_per_category, $bulk_inserts); + } + + // update counter + $objArticleCategoryModel = new ArticleCategoryModel(); + foreach($category_list_id as $cat_id) { + $objArticleCategoryModel->updateItemCount($cat_id); + } + + } + + + public function updateUrl($id, $url_index): bool + { + + $module_routing = ModuleManager::getModuleRouting("article"); + $request_path_config = isset($module_routing["detail"]) ? $module_routing["detail"]['url_manager']['request_path'] : ''; + + if(!$request_path_config) { + return false; + } + + $request_path = UrlManagerController::translateRequestPathConfig($request_path_config, $id, $url_index); + $id_path = UrlManagerController::createIdPath("article", "detail", $id); + + $objUrlManager = new UrlManagerController(); + $new_request_path = $objUrlManager->createUrl("article:detail", $request_path, $id_path, 0); + + if($new_request_path) { + $this->db->update( + $this->tb_entity, + [ + 'request_path' => $new_request_path, + ], + [ + 'id' => $id, + ] + ); + } + + return true; + } + + + +} diff --git a/inc/Hura8/Components/Article/Model/ArticleSearchModel.php b/inc/Hura8/Components/Article/Model/ArticleSearchModel.php new file mode 100644 index 0000000..0daa06c --- /dev/null +++ b/inc/Hura8/Components/Article/Model/ArticleSearchModel.php @@ -0,0 +1,29 @@ + "tb_article.status", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_article.title", ], + ]; + + + public function __construct() + { + parent::__construct( + "tb_article", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Article/Model/UArticleModel.php b/inc/Hura8/Components/Article/Model/UArticleModel.php new file mode 100644 index 0000000..52d849b --- /dev/null +++ b/inc/Hura8/Components/Article/Model/UArticleModel.php @@ -0,0 +1,55 @@ +db->runQuery(" + ( + SELECT `item_id` + FROM ".$this->tb_article_per_category." + WHERE `category_id` = ? AND `status`=1 AND `item_id` > ? + ORDER BY `item_id` DESC + LIMIT 10 + + ) UNION ALL ( + SELECT `item_id` + FROM ".$this->tb_article_per_category." + WHERE `category_id` = ? AND `status`=1 AND `item_id` < ? + ORDER BY `item_id` DESC + LIMIT 10 + ) + ", + ['d', 'd', 'd', 'd'], + [$category_id, $main_id, $category_id, $main_id] + ); + + $article_list_id = []; + $article_item_info = array(); + $article_item = []; + + foreach ( $this->db->fetchAll($query) as $rs ) { + if(!isset($article_item_info[$rs["item_id"]])) $article_item_info[$rs["item_id"]] = array(); + if(!in_array($rs["item_id"], $article_list_id)) $article_list_id[] = $rs["item_id"]; + + if($rs["item_id"] > $main_id) { + $article_item['new'][$rs["item_id"]] = &$article_item_info[$rs["item_id"]]; + } + else { + $article_item['old'][$rs["item_id"]] = &$article_item_info[$rs["item_id"]]; + } + } + + $list_article_info = $this->getListByIds($article_list_id); + foreach ($article_list_id as $_id) { + if(isset($list_article_info[$_id])) $article_item_info[$_id] = $list_article_info[$_id]; + } + + + return $article_item; + } + +} diff --git a/inc/Hura8/Components/Banner/AdminController/ABannerController.php b/inc/Hura8/Components/Banner/AdminController/ABannerController.php new file mode 100644 index 0000000..ee1196a --- /dev/null +++ b/inc/Hura8/Components/Banner/AdminController/ABannerController.php @@ -0,0 +1,16 @@ + "Toàn bộ website" , + "header" => "Đầu trang" , + "homepage" => "Trang chủ" , + "column_left" => "Cột trái" , + "column_right" => "Cột phải" , + "footer" => "Chân trang" , + "product_detail"=> "Chi tiết sản phẩm" , + "product_list" => "Danh sách & Danh mục sản phẩm" , + "collection_list" => "Bộ sưu tập" , + "article_home" => "Trang chủ tin tức" , + "brand_detail" => "Chi tiết thương hiệu", + ); + + + protected $objBannerModel; + protected $objBannerLocationModel; + + public function __construct() + { + $this->objBannerModel = new BannerModel(); + $this->objBannerLocationModel = new BannerLocationModel(); + + parent::__construct($this->objBannerModel); + } + + + protected function formatItemInList(array $item_info) + { + return self::formatFile($item_info); + } + + protected function formatItemInfo(array $item_info) + { + return self::formatFile($item_info); + } + + + public static function formatFile(array $item_info) + { + + if($item_info['file_url']) { + $item_info['display_file'] = STATIC_DOMAIN ."/". static::$image_folder ."/". $item_info['file_url']; + }else if($item_info['file_external_url']) { + $item_info['display_file'] = $item_info['file_external_url']; + } + + $item_info['html_code'] = "\"".htmlspecialchars($item_info['title'])."\""; + + return $item_info; + } + + +} diff --git a/inc/Hura8/Components/Banner/Model/BannerLocationModel.php b/inc/Hura8/Components/Banner/Model/BannerLocationModel.php new file mode 100644 index 0000000..d3208cb --- /dev/null +++ b/inc/Hura8/Components/Banner/Model/BannerLocationModel.php @@ -0,0 +1,30 @@ +db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `tracking_id` = ? LIMIT 1 ", ['s'], [$tracking_id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + public function getBannerPerTemplate(array $template_list, $numberOfBannerPerTpl=100){ + + $all_bind_types = []; + $all_bind_values = []; + + $view_id = 0; + + $build_query = []; + foreach($template_list as $tpl) { + + list($where_condition, $bind_types, $bind_values) = $this->buildQueryPerTpl($tpl, $view_id, $numberOfBannerPerTpl); + + $build_query[] = " (".$where_condition.") "; + $all_bind_types = array_merge($all_bind_types, $bind_types); + $all_bind_values = array_merge($all_bind_values, $bind_values); + } + + if(!sizeof($build_query)) return []; + + $query = $this->db->runQuery(join(" UNION ALL ", $build_query), $all_bind_types, $all_bind_values); + + return $this->db->fetchAll($query); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + [location] => 2 + [category] => 0 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($filter_condition['location']) && $filter_condition['location']) { + $catCondition[] = " AND `location` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['location']; + } + + if(isset($filter_condition['category']) && $filter_condition['category']) { + $catCondition[] = " AND `id` IN ( SELECT `banner_id` FROM `".$this->tb_banner_per_category."` WHERE `category_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['category']; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/Banner/Model/BannerSearchModel.php b/inc/Hura8/Components/Banner/Model/BannerSearchModel.php new file mode 100644 index 0000000..6fd6b38 --- /dev/null +++ b/inc/Hura8/Components/Banner/Model/BannerSearchModel.php @@ -0,0 +1,33 @@ + "tb_banner.location", + "status" => "tb_banner.status", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_banner.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_banner", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Brand/AdminController/ABrandController.php b/inc/Hura8/Components/Brand/AdminController/ABrandController.php new file mode 100644 index 0000000..c56efb1 --- /dev/null +++ b/inc/Hura8/Components/Brand/AdminController/ABrandController.php @@ -0,0 +1,44 @@ +objBrandModel->getGroupByFirstLetter(); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + if($item_info['thumbnail']) { + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $item_info['thumbnail']; + unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $item_info['thumbnail']; + unlink($file_local_path); + } + + //delete media files? + // todo: + + // ok + return true; + } + + +} diff --git a/inc/Hura8/Components/Brand/Controller/bBrandController.php b/inc/Hura8/Components/Brand/Controller/bBrandController.php new file mode 100644 index 0000000..8376b0e --- /dev/null +++ b/inc/Hura8/Components/Brand/Controller/bBrandController.php @@ -0,0 +1,73 @@ + ['width' => 200,] , + ); + + /* @var BrandModel $objBrandModel */ + protected $objBrandModel; + /* @var BrandLanguageModel $objBrandLanguageModel */ + protected $objBrandLanguageModel; + protected $view_language = ''; + + public function __construct() + { + $this->objBrandModel = new BrandModel(); + + if(!$this->isDefaultLanguage()) { + $this->objBrandLanguageModel = new BrandLanguageModel(); + //$this->objVideoLanguageModel->createTableLang(); + parent::__construct($this->objBrandModel, $this->objBrandLanguageModel); + + }else{ + parent::__construct($this->objBrandModel); + } + } + + public function getInfoByUrl(string $band_index) : ?array + { + return $this->objBrandModel->getInfoByUrl($band_index); + } + + 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 = static::formatItemImage($item_info); + + $info['url'] = "/brand/".$info['brand_index']; + + return $info; + } + + + public static function formatItemImage(array $item_info) { + $info = $item_info; + + foreach (static::$resized_sizes as $size => $value) { + $info['image'][$size] = ($info['thumbnail']) ? STATIC_DOMAIN . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $info['thumbnail'] : ''; + } + + return $info; + } + + +} diff --git a/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php b/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php new file mode 100644 index 0000000..543df94 --- /dev/null +++ b/inc/Hura8/Components/Brand/Model/BrandLanguageModel.php @@ -0,0 +1,19 @@ +richtext_fields); + } + +} diff --git a/inc/Hura8/Components/Brand/Model/BrandModel.php b/inc/Hura8/Components/Brand/Model/BrandModel.php new file mode 100644 index 0000000..344e4a2 --- /dev/null +++ b/inc/Hura8/Components/Brand/Model/BrandModel.php @@ -0,0 +1,72 @@ +db->runQuery( + "SELECT `letter`, COUNT(*) AS item_count FROM `".$this->tb_entity."` GROUP BY `letter` ORDER BY `letter` ASC " + ); + return $this->db->fetchAll($query); + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + public function getInfoByUrl($brand_index) : ?array + { + $brand_index = preg_replace("/[^a-z0-9\.\-\_]/i", '', $brand_index); + + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `brand_index` = ? LIMIT 1 ", ['s'], [$brand_index]); + if($item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + +} diff --git a/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php b/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php new file mode 100644 index 0000000..f64db01 --- /dev/null +++ b/inc/Hura8/Components/ComboSet/AdminController/AComboSetController.php @@ -0,0 +1,15 @@ +objComboSetModel = new ComboSetModel(); + + if(!$this->isDefaultLanguage()) { + $this->objComboSetLanguageModel = new ComboSetLanguageModel(); + //$this->objVideoLanguageModel->createTableLang(); + parent::__construct($this->objComboSetModel, $this->objComboSetLanguageModel); + + }else{ + parent::__construct($this->objComboSetModel); + } + } + + + public function getAllSetIdsForAProduct($product_id) + { + return $this->objComboSetModel->getAllSetIdsForAProduct($product_id); + } + + + public function getTotalProductUseSet($set_id) + { + return $this->objComboSetModel->getTotalProductUseSet($set_id); + } + + + public function getListProductUseSet($set_id, $numPerPage) + { + return $this->objComboSetModel->getListProductUseSet($set_id, $numPerPage); + } + + + + + + public function getProductListInfoInConfig(array $category) { + $product_list_ids = []; + foreach ($category as $index => $_category_info) { + foreach ($_category_info['suggest_list'] as $_proindex => $_pro_info) { + $product_list_ids[] = $_pro_info['real_id']; + } + } + + return array_unique($product_list_ids); + } + + + public function buildConfig( $category, $product) { + + $group_category = []; + + foreach ($category as $category_index => $_category_info) { + $category_product = []; + foreach ($product[$category_index] as $product_index => $_product_info) { + //$_product_info['price'] = clean_price($_product_info['price']); + $category_product[] = $_product_info; + } + + $group_category[] = [ + "title" => $_category_info['title'], + //"type" => "category", + //"real_id" => $_category_info['real_id'], + //"select_type" => $_category_info['select_type'],//checkbox|radio + "suggest_list" => $category_product, + ]; + } + + return $group_category; + } + + + public function decomposeConfig($config) { + $tab = []; + $group = []; + $category = []; + $product = []; + + $group_index = 0; + $category_index = 0; + $product_index = 0; + + foreach ($config as $tab_index => $tab_info) { + //construct tab + $tab[$tab_index] = [ + 'title' => $tab_info['title'], + ]; + + //construct group + foreach ($tab_info['child'] as $child_group) { + $group_index += 1; + + $group[$tab_index][$group_index] = [ + 'title' => $child_group['title'], + ]; + + //construct category + foreach ($child_group['child'] as $child_category) { + $category_index += 1; + + $category[$group_index][$category_index] = [ + 'title' => $child_category['title'], + 'real_id' => $child_category['real_id'], + 'select_type' => $child_category['select_type'], + ]; + + //construct product + foreach ($child_category['suggest_list'] as $child_product) { + $product_index += 1; + + $product[$category_index][$product_index] = [ + 'title' => $child_product['title'], + 'real_id' => $child_product['real_id'], + 'is_default' => $child_product['is_default'], + ]; + } + } + } + } + + return [ + "tab" => $tab, + "group" => $group, + 'category' => $category, + 'product' => $product, + ]; + } + + + + +} diff --git a/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php b/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php new file mode 100644 index 0000000..c312404 --- /dev/null +++ b/inc/Hura8/Components/ComboSet/Model/ComboSetLanguageModel.php @@ -0,0 +1,17 @@ +db->runQuery( + " SELECT `set_id` FROM ".$this->tb_set_product." WHERE `product_id` = ? ", + ['d'], [$product_id] + ); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $item_list[] = $info['set_id']; + } + + return $item_list; + } + + + public function getTotalProductUseSet($set_id) + { + // search + $keyword = getRequest("q"); + if($keyword) { + $search = new ProductSearchModel(); + $match_result = $search->find($keyword); + $catCondition = (sizeof($match_result) > 0) ? " AND `product_id` IN (".join(",", $match_result).") " : " AND `product_id` = -1 "; + + $query = $this->db->runQuery(" + SELECT COUNT(product_id) AS total_product + FROM ".$this->tb_set_product." + WHERE `set_id` = ? " . $catCondition ." + ", ['d'], [$set_id]); + + if ($info = $this->db->fetchAssoc($query)) { + return $info['total_product']; + } + + return 0; + + } else { + $set_info = $this->getInfo($set_id); + + return $set_info['product_count']; + } + } + + + public function getListProductUseSet($set_id, $numPerPage) + { + $page = getPageId(); + + // search + $catCondition = ""; + $keyword = getRequest("q"); + if($keyword) { + $search = new ProductSearchModel(); + $match_result = $search->find($keyword); + $catCondition = (sizeof($match_result) > 0) ? " AND `product_id` IN (".join(",", $match_result).") " : " AND `product_id` = -1 "; + } + + $query = $this->db->runQuery(" + SELECT `product_id` + FROM ".$this->tb_set_product." + WHERE `set_id` = ? " . $catCondition ." + ORDER BY id desc + LIMIT ".($page - 1) * $numPerPage .", ".$numPerPage." + ", ['d'], [$set_id]); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $item_list[] = $info['product_id']; + } + + return $item_list; + } + + + protected function _buildQueryOrderBy(string $sort_by = "new") + { + $order_condition = ""; + + switch ($sort_by) { + case "ordering"; + $order_condition = " `ordering` desc "; + break; + case "old"; + $order_condition = " id asc "; + break; + case "last_show_time"; + $order_condition = " last_show_time ASC "; + break; + } + + return $order_condition; + } + + + protected function formatItemInfo(array $item_info) : array + { + $from_time = $item_info['from_time']; + $from_time_date = ($from_time > 0) ? date("d-m-Y", $from_time) : ''; + $from_time_minute = ($from_time > 0) ? date("H:i", $from_time) : "00:00"; + + $to_time = $item_info['to_time']; + $to_time_date = ($to_time > 0) ? date("d-m-Y", $to_time) : ''; + $to_time_minute = ($to_time > 0) ? date("H:i", $to_time) : "00:00"; + + $item_info['from_time_date'] = $from_time_date; + $item_info['from_time_minute'] = $from_time_minute; + $item_info['to_time_date'] = $to_time_date; + $item_info['to_time_minute'] = $to_time_minute; + + return $item_info; + } + + + + ///--------- + /// + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = ""; + $bind_types = []; + $bind_values = []; + + if(isset($filter_condition["product_id"]) && $filter_condition["product_id"]){ + $catCondition .= " AND `id` IN ( SELECT `set_id` FROM ".$this->tb_set_product." WHERE `product_id` = ? ) "; + + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['product_id']; + } + + return [$catCondition, $bind_types, $bind_values]; + } + + +} diff --git a/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php b/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php new file mode 100644 index 0000000..177ed0c --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/AdminController/AConfigGroupController.php @@ -0,0 +1,77 @@ +objConfigGroupModel->deleteAttribute($id, $group_id); + } + + + public function updateAttribute($id, $info) { + $this->objConfigGroupModel->updateAttribute($id, $info) ; + } + + + public function createAttribute($info) { + return $this->objConfigGroupModel->createAttribute($info) ; + } + + + public function createAttributeValue($info) { + $this->objConfigGroupModel->createAttributeValue($info); + } + + + public function deleteAttributeValue($id) { + $this->objConfigGroupModel->deleteAttributeValue($id); + } + + + public function updateAttributeValue($id, $info) { + $this->objConfigGroupModel->updateAttributeValue($id, $info); + } + + + public function createProduct($product_id, $group_id, array $attribute_config) { + $this->objConfigGroupModel->createProduct($product_id, $group_id, $attribute_config); + } + + + public function deleteProduct($product_id, $group_id) { + $this->objConfigGroupModel->deleteProduct($product_id, $group_id); + } + + + public function updateProduct($product_id, $group_id, array $attribute_config, $product_name_in_group = '') { + + return $this->objConfigGroupModel->updateProduct($product_id, $group_id, $attribute_config, $product_name_in_group); + } + + + public function getProductInGroup($group_id){ + return $this->objConfigGroupModel->getProductInGroup($group_id); + } + + + public function getGroupConfig($item_id) { + return $this->objConfigGroupModel->getGroupConfig($item_id); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + + +} diff --git a/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php b/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php new file mode 100644 index 0000000..3f5baff --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/Controller/bConfigGroupController.php @@ -0,0 +1,24 @@ +objConfigGroupModel = new ConfigGroupModel(); + parent::__construct($this->objConfigGroupModel); + } + + + +} diff --git a/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php b/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php new file mode 100644 index 0000000..e149db8 --- /dev/null +++ b/inc/Hura8/Components/ConfigGroup/Model/ConfigGroupModel.php @@ -0,0 +1,500 @@ +tb_config_group = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function updateGroupAttributeCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group."` SET + `attribute_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_attribute."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], [ $group_id, $group_id ] + ); + } + + + protected function updateGroupProductCount($group_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group."` SET + `item_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], [ $group_id, $group_id ] + ); + } + + + //attribute value + public function deleteAttributeValue($att_value_id) { + + $attr_id = 0; + $query = $this->db->runQuery("SELECT `attr_id` FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $attr_id = $info['attr_id']; + } + + $group_id = $this->getGroupIdFromAttribute($attr_id); + + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + + $this->resetProductConfigCache($group_id); + + $this->updateAttributeValueCount($attr_id); + } + + + protected function updateAttributeValueCount($attr_id) { + $this->db->runQuery( + "UPDATE `".$this->tb_config_group_attribute."` SET + `value_count` = ( SELECT COUNT(*) FROM `".$this->tb_config_group_attribute_value."` WHERE `attr_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], + [$attr_id, $attr_id ] + ); + } + + + public function updateAttributeValue($id, $info) { + + $updated_info = $info; + + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->update( + $this->tb_config_group_attribute_value, + $updated_info, + [ + "id" => $id, + ] + ); + + $group_id = $this->getGroupIdFromAttributeValue($id); + $this->resetProductConfigCache($group_id); + } + + + public function createAttributeValue($info) { + $updated_info = $info; + + $updated_info['create_time'] = CURRENT_TIME; + $updated_info['create_by'] = ADMIN_NAME; + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->insert($this->tb_config_group_attribute_value, $updated_info ); + + $group_id = $this->getGroupIdFromAttribute($info['attr_id']); + $this->resetProductConfigCache($group_id); + + $this->updateAttributeValueCount($info['attr_id']); + } + + //attribute + public function deleteAttribute($id, $group_id = 0) { + if(!$group_id) $group_id = $this->getGroupIdFromAttribute($id); + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_attribute_value."` WHERE `attr_id` = ? LIMIT 1 ", ['d'], [ $id ]); + + $this->updateGroupAttributeCount($group_id); + + //todo: update for product attribute_config ? + $this->resetProductConfigCache($group_id); + } + + public function updateAttribute($id, $info) { + + $updated_info = $info; + + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->update( + $this->tb_config_group_attribute, + $updated_info, + [ + 'id' => $id, + ] + ); + + $group_id = $this->getGroupIdFromAttribute($id); + $this->resetProductConfigCache($group_id); + } + + public function createAttribute($info) { + + $updated_info = $info; + + $updated_info['create_time'] = CURRENT_TIME; + $updated_info['create_by'] = ADMIN_NAME; + $updated_info['last_update'] = CURRENT_TIME; + $updated_info['last_update_by'] = ADMIN_NAME; + + $this->db->insert($this->tb_config_group_attribute, $updated_info ); + + $this->resetProductConfigCache($info['group_id']); + + $this->updateGroupAttributeCount($info['group_id']); + + return $this->db->get_insert_id(); + } + + private function getGroupIdFromAttributeValue($att_value_id) { + $attr_id = 0; + $query = $this->db->runQuery("SELECT `attr_id` FROM `".$this->tb_config_group_attribute_value."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $att_value_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $attr_id = $info['attr_id']; + } + + return $this->getGroupIdFromAttribute($attr_id); + } + + private function getGroupIdFromAttribute($attr_id) { + $group_id = 0; + $query = $this->db->runQuery("SELECT `group_id` FROM `".$this->tb_config_group_attribute."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $attr_id ]); + if ($info = $this->db->fetchAssoc($query)) { + $group_id = $info['group_id']; + } + + return $group_id; + } + + //create or update product in a group + public function createProduct($product_id, $group_id, array $attribute_config) { + if($this->isProductInGroup($product_id, $group_id)) { + + $this->updateProduct($product_id, $group_id, $attribute_config); + + }else { + + $updated_info = [ + "product_id" => $product_id, + "group_id" => $group_id, + "attribute_config" => json_encode($attribute_config), + + "create_time" => CURRENT_TIME, + "create_by" => ADMIN_NAME, + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ]; + + $this->db->insert($this->tb_config_group_product, $updated_info ); + + $this->updateGroupProductCount($group_id); + + } + + $this->resetProductConfigCache($group_id); + } + + public function deleteProduct($product_id, $group_id) { + $this->db->runQuery( + "DELETE FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? AND `group_id` = ? LIMIT 1 ", + ['d', 'd'], [ $product_id, $group_id ] + ); + + $this->deleteProductConfigCache($product_id); + + $this->updateGroupProductCount($group_id); + } + + public function updateProduct($product_id, $group_id, array $attribute_config, $product_name_in_group = '') { + + $this->db->update( + $this->tb_config_group_product, + [ + "attribute_config" => json_encode($attribute_config), + "product_name_in_group" => substr($product_name_in_group, 0, 140), + "last_update" => CURRENT_TIME, + "last_update_by" => ADMIN_NAME, + ], + [ + "product_id" => $product_id, + "group_id" => $group_id, + ] + ); + + $this->resetProductConfigCache($group_id); + + return true; + } + + + //we want to reset all caches for products in a group + //we need to do this when we make changes to group's attributes, or create new products/ update product in group + protected function resetProductConfigCache($group_id) { + $query = $this->db->runQuery("SELECT `product_id` FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ", ['d'], [$group_id]) ; + $product_list = array(); + foreach( $this->db->fetchAll($query) as $item ){ + $product_list[] = $item['product_id']; + } + + if(sizeof($product_list)) { + $this->db->query("UPDATE `".$this->tb_config_group_product_cache."` SET + `value` = NULL + WHERE `product_id` IN (".join(",", $product_list).") ") ; + } + } + + + protected function deleteProductConfigCache($product_id) { + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", ['d'], [$product_id]) ; + } + + + public function saveProductConfigCache($product_id, $value) { + + $query = $this->db->runQuery( + "SELECT `product_id` FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + + if($this->db->fetchAssoc($query)){ + $this->db->runQuery( + "UPDATE `".$this->tb_config_group_product_cache."` SET + `value` = '".$this->db->escape(json_encode($value))."' + WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + }else{ + $this->db->runQuery("INSERT INTO `".$this->tb_config_group_product_cache."` (`product_id`, `value`) + VALUES ('". (int) $product_id."', '".$this->db->escape(json_encode($value))."') ") ; + } + } + + + public function getProductConfigCache($product_id) { + $query = $this->db->runQuery("SELECT `value` FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` = ? LIMIT 1 ", ['d'], [$product_id]) ; + if($item_info = $this->db->fetchAssoc($query)){ + return ($item_info['value']) ? \json_decode($item_info['value'], true) : false; + } + + return false; + } + + + //get all group config + public function getGroupConfig($group_id) { + + $query = $this->db->runQuery( + "SELECT + a.id AS attribute_id , + a.name AS attribute_name , + a.ordering AS attr_ordering , + v.id AS value_id , + v.name AS value_name , + v.image AS image , + v.color_code AS color , + v.ordering , + v.description AS description + FROM `".$this->tb_config_group_attribute."` a + LEFT JOIN `".$this->tb_config_group_attribute_value."` v ON a.id = v.attr_id + WHERE a.group_id = ? + ORDER BY attr_ordering DESC, `ordering` DESC + ", + ['d'], [$group_id] + ); + + $group_attribute = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + if(!isset($group_attribute[$info['attribute_id']])) { + $group_attribute[$info['attribute_id']] = array( + 'id' => $info['attribute_id'], + 'name' => $info['attribute_name'], + 'ordering' => $info['attr_ordering'], + 'list' => array(), + ); + } + + if($info['value_id']) { + $group_attribute[$info['attribute_id']]['list'][] = array( + 'id' => $info['value_id'], + 'name' => $info['value_name'], + 'image' => $info['image'], + 'color' => $info['color'], + 'ordering' => $info['ordering'], + 'description' => $info['description'], + ); + } + } + + return $group_attribute; + } + + + public function getProductConfigGroupId($product_id) { + $query = $this->db->runQuery( + "SELECT `group_id` FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? LIMIT 1 ", + ['d'], [$product_id] + ) ; + + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info['group_id']; + } + + return 0; + } + + + public function getProductInGroup($group_id){ + $query = $this->db->runQuery( + "SELECT `product_id`, `product_name_in_group`, `attribute_config` + FROM `".$this->tb_config_group_product."` + WHERE `group_id` = ? + ", + ['d'], [$group_id] + ); + + $product_list = array(); + $product_ids = array(); + + foreach ( $this->db->fetchAll($query) as $info ) { + $product_ids[] = $info['product_id']; + + $product_list[$info['product_id']] = array( + "id" => $info['product_id'], + "name" => "", + "product_name_in_group" => $info['product_name_in_group'], + "attribute" => \json_decode($info['attribute_config'], true), + "url" => "", + "sku" => "", + "price" => 0, + "image" => "", + "status" => "", + ); + } + + //find product urls + if(sizeof($product_ids)) { + $objProductModel = new ProductModel(); + $product_list_info = $objProductModel->getListByIds($product_ids); + + // debug_var($product_list_info); + // update $product_list + foreach ($product_list as $_pro_id => $_info) { + $_pro_info = $product_list_info[$_pro_id] ?? null; + if($_pro_info) { + $product_list[$_pro_id]['name'] = $_pro_info['title']; + $product_list[$_pro_id]['price'] = $_pro_info['price']; + $product_list[$_pro_id]['sku'] = $_pro_info['sku']; + $product_list[$_pro_id]['image'] = $_pro_info['thumbnail']; + $product_list[$_pro_id]['url'] = $_pro_info['request_path']; + $product_list[$_pro_id]['status'] = $_pro_info['status']; + } + } + } + + return $product_list; + } + + + protected function isProductInGroup($product_id, $group_id) { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_config_group_product."` WHERE `product_id` = ? AND `group_id` = ? LIMIT 1 ", + ['d', 'd'], + [$product_id, $group_id] + ) ; + + return ($this->db->fetchAssoc($query)); + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => "", + "numPerPage" => 20, + "order_by" => '', + );*/ + + $catCondition = ""; + + return [ + $catCondition, + [], + [] + ]; + } + + protected function beforeCreateItem(array $input_info): AppResponse + { + $info = $input_info; + + $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; + + $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) + { + + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_config_group_attribute."` WHERE `group_id` = ? ", ['d'], [$item_id]); + + foreach ( $this->db->fetchAll($query) as $info ) { + $this->deleteAttribute($info['id'], $item_id); + } + + $this->db->runQuery( + "DELETE FROM `".$this->tb_config_group_product_cache."` WHERE `product_id` IN (SELECT product_id FROM config_group_product WHERE `group_id` = ? ) ", + ['d'], [$item_id] + ) ; + + $this->db->runQuery("DELETE FROM `".$this->tb_config_group_product."` WHERE `group_id` = ? ", ['d'], [$item_id]) ; + } +} diff --git a/inc/Hura8/Components/Customer/AdminController/ACustomerController.php b/inc/Hura8/Components/Customer/AdminController/ACustomerController.php new file mode 100644 index 0000000..f01f7f6 --- /dev/null +++ b/inc/Hura8/Components/Customer/AdminController/ACustomerController.php @@ -0,0 +1,23 @@ +objCustomerGroupModel = new CustomerGroupModel(); + parent::__construct($this->objCustomerGroupModel); + } + + + public function getAllGroup() { + return $this->objCustomerGroupModel->getList(["numPerPage" => 1000]); + } + + + public function removeCustomer($customer_id, $group_id) + { + return $this->objCustomerGroupModel->removeCustomer($customer_id, $group_id); + } + + + public function addCustomer($customer_id, $group_id) + { + return $this->objCustomerGroupModel->addCustomer($customer_id, $group_id); + } + + + public function getTotalCustomer($group_id, array $condition = []) + { + return $this->objCustomerGroupModel->getTotalCustomer($group_id, $condition); + } + + + public function getListCustomer($group_id, array $condition = []) { + + $objProvinceController = new AProvinceController(); + + return array_map(function ($item) use ($objProvinceController){ + + $item['province_name'] = $objProvinceController->getProvinceName($item['province']); + + return $item; + + }, $this->objCustomerGroupModel->getListCustomer($group_id, $condition)); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } +} diff --git a/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php b/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php new file mode 100644 index 0000000..d3eb87c --- /dev/null +++ b/inc/Hura8/Components/Customer/AdminController/ACustomerLoyaltyController.php @@ -0,0 +1,9 @@ +objCustomerModel = new CustomerModel(); + $this->objProvinceController = new AProvinceController(); + parent::__construct($this->objCustomerModel); + } + + + public function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_info); + } + + + public function formatItemInfo(array $item_info) + { + $info = $item_info; + $info['province_name'] = $this->objProvinceController->getProvinceName($item_info['province']); + + return $info; + } + +} diff --git a/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php b/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php new file mode 100644 index 0000000..8ca17ce --- /dev/null +++ b/inc/Hura8/Components/Customer/Controller/bCustomerLoyaltyController.php @@ -0,0 +1,206 @@ +objCustomerLoyaltyModel = new CustomerLoyaltyModel(); + + // import settings + //$new_info_file = "../config/build/customer_point.php" ; + //$config_file = ROOT_DIR . "/config/build/customer_point.php"; + if(@file_exists($this->point_setting_config_file)) { + $this->point_setting = include $this->point_setting_config_file; + } + + // customer level based on point gain + if( defined("ENABLE_CUSTOMER_POINT") && ENABLE_CUSTOMER_POINT ) { + if(@file_exists($this->level_setting_config_file)) { + $this->level_setting = include $this->level_setting_config_file; + } + } + + // default is point + $this->level_by = (defined('CHANGE_CUSTOMER_LEVEL_BY')) ? CHANGE_CUSTOMER_LEVEL_BY : 'point'; + + $this->objUserLoyaltyPointCalculation = new UserLoyaltyPointCalculation($this->point_setting); + + } + + + public function getPointSettingConfigFile() { + return $this->point_setting_config_file; + } + + public function getPointSetting() { + return $this->point_setting; + } + + public function getLevelSetting(){ + return $this->level_setting; + } + + // show estimate cart point + public function getEstimateCartPoint($cart_value){ + $conversion_rate = (isset($this->point_setting['reward']['buy']['rate'])) ? $this->point_setting['reward']['buy']['rate'] : 0; + return ($conversion_rate) ? round($cart_value / $conversion_rate) : 0; + } + + public function getUserPoint($user_id, array $condition, $return_type) + { + return $this->objCustomerLoyaltyModel->getUserPoint($user_id, $condition, $return_type); + } + + // remove rewarded point i.e. reward for successfull order and now order is marked canceled + public function reclaimRewardedPoint($user_id, $activity_type_tracker){ + // todo: + } + + public function usePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_value' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $result = $this->objUserLoyaltyPointCalculation->calculateUsePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('use', $user_id, $result['use_point'], $activity_type, $activity_type_tracker, $reason); + + return $result; + } + + + public function rewardPoint($user_id, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_id' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $point = $this->objUserLoyaltyPointCalculation->calculateRewardPoint($user_id, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('reward', $user_id, $point, $activity_type, $activity_type_tracker, $reason); + + return $point; + } + + // $operation: reward|use + // $change_point: positive (reward) or nagative (use) + protected function pointOp($operation, $user_id, $change_point, $activity_type, $activity_type_tracker, $reason = '') { + + if(!$change_point) return false; + + $reason_prefix = ($operation == 'use') ? 'Sử dụng' : 'Thưởng'; + if($activity_type == 'return') $reason_prefix = 'Hoàn lại'; + $full_reason = join(" ", [$reason_prefix, $change_point, static::$POINT_NAME, ":", $reason]); + + if($operation == 'use') $change_point = -1 * $change_point; + + // security: hash the row to avoid editing point directly in the database + $hash_value = sha1(join(".", [ + $operation, + $user_id, + $change_point, + $activity_type, + $activity_type_tracker, + CURRENT_TIME, + 'ass@ss' + ])); + + $new_id = $this->db->insert( + $this->tb_point , + [ + 'customer_id' => $user_id , + 'activity_type' => $activity_type, + 'activity_type_tracker' => $activity_type_tracker , + 'operation' => $operation, + 'point' => $change_point, + 'create_time' => CURRENT_TIME, + 'reason' => substr($full_reason, 0, 200), + 'referer_url' => substr(REFERER_URL, 0, 150) , + 'hash_value' => $hash_value , + ] + ); + + //update user reward balance + if($new_id) { + $this->updateStat($user_id, $change_point); + } + + return $new_id; + } + + protected function updateStat($user_id, $changed_point, $changed_order_value=0) { + return $this->objCustomerLoyaltyModel->updateStat($user_id, $changed_point, $changed_order_value); + } + + private function calculateLevelByPoint($point) { + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + foreach ( $all_level as $level) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $point >= $this->level_setting[$level]["point_require"] + && $point < $this->level_setting[$next_level]["point_require"] ) { + return $level; + } + }else{ + if($point >= $this->level_setting[$level]["point_require"]) { + return $level; + } + } + } + + return 0; + } + + private function calculateLevelByOrderValue($aggregate_purchase_value = 0) { + + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + //tinh hang thanh vien theo so tien tich luy + foreach ( $all_level as $level ) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"] && + $aggregate_purchase_value < $this->level_setting[$next_level]["total_order_value"] + ) { + return $level; + } + }else{ + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"]) { + return $level; + } + } + } + + return 0; + } + + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php b/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php new file mode 100644 index 0000000..3360bad --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerAuthModel.php @@ -0,0 +1,79 @@ +tb_customer_login, $this->tb_customer_access_code); + } + + + public function getLoginListByIds(array $staff_ids) { + if(!sizeof($staff_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($staff_ids, 'int'); + + $bind_values = $staff_ids; + + $query = $this->db->runQuery( + "SELECT `user_id`, `last_login_time`, `last_login_ip`, `last_login_device`, `last_login_browser` + FROM ".$this->tb_customer_login." + WHERE `user_id` IN (".$parameterized_ids.") ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['user_id']] = $item; + } + + return $item_list; + } + + + public function getLoginLog(array $conditions = []) { + $bind_types = []; + $bind_values = []; + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_customer_login_log." WHERE 1 ORDER BY `id` DESC LIMIT 100 ", + $bind_types, + $bind_values + ); + + return $this->db->fetchAll($query) ; + } + + + /** + * @param $email + * @param string $login_status ok or error + * @param string $login_msg + */ + public function logLogin($email, $login_status, $login_msg) { + $this->db->insert( + $this->tb_customer_login_log, + [ + "email" => substr($email, 0, 45), + "login_status" => $login_status, + "login_msg" => substr($login_msg, 0, 45), + "ip_address" => substr(USER_IP, 0, 45), + "user_agent" => substr(USER_AGENT, 0, 99), + "create_time" => CURRENT_TIME, + ] + ); + } + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php b/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php new file mode 100644 index 0000000..ee243fd --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerGroupModel.php @@ -0,0 +1,213 @@ +db->runQuery( + "UPDATE `".$this->tb_entity."` SET + `customer_count` = (SELECT COUNT(*) AS total FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? ) + WHERE `id` = ? LIMIT 1 + ", + ['d', 'd'], [$group_id, $group_id] + ); + } + + + public function getTotalCustomer($group_id, array $condition = []) + { + $query = $this->db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? ", + ['d'], [$group_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListCustomer($group_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 = " `id` DESC"; + + $query = $this->db->runQuery( + "SELECT `customer_id` FROM ".$this->tb_customer_per_group." WHERE `group_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$group_id] + ) ; + + $item_list_ids = array_map(function ($item){ return $item['customer_id'];}, $this->db->fetchAll($query)); + + $objCustomerModel = new CustomerModel(); + $list_info = $objCustomerModel->getListByIds($item_list_ids); + + // final list + $final_list = []; + foreach ($item_list_ids as $_id) { + $final_list[] = $list_info[$_id] ?? null; + } + + return $final_list; + } + + + public function removeCustomerFromAllGroup($customer_id) + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_customer_per_group."` WHERE `customer_id` = ?", + ['d'], [$customer_id] + ); + + return true; + } + + + public function removeCustomer($customer_id, $group_id) + { + $this->db->runQuery( + "DELETE FROM `".$this->tb_customer_per_group."` WHERE `group_id` =? AND `customer_id` = ? LIMIT 1 ", + ['d', 'd'], + [$group_id, $customer_id] + ); + + $this->updateItemCount($group_id); + + return true; + } + + + public function addCustomer($customer_id, $group_id) + { + $query = $this->db->runQuery( + " SELECT * FROM `".$this->tb_customer_per_group."` WHERE `group_id` = ? AND `customer_id` = ? LIMIT 1 ", + ['d', 'd'], + [$group_id, $customer_id] + ); + + if ($this->db->fetchAssoc($query)) { + return false; + } + + $this->db->insert( + $this->tb_customer_per_group, + [ + "group_id" => $group_id, + "customer_id" => $customer_id, + ] + ); + + $this->updateItemCount($group_id); + + return true; + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + if(!$info['group_code']) $info['group_code'] = $info['title']; + $info['group_code'] = $this->createUniqueCode(0, $info['group_code']); + + $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) + { + + } + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info): AppResponse + { + $info = $new_input_info; + + if(isset($info['group_code'])) { + if(!$info['group_code']) $info['group_code'] = $info['title']; + $info['group_code'] = $this->createUniqueCode($item_id, $info['group_code']); + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + + protected function createUniqueCode($current_item_id, $wanted_code){ + + $clean_code = UrlManagerController::create_url_index($wanted_code); + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `group_code` = ? LIMIT 1 ", ['s'], [$clean_code]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $current_item_id) { + $new_code = $clean_code."-".IDGenerator::createStringId(3); + return $this->createUniqueCode($current_item_id, $new_code); + } + } + + return $clean_code; + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + + protected function extendedFilterOptions(): array + { + return []; + } +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php b/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php new file mode 100644 index 0000000..a10b3ea --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerLoyaltyModel.php @@ -0,0 +1,268 @@ +db = get_db("", ENABLE_DB_DEBUG); + + // import settings + //$new_info_file = "../config/build/customer_point.php" ; + //$config_file = ROOT_DIR . "/config/build/customer_point.php"; + if(@file_exists($this->point_setting_config_file)) { + $this->point_setting = include $this->point_setting_config_file; + } + + // customer level based on point gain + if( defined("ENABLE_CUSTOMER_POINT") && ENABLE_CUSTOMER_POINT ) { + if(@file_exists($this->level_setting_config_file)) { + $this->level_setting = include $this->level_setting_config_file; + } + } + + // default is point + $this->level_by = (defined('CHANGE_CUSTOMER_LEVEL_BY')) ? CHANGE_CUSTOMER_LEVEL_BY : 'point'; + + $this->objUserLoyaltyPointCalculation = new UserLoyaltyPointCalculation($this->point_setting); + } + + + public function getPointSettingConfigFile() { + return $this->point_setting_config_file; + } + + public function getPointSetting() { + return $this->point_setting; + } + + public function getLevelSetting(){ + return $this->level_setting; + } + + // show estimate cart point + public function getEstimateCartPoint($cart_value){ + $conversion_rate = (isset($this->point_setting['reward']['buy']['rate'])) ? $this->point_setting['reward']['buy']['rate'] : 0; + return ($conversion_rate) ? round($cart_value / $conversion_rate) : 0; + } + + public function getUserPoint($user_id, array $condition, $return_type) + { + + if($return_type == "total") { + //Lay tong so + $query = $this->db->runQuery("SELECT COUNT(*) AS total FROM `". $this->tb_point ."` WHERE `customer_id` = ? " , ['d'], [$user_id]); + if($resultTotal = $this->db->fetchAssoc($query)){ + return $resultTotal['total']; + } + return 0; + + } else { + + $numPerPage = (isset($condition['numPerPage'])) ? intval($condition['numPerPage']) : 50; + $page = getPageId(); + + $query = $this->db->runQuery(" + SELECT * FROM `". $this->tb_point ."` + WHERE `customer_id` = ? + ORDER BY id DESC + LIMIT ".($page - 1) * $numPerPage .", ".$numPerPage." " , ['d'], [$user_id]); + + $result = array(); + $i = ($page - 1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $rs){ + $i++; + $rs['counter'] = $i; + $rs['activity_type_name'] = (isset($this->point_setting[$rs['operation']][$rs['activity_type']])) ? $this->point_setting[$rs['operation']][$rs['activity_type']]['name'] : '--'; + $result[] = $rs; + } + + return $result; + } + + } + + + public function usePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_value' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $result = $this->objUserLoyaltyPointCalculation->calculateUsePoint($user_id, $use_point, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('use', $user_id, $result['use_point'], $activity_type, $activity_type_tracker, $reason); + + return $result; + } + + + public function rewardPoint($user_id, $activity_type, $activity_type_tracker, $reason = '', $point_args = ['order_id' => 0]){ + // no user or no config + if(!$user_id || !ENABLE_CUSTOMER_POINT) return false; + + $point = $this->objUserLoyaltyPointCalculation->calculateRewardPoint($user_id, $activity_type, $activity_type_tracker, $point_args); + + $this->pointOp('reward', $user_id, $point, $activity_type, $activity_type_tracker, $reason); + + return $point; + } + + + // $operation: reward|use + // $change_point: positive (reward) or nagative (use) + protected function pointOp($operation, $user_id, $change_point, $activity_type, $activity_type_tracker, $reason = '') { + + if(!$change_point) return false; + + $reason_prefix = ($operation == 'use') ? 'Sử dụng' : 'Thưởng'; + if($activity_type == 'return') $reason_prefix = 'Hoàn lại'; + $full_reason = join(" ", [$reason_prefix, $change_point, static::$POINT_NAME, ":", $reason]); + + if($operation == 'use') $change_point = -1 * $change_point; + + // security: hash the row to avoid editing point directly in the database + $hash_value = sha1(join(".", [ + $operation, + $user_id, + $change_point, + $activity_type, + $activity_type_tracker, + CURRENT_TIME, + 'ass@ss' + ])); + + $new_id = $this->db->insert( + $this->tb_point , + [ + 'customer_id' => $user_id , + 'activity_type' => $activity_type, + 'activity_type_tracker' => $activity_type_tracker , + 'operation' => $operation, + 'point' => $change_point, + 'create_time' => CURRENT_TIME, + 'reason' => substr($full_reason, 0, 200), + 'referer_url' => substr(REFERER_URL, 0, 150) , + 'hash_value' => $hash_value , + ] + ); + + //update user reward balance + if($new_id) { + $this->updateStat($user_id, $change_point); + } + + return $new_id; + } + + + public function updateStat($user_id, $changed_point, $changed_order_value=0) { + $user_id = intval($user_id); + + $query = $this->db->runQuery("SELECT + `loyalty_point`, + `loyalty_level`, + `total_value_success` + FROM ".$this->tb_customer." + WHERE `id` = ? + LIMIT 1 " , ['d'], [$user_id]); + + if($current = $this->db->fetchAssoc($query)){ + $new_point = $current['loyalty_point'] + $changed_point; + $new_purchase_value = $current['total_value_success'] + $changed_order_value; + + $level = $current['loyalty_level']; + if($this->level_by == 'point' && $changed_point != 0) $level = $this->calculateLevelByPoint($new_point); + else if($changed_order_value) $level = $this->calculateLevelByOrderValue($new_purchase_value); + + $this->db->update( + $this->tb_customer , + [ + 'loyalty_point' => $new_point, + 'loyalty_level' => $level, + 'total_value_success' => $new_purchase_value, + ], + [ + 'id' => $user_id, + ], + 1 + ); + } + + return true; + } + + + private function calculateLevelByPoint($point) { + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + foreach ( $all_level as $level) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $point >= $this->level_setting[$level]["point_require"] + && $point < $this->level_setting[$next_level]["point_require"] ) { + return $level; + } + }else{ + if($point >= $this->level_setting[$level]["point_require"]) { + return $level; + } + } + } + + return 0; + } + + private function calculateLevelByOrderValue($aggregate_purchase_value = 0) { + + //if the point in between -> return the lowest level + $all_level = array_keys($this->level_setting); + + //tinh hang thanh vien theo so tien tich luy + foreach ( $all_level as $level ) { + $next_level = $level + 1; + if(!in_array($next_level, $all_level)) $next_level = 0; + + if($next_level) { + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"] && + $aggregate_purchase_value < $this->level_setting[$next_level]["total_order_value"] + ) { + return $level; + } + }else{ + if( $aggregate_purchase_value >= $this->level_setting[$level]["total_order_value"]) { + return $level; + } + } + } + + return 0; + } +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerModel.php b/inc/Hura8/Components/Customer/Model/CustomerModel.php new file mode 100644 index 0000000..3257c7c --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerModel.php @@ -0,0 +1,128 @@ + "Chưa đăng ký", + "register" => "Đăng ký thành viên", + ]; + + /* @var iSearch $objSearchModel */ + protected $objSearchModel; + + public function __construct() { + $this->objSearchModel = new CustomerSearchModel(); + + parent::__construct( + EntityType::CUSTOMER, + "", + $this->objSearchModel + ); + } + + + protected function extendedFilterOptions() : array + { + return [ + 'province' => 0, + 'user_type' => '', + ]; + } + + + public function getInfoByCRMCode($code) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `crm_code` = ? LIMIT 1 ", ['s'], [$code]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + + public function getInfoByEmail($email, $user_type='register') + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` WHERE `email` = ? AND `type` = ? LIMIT 1 ", + ['s', 's'], [$email, $user_type] + ) ; + + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + public function getInfoByVerifiedEmail($email) + { + $info = $this->getInfoByEmail($email); + if($info && $info['is_email_verify']) { + return $info; + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "user_type" => '' + "q" => "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["province"]) && $filter_condition["province"]) { + $catCondition[] = " AND `province` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["province"]; + } + + // user_type + if(isset($filter_condition["user_type"]) && array_key_exists($filter_condition["user_type"], $this->user_types) ){ + $catCondition[] = " AND `type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["user_type"]; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function formatItemInfo(array $item_info): array + { + if($item_info["birth_day"] && $item_info["birth_year"]) { + $item_info["birth_day"] = $item_info["birth_day"]."-".$item_info["birth_year"]; + } + + return $item_info; + } + + + protected function createWebCRMCode($input_code = '') { + return $input_code ?: "Web-".IDGenerator::createStringId(8); + } + + +} diff --git a/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php b/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php new file mode 100644 index 0000000..42e9bcd --- /dev/null +++ b/inc/Hura8/Components/Customer/Model/CustomerSearchModel.php @@ -0,0 +1,34 @@ + "tb_customer.type", + "province" => "tb_customer.province", + "is_email_verify" => "tb_customer.is_email_verify", + "total_value" => "tb_customer.total_value", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_customer.name", "tb_customer.crm_code", "tb_customer.email","tb_customer.mobile",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_customer", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php b/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php new file mode 100644 index 0000000..6cc7b73 --- /dev/null +++ b/inc/Hura8/Components/Deal/AdminController/ADealCollectionController.php @@ -0,0 +1,51 @@ +objDealCollectionModel = new DealCollectionModel(); + parent::__construct($this->objDealCollectionModel); + } + + public function getAllProductIdInCollection($collection_id) { + return $this->objDealCollectionModel->getAllProductIdInCollection($collection_id); + } + + public function getAllDealIdInCollection($collection_id) { + return $this->objDealCollectionModel->getAllDealIdInCollection($collection_id); + } + + public function updateAllDealInCollection($collection_id, $price, $time){ + return $this->objDealCollectionModel->updateAllDealInCollection($collection_id, $price, $time); + } + + public function addProductToCollection($product_id, $collection_id){ + return $this->objDealCollectionModel->addProductToCollection($product_id, $collection_id); + } + + public function removeFromCollection($deal_id, $collection_id){ + $this->objDealCollectionModel->removeFromCollection($deal_id, $collection_id); + } + + public function addToCollection($deal_id, $collection_id){ + $this->objDealCollectionModel->addToCollection($deal_id, $collection_id); + } + + public function updateCollectionView($id){ + $this->objDealCollectionModel->updateCollectionView($id); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Deal/AdminController/ADealController.php b/inc/Hura8/Components/Deal/AdminController/ADealController.php new file mode 100644 index 0000000..4ed9df7 --- /dev/null +++ b/inc/Hura8/Components/Deal/AdminController/ADealController.php @@ -0,0 +1,58 @@ +objDealModel->getAllAutoRenewableDeal(); + + //then renew & log history + foreach ($deal_list as $info) { + $this->renewDeal($info['id'], $info['from_time'], $info['to_time'], $info['auto_renew_history']); + } + } + + protected function renewDeal($id, $from_time, $to_time, array $auto_renew_history) { + /*$obj_from_date = new \DateTime(date("Y-m-d", $from_time)); + $obj_to_date = new \DateTime(date("Y-m-d", $to_time)); + $day_diff = $obj_to_date->diff($obj_from_date)->format('%a'); + + $obj_to_date->add(new \DateInterval('P'.$day_diff.'D')); + $new_date = $obj_to_date->format('Y-m-d'); + + $current_date = date("Y-m-d"); + $from_time_minute = ($from_time > 0) ? date("H:i", $from_time) : "00:00"; + $to_time_minute = ($to_time > 0) ? date("H:i", $to_time) : "00:00";*/ + + $to_time_new = CURRENT_TIME + ($to_time - $from_time); + + $auto_renew_history[] = [ + "renew_time" => show_datetime_from_unix(CURRENT_TIME), + "from_time" => show_datetime_from_unix(CURRENT_TIME), + "to_time" => show_datetime_from_unix($to_time_new), + ]; + + return $this->updateFields( $id, + [ + 'from_time' => CURRENT_TIME, + 'to_time' => $to_time_new, + 'auto_renew_history' => serialize($auto_renew_history), + ] + ); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Deal/Controller/bDealController.php b/inc/Hura8/Components/Deal/Controller/bDealController.php new file mode 100644 index 0000000..8f714d2 --- /dev/null +++ b/inc/Hura8/Components/Deal/Controller/bDealController.php @@ -0,0 +1,48 @@ +objDealModel = new DealModel(); + parent::__construct($this->objDealModel); + } + + + public function getList(array $condition) : array + { + $deal_list = parent::getList($condition); + + $product_list_info = []; + $product_list_ids = []; + $final_list = []; + foreach ($deal_list as $item) { + if(!isset($product_list_info[$item['pro_id']])) { + $product_list_info[$item['pro_id']]= null; + $product_list_ids[] = $item['pro_id']; + } + + $copy = $item; + $copy['product_info'] = &$product_list_info[$item['pro_id']]; + $final_list[] = $copy; + } + + $objAProductController = new AProductController(); + foreach ($objAProductController->getListByIds($product_list_ids) as $pro_id => $info) { + $product_list_info[$pro_id] = $info; + } + + return $final_list; + } + + +} diff --git a/inc/Hura8/Components/Deal/Model/DealCollectionModel.php b/inc/Hura8/Components/Deal/Model/DealCollectionModel.php new file mode 100644 index 0000000..446d3f7 --- /dev/null +++ b/inc/Hura8/Components/Deal/Model/DealCollectionModel.php @@ -0,0 +1,223 @@ +getAllDealIdInCollection($collection_id); + + if(!sizeof($deal_id_list)) return array(); + + $query = $this->db->runQuery("SELECT `pro_id` FROM `".$this->tb_deal."` WHERE `id` IN (". join(',', $deal_id_list).") "); + + return array_map(function ($item){ + return $item['pro_id']; + }, $this->db->fetchAll($query)); + } + + public function getAllDealIdInCollection($collection_id) { + $query = $this->db->runQuery("SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ", ['d'], [$collection_id]); + + return array_map(function ($item){ + return $item['deal_id']; + }, $this->db->fetchAll($query)); + } + + public function updateAllDealInCollection($collection_id, $price, $time){ + + $from_time = ($time['from_date'] != '') ? strtotime(TimeManager::convert_date_from_javascript($time['from_date'])." ".$time['from_time_minute'].":00") : 0; + $to_time = ($time['to_date'] != '') ? strtotime(TimeManager::convert_date_from_javascript($time['to_date'])." ".$time['to_time_minute'].":00") : 0; + + $deal_id_list = $this->getAllDealIdInCollection($collection_id); + + if(!sizeof($deal_id_list)) return false; + + $query = $this->db->runQuery("SELECT `id`, `pro_id` FROM `".$this->tb_deal."` WHERE `id` IN (". join(',', $deal_id_list).") "); + $product_list_id = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + $product_list_id[$info['pro_id']] = $info['id']; + } + + //get product prices + $query = $this->db->runQuery(" + SELECT `id`, price FROM ".TableName::PRODUCT." + WHERE `id` IN (". join(',', array_keys($product_list_id)) .") + "); + + $objDealModel = new DealModel(); + + foreach ( $this->db->fetchAll($query) as $_info ) { + $product_id = $_info['id']; + $product_price = $_info['price']; + $deal_price = ($price['type'] == 'percent') ? round($product_price * (100 - $price['value']) / 100) : ($product_price - $price['value']); + + //update + $objDealModel->updatePriceAndTime( + $product_list_id[$product_id], + array( + "price" => $deal_price, + "from_time" => $from_time, + "to_time" => $to_time, + ) + ); + } + + return true; + } + + public function addProductToCollection($product_id, $collection_id){ + + $objAProductController = new AProductController(); + $product_info = $objAProductController->getInfo($product_id); + + if($product_info){ + + $objDealModel = new DealModel(); + + $deal_id = $objDealModel->create(array( + "pro_id" => $product_id, + "title" => $product_info['title'], + "price" => $product_info['price'], + "quantity" => $product_info['quantity'], + "min_purchase" => 1, + "from_time" => 0, + "to_time" => 0, + "ordering" => $product_info[''], + "description" => '', + )); + + $this->addToCollection($deal_id, $collection_id); + + return $deal_id; + } + + return 0; + } + + public function removeFromCollection($deal_id, $collection_id){ + $this->db->runQuery( + "DELETE FROM `".$this->tb_collection_item."` WHERE `deal_id` = ? AND `collection_id` = ? ", + ['d', 'd'], + [$deal_id, $collection_id] + ); + + $this->updateCollectionCount($collection_id); + } + + public function addToCollection($deal_id, $collection_id){ + $query = $this->db->runQuery( + "SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `deal_id`= ? AND `collection_id` = ? LIMIT 1 ", + ['d', 'd'], [$deal_id, $collection_id] + ); + + if( ! $this->db->fetchAssoc($query) ){ + $this->db->insert( + $this->tb_collection_item , + [ + 'collection_id' => $collection_id , + 'deal_id' => $deal_id, + 'create_by' => ADMIN_ID, + 'create_time' => CURRENT_TIME, + ] + ); + + $this->updateCollectionCount($collection_id); + } + } + + protected function updateCollectionCount($collection_id){ + $this->db->runQuery( + "UPDATE `".$this->tb_entity."` SET + `deal_count` = (SELECT COUNT(*) FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) + WHERE `id` = ? LIMIT 1 ", + ['d', 'd'], + [$collection_id, $collection_id] + ); + } + + public function updateCollectionView($id){ + $this->db->runQuery("UPDATE `".$this->tb_entity."` SET `visit` = `visit` + 1 WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $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; + + $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/Components/Deal/Model/DealModel.php b/inc/Hura8/Components/Deal/Model/DealModel.php new file mode 100644 index 0000000..9bdb007 --- /dev/null +++ b/inc/Hura8/Components/Deal/Model/DealModel.php @@ -0,0 +1,196 @@ +db->runQuery( + "SELECT `id`, `from_time`, `to_time` FROM `". $this->tb_entity ."` + WHERE `to_time` < '".CURRENT_TIME."' AND `to_time` > 0 AND `auto_renew` = 1 " + ); + + return $this->db->fetchAll($query); + } + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $where_clause = []; + $bind_types = []; + $bind_values = []; + + $limit_by_time = false; + $limit_by_time_condition = ''; + + // get deal by start time + // require format: YY-mm-dd h:m:i or YY-mm-dd h:m or timestamp + $datetime_pattern = '/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?$/i'; + + if(isset($filter_condition['start_time']) && $filter_condition['start_time'] ) { + $limit_by_time = true; + if(preg_match($datetime_pattern, trim($filter_condition['start_time']))) { + $check_time = strtotime($filter_condition['start_time']); + }else{ + $check_time = intval($filter_condition['start_time']); + } + + $limit_by_time_condition .= " AND `from_time` >= '".$check_time."' "; + + $bind_types[] = 'd'; + $bind_values[] = $check_time; + } + + // get deal by end time + // require format: YY-mm-dd h:m:i or YY-mm-dd h:m or timestamp + if(isset($filter_condition['end_time']) && $filter_condition['end_time'] ) { + $limit_by_time = true; + if(preg_match($datetime_pattern, trim($filter_condition['end_time']))) { + $check_time = strtotime($filter_condition['end_time']); + }else{ + $check_time = intval($filter_condition['end_time']); + } + + $limit_by_time_condition .= " AND `to_time` <= '".$check_time."' "; + $bind_types[] = 'd'; + $bind_values[] = $check_time; + } + + if($limit_by_time) { + $where_clause[] = " AND `status`= 1 ".$limit_by_time_condition; + } + + //type expire + if(isset($filter_condition['type']) && $filter_condition['type'] == 'expire' ) { + $where_clause[] = "AND `status`= 1 AND `to_time` < ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + //type active: might not have begun yet + if(isset($filter_condition['type']) && $filter_condition['type'] == 'active' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `to_time` >= ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + // Đang bắt đầu + if(isset($filter_condition['type']) && $filter_condition['type'] == 'started' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `to_time` >= ? AND from_time < ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + // Chưa bắt đầu + if(isset($filter_condition['type']) && $filter_condition['type'] == 'coming' && !$limit_by_time ) { + $where_clause[] = " AND `status` = 1 AND `from_time` >= ? "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + } + + //deal collection + if(isset($filter_condition['collection_id']) && $filter_condition['collection_id'] > 0) { + $where_clause[] = " AND `id` IN ( SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['collection_id']; + } + + // exclude from collection + if(isset($filter_condition['add_to_collection']) && $filter_condition['add_to_collection'] > 0) { + $where_clause[] = " AND `id` NOT IN ( SELECT `deal_id` FROM `".$this->tb_collection_item."` WHERE `collection_id` = ? ) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition['add_to_collection']; + } + + // by word filter + $filter = $filter_condition['filter'] ?? ''; + switch ($filter) { + case "not-started": // Chưa bắt đầu + $where_clause[] = " AND ( ? - `from_time` ) < 0 "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + break; + + case "started": // Đang bắt đầu + $where_clause[] = " AND ( ? - `from_time` ) > 0 AND ( `to_time` - ?) > 0 "; + $bind_types[] = 'd'; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + $bind_values[] = CURRENT_TIME; + break; + + case "ended": // Hết thời gian + $where_clause[] = " AND (`to_time` - ?) < 0 "; + $bind_types[] = 'd'; + $bind_values[] = CURRENT_TIME; + break; + + case "hidden": // Ẩn hiển thị + $where_clause[] = " AND `status` = 0 "; + break; + + case "featured": // Đang nổi bật + $where_clause[] = " AND `is_featured` = 1 "; + break; + } + + + return [ + join(" ", $where_clause), + $bind_types, + $bind_values + ]; + } + + + protected function formatItemInList(array $item_info): array + { + $copy = $item_info; + + $copy['deal_time_happen'] = CURRENT_TIME - $item_info['from_time']; + $copy['deal_time_left'] = $item_info['to_time'] - CURRENT_TIME; + $copy['is_start'] = (CURRENT_TIME - $item_info['from_time'] > 0) ? 1 : 0; + $copy['is_end'] = ($item_info['to_time'] - CURRENT_TIME > 0) ? 0 : 1; + + return $copy; + } + + protected function formatItemInfo(array $item_info): array + { + $copy = $item_info; + + $copy['deal_time_happen'] = CURRENT_TIME - $item_info['from_time']; + $copy['deal_time_left'] = $item_info['to_time'] - CURRENT_TIME; + $copy['is_start'] = (CURRENT_TIME - $item_info['from_time'] > 0) ? 1 : 0; + $copy['is_end'] = ($item_info['to_time'] - CURRENT_TIME > 0) ? 0 : 1; + + return $copy; + } + + + +} diff --git a/inc/Hura8/Components/Marketing/AdminController/ACouponController.php b/inc/Hura8/Components/Marketing/AdminController/ACouponController.php new file mode 100644 index 0000000..9b569d0 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/ACouponController.php @@ -0,0 +1,40 @@ +objCouponModel->updateProduct($product_id, $coupon_id, $info); + } + + + public function removeProduct($product_id, $coupon_id) + { + return $this->objCouponModel->removeProduct($product_id, $coupon_id); + } + + + public function addProduct($product_id, $coupon_id, $ordering=0) + { + return $this->objCouponModel->addProduct($product_id, $coupon_id, $ordering); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + + +} diff --git a/inc/Hura8/Components/Marketing/AdminController/APosterController.php b/inc/Hura8/Components/Marketing/AdminController/APosterController.php new file mode 100644 index 0000000..bad0fd4 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/APosterController.php @@ -0,0 +1,26 @@ +objPosterModel = new PosterModel(); + parent::__construct($this->objPosterModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } +} diff --git a/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php b/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php new file mode 100644 index 0000000..e390b22 --- /dev/null +++ b/inc/Hura8/Components/Marketing/AdminController/AProductFeedController.php @@ -0,0 +1,51 @@ +objProductFeedModel->getAllCategories(); + } + + public function getAllProductListIds( $list_id){ + return $this->objProductFeedModel->getAllProductListIds($list_id); + } + + public function getProductListTotal($list_id) { + return $this->objProductFeedModel->getProductListTotal($list_id); + } + + public function getProductList($list_id, $page = 1, $numPerPage = 30) { + return $this->objProductFeedModel->getProductList($list_id, $page, $numPerPage); + } + + public function deleteAllProductFromList($list_id) { + $this->objProductFeedModel->deleteAllProductFromList($list_id); + } + + //remove product from a list + public function deleteProductFromList($pro_list, $list_id){ + return $this->objProductFeedModel->deleteProductFromList($pro_list, $list_id); + } + + //add product to a list + public function addProductToList($pro_list, $list_id){ + return $this->objProductFeedModel->addProductToList($pro_list, $list_id); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } +} diff --git a/inc/Hura8/Components/Marketing/Controller/bCouponController.php b/inc/Hura8/Components/Marketing/Controller/bCouponController.php new file mode 100644 index 0000000..96c2fda --- /dev/null +++ b/inc/Hura8/Components/Marketing/Controller/bCouponController.php @@ -0,0 +1,46 @@ + "Tặng sản phẩm", + 'cash' => "Tặng tiền mặt", + 'priceoff' => "Giảm giá %", + 'other' => "Khác" + ); + + + /* @var CouponModel $objCouponModel */ + protected $objCouponModel; + + public function __construct() + { + $this->objCouponModel = new CouponModel(); + parent::__construct($this->objCouponModel); + } + + + public function getTotalProduct($coupon_id, array $condition = []) + { + return $this->objCouponModel->getTotalProduct($coupon_id, $condition); + } + + + public function getListProduct($coupon_id, array $condition = []) { + return $this->objCouponModel->getListProduct($coupon_id, $condition); + } + + + public function getTypeList() { + return $this->type_list; + } + + +} diff --git a/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php b/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php new file mode 100644 index 0000000..7dfdb6d --- /dev/null +++ b/inc/Hura8/Components/Marketing/Controller/bProductFeedController.php @@ -0,0 +1,26 @@ +objProductFeedModel = new ProductFeedModel(); + parent::__construct($this->objProductFeedModel); + } + + + public function getInfoByPublicId($public_id) + { + return $this->objProductFeedModel->getInfoByPublicId($public_id); + } + + +} diff --git a/inc/Hura8/Components/Marketing/Model/CouponModel.php b/inc/Hura8/Components/Marketing/Model/CouponModel.php new file mode 100644 index 0000000..8d1896a --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/CouponModel.php @@ -0,0 +1,130 @@ +db->runQuery( + " SELECT COUNT(*) as total FROM `".$this->tb_coupon_product."` WHERE `coupon_id` = ? ", + ['d'], [$coupon_id] + ); + + $total = 0; + if ($rs = $this->db->fetchAssoc($query)) { + $total = $rs['total']; + } + + return $total; + } + + + public function getListProduct($coupon_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` FROM ".$this->tb_coupon_product." WHERE `coupon_id` = ? + ORDER BY ".$order_by." + LIMIT ".(($page-1) * $numPerPage).", ".$numPerPage , + ['d'], [$coupon_id] + ) ; + + $item_list = array(); + $counter = ($page-1) * $numPerPage; + foreach ( $this->db->fetchAll($query) as $item ) { + $counter += 1; + + $item_list[$item['product_id']] = [ + 'counter' => $counter, + ]; + } + + $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; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "q" => getRequest("q", ''), + "featured" => (int) getRequest("featured"), + "status" => (int) getRequest("status"), + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function createUniqueCode($current_item_id, $wanted_code = ''){ + if(!$wanted_code) $wanted_code = IDGenerator::createStringId(10); + + $clean_code = DataClean::makeInputSafe($wanted_code, DataType::ID); + $clean_code = strtoupper($clean_code); + + //if exist and belong other id, create a new one + $query = $this->db->runQuery("SELECT `id` FROM `".$this->tb_entity."` WHERE `code` = ? LIMIT 1 ", ['s'], [$clean_code]) ; + if($info = $this->db->fetchAssoc($query)){ + if($info['id'] != $current_item_id) { + $new_code = IDGenerator::createStringId(6); + return $this->createUniqueCode($current_item_id, $new_code); + } + } + + return $clean_code; + } + + +} diff --git a/inc/Hura8/Components/Marketing/Model/PosterModel.php b/inc/Hura8/Components/Marketing/Model/PosterModel.php new file mode 100644 index 0000000..151133b --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/PosterModel.php @@ -0,0 +1,33 @@ +all_brands = $this->buildAllBrands(); + //$this->all_categories = $this->buildAllCategories(); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getInfoByPublicId($public_id) + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `public_id` = ? LIMIT 1 ", ['s'], [$public_id]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return false; + } + + public function getProductListTotal($list_id) { + $query = $this->db->runQuery( + " SELECT COUNT(*) AS total FROM `".$this->tb_feed_product."` WHERE `list_id` = ? ", + ['d'], [$list_id] + ); + + $total = 0; + if ($info = $this->db->fetchAssoc($query)) { + $total = $info['total']; + } + + return $total; + } + + public function getProductList($list_id, $page = 1, $numPerPage = 30) { + + $query = $this->db->runQuery( + " SELECT `pro_id` FROM `".$this->tb_feed_product."` + WHERE `list_id` = ? + ORDER BY `id` DESC + LIMIT ".( ($page - 1) * $numPerPage ).", ".$numPerPage, + ['d'], [$list_id] + ); + + $product_list_ids = array_map(function ($item) { return $item['pro_id'];}, $this->db->fetchAll($query)); + + $objAProductController = new AProductController(); + + $stt = ($page - 1) * $numPerPage; + $item_list = []; + foreach ($objAProductController->getListByIds($product_list_ids) as $_id => $info) { + $stt++; + $info["counter"] = $stt; + $item_list[] = $info; + } + + return $item_list; + } + + //-------------------- + + public function getAllCategories() { + return $this->all_categories; + } + + + public function getAllProductListIds( $list_id){ + if(!$list_id) return []; + + $query = $this->db->runQuery("SELECT `pro_id` FROM `".$this->tb_feed_product."` WHERE `list_id` = ? ", ['d'], [ $list_id ]); + + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + $item_list[] = $rs['pro_id']; + } + + return $item_list; + } + + + //-------------------- + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php b/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php new file mode 100644 index 0000000..0883921 --- /dev/null +++ b/inc/Hura8/Components/Marketing/Model/UProductFeedModel.php @@ -0,0 +1,8 @@ +getInfo($item_id); + if(!$item_info['file_url']) { + return true; + } + + $file_name = $item_info['file_url']; + + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + @unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/". $file_name; + @unlink($file_local_path); + + return true; + } + +} diff --git a/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php b/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php new file mode 100644 index 0000000..6fd0203 --- /dev/null +++ b/inc/Hura8/Components/Media/AdminController/AMediaCategoryController.php @@ -0,0 +1,14 @@ +getInfo($item_id); + if(!$item_info['file_url']) { + return true; + } + + list($sub_folder, $file_name) = explode("/", $item_info['file_url']); // format: Y-m-d/file_name + + foreach (static::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/" . $sub_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + @unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". static::$image_folder . "/" . $sub_folder . "/". $file_name; + @unlink($file_local_path); + + return true; + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bItemMediaController.php b/inc/Hura8/Components/Media/Controller/bItemMediaController.php new file mode 100644 index 0000000..d370ea3 --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bItemMediaController.php @@ -0,0 +1,61 @@ + ['width' => 300,] + ); + + /* @var ItemMediaModel $objItemMediaModel */ + protected $objItemMediaModel; + + protected $item_type; + protected $item_id; + + public function __construct(string $item_type = '', $item_id = 0) + { + $this->objItemMediaModel = new ItemMediaModel($item_type, $item_id); + parent::__construct($this->objItemMediaModel); + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) : ?array + { + if(!$item_info) return null; + + $item_info['display_file'] = STATIC_DOMAIN . "/". static::$image_folder. "/". $item_info['file_url']; + $item_info['image'] = self::getResizedImageCollection($item_info['file_url']); + + return $item_info; + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 's' => 'small' , + ]; + + 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 : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php b/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php new file mode 100644 index 0000000..6ac394e --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bMediaCategoryController.php @@ -0,0 +1,20 @@ +objMediaCategoryModel = new MediaCategoryModel(); + parent::__construct($this->objMediaCategoryModel); + } + +} diff --git a/inc/Hura8/Components/Media/Controller/bMediaController.php b/inc/Hura8/Components/Media/Controller/bMediaController.php new file mode 100644 index 0000000..637ab9e --- /dev/null +++ b/inc/Hura8/Components/Media/Controller/bMediaController.php @@ -0,0 +1,59 @@ + ['width' => 300,] + ); + + protected $objMediaModel; + + + public function __construct() + { + $this->objMediaModel = new MediaModel(); + parent::__construct($this->objMediaModel); + } + + + protected function formatItemInList(array $item_info) : array + { + return $this->formatItemInfo($item_info); + } + + + protected function formatItemInfo(array $item_info) : ?array + { + if(!$item_info) return null; + + $item_info['display_file'] = STATIC_DOMAIN . "/". static::$image_folder. "/". $item_info['file_url']; + $item_info['image'] = ($item_info['file_type'] == 'image') ? self::getResizedImageCollection($item_info['file_url']) : null; + + return $item_info; + } + + + public static function getResizedImageCollection($image_name) { + $image = []; + + $size_in_full = [ + 's' => 'small' , + ]; + + 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 : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Media/Model/ItemMediaModel.php b/inc/Hura8/Components/Media/Model/ItemMediaModel.php new file mode 100644 index 0000000..57d2a59 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/ItemMediaModel.php @@ -0,0 +1,76 @@ +item_type = $item_type; + $this->item_id = $item_id; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $filter_condition) : ?array + { + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if($this->item_type) { + $catCondition[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_type; + } + + if($this->item_id) { + $catCondition[] = " AND `item_id` = ? "; + $bind_types[] = 'd'; + $bind_values[] = $this->item_id; + } + + if(isset($filter_condition["item_type"]) && $filter_condition["item_type"]){ + $catCondition[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["item_type"]; + } + + if(isset($filter_condition["item_id"]) && $filter_condition["item_id"]){ + $catCondition[] = " AND `item_id` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["item_id"]; + } + + if(isset($filter_condition["file_type"]) && $filter_condition["file_type"]){ + $catCondition[] = " AND `file_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["file_type"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + + +} diff --git a/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php b/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php new file mode 100644 index 0000000..f92f5e2 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/ItemMediaSearchModel.php @@ -0,0 +1,34 @@ + "tb_item_media.item_type", + 'item_id' => "tb_item_media.item_id", + 'file_type' => "tb_item_media.file_type", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_item_media.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_item_media", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Media/Model/MediaCategoryModel.php b/inc/Hura8/Components/Media/Model/MediaCategoryModel.php new file mode 100644 index 0000000..86cd5cd --- /dev/null +++ b/inc/Hura8/Components/Media/Model/MediaCategoryModel.php @@ -0,0 +1,20 @@ + getRequestInt("category"), + "file_type" => '', + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + //Tim danh muc + if(isset($filter_condition["category"]) && $filter_condition["category"]){ + $catCondition[] = " AND `id` IN (SELECT `item_id` FROM `".$this->tb_media_per_category."` WHERE `category_id` = ?) "; + $bind_types[] = 'd'; + $bind_values[] = $filter_condition["category"]; + } + + if(isset($filter_condition["file_type"]) && $filter_condition["file_type"]){ + $catCondition[] = " AND `file_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["file_type"]; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + +} diff --git a/inc/Hura8/Components/Media/Model/MediaSearchModel.php b/inc/Hura8/Components/Media/Model/MediaSearchModel.php new file mode 100644 index 0000000..45c3890 --- /dev/null +++ b/inc/Hura8/Components/Media/Model/MediaSearchModel.php @@ -0,0 +1,32 @@ + "tb_media.file_type", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_media.title",], + ]; + + + public function __construct() + { + parent::__construct( + "tb_media", + $this->fulltext_fields, + $this->filter_fields + ); + + //$this->createTableSearch(); + } + +} diff --git a/inc/Hura8/Components/Order/AdminController/AOrderController.php b/inc/Hura8/Components/Order/AdminController/AOrderController.php new file mode 100644 index 0000000..1e3b86c --- /dev/null +++ b/inc/Hura8/Components/Order/AdminController/AOrderController.php @@ -0,0 +1,235 @@ +objAOrderStatusController = new AOrderStatusController(); + $this->objOrderModel = new OrderModel(); + parent::__construct($this->objOrderModel); + } + + public function updateOrderFulfillmentStatus($order_id, $new_status, $comment, $payment_data = []) { + + if(!array_key_exists($new_status, OrderStatus::FULFILLMENT_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'fulfillment', $new_status, $comment, $payment_data ); + + //cap nhat don hang + $this->objOrderModel->update( $order_id, [ + 'fulfillment_status' => $new_status + ]); + + $this->updateDerivedOrderStatus($order_id); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + public function updateOrderPaymentStatus($order_id, $new_status, $comment, $payment_data = []) { + + if(!array_key_exists($new_status, OrderStatus::PAYMENT_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'payment', $new_status, $comment, $payment_data ); + + //cap nhat don hang + $this->objOrderModel->update( $order_id, [ + 'payment_status' => $new_status + ]); + + $this->updateDerivedOrderStatus($order_id); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + protected function updateDerivedOrderStatus($order_id) { + + $current_info = $this->getInfo($order_id); + + // order must be success + if( + $current_info['payment_status'] == OrderStatus::PAYMENT_STATUS['paid']['id'] && + $current_info['fulfillment_status'] == OrderStatus::FULFILLMENT_STATUS['fulfilled']['id'] + ) { + + return $this->objOrderModel->update($order_id, [ + 'order_status' => OrderStatus::ORDER_STATUS['success']['id'] + ]); + + } + + + // update processing + return $this->objOrderModel->update($order_id, [ + 'order_status' => OrderStatus::ORDER_STATUS['processing']['id'] + ]); + + } + + public function updateOrderStatus($order_id, $status, $comment) { + + $current_info = $this->getInfo($order_id); + + // cannot update if order has: success or cancel + if(in_array($current_info['order_status'], [ + OrderStatus::ORDER_STATUS['success']['id'], + OrderStatus::ORDER_STATUS['canceled']['id'], + ])) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + if(!array_key_exists($status, OrderStatus::ORDER_STATUS)) { + return [ + "status" => 'error', + 'message' => '', + ]; + } + + $this->objAOrderStatusController->createHistory($order_id, 'order', $status, $comment ); + + $this->objOrderModel->update($order_id, [ + 'order_status' => $status + ]); + + // dispatch + $objOrderEvent = new OrderEvent(); + $objOrderEvent->setUpdateOrderInfo([ + 'orderId' => $order_id, + 'order_status' => $status, + 'old_status' => $current_info['order_status'] + ]); + get_dispatcher()->dispatch(EventName::ORDER['updated_status'], $objOrderEvent); + + return [ + "status" => 'success', + 'message' => 'Cập nhật thành công', + ]; + } + + + protected function formatItemInList(array $order_info ) { + + $rs = $order_info; + + $rs["order_date"] = date("d-m-Y", $rs['order_time']); + $rs["order_hour"] = date("h:i a", $rs['order_time']); + + if($rs['order_discount']) $rs['order_discount'] = \json_decode($rs['order_discount'], true); + + $rs["payment_status_name"] = (array_key_exists($rs["payment_status"], OrderStatus::PAYMENT_STATUS)) ? OrderStatus::PAYMENT_STATUS[$rs["payment_status"]]['title'] : ''; + + $rs["fulfillment_status_name"] = (array_key_exists($rs["fulfillment_status"], OrderStatus::FULFILLMENT_STATUS)) ? OrderStatus::FULFILLMENT_STATUS[$rs["fulfillment_status"]]['title'] : ''; + + $rs["order_status_name"] = (array_key_exists($rs["order_status"], OrderStatus::ORDER_STATUS)) ? OrderStatus::ORDER_STATUS[$rs["order_status"]]['title'] : ''; + + return $rs; + } + + protected function getListFilterCondition($list_id) { + + if($list_id == 'mine') { + return [ + 'assign_to' => ADMIN_ID, + ]; + } + + if($list_id == 'new') { + return [ + 'status' => 'new', + ]; + } + + if($list_id == 'unpaid') { + return [ + 'payment' => 'unpaid', + ]; + } + + if($list_id == 'partially-paid') { + return [ + 'payment' => 'partially-paid', + ]; + } + + if($list_id == 'unfulfilled') { + return [ + 'fullfillment' => 'unfulfilled', + ]; + } + + if($list_id == 'partially-fulfilled') { + return [ + 'fullfillment' => 'partially-fulfilled', + ]; + } + + return null; + } + + public function getStatusHistory($orderId){ + + return $this->objAOrderStatusController->getStatusHistory($orderId); + + } + + public function getItemsForOrderList(array $list_ids, $fields = "*") { + + return $this->objOrderModel->getItemsForOrderList($list_ids, $fields); + + } + + public static function createId($order_id){ + $key_length = strlen($order_id); + + $order_keys = []; + for($i=0; $i < 9 - $key_length; $i++){ + $order_keys[] = "0"; + } + $order_keys[] = $order_id; + + $order_eles = str_split(join("", $order_keys), 3); + + return join("-", $order_eles); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + return true; + } + +} diff --git a/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php b/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php new file mode 100644 index 0000000..a4773f4 --- /dev/null +++ b/inc/Hura8/Components/Order/AdminController/AOrderStatusController.php @@ -0,0 +1,43 @@ +objOrderStatusModel = new OrderStatusModel(); + } + + public function getStatusHistory($orderId){ + return $this->objOrderStatusModel->getStatusHistory($orderId); + } + + public function createHistory($order_id, $status_type = 'order', $system_status = '', $comment = '', array $data= [], array $other_info = []) { + return $this->objOrderStatusModel->createHistory($order_id, $status_type , $system_status , $comment, $data, $other_info); + } + + public function getInfo($status_id) { + return $this->objOrderStatusModel->getInfo($status_id); + } + + public function getAll() { + return $this->objOrderStatusModel->getAll(); + } + + public function delete($id) { + return $this->objOrderStatusModel->delete($id); + } + + public function update($id, array $info) { + return $this->objOrderStatusModel->update($id, $info); + } + + public function create(array $info) { + return $this->objOrderStatusModel->create($info); + } +} diff --git a/inc/Hura8/Components/Order/Controller/OrderStatus.php b/inc/Hura8/Components/Order/Controller/OrderStatus.php new file mode 100644 index 0000000..69aefe3 --- /dev/null +++ b/inc/Hura8/Components/Order/Controller/OrderStatus.php @@ -0,0 +1,43 @@ + ['id' => 'new', 'title' => 'Mới'],// 'Mới', // + 'processing' => ['id' => 'processing', 'title' => 'Đang xử lý'],// 'Đang xử lý', // + 'success' => ['id' => 'success', 'title' => 'Thành công'],// 'Thành công', // + 'closed' => ['id' => 'closed', 'title' => 'Đóng lại'],// 'Đóng lại', // + //'archived' => 'Lưu kho', // + 'canceled' => ['id' => 'canceled', 'title' => 'Hủy'],// 'Hủy', // + ]; + + const PAYMENT_STATUS = [ + 'pending' => ['id' => 'pending', 'title' => 'Chờ'],// 'Chờ', // Orders have the Payment pending status for one of the following reasons: During checkout, You create an order, Manual payment methods, such as Cash On Delivery + 'authorized' => ['id' => 'authorized', 'title' => 'Đã xác nhận'],// 'Đã xác nhận', // The payment provider validated your customer’s payment information. The order is created, customers can complete their checkout, and inventory is reserved. + 'overdue' => ['id' => 'overdue', 'title' => 'Quá hạn'],// 'Quá hạn', // Payment wasn't captured before the due date that was set in the payment terms on an order that had the Payment pending status. + 'expiring' => ['id' => 'expiring', 'title' => 'Sắp hết hạn'],// 'Sắp hết hạn', // Expiring isn't a payment status, but the Expiring badge is displayed two days before the deadline for capturing payment on orders that have the Authorized payment status. + 'expired' => ['id' => 'expired', 'title' => 'Hết hạn'],// 'Hết hạn', // Payment wasn't captured before the date that was set by the payment provider on an order that had the Authorized payment status. + 'paid' => ['id' => 'paid', 'title' => 'Đã thanh toán'],// 'Đã thanh toán', // Payment was automatically or manually captured, or the order was marked as paid. + 'refunded' => ['id' => 'refunded', 'title' => 'Đã hoàn lại'],// 'Đã hoàn lại', // The full amount that the customer paid for an order was returned to the customer. + 'partially-refunded' => ['id' => 'partially-refunded', 'title' => 'Hoàn lại 1 phần'],// 'Hoàn lại 1 phần', // The amount that was returned to a customer is less than the full amount that the customer paid for an order. + 'partially-paid' => ['id' => 'partially-paid', 'title' => 'Thanh toán 1 phần'],// 'Thanh toán 1 phần', // You manually captured payment for the order and specified less than the full order value as the payment amount. + 'voided' => ['id' => 'voided', 'title' => 'Hủy'],// 'Hủy', // An unpaid order was manually canceled. + 'unpaid' => ['id' => 'unpaid', 'title' => 'Chưa thanh toán'],// 'Chưa thanh toán', // Unpaid payment status includes orders that are in Authorized, Pending, Expired, and Partially paid payment status. + ]; + + const FULFILLMENT_STATUS = [ + 'fulfilled' => ['id' => 'fulfilled', 'title' => 'Đã chuyển hết'],// 'Đã chuyển hết', // When you've shipped all the items in an order, it is Fulfilled. + 'unfulfilled' => ['id' => 'unfulfilled', 'title' => 'Chưa chuyển'],// 'Chưa chuyển', // When an order is placed, it has an Unfulfilled fulfillment status, unless you have selected to automatically capture the payment and automatically fulfill all of the order's line items in the checkout settings. + 'partially-fulfilled' => ['id' => 'partially-fulfilled', 'title' => 'Chuyển 1 phần'],// 'Chuyển 1 phần', // If you have shipped some, but not all, of the items in an order, then the order has a Partially fulfilled fulfillment status. + 'scheduled' => ['id' => 'scheduled', 'title' => 'Đã lên kế hoạch'],// 'Đã lên kế hoạch', // Prepaid subscription orders have a Scheduled status until the fulfillment date is reached + 'onhold ' => ['id' => 'onhold', 'title' => 'Giữ hàng'],// 'Giữ hàng', // When upsell offers are presented to customers at checkout, the order fulfillment status is set to On hold temporarily. When a fulfillment is On hold you can reserve inventory for the order, but you can't fulfill the order until the fulfillment hold is released, and the order fulfillment status changes to Unfulfilled. + ]; + +} + diff --git a/inc/Hura8/Components/Order/Model/OrderModel.php b/inc/Hura8/Components/Order/Model/OrderModel.php new file mode 100644 index 0000000..0803480 --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderModel.php @@ -0,0 +1,557 @@ +objSearchModel = new OrderSearchModel(); + + parent::__construct(EntityType::ORDER, '', $this->objSearchModel); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getItems($orderId, $fields = "*") { + $query = $this->db->runQuery( + " SELECT ".$fields." FROM `".$this->tb_order_detail."` WHERE `order_id` = ? ", + ['d'], [ $orderId ]); + + $order_item = array(); + foreach ( $this->db->fetchAll($query) as $info ) { + + $item_info = ($info["item_info"]) ? \json_decode($info["item_info"], true) : false; + $in_cart = ($info["in_cart"]) ? \json_decode($info["in_cart"], true) : false; + + + $info['item_info'] = $item_info; + $info['in_cart'] = $in_cart; + + $order_item[] = $info; + } + + return $order_item; + } + + //since we save the entire cart-structure for item_info, we need to re-construct it to make it easier to read and for various reporting/ email etc.. + protected function buildOrderItemInfo($item_type, array $item_info) { + $new_info = []; + + if($item_type == 'product') { + + $new_info = System::stripProperty(array($item_info['info']), [ + "variant_option", + 'id', + 'productId', + 'priceUnit', + 'price', + 'currency', + 'lastUpdate', + 'warranty', + 'productName', + 'productUrl', + 'productModel', + 'productSummary', + 'marketPrice', + 'productImage', + 'brand', + 'visit', + 'rating', + 'reviewCount', + 'quantity', + 'productSKU', + 'hasVAT', + 'condition', + 'specialOffer', + 'specialOfferGroup', + 'productType', + 'thum_poster', + 'promotion_price', + 'addon' + ])[0]; + + //find variants bought + if($new_info['config_count'] > 0) { + + foreach ($item_info['in_cart'] as $_variant) { + $new_info['variants'][] = array( + "id" => $_variant['id'], + "sku" => $_variant['sku'], + "title" => $_variant['name'], + "image" => ($_variant['image']) ? $_variant['image'] : $item_info['info']['productImage']["medium"], + "url" => $item_info['info']['productUrl'], + "attribute" => $_variant['attribute'], + "price" => $_variant['price'], + "addon_total" => 0, + "currency" => $item_info['info']['currency'], + "priceUnit" => $item_info['info']['priceUnit'], + "quantity" => $_variant['quantity'], + "buyer_note" => $_variant['buyer_note'], + "addon" => [], + "promotion" => [ + "list" => $item_info['info']['specialOffer'], + "group" => (isset($_variant['promotion'])) ? $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], $_variant['promotion']) : $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], array() ), + ], + //..any other + ); + } + + }else{ + + //product does not have variant + $build_addon = []; + $total_addon_price = 0; + if(isset($item_info['info']['addon']) && isset($item_info['in_cart'][0]['addon'])) { + $build_addon = $this->getSelectedAddon($item_info['info']['addon'], $item_info['in_cart'][0]['addon']); + foreach ($build_addon as $addon) { + $total_addon_price += $addon['price'] * $addon['quantity']; + } + } + + $variant_in_cart = (isset($item_info['in_cart'][0])) ? $item_info['in_cart'][0] : false; + + $new_info['variants'][] = array( + "id" => 0, + "sku" => $item_info['info']['productSKU'], + "title" => $item_info['info']['productName'], + "image" => $item_info['info']['productImage']["medium"], + "url" => $item_info['info']['productUrl'], + "attribute" => [], + "price" => $item_info['info']['price'], + "addon_total" => $total_addon_price, + "currency" => $item_info['info']['currency'], + "priceUnit" => $item_info['info']['priceUnit'], + "quantity" => $variant_in_cart['quantity'], + "buyer_note" => $variant_in_cart['buyer_note'], + "addon" => $build_addon, + "promotion" => [ + "list" => $item_info['info']['specialOffer'], + "group" => (isset($variant_in_cart['promotion'])) ? $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], $variant_in_cart['promotion']) : $this->buildPromotionFromGroup( $item_info['info']['specialOfferGroup'], array() ), + ], + //..any other + ); + } + + // + return $new_info; + } + + if($item_type == 'combo') { + + $new_info = [ + "price" => $item_info['info']['sale_price'], + "quantity" => $item_info['in_cart']['quantity'], + "buyer_note" => $item_info['in_cart']['buyer_note'], + "url" => '', + ]; + + $new_info['product_list'] = System::stripProperty($item_info['info']['product_list'], [ + "variant_option", + 'id', + 'productId', + 'priceUnit', + 'price', + 'currency', + 'lastUpdate', + 'warranty', + //'productName', + //'productUrl', + //'productModel', + 'productSummary', + 'marketPrice', + 'productImage', + 'brand', + 'visit', + 'rating', + 'reviewCount', + 'config_count', + 'quantity', + //'productSKU', + 'hasVAT', + 'condition', + 'specialOffer', + 'specialOfferGroup', + 'productType', + 'thum_poster', + 'promotion_price', + 'addon', + 'imageCollection', + 'variants' + ]); + + return $new_info; + } + + if($item_type == 'deal') { + + $new_info = [ + "price" => $item_info['info']['price'], + "quantity" => $item_info['in_cart']['quantity'], + "buyer_note" => $item_info['in_cart']['buyer_note'], + "url" => '/deal/'.$item_info['info']['id'], + ]; + + return $new_info; + } + + return $new_info; + } + + protected function getSelectedAddon($all_product_addons, $in_cart) { + + if(!is_array($all_product_addons) || !is_array($in_cart)) return []; + + $result = []; + foreach ($all_product_addons as $item) { + foreach ($in_cart as $selected) { + if($item['addon_id'] == $selected['id']) { + $result[] = [ + "id" => $selected['id'], + "title" => $item['title'], + "price" => $item['price'], + "quantity" => (isset($selected['quantity'])) ? intval($selected['quantity']) : 1, + "related_article_url" => $item['related_article_url'], + ]; + } + } + } + + return $result; + } + + protected function buildPromotionFromGroup( $offer_group, $selected_one_promotion) { + + if(!is_array($offer_group) && !is_array($selected_one_promotion)) return false; + + $promotion = []; + + foreach ($offer_group as $group) { + if($group['type'] == 'one') { + + $selected_promotion = []; + foreach ($group['promotion'] as $promo) { + foreach ($selected_one_promotion as $selected) { + if($selected['promotion_id'] == $promo['id'] && $selected['group_id'] == $group['id']) { + $selected_promotion[] = $promo; + } + } + } + + $promotion[] = [ + "id" => $group['id'], + "title" => $group['title'], + "note" => $group['note'], + "promotion" => $selected_promotion, + ]; + + + }else{ + $promotion[] = [ + "id" => $group['id'], + "title" => $group['title'], + "note" => $group['note'], + "promotion" => $group['promotion'], + ]; + } + } + + return $promotion; + + } + + + protected function getListFilterCondition($list_id) { + + if($list_id == 'mine') { + return [ + 'assign_to' => ADMIN_ID, + ]; + } + + if($list_id == 'new') { + return [ + 'status' => 'new', + ]; + } + + if($list_id == 'unpaid') { + return [ + 'payment' => 'unpaid', + ]; + } + + if($list_id == 'partially-paid') { + return [ + 'payment' => 'partially-paid', + ]; + } + + if($list_id == 'unfulfilled') { + return [ + 'fullfillment' => 'unfulfilled', + ]; + } + + if($list_id == 'partially-fulfilled') { + return [ + 'fullfillment' => 'partially-fulfilled', + ]; + } + + return null; + } + + public function getStatusHistory($orderId){ + $query = $this->db->runQuery(" + select * from ".TB_ORDER_STATUS_HISTORY." + WHERE order_id = ? + order by id desc + limit 100 + ", ['d'], [ $orderId ] ) ; + + return $this->db->fetchAll($query); + } + + public function getItemsForOrderList(array $list_ids, $fields = "*") { + + if(!sizeof($list_ids)) return []; + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_ids, 'int'); + + $query = $this->db->runQuery(" + SELECT ".$fields." FROM `".$this->tb_order_detail."` + WHERE `order_id` IN (".$parameterized_ids.") + ", $bind_types, $list_ids); + + $result = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + + $item_info = unserialize($rs['item_info']); + if( isset($item_info["info"]) && isset($item_info["in_cart"])) { + $rs['item_info'] = $this->buildOrderItemInfo($rs['item_type'], $item_info ); + } else { + $rs['item_info'] = null; + } + + $result[$rs['order_id']][] = $rs; + } + + return $result; + } + + protected function _buildQueryConditionExtend(array $conditions) : ?array + { + $catCondition = []; + $bind_types = []; + $bind_values = []; + + /* + $conditions = array( + 'orderCode' => '', + 'query' => '', + 'coupon' => '', + 'cus_id' => '', + 'province' => '', + 'district' => '', + 'folder' => '', + 'view_status' => '', + 'update_by' => '', + 'shipping_status' => '', + 'assign_to' => '', + 'from_date' => '', + 'to_date' => '', + 'from_hour' => '', + 'to_hour' => '', + 'excluded_ids' => '', + 'included_ids' => '', + 'payment' => '', + 'fullfillment' => '', + 'list' => '', + );*/ + + + // merge with special list + if(isset($conditions['list']) && $conditions['list']) { + $list_condition = $this->getListFilterCondition($conditions['list']); + if($list_condition) { + // update and over-write any key in $conditions if exist + foreach ($list_condition as $key => $value) { + $conditions[$key] = $value; + } + } + } + + if(isset($conditions['orderCode']) && $conditions['orderCode']) { + $orderCode = DataClean::makeInputSafe($conditions['orderCode'],DataType::INTEGER); + $catCondition[] = " AND `orderId` LIKE '".$orderCode."%' "; + } + + if(isset($conditions['coupon']) && $conditions['coupon']) { + $coupon = preg_replace("/[^a-z0-9_\-\.]/i","", $conditions['coupon']); + if($coupon) $catCondition[] = " AND ( LENGTH(`order_discount`) > 10 AND `order_discount` LIKE '%".$coupon."%' ) "; + } + + if(isset($conditions['cus_id']) && $conditions['cus_id']) { + $catCondition[] = " AND buyerId = '".intval($conditions['cus_id'])."'"; + } + + if(isset($conditions['province']) && $conditions['province']) { + $catCondition[] = " AND province = '".intval($conditions['province'])."'"; + } + + if(isset($conditions['district']) && $conditions['district']) { + $catCondition[] = " AND district = '".intval($conditions['district'])."'"; + } + + if(isset($conditions['folder']) && $conditions['folder']) { + $catCondition[] = " AND `folder` = '". preg_replace("/[^a-z0-9_\-\.]/i", "", $conditions['folder'])."' "; + } + + if(isset($conditions['payment']) && array_key_exists($conditions['payment'], OrderStatus::PAYMENT_STATUS ) ) { + $catCondition[] = " AND `payment_status` = '". $this->db->escape($conditions['payment'])."' "; + } + + if(isset($conditions['fullfillment']) && array_key_exists($conditions['fullfillment'], OrderStatus::FULFILLMENT_STATUS) ) { + $catCondition[] = " AND `fulfillment_status` = '". $this->db->escape($conditions['fullfillment'])."' "; + } + + if(isset($conditions['status']) && array_key_exists($conditions['status'], OrderStatus::ORDER_STATUS) ) { + $catCondition[] = " AND `order_status` = '". $this->db->escape($conditions['status'])."' "; + } + + if(isset($conditions['view_status']) && $conditions['view_status']) { + if($conditions['view_status'] == 'no-status') { + $catCondition[] = " AND `status_id` = 0 "; + } else { + $catCondition[] = " AND `status_id` = '". intval($conditions['view_status'])."' "; + } + } + + if(isset($conditions['update_by']) && $conditions['update_by']) { + $catCondition[] = " AND `status_update_by` = '". $this->db->escape($conditions['update_by'])."' "; + } + + if(isset($conditions['shipping_status']) && $conditions['shipping_status']) { + $catCondition[] = " AND `admin_shipping_status` = '". preg_replace("/[^a-z0-9_\-\.]/i", "", $conditions['shipping_status'])."' "; + } + + if(isset($conditions['assign_to']) && $conditions['assign_to']) { + $catCondition[] = " AND `assign_to` = '". intval($conditions['assign_to'])."' "; + } + + //filter by date + if(isset($conditions['from_date']) && $conditions['from_date']) { + $catCondition[] = " AND order_time >= '". intval(strtotime(TimeManager::convert_date_from_javascript($conditions['from_date'])." 00:00"))."' "; + } + + if(isset($conditions['to_date']) && $conditions['to_date']) { + $catCondition[] = " AND order_time <= '".strtotime(TimeManager::convert_date_from_javascript($conditions['to_date'])." 23:59")."' "; + } + + //filter by hour + if(isset($conditions['from_hour']) && $conditions['from_hour']) { + $catCondition[] = " AND `order_hour` >= '".intval($conditions['from_hour'])."' "; + } + if(isset($conditions['to_hour']) && $conditions['to_hour']) { + $catCondition[] = " AND `order_hour` <= '".intval($conditions['to_hour'])."' "; + } + + if(isset($conditions["excluded_ids"]) && $conditions["excluded_ids"] ){ + $list_ids = filterNumber(explode(",", $conditions["excluded_ids"])); + if(sizeof($list_ids)) $catCondition[] = " AND `orderId` NOT IN (".join(',', $list_ids ).") "; + } + + if(isset($conditions["included_ids"]) && $conditions["included_ids"] ){ + $list_ids = filterNumber(explode(",", $conditions["included_ids"])); + if(sizeof($list_ids)) $catCondition[] = " AND `orderId` IN (".join(',', $list_ids ).") "; + } + + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + if(isset($info['file_external_url'])) { + if($info['file_external_url'] && !Url::isUrlValid($info['file_external_url'])) { + $info['file_external_url'] = ''; + } + } + + + $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 beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + $info = $new_input_info; + + unset($info['id']); + + if(isset($info['file_external_url']) && $info['file_external_url'] && !Url::isUrlValid($info['file_external_url'])) { + $info['file_external_url'] = ''; + } + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ADMIN_NAME; + + return new AppResponse('ok', null, $info); + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + + } + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + +} diff --git a/inc/Hura8/Components/Order/Model/OrderSearchModel.php b/inc/Hura8/Components/Order/Model/OrderSearchModel.php new file mode 100644 index 0000000..305670b --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderSearchModel.php @@ -0,0 +1,30 @@ + "tb_order.price", + ]; + + private $fulltext_fields = [ + "keywords" => ["tb_order.buyerName", ], + ]; + + + public function __construct() + { + parent::__construct( + "tb_order", + $this->fulltext_fields, + $this->filter_fields + ); + } + +} diff --git a/inc/Hura8/Components/Order/Model/OrderStatusModel.php b/inc/Hura8/Components/Order/Model/OrderStatusModel.php new file mode 100644 index 0000000..aae45f0 --- /dev/null +++ b/inc/Hura8/Components/Order/Model/OrderStatusModel.php @@ -0,0 +1,94 @@ +db = get_db('', ENABLE_DB_DEBUG); + } + + public function getStatusHistory($orderId){ + $query = $this->db->runQuery(" + select * from ".$this->tb_status_history." + WHERE order_id = ? + order by id desc + limit 100 + ", ['d'], [ $orderId ] ) ; + + return $this->db->fetchAll($query); + } + + public function createHistory($order_id, $status_type = 'order', $system_status = '', $comment = '', array $data= [], array $other_info = []) { + + $info = $other_info; + + $info['order_id'] = $order_id; + $info['status_type'] = $status_type; + $info['system_status'] = $system_status; + $info['data'] = \json_encode($data); + $info['comment'] = substr($comment, 0, 150); + $info['create_time'] = CURRENT_TIME; + $info['create_by'] = (defined('ADMIN_NAME')) ? ADMIN_NAME : ''; + + return $this->db->insert($this->tb_status_history, $info); + } + + public function getCustomerCancelStatusId() { + $info = $this->db->select($this->tb_status, ['id'], [ + "system_status" => ["=", "cancel"], + 'message' => 'Khách hàng hủy', + ], '', 1); + + if($info) { + return $info['id']; + } + + // create and return + return $this->create([ + 'message' => 'Khách hàng hủy', + 'system_status' => "cancel", + 'create_time' => CURRENT_TIME, + 'create_by' => 'System', + ]); + } + + + public function getInfo($status_id) { + return $this->db->getItemInfo($this->tb_status, $status_id, 'id'); + } + + public function getAll() { + $query = $this->db->runQuery(" select * from ".$this->tb_status." order by `id` desc ") ; + $item_list = array(); + foreach ( $this->db->fetchAll($query) as $rs ) { + if(!$rs['system_status']) $rs['system_status'] = 'new'; + //$rs['system_order_status'] = System::$ORDER_STATUS[$rs['system_status']]; + $item_list[] = $rs; + } + + return $item_list; + } + + public function delete($id) { + return $this->db->runQuery("DELETE FROM ".$this->tb_status." WHERE `id` = ? limit 1 ", ['d'], [ $id ]) ; + } + + public function update($id, array $info) { + return $this->db->update($this->tb_status, $info, ['id' => $id]); + } + + public function create(array $info) { + return $this->db->insert($this->tb_status, $info); + } + +} diff --git a/inc/Hura8/Components/Page/AdminController/APageController.php b/inc/Hura8/Components/Page/AdminController/APageController.php new file mode 100644 index 0000000..48b10ff --- /dev/null +++ b/inc/Hura8/Components/Page/AdminController/APageController.php @@ -0,0 +1,50 @@ +objPageModel->updateTableInfo($item_id, $new_item_info); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + $this->deleteThumbnailFile($item_info['thumbnail']); + + //delete media files? + // todo: + + return true; + } + + + protected function deleteThumbnailFile($file_name): bool + { + if(!$file_name) { + return false; + } + + foreach (self::$resized_sizes as $size => $value) { + $file_local_path = PUBLIC_DIR . "/". self::$image_folder . "/". $size. IMAGE_FILE_SEPARATOR . $file_name; + unlink($file_local_path); + } + + // remove original file + $file_local_path = PUBLIC_DIR . "/". self::$image_folder . "/". $file_name; + return unlink($file_local_path); + } + +} diff --git a/inc/Hura8/Components/Page/Controller/bPageController.php b/inc/Hura8/Components/Page/Controller/bPageController.php new file mode 100644 index 0000000..00a3629 --- /dev/null +++ b/inc/Hura8/Components/Page/Controller/bPageController.php @@ -0,0 +1,92 @@ + ['width' => 200,] , + 'l' => ['width' => 600,] , + ); + + + protected $objPageModel; + + + public function __construct() + { + $this->objPageModel = new PageModel(); + + if(!$this->isDefaultLanguage()) { + //$this->objPageLanguageModel->createTableLang(); + parent::__construct($this->objPageModel, new PageLanguageModel()); + }else{ + parent::__construct($this->objPageModel); + } + } + + + // get full info- basic with description + public function getFullInfo($id) : ?array + { + if(!$id) return null; + + return self::getCache("getFullInfo-".$id."-".$this->view_language, function () use ($id){ + + $info = $this->objPageModel->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 $this->formatItemInfo($info); + + }); + } + + + protected function formatItemInfo(array $item_info) + { + if(!$item_info) return null; + + $info = $item_info; + $info['image'] = self::getResizedImageCollection($info['thumbnail']); + + return $info; + } + + + protected function formatItemInList(array $item_info) + { + return $this->formatItemInfo($item_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 : ''; + } + + return $image; + } + +} diff --git a/inc/Hura8/Components/Page/Model/PageLanguageModel.php b/inc/Hura8/Components/Page/Model/PageLanguageModel.php new file mode 100644 index 0000000..5098c36 --- /dev/null +++ b/inc/Hura8/Components/Page/Model/PageLanguageModel.php @@ -0,0 +1,21 @@ +richtext_fields); + } + + +} diff --git a/inc/Hura8/Components/Page/Model/PageModel.php b/inc/Hura8/Components/Page/Model/PageModel.php new file mode 100644 index 0000000..9f72300 --- /dev/null +++ b/inc/Hura8/Components/Page/Model/PageModel.php @@ -0,0 +1,76 @@ +richtext_fields); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getFullInfo($id) : ?array + { + $query = $this->db->runQuery( + "SELECT * FROM `".$this->tb_entity."` basic, `".$this->tb_page_info."` info + WHERE basic.`id` = info.`page_id` AND basic.id = ? + LIMIT 1 ", + ['d'], [$id] + ); + + if( $item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return null; + } + + protected function _buildQueryConditionExtend(array $filter_condition): ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + /*if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + }*/ + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} 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..8e7da59 --- /dev/null +++ b/inc/Hura8/Components/Product/AdminController/AProductCategoryController.php @@ -0,0 +1,18 @@ +objProductCategoryModel->getAttributeList($catId); + } + +} 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..fe67f3a --- /dev/null +++ b/inc/Hura8/Components/Product/Model/ProductCategoryModel.php @@ -0,0 +1,83 @@ +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/Components/Province/AdminController/AProvinceController.php b/inc/Hura8/Components/Province/AdminController/AProvinceController.php new file mode 100644 index 0000000..d8589b7 --- /dev/null +++ b/inc/Hura8/Components/Province/AdminController/AProvinceController.php @@ -0,0 +1,10 @@ +objProvinceModel = new ProvinceModel(); + } + + + public function getProvinceName($id) { + return self::getCache("getProvinceName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getProvinceInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + public function getDistrictName($id) { + return self::getCache("getDistrictName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getDistrictInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + public function getWardName($id) { + return self::getCache("getWardName-".$id, function () use ($id) { + $info = $this->objProvinceModel->getWardInfo($id, 'name'); + return ($info) ? $info['name'] : ''; + }); + } + + + public function getProvinceList($field="*") { + return self::getCache("getProvinceList-", function () use ($field) { + return $this->objProvinceModel->getProvinceList($field); + //$pick_keys = ['id', 'api_id', 'name']; + + /*return array_map(function ($item) use($pick_keys){ + return pick_return_array_key($item, $pick_keys); + }, $province_list);*/ + }); + } + + public function getProvinceDistrictList($province_id, $field="*") { + + return self::getCache("getProvinceDistrictList-".$province_id, function () use ($province_id) { + return $this->objProvinceModel->getProvinceDistrictList($province_id); + //$pick_keys = ['id', 'api_id', 'name']; + + /*return array_map(function ($item) use($pick_keys){ + return pick_return_array_key($item, $pick_keys); + }, $province_list);*/ + }); + } + + public function getAllDistrictList() { + return self::getCache("getAllDistrictList", function (){ + return $this->objProvinceModel->getAllDistrictList(); + }); + } + + + public function getDistrictWardList($district_id, $field="*") { + return self::getCache("getDistrictWardList-".$district_id, function () use ($district_id) { + return $this->objProvinceModel->getDistrictWardList($district_id); + }); + } + + // these are kept for old time sake + /*public static $province_list = array( + "1" => "Hà Nội", + "2" => "TP HCM", + "5" => "Hải Phòng" , + "4" => "Đà Nẵng" , + "6" => "An Giang" , + "7" => "Bà Rịa-Vũng Tàu", + "13" => "Bình Dương" , + "15" => "Bình Phước" , + "16" => "Bình Thuận" , + "14" => "Bình Định" , + "8" => "Bạc Liêu", + "10" => "Bắc Giang" , + "9" => "Bắc Kạn", + "11" => "Bắc Ninh", + "12" => "Bến Tre", + "18" => "Cao Bằng", + "17" => "Cà Mau", + "3" => "Cần Thơ", + "24" => "Gia Lai", + "25" => "Hà Giang", + "26" => "Hà Nam", + "27" => "Hà Tĩnh", + "30" => "Hòa Bình", + "28" => "Hải Dương", + "29" => "Hậu Giang", + "31" => "Hưng Yên", + "32" => "Khánh Hòa", + "33" => "Kiên Giang", + "34" => "Kon Tum", + "35" => "Lai Châu", + "38" => "Lào Cai", + "36" => "Lâm Đồng", + "37" => "Lạng Sơn", + "39" => "Long An", + "40" => "Nam Định", + "41" => "Nghệ An", + "42" => "Ninh Bình", + "43" => "Ninh Thuận", + "44" => "Phú Thọ", + "45" => "Phú Yên", + "46" => "Quảng Bình", + "47" => "Quảng Nam", + "48" => "Quảng Ngãi", + "49" => "Quảng Ninh", + "50" => "Quảng Trị", + "51" => "Sóc Trăng", + "52" => "Sơn La", + "53" => "Tây Ninh", + "56" => "Thanh Hóa", + "54" => "Thái Bình", + "55" => "Thái Nguyên", + "57" => "Thừa Thiên-Huế", + "58" => "Tiền Giang", + "59" => "Trà Vinh", + "60" => "Tuyên Quang", + "61" => "Vĩnh Long", + "62" => "Vĩnh Phúc", + "63" => "Yên Bái", + "19" => "Đắk Lắk", + "22" => "Đồng Nai", + "23" => "Đồng Tháp", + "21" => "Điện Biên", + "20" => "Đăk Nông", + ); + + public static function getName($id) { + return (isset(static::$province_list[$id])) ? static::$province_list[$id] : ''; + }*/ + +} diff --git a/inc/Hura8/Components/Province/Model/ProvinceModel.php b/inc/Hura8/Components/Province/Model/ProvinceModel.php new file mode 100644 index 0000000..5d37f81 --- /dev/null +++ b/inc/Hura8/Components/Province/Model/ProvinceModel.php @@ -0,0 +1,73 @@ +db = get_db(); + } + + public function getProvinceInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getDistrictInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province_district."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getWardInfo($id, $field="*") { + if(!$id) return null; + + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province_ward."` WHERE `id` = ? LIMIT 1 ", ['d'], [ $id ]); + return $this->db->fetchAssoc($query); + } + + public function getProvinceList($field="*") { + $query = $this->db->runQuery("SELECT ".$field." FROM `".$this->tb_province."` ORDER BY `id` ASC "); + return $this->db->fetchAll($query); + } + + public function getProvinceDistrictList($province_id, $field="*") { + $query = $this->db->runQuery( + "SELECT ".$field." FROM `".$this->tb_province_district."` WHERE `province_id` = ? ORDER BY `ordering` DESC ", + ['d'], [ $province_id ] + ); + + return $this->db->fetchAll($query); + } + + public function getAllDistrictList() { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_province_district."` WHERE 1 ORDER BY id ASC "); + return $this->db->fetchAll($query); + } + + public function getDistrictWardList($district_id, $field="*") { + $query = $this->db->runQuery( + "SELECT ".$field." FROM `".$this->tb_province_ward."` WHERE `district_id` = ? ORDER BY `ordering` DESC ", + ['d'], + [ $district_id ] + ); + + return $this->db->fetchAll($query); + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php b/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php new file mode 100644 index 0000000..30936aa --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/ClientPermissionController.php @@ -0,0 +1,76 @@ +getClientEntityPermission_raw(); + }); + } + + protected function getClientEntityPermission_raw() { + + $system_file = ROOT_DIR. "/config/system/admin.entity.permission.php"; + $entity_group = include $system_file; + + $client_allowed_entities = Permission::getClientEntities(); + + $final_config = []; + foreach ($entity_group as $_group) { + + $settings = include ROOT_DIR. "/config/system/entity_permission/".$_group.".php"; + + $children_match = []; + foreach ($settings['children'] as $_entity => $_p) { + if(in_array($_entity, $client_allowed_entities)) { + $children_match[$_entity] = $_p; + } + } + + if(sizeof($children_match)) { + $final_config[$_group] = [ + 'title' => $settings['title'], + 'children' => $children_match, + ]; + } + } + + + return $final_config; + } + + + public function getClientMenu() { + return static::getCache("getClientMenu", function (){ + $menu_config_file = ROOT_DIR. "/config/client/admin/admin.menu.php"; + + $header_admin_config = []; + $menu_group = include $menu_config_file; + foreach ($menu_group as $_group) { + $content = include ROOT_DIR. "/config/client/admin/admin_menu/".$_group.".php"; + $enabled_menu_item = array_filter($content['menu'], function ($item){ return $item['enable'];}); + + if(sizeof($enabled_menu_item) > 0) { + $header_admin_config[$_group] = [ + 'enable' => $content['enable'], + 'name' => $content['name'], + 'url' => $content['url'], + 'menu' => $enabled_menu_item, + ]; + } + } + + return $header_admin_config; + }); + } + + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php b/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php new file mode 100644 index 0000000..314a63a --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffAdminController.php @@ -0,0 +1,72 @@ +objStaffAuthModel = new StaffAuthModel(); + $this->objStaffModel = new StaffModel(); + } + + public function getLoginListByIds(array $staff_ids) : array + { + return $this->objStaffAuthModel->getLoginListByIds($staff_ids); + } + + public function getList(array $conditions) : array + { + return $this->objStaffModel->getList($conditions); + } + + public function getInfo($id) : ?array + { + return $this->objStaffModel->getInfo($id); + } + + public function getEmptyInfo(array $additional_fields = []) : array + { + return $this->objStaffModel->getEmptyInfo($additional_fields); + } + + public function update($id, array $input_info) : AppResponse + { + // change password + if(isset($input_info['password']) && strlen($input_info['password']) > 5) { + $this->objStaffAuthModel->createOrUpdatePassword($id, $input_info['password']); + } + + return $this->objStaffModel->update($id, $input_info); + } + + public function create(array $input_info, $password = "") : AppResponse + { + $db_res = $this->objStaffModel->create($input_info); + + if($db_res->getStatus() == 'ok') { + + $new_id = $db_res->getData(); + + if(!$password) $password = IDGenerator::createStringId(6); + + $this->objStaffAuthModel->createOrUpdatePassword($new_id, $password); + + return new AppResponse('ok', '', ["id" => $new_id, "password" => $password]); + } + + return new AppResponse('error', 'Cannot create'); + } + + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php b/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php new file mode 100644 index 0000000..54ffdc5 --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffAdminDepartmentController.php @@ -0,0 +1,44 @@ +objStaffDepartmentModel = new StaffDepartmentModel(); + } + + public function getList(array $conditions) { + return $this->objStaffDepartmentModel->getList($conditions); + } + + public function getInfo($id) { + return self::getCache("getInfo-".$id, function () use ($id){ + return $this->objStaffDepartmentModel->getInfo($id); + }); + } + + public function getEmptyInfo(array $additional_fields = []) + { + return $this->objStaffDepartmentModel->getEmptyInfo($additional_fields); + } + + public function update($id, array $input_info) { + return $this->objStaffDepartmentModel->update($id, $input_info); + } + + public function create(array $input_info) { + return $this->objStaffDepartmentModel->create($input_info); + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php b/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php new file mode 100644 index 0000000..d54830d --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffDepartmentPermissionController.php @@ -0,0 +1,98 @@ +objStaffDepartmentPermissionModel = new StaffDepartmentPermissionModel(); + } + + + public function getDepartmentMenuPermissionSetting($department_id) { + + $current_permission = $this->getDepartmentMenuPermission($department_id); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + $rebuild_menu[$index] = $menu; + + $rebuild_menu[$index]['is_user_permitted'] = (in_array($menu['id'], $current_permission)) ? 1 : 0; + } + + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + + return $user_menu_settings; + } + + + public function saveDepartmentMenuPermission($department_id, array $new_permission) { + $this->objStaffDepartmentPermissionModel->saveDepartmentMenuPermission($department_id, $new_permission); + } + + + public function getDepartmentMenuPermission($department_id) { + return $this->objStaffDepartmentPermissionModel->getDepartmentMenuPermission($department_id); + } + + + public function getDepartmentEntityPermissionSetting($department_id) { + + $current_permission = $this->getDepartmentEntityPermission($department_id); + + $_settings = []; + foreach ($this->getClientEntityPermission() as $group_id => $group_info) { + + $rebuild_children = []; + foreach ($group_info['children'] as $entity => $info) { + + $actions = []; + foreach ($info['action'] as $action_key => $action_title) { + $actions[] = [ + 'action' => $action_key, + 'title' => $action_title, + 'is_user_permitted' => (isset($current_permission[$group_id]) && isset($current_permission[$group_id][$entity]) && isset($current_permission[$group_id][$entity][$action_key])) ? $current_permission[$group_id][$entity][$action_key] : false , + ]; + } + + $rebuild_children[$entity] = [ + 'title' => $info['title'], + 'action_list' => $actions, + ]; + + } + + $_settings[$group_id] = $group_info; + $_settings[$group_id]['children'] = $rebuild_children; + } + + return $_settings; + } + + + public function saveDepartmentEntityPermission($department_id, array $new_permission) { + $this->objStaffDepartmentPermissionModel->saveDepartmentEntityPermission( + $department_id, $new_permission + ); + } + + + public function getDepartmentEntityPermission($department_id) { + return static::getCache("getDepartmentEntityPermission-".$department_id, function () use ($department_id){ + return $this->objStaffDepartmentPermissionModel->getDepartmentEntityPermission($department_id); + }) ; + } + +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffLogController.php b/inc/Hura8/Components/Staff/AdminController/StaffLogController.php new file mode 100644 index 0000000..f1f7e80 --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffLogController.php @@ -0,0 +1,27 @@ +objStaffLogModel = new StaffLogModel(); + } + + public function getList(array $conditions) { + return $this->objStaffLogModel->getList($conditions); + } + + public function getTotal(array $conditions) { + return $this->objStaffLogModel->getTotal($conditions); + } + + public function create(array $input_info) { + + } +} diff --git a/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php b/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php new file mode 100644 index 0000000..d87f2ca --- /dev/null +++ b/inc/Hura8/Components/Staff/AdminController/StaffPermissionController.php @@ -0,0 +1,180 @@ +objStaffPermissionModel = new StaffPermissionModel(); + } + + /** + * @param $entity_group + * @param $entity + * @param string $action valid values in PermissionType:: + * @return bool + */ + public function checkEntityActionPermission($is_super_user, $entity_group, $entity, $action ) : bool { + + // super is allowed by default + if($is_super_user) { + return true; + } + + $current_permission = $this->getCurrentUserEntityPermission(); + + if ( + isset($current_permission[$entity_group]) && + isset($current_permission[$entity_group][$entity]) && + isset($current_permission[$entity_group][$entity][$action]) + ) { + return $current_permission[$entity_group][$entity][$action] ; + } + + return false; + } + + + public function getUserProductMenuList() { + + return self::getCache("getUserProductMenuList", function (){ + $menu_list = include CONFIG_DIR. '/client/admin/product_menu.php'; + + $final_list = []; + foreach (array_filter($menu_list, function ($item){ return $item['enabled'];}) as $key => $value) { + + // check if the current staff can see + // todo + // ... + + $final_list[$key] = $value; + } + + return $final_list; + }); + + } + + + public function saveUserMenuPermission($admin_id, array $new_permission) { + $this->objStaffPermissionModel->saveUserMenuPermission($admin_id, $new_permission); + } + + + public function saveUserEntityPermission($admin_id, array $new_permission) { + $this->objStaffPermissionModel->saveUserEntityPermission($admin_id, $new_permission); + } + + /* + * get menu for currently logged-in user + */ + public function getUserMenu($is_super_user) { + + // super can see all + if($is_super_user) { + return $this->getClientMenu(); + } + + $current_permission = $this->getCurrentUserMenuPermission(); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + if(!in_array($menu['id'], $current_permission)) continue; + + $rebuild_menu[$index] = $menu; + } + + if(sizeof($rebuild_menu)) { + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + } + + return $user_menu_settings; + } + + + public function getCurrentUserMenuPermission() { + $current_staff_id = StaffLoginController::getLoggedInStaffId(); + return $this->objStaffPermissionModel->getUserMenuPermission($current_staff_id); + } + + + public function getCurrentUserEntityPermission() { + $current_staff_id = StaffLoginController::getLoggedInStaffId(); + return $this->objStaffPermissionModel->getUserEntityPermission($current_staff_id); + } + + + public function getUserMenuPermissionSetting($admin_id) { + + $current_permission = $this->objStaffPermissionModel->getUserMenuPermission($admin_id); + + $user_menu_settings = []; + foreach ($this->getClientMenu() as $group_id => $group_info) { + + $rebuild_menu = []; + foreach ($group_info['menu'] as $index => $menu) { + $rebuild_menu[$index] = $menu; + + $rebuild_menu[$index]['is_user_permitted'] = (in_array($menu['id'], $current_permission)) ? 1 : 0; + } + + $user_menu_settings[$group_id] = $group_info; + $user_menu_settings[$group_id]['menu'] = $rebuild_menu; + } + + return $user_menu_settings; + } + + + public function getUserEntityPermissionSetting($admin_id) { + + $current_permission = $this->objStaffPermissionModel->getUserEntityPermission($admin_id); + + $_settings = []; + + foreach ($this->getClientEntityPermission() as $group_id => $group_info) { + + $rebuild_children = []; + foreach ($group_info['children'] as $entity => $info) { + + $actions = []; + foreach ($info['action'] as $action_key => $action_title) { + $actions[] = [ + 'action' => $action_key, + 'title' => $action_title, + 'is_user_permitted' => ( + isset($current_permission[$group_id]) && + isset($current_permission[$group_id][$entity]) && + isset($current_permission[$group_id][$entity][$action_key]) + ) ? $current_permission[$group_id][$entity][$action_key] : false , + ]; + } + + $rebuild_children[$entity] = [ + 'title' => $info['title'], + 'action_list' => $actions, + ]; + + } + + $_settings[$group_id] = $group_info; + $_settings[$group_id]['children'] = $rebuild_children; + } + + return $_settings; + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffAuthModel.php b/inc/Hura8/Components/Staff/Model/StaffAuthModel.php new file mode 100644 index 0000000..8430a6d --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffAuthModel.php @@ -0,0 +1,64 @@ +tb_staff_login, $this->tb_staff_access_code); + } + + + public function getLoginListByIds(array $staff_ids) { + if(!sizeof($staff_ids)) { + return []; + } + + list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($staff_ids, 'int'); + + $bind_values = $staff_ids; + + $query = $this->db->runQuery( + "SELECT `user_id`, `last_login_time`, `last_login_ip`, `last_login_device`, `last_login_browser` + FROM ".$this->tb_staff_login." + WHERE `user_id` IN (".$parameterized_ids.") ", + $bind_types, + $bind_values + ); + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + $item_list[$item['user_id']] = $item; + } + + return $item_list; + } + + + public function getLoginLog(array $conditions = []) { + $bind_types = []; + $bind_values = []; + + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_staff_login_log." WHERE 1 ORDER BY `id` DESC LIMIT 100 ", + $bind_types, + $bind_values + ); + + return $this->db->fetchAll($query) ; + } + + + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php b/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php new file mode 100644 index 0000000..1e15000 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffDepartmentModel.php @@ -0,0 +1,40 @@ +objStaffDepartmentModel = new StaffDepartmentModel(); + } + + + public function getDepartmentMenuPermission($department_id) { + $_info = $this->objStaffDepartmentModel->getInfo($department_id); + + return ($_info['menu_permission']) ? \json_decode($_info['menu_permission'], true) : []; + } + + + + public function getDepartmentEntityPermission($department_id) { + $_info = $this->objStaffDepartmentModel->getInfo($department_id); + + return ($_info['entity_permission']) ? \json_decode($_info['entity_permission'], true) : []; + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffLogModel.php b/inc/Hura8/Components/Staff/Model/StaffLogModel.php new file mode 100644 index 0000000..087b6a8 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffLogModel.php @@ -0,0 +1,38 @@ +tb_staff = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getInfoByEmail($email) : ?array + { + $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `email` = ? LIMIT 1 ", ['s'], [$email]) ; + if( $item_info = $this->db->fetchAssoc($query)){ + return $this->formatItemInfo($item_info); + } + + return null; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + +} diff --git a/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php b/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php new file mode 100644 index 0000000..f9f9049 --- /dev/null +++ b/inc/Hura8/Components/Staff/Model/StaffPermissionModel.php @@ -0,0 +1,67 @@ +objStaffModel = new StaffModel(); + } + + + public function getUserEntityPermission($admin_id) { + + /*return [ + // entity group => entity + 'product' => [ + 'item' => [ + 'view' => true, + 'create' => false, + 'delete' => true, + 'update' => false, + ], + ], + + 'article' => [ + 'item' => [ + 'view' => true, + 'create' => false, + 'delete' => false, + 'update' => true, + ], + ] + ];*/ + + $admin_info = $this->objStaffModel->getInfo($admin_id); + + if($admin_info['entity_permission']) { + $entity_permission = \json_decode($admin_info['entity_permission'], true); + + if(sizeof($entity_permission) > 0) { + return $entity_permission; + } + } + + // else use department + if($admin_info['department']) { + $objStaffDepartmentPermissionModel = new StaffDepartmentPermissionModel(); + $department_entity_permission = $objStaffDepartmentPermissionModel->getDepartmentEntityPermission($admin_info['department']); + + return $department_entity_permission; + } + + return []; + } + + + public function getUserMenuPermission($admin_id) { + $admin_info = $this->objStaffModel->getInfo($admin_id); + return ($admin_info['menu_permission']) ? \json_decode($admin_info['menu_permission'], true) : []; + } + +} diff --git a/inc/Hura8/Components/Template/AdminController/ATemplateController.php b/inc/Hura8/Components/Template/AdminController/ATemplateController.php new file mode 100644 index 0000000..06d12a8 --- /dev/null +++ b/inc/Hura8/Components/Template/AdminController/ATemplateController.php @@ -0,0 +1,216 @@ +objTemplateModel = new TemplateModel($template_set); + $this->template_set = $template_set; + + $this->checkAndCreateDirectory(); + } + + public function checkFileExist($file_name, $f_type ) { + return $this->objTemplateModel->checkFileExist($file_name, $f_type); + } + + public function getInfo($id) { + return $this->objTemplateModel->getInfo($id); + } + + public function getEmptyInfo($addition_field_value = []) { + return $this->objTemplateModel->getEmptyInfo($addition_field_value); + } + + public function getModuleList() { + return $this->objTemplateModel->getModuleList(); + } + + + /** + * @description full tpl file name has this format: language_layout_base-name + * language is omitted if language = 'vnd' + * layout is omitted if layout = 'pc' + Example: + * for vn/pc: + - index + - product_detail + * for vn/mobile: + - mobile_index + - mobile_product_detail + * for en/pc: + - en_index + - en_product_detail + * for en/mobile: + - en_mobile_index + - en_mobile_product_detail + * @description contruct a full tpl file name to store in folder filesystem + * @param string $language vn or en + * @param string $layout pc or mobile + * @param string $base_name should not contain reserved keywords used for language (i.e en) & layout (i.e mobile) + * @return string + * @test + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "vn", "pc")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "vn", "mobile")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "en", "pc")); + debug_var(\Hura8\Admin\ATemplate::buildFullTplFileName("product_detail", "en", "mobile")); + */ + public static function buildFullTplFileName(string $base_name, string $language=DEFAULT_LANGUAGE, string $layout='pc') { + if($language == DEFAULT_LANGUAGE) { + return ($layout == 'pc') ? $base_name : join("_", [$layout, $base_name]); + } + + return ($layout == 'pc') ? join("_", [$language, $base_name]) : join("_", [$language, $layout, $base_name]); + } + + /** + * @description extract $language, $layout, $base_name from $full_tpl_file + * @param string $full_tpl_file + * @return array + * @test + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("mobile_product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("en_product_detail")); + debug_var(\Hura8\Admin\ATemplate::extractFullTplFileName("en_mobile_product_detail")); + */ + public static function extractFullTplFileName(string $full_tpl_file) { + $language = DEFAULT_LANGUAGE; + $layout = "pc"; + + // check for different language + if(substr($full_tpl_file, 0, 3) == "en_") { + $language = "en"; + } + + // check for different layout + $clean_file = str_replace($language."_", "", $full_tpl_file); + if(substr($clean_file, 0, 7) == "mobile_") { + $layout = "mobile"; + } + + $base_name = str_replace($layout."_", "", $clean_file); + + return [ $language, $layout, $base_name ]; + } + + protected function checkAndCreateDirectory() { + // check and create directory + if (!is_dir($this->web_template_path)) { + mkdir($this->web_template_path, 0755, true); + } + + if (!is_dir($this->assets_template_path)) { + mkdir($this->assets_template_path, 0755, true); + mkdir(join(DIRECTORY_SEPARATOR, [$this->assets_template_path, 'script']), 0755, true); + mkdir(join(DIRECTORY_SEPARATOR, [$this->assets_template_path, 'images']), 0755, true); + } + } + + public function getFiles(array $conditions = []) { + + $conditions['numPerPage'] = 2000; + + return $this->objTemplateModel->getList($conditions); + } + + + public function getVersionHistory($module, $tpl_file) { + return $this->objTemplateModel->getVersionHistory($module, $tpl_file); + } + + public function getVersionContent($module, $tpl_file, $version) { + return $this->objTemplateModel->getVersionContent($module, $tpl_file, $version); + + } + + public function getFileContent($tpl_file, $folder, $is_setting = false) { + $ext = strrchr($tpl_file, "."); + if ($ext) $tpl_file = str_replace($ext, "", $tpl_file); + + switch ($ext) { + // css or js files + case ".script": + $full_file_path = join(DIRECTORY_SEPARATOR, [$this->assets_template_path, "script", $tpl_file]); + $content = (@file_exists($full_file_path)) ? @file_get_contents($full_file_path) : ''; + $real_ext = strrchr($tpl_file, "."); + $editor_mode = ($real_ext == ".js") ? 'text/javascript' : 'text/css'; + + return "
+ + +
+ "; + + + case ".image": + + $full_file_path = join(DIRECTORY_SEPARATOR, [$this->assets_template_path, "images", $tpl_file]); + return "
+ +
"; + + + default; + + $full_file_path = ($folder != 'index') ? + join(DIRECTORY_SEPARATOR, [$this->web_template_path, $folder, $tpl_file.".html"]) : + join(DIRECTORY_SEPARATOR, [$this->web_template_path, $tpl_file.".html"]) ; + + $content = (@file_exists($full_file_path)) ? @file_get_contents($full_file_path) : ''; + + return " + +
+ +
+ + "; + + } + } + +} diff --git a/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php b/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php new file mode 100644 index 0000000..509b532 --- /dev/null +++ b/inc/Hura8/Components/Template/AdminController/ATemplateSetController.php @@ -0,0 +1,46 @@ +objTemplateSetModel = new TemplateSetModel(); + } + + + // get empty/default item for form + public function getEmptyInfo($addition_field_value = []) { + return $this->objTemplateSetModel->getEmptyInfo($addition_field_value); + } + + public function checkSetExist($set_name) + { + return $this->objTemplateSetModel->getInfoBySet($set_name); + } + + public function getActivatedSet() { + return $this->objTemplateSetModel->getActivatedSet(); + } + + public function getList(array $condition) + { + return $this->objTemplateSetModel->getList($condition); + } + + public function getInfo($id): ?array + { + return $this->objTemplateSetModel->getInfo($id); + } + +} diff --git a/inc/Hura8/Components/Template/Controller/TemplateController.php b/inc/Hura8/Components/Template/Controller/TemplateController.php new file mode 100644 index 0000000..a9ef656 --- /dev/null +++ b/inc/Hura8/Components/Template/Controller/TemplateController.php @@ -0,0 +1,167 @@ +template_set = $template_set; + $this->web_template_path = self::TEMPLATE_DIR . $template_set; + $this->assets_template_path = self::TEMPLATE_ASSETS_DIR . $template_set; + $this->assets_public_path = join('/', ['', 'static', 'assets', $template_set]) ; + } + + + /** + * @param string $language_prefix + * @param string $layout + * @param $routing array + * @return string + */ + public static function getPublicTemplateFile($language_prefix="", $layout="pc", array $routing=[]): string{ + $tpl_file = str_replace('-', '_', $routing['view']). '.html'; + if($layout != 'pc') $tpl_file = $layout.'_'.$tpl_file; + + if($language_prefix) $tpl_file = $language_prefix."_".$tpl_file; + + return join( DIRECTORY_SEPARATOR, [ + $routing['module'], + $tpl_file + ]); + } + + + /** + * @param string $language_prefix + * @param string $layout + * @return string + */ + public static function getSiteThemeFile($language_prefix="", $layout = 'pc'): string + { + $tpl_file = "index.html"; + if($layout != 'pc') $tpl_file = $layout.'_'.$tpl_file; + + if($language_prefix) return $language_prefix."_".$tpl_file; + + return $tpl_file; + } + + + public static function flushCache() { + try { + + $cache = new File([ + 'cache_dir' => self::$cache_dir + ]); + + $cache->flush(); + + }catch (\Exception $exception) { + + } + } + + /* + * 2 ways to render a html template + * 1. Use $html_to_parse, which requires no dependencies + * Example: + * Template::parse(null, 'Age = {{age}}', ['age' => 21], ''); + * + * 2. Use $template_file_path, which requires dependency $path + * Template::parse(Template::$setting_template_path, null, ['age' => 21], 'email/test'); + * + * */ + public static function parse($path = null, $html_to_parse = null, array $data = [], $template_file_path = '') { + + if(!$html_to_parse && !$template_file_path) { + return 'Nothing to parse'; + } + + //output to html + Liquid::set('INCLUDE_SUFFIX', 'html'); + Liquid::set('INCLUDE_PREFIX', ''); + //Liquid::set('INCLUDE_ALLOW_EXT', true); + Liquid::set('ESCAPE_BY_DEFAULT', false); + + $enable_cache = false; // default = true, turn this on-off to disable cache while working on local mode + //$enable_cache = true; + + //catch exception and print friendly notice + try { + + $objLiquidTemplate = new LiquidTemplate( $path ); + $objLiquidTemplate->registerFilter( TemplateFilter::class ); + if($enable_cache) { + $objLiquidTemplate->setCache(new File([ + 'cache_dir' => self::$cache_dir + ])); + } + + if($html_to_parse) { + $objLiquidTemplate->parse($html_to_parse); + }elseif ($template_file_path) { + $objLiquidTemplate->parseFile($template_file_path); + } + + return $objLiquidTemplate->render($data); + + } catch (\Exception $e) { + $result = []; + do { + //printf("%s:%d %s (%d) [%s]\n", $e->getFile(), $e->getLine(), $e->getMessage(), $e->getCode(), get_class($e)); + //echo $e->getTraceAsString(); + //$code = $e->getTrace()[0]['args'][0]; + //if(is_array($code)) $code = serialize($code); + $result[] = sprintf( + " +Lỗi code trong file template html:
+- Chi tiết lỗi: %s
+- File template: %s
+- Cache Dir: %s
+- Hướng dẫn xử lý: Tách từng phần html để kiểm tra và nhấn F5 mỗi lần. Nếu không xuất hiện thông báo này nghĩa là phần đó không tạo lỗi +", + $e->getMessage(), + substr($template_file_path, strrpos($template_file_path, DIRECTORY_SEPARATOR) + 1 ), + static::$cache_dir + ); + + } while($e = $e->getPrevious()); + + return join(" - ", $result); + } + } + + + public function getWebTemplatePath() { + return $this->web_template_path; + } + + public function getWebPublicAssetsUrl() { + return $this->assets_public_path; + } + + public function getWebTemplateAssetsPath() { + return $this->assets_template_path; + } + +} diff --git a/inc/Hura8/Components/Template/Controller/TemplateFilter.php b/inc/Hura8/Components/Template/Controller/TemplateFilter.php new file mode 100644 index 0000000..b164647 --- /dev/null +++ b/inc/Hura8/Components/Template/Controller/TemplateFilter.php @@ -0,0 +1,262 @@ + '', 'value' => ] from [key1 => value1, key2=>value2, ...] + * + * @param array $key_values [key1 => value1, key2=>value2] + * + * @return array [['key' => 'key1', 'value' => value1], ['key' => 'key2', 'value' => value2]] + */ + public static function to_array(array $key_values) { + $result = []; + foreach ($key_values as $key => $value) { + $result[] = [ + 'key' => $key, + 'value' => $value, + ]; + } + + return $result; + } + + + /** + * split a s by line to create array + * + * @param string $txt + * + * @return array + */ + public static function get_line($txt) { + + if(is_array($txt)) { + return $txt; + } + + $txt = trim($txt); + if( ! $txt ) return []; + + return preg_split("/\n/", $txt); + } + + /** + * Implement strlen + * + * @param string $str + * + * @return int + */ + public static function length($str) { + return strlen(trim($str)); + } + + + /** + * Make number easier to read: 1000 -> 1.000 + * + * @param string $number + * + * @return string + */ + public static function format_number($number) { + if(!$number) return ''; + $number = floatval($number); + + $number = number_format($number, 0, ",", "."); //Vietnamese format with decimals by a coma + + return $number; + } + + public static function format_price($p_price, $currency = ''){ + if(!$p_price) return ''; + if(!$currency) $currency = (defined("DEFAULT_CURRENCY")) ? DEFAULT_CURRENCY : "vnd"; + //if(is_string($p_price)) return 0; + if($currency == 'usd') { + return number_format($p_price,2,".",","); + }else { + return number_format($p_price,0,",","."); + } + } + + public static function global_asset_url($file_name = '') + { + return GLOBAL_ASSETS_PATH . $file_name; + } + + /** + * + * Description: get the shop's full asset url for template's images/js/css + * + * //Returns the URL of a file in the "assets" folder of a theme. + // {{ 'shop.css' | asset_url : 'arg1', 'arg2' ...}} -> //cdn.shopify.com/s/files/1/0087/0462/t/394/assets/shop.css?28253 + * + * @param string $file_name + * + * @return string + */ + public static function asset_url($file_name = '') + { + if( !$file_name ) return ''; + + $file_ext = strtolower(strrchr($file_name, ".")); + + // script tags + if(in_array($file_ext, ['.js', '.css'])) return TEMPLATE_ASSET_SCRIPT . $file_name; + + // default image + return TEMPLATE_ASSET_IMAGE . $file_name; + } + + + /** + * + * Description: construct a full html tag for images/js/css file + * + * @param string $file_path domain.com/static/style.css?v=3.1.1 + * + * @return string + */ + public static function script_tag($file_path) { + if( ! $file_path ) return ''; + + //check for ? + if(strpos($file_path, "?") !== false) { + $file_ext = str_replace(strrchr($file_path, "?"), "", $file_path); + $file_ext = strtolower(strrchr($file_ext, ".")); + } else { + $file_ext = strtolower(strrchr($file_path, ".")); + } + + $tag_config = [ + ".css" => "", + ".js" => "", + ".jpg" => "\"n\"/", + ".jpeg" => "\"\"/", + ".gif" => "\"\"/", + ".png" => "\"\"/", + ]; + + return (isset($tag_config[$file_ext])) ? $tag_config[$file_ext] : ''; + } + + /** + * {{ product_info.main_image | img_url: '300x300' }} => https://cdn.shopify.com/s/files/1/1183/1048/products/boat-shoes_300x300.jpeg?1459175177 + * @param string $full_path + * @param string $modifier + * $modifier: + * - must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + * - or be one of these: small | medium | large + * @return string + */ + public static function img_url($full_path, $modifier) + { + $clean_modifier = ($modifier) ? trim($modifier) : ""; + + // verify $modifier + // must be in format: NumberxNumber or Numberx where Number must within 10 -> 9999 + if($clean_modifier + && !preg_match("/^[0-9]{2,4}x([0-9]{2,4})?$/i", $clean_modifier) + && !in_array($clean_modifier, ["small", "medium", "large"]) + ) { + $clean_modifier = ""; + } + + // return if no valid modifier + if( ! $clean_modifier ) { + return $full_path; + } + + $last_dot_position = strrpos($full_path, "."); + if( ! $last_dot_position ) return $full_path . $clean_modifier; + + return join("", [ + substr($full_path, 0, $last_dot_position), + "_", + $clean_modifier, + substr($full_path, $last_dot_position) + ]); + } + + /** + * //Returns the URL of a file in the Files page of the admin. + //{{ 'size-chart.pdf' | file_url }} -> //cdn.shopify.com/s/files/1/0087/0462/files/size-chart.pdf?28261 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_url($input, $string) + { + return strtoupper($input) . " = " . $string; + } + + /** + * //Returns the asset URL of an image in the Files page of the admin. file_img_url accepts an image size parameter. + //{{ 'logo.png' | file_img_url: '1024x768' }} -> //cdn.shopify.com/s/files/1/0246/0527/files/logo_1024x768.png?42 + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function file_img_url($input, $string) + { + return ''; + } + + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function print_r($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } + + /** + * Show all content of a variable, useful for template development + * + * @param string + * + * @return string + */ + public static function show_var($input) + { + @ob_start(); + print_r($input); + $content = ob_get_contents(); + @ob_end_clean(); + + return join("\r", ['']) ; + } +} diff --git a/inc/Hura8/Components/Template/Model/TemplateModel.php b/inc/Hura8/Components/Template/Model/TemplateModel.php new file mode 100644 index 0000000..da07a12 --- /dev/null +++ b/inc/Hura8/Components/Template/Model/TemplateModel.php @@ -0,0 +1,132 @@ +template_set = $template_set; + $this->tb_template = $this->tb_entity; + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getModuleList() { + $query = $this->db->runQuery( + " select distinct `module` from `".$this->tb_template."` where `set_name` = ? order by `module` asc ", + [ 's' ], + [ $this->template_set ] + ) ; + + $item_list = []; + foreach ($this->db->fetchAll($query) as $item) { + if($item['module']) $item_list[] = $item['module']; + } + + return $item_list; + } + + + public function checkFileExist($file_name, $f_type ) { + $query = $this->db->runQuery( + "SELECT * FROM ".$this->tb_template." WHERE `file_name` = ? AND `file_type` = ? AND `set_name` = ? LIMIT 1 ", + [ 's', 's' , 's' ], + [ $file_name, $f_type, $this->template_set ] + ); + + return $this->db->fetchAssoc($query); + } + + + public function getVersionHistory($module, $tpl_file) { + + if(!$tpl_file) return []; + + $history_tpl_file = $this->historyFile($module, $tpl_file); + + $query = $this->db->runQuery( + "SELECT tpl_version, last_update, last_update_by FROM ".$this->tb_template_history." + WHERE `template` = ? AND `set_name` = ? ORDER BY `tpl_version` DESC LIMIT 30 ", + [ 's', 's' ], + [ $history_tpl_file, $this->template_set ] + ); + + return $this->db->fetchAll($query); + } + + + public function getVersionContent($module, $tpl_file, $version) { + + $history_tpl_file = $this->historyFile($module, $tpl_file); + + $query = $this->db->runQuery( + "SELECT `content` FROM ".$this->tb_template_history." + WHERE `template`= ? AND `set_name` = ? AND `tpl_version` = ? + LIMIT 1 ", + [ 's', 's', 'd' ], + [ $history_tpl_file, $this->template_set, $version ] + ); + + if ($rs = $this->db->fetchAssoc($query)) { + return $rs['content']; + } + + return ''; + } + + protected function historyFile($module, $tpl_file) { + return join("/", [$module, $tpl_file]); + } + + + protected function _buildQueryConditionExtend(array $conditions) : ?array + { + + /*$conditions = [ + 'file_folder' => '', + 'not_image' => true, + ];*/ + + $where_clause = [ " AND `set_name` = ? "]; + $bind_types = ['s']; + $bind_values = [$this->template_set]; + + if(isset($conditions['file_folder']) && in_array($conditions['file_folder'], ['layout', 'images', 'script']) ) { + $where_clause[] = " AND `file_folder` = ? "; + $bind_types[] = 's'; + $bind_values[] = $conditions['file_folder']; + } + + if(isset($conditions['not_image']) && $conditions['not_image']) { + $where_clause[] = " AND `file_folder` != 'images' "; + } + + return [ + join(" ", $where_clause), + $bind_types, + $bind_values + ]; + } + + +} diff --git a/inc/Hura8/Components/Template/Model/TemplateSetModel.php b/inc/Hura8/Components/Template/Model/TemplateSetModel.php new file mode 100644 index 0000000..f7733ac --- /dev/null +++ b/inc/Hura8/Components/Template/Model/TemplateSetModel.php @@ -0,0 +1,64 @@ +db->runQuery("SELECT * FROM `". $this->tb_entity ."` WHERE `folder_name` = ? LIMIT 1 ", ['s'], [$set_name]); + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info; + } + + return false; + } + + + public function getActivatedSet() { + $query = $this->db->runQuery("SELECT `folder_name` FROM `". $this->tb_entity ."` WHERE `is_activated` = 1 LIMIT 1 "); + if($item_info = $this->db->fetchAssoc($query)){ + return $item_info['folder_name']; + } + + return 'default'; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + $where_clause = ""; + $bind_types = []; + $bind_values = []; + + return [ + $where_clause, + $bind_types, + $bind_values + ]; + } + + + +} diff --git a/inc/Hura8/Components/User/AdminController/ANewsletterController.php b/inc/Hura8/Components/User/AdminController/ANewsletterController.php new file mode 100644 index 0000000..d208f8f --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/ANewsletterController.php @@ -0,0 +1,27 @@ +objNewsletterModel = new NewsletterModel(); + parent::__construct($this->objNewsletterModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserCommentController.php b/inc/Hura8/Components/User/AdminController/AUserCommentController.php new file mode 100644 index 0000000..5f854bf --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserCommentController.php @@ -0,0 +1,27 @@ +objUserCommentModel = new UserCommentModel(); + parent::__construct($this->objUserCommentModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserContactController.php b/inc/Hura8/Components/User/AdminController/AUserContactController.php new file mode 100644 index 0000000..05ea5b2 --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserContactController.php @@ -0,0 +1,27 @@ +objUserContactModel = new UserContactModel(); + parent::__construct($this->objUserContactModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/AdminController/AUserController.php b/inc/Hura8/Components/User/AdminController/AUserController.php new file mode 100644 index 0000000..bd6eba4 --- /dev/null +++ b/inc/Hura8/Components/User/AdminController/AUserController.php @@ -0,0 +1,18 @@ +objUserReviewModel = new UserReviewModel(); + parent::__construct($this->objUserReviewModel); + } + + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // TODO: Implement deleteFileBeforeDeleteItem() method. + return true; + } + +} diff --git a/inc/Hura8/Components/User/Controller/bUserController.php b/inc/Hura8/Components/User/Controller/bUserController.php new file mode 100644 index 0000000..becf701 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserController.php @@ -0,0 +1,35 @@ +web_user_id = self::getWebUserId(); + $this->objUserModel = new UserModel($this->web_user_id ); + parent::__construct($this->objUserModel); + } + + + /** + * @description this value is set by hura.js for anonymous web users + * @return string + */ + public static function getWebUserId() { + return Cookie::get("uID", ''); + } + + + +} diff --git a/inc/Hura8/Components/User/Controller/bUserReviewController.php b/inc/Hura8/Components/User/Controller/bUserReviewController.php new file mode 100644 index 0000000..d08bc58 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserReviewController.php @@ -0,0 +1,27 @@ +web_user_id = UUserController::getWebUserId(); + + $this->objUserReviewModel = new UserReviewModel($item_type, $item_id); + + parent::__construct($this->objUserReviewModel); + } + + + +} diff --git a/inc/Hura8/Components/User/Controller/bUserUploadController.php b/inc/Hura8/Components/User/Controller/bUserUploadController.php new file mode 100644 index 0000000..731c7e3 --- /dev/null +++ b/inc/Hura8/Components/User/Controller/bUserUploadController.php @@ -0,0 +1,41 @@ +web_user_id = AUserController::getWebUserId(); + $this->customer_id = UCustomerLoginController::getLoggedInCustomerId(); + + $this->item_type = $item_type; + $this->item_id = $item_id; + + parent::__construct(new UserUploadModel($this->web_user_id, $this->customer_id, $item_type, $item_id)); + } + + protected function deleteFileBeforeDeleteItem($item_id): bool + { + // delete thumb files + $item_info = $this->getInfo($item_id); + + if($item_info['file_path']) { + $file_local_path = PUBLIC_DIR . "/". $item_info['file_path']; + @unlink($file_local_path); + } + + // ok + return true; + } + +} diff --git a/inc/Hura8/Components/User/Model/NewsletterModel.php b/inc/Hura8/Components/User/Model/NewsletterModel.php new file mode 100644 index 0000000..4e25fff --- /dev/null +++ b/inc/Hura8/Components/User/Model/NewsletterModel.php @@ -0,0 +1,73 @@ + "", + "status" => 0, + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $info['create_time'] = CURRENT_TIME; + + 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 + { + return new AppResponse('ok', null, $new_input_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/Components/User/Model/UserCommentModel.php b/inc/Hura8/Components/User/Model/UserCommentModel.php new file mode 100644 index 0000000..7a18f1c --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserCommentModel.php @@ -0,0 +1,90 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $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) + { + + } + + 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) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + +} diff --git a/inc/Hura8/Components/User/Model/UserCommentReplyModel.php b/inc/Hura8/Components/User/Model/UserCommentReplyModel.php new file mode 100644 index 0000000..d21dc50 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserCommentReplyModel.php @@ -0,0 +1,96 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return parent::_buildQueryOrderBy($sort_by); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + $info = $input_info; + + $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) + { + + } + + 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) + { + + } + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + return new AppResponse('ok'); + } + + protected function afterDeleteItem($item_id, $item_info) + { + + } + +} diff --git a/inc/Hura8/Components/User/Model/UserContactModel.php b/inc/Hura8/Components/User/Model/UserContactModel.php new file mode 100644 index 0000000..bbde4b3 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserContactModel.php @@ -0,0 +1,54 @@ + "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + protected function _buildQueryOrderBy($sort_by = "new") + { + return parent::_buildQueryOrderBy($sort_by); + } + +} diff --git a/inc/Hura8/Components/User/Model/UserModel.php b/inc/Hura8/Components/User/Model/UserModel.php new file mode 100644 index 0000000..63881aa --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserModel.php @@ -0,0 +1,58 @@ +web_user_id = $web_user_id; + $this->user_db_id = $user_db_id; + + parent::__construct('web_user_info'); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + );*/ + + /*$condition = array( + "letter" => "", + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + + if(isset($filter_condition["letter"]) && strlen($filter_condition["letter"]) == 1){ + $catCondition[] = " AND `letter` = ? "; + $bind_types[] = 's'; + $bind_values[] = $filter_condition["letter"]; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + +} diff --git a/inc/Hura8/Components/User/Model/UserReviewModel.php b/inc/Hura8/Components/User/Model/UserReviewModel.php new file mode 100644 index 0000000..336df9c --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserReviewModel.php @@ -0,0 +1,218 @@ + true, + 'auto-approve-message' => false, + 'detect-spam-before-create' => true, + ]; + + protected $item_type = ''; + protected $item_id = 0; + + /* @var ?iEntityStatistic $iEntityStatisticModel */ + protected $iEntityStatisticModel; + + public function __construct($item_type, $item_id = 0) { + + $this->item_type = $item_type; + $this->item_id = $item_id; + + $this->iEntityStatisticModel = get_statistic_model_instance($this->item_type); + + // todo: overwrite default rules by client + + parent::__construct("user-review"); + } + + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + + public function getSummary(array $conditions = []) + { + $where_conditions = []; + $bind_types = []; + $bind_values = []; + + //item_type + $where_conditions[] = " AND `item_type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_type; + + + //item_id + $where_conditions[] = " AND `item_id` = ? "; + $bind_types[] = 's'; + $bind_values[] = $this->item_id; + + + //approved + if(isset($conditions["approved"]) ){ + $where_conditions[] = " AND `approved` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($conditions["approved"] == 1) ? 1 : 0; + } + + $query = $this->db->runQuery( + "SELECT AVG(`rate`) AS avgRate, COUNT(*) AS total FROM `". $this->tb_entity ."` WHERE 1 ".join(" ", $where_conditions), + $bind_types, $bind_values + ); + + if ( $info = $this->db->fetchAssoc($query) ) { + return [ + "avgRate" => $info['avgRate'], + "total" => $info['total'], + ]; + } + + return [ + "avgRate" => 0, + "total" => 0, + ]; + } + + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["read"]) && $condition["read"]){ + $catCondition[] = " AND `is_read` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["read"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + + + protected function beforeCreateItem(array $input_info) : AppResponse + { + //todo: check user's authentication and authorization + if($this->rules['require-user-login'] && !UCustomerLoginController::getLoggedInCustomerId()) { + return new AppResponse('error', "Login required"); + } + + $info = $input_info; + + $info['item_type'] = $this->item_type; + $info['item_id'] = $this->item_id; + + $info['item_title'] = DataClean::limitLengthFullWords($input_info['item_title'], 50); + $info['title'] = DataClean::limitLengthFullWords($input_info['title'], 50); + $info['content'] = DataClean::limitLengthFullWords($input_info['content'], 500); + + $info['user_id'] = ''; + $info['user_email'] = DataClean::makeInputSafe($input_info['user_email'], DataType::EMAIL); + $info['user_name'] = DataClean::limitLengthFullWords($input_info['user_name'], 50); + + // todo: check files actually exists and belong to this user + $info['files'] = (is_array($input_info['files'])) ? DataClean::makeListOfInputSafe($input_info['files'], DataType::INTEGER) : null; + + //$info['approved'] = (CONFIG_AUTO_APPROVE_REVIEW) ? 1 : 0; + $info['rate'] = (in_array($info['rate'], [1,2,3,4,5])) ? $info['rate'] : 0; + $info['ip_address'] = USER_IP; + $info['user_agent'] = substr(USER_AGENT, 0, 200); + + /* + "item_type" => $post_info['item_type'], + "item_id" => $post_info['item_id'], + "item_title" => $post_info['item_title'], + "is_user_admin" => $post_info['is_user_admin'], + "user_id" => USER_ID, + "user_email" => $post_info['user_email'], + "user_name" => $post_info['user_name'], + "user_avatar" => $post_info['user_avatar'], + "user_note" => $post_info['user_note'], + "rate" => (int) $post_info['rate'], + "title" => $post_info['title'], + "content" => $post_info['content'], + "files" => $post_info['files'], + "approved" => (CONFIG_AUTO_APPROVE_REVIEW) ? 1 : 0, + "post_time" => CURRENT_TIME, + "ip_address" => USER_IP, + "user_agent" => substr(USER_AGENT, 0, 200),*/ + + $info['create_time'] = CURRENT_TIME; + + return new AppResponse('ok', null, $info); + } + + + protected function afterCreateItem($new_item_id, $new_item_info) + { + // update summary for item + $this->updateItemReviewCount(); + } + + + protected function beforeUpdateItem($item_id, $current_item_info, $new_input_info) : AppResponse + { + //todo: check user's authentication and authorization + + $info = $new_input_info; + + $info['last_update'] = CURRENT_TIME; + $info['last_update_by'] = ''; + + return new AppResponse('ok', null, $info); + } + + + protected function afterUpdateItem($item_id, $old_item_info, $new_item_info) + { + + } + + + protected function beforeDeleteItem($item_id, $item_info) : AppResponse + { + //todo: check user's authentication and authorization + + return new AppResponse('ok'); + } + + + protected function afterDeleteItem($item_id, $item_info) + { + // update summary for item + $this->updateItemReviewCount(); + } + + + protected function updateItemReviewCount() + { + // update summary for item + if($this->iEntityStatisticModel) { + $summary = $this->getSummary([]); + $this->iEntityStatisticModel->updateReviewCount($this->item_id, $summary['total'], $summary['avgRate']); + } + } +} diff --git a/inc/Hura8/Components/User/Model/UserUploadModel.php b/inc/Hura8/Components/User/Model/UserUploadModel.php new file mode 100644 index 0000000..08032c2 --- /dev/null +++ b/inc/Hura8/Components/User/Model/UserUploadModel.php @@ -0,0 +1,63 @@ +web_user_id = $web_user_id; + $this->customer_id = $customer_id; + $this->item_id = $item_id; + $this->item_type = $item_type; + } + + protected function extendedFilterOptions() : array + { + return [ + // empty for now + ]; + } + + protected function _buildQueryConditionExtend(array $condition) : ?array + { + /*$condition = array( + "q" => "", + "status" => 0, + 'read' => 1,-1 + );*/ + + $catCondition = []; + $bind_types = []; + $bind_values = []; + + if(isset($condition["type"]) && $condition["type"]){ + $catCondition[] = " AND `type` = ? "; + $bind_types[] = 's'; + $bind_values[] = $condition["type"]; + } + + if(isset($condition["approved"]) && $condition["approved"]){ + $catCondition[] = " AND `approved` = ? "; + $bind_types[] = 'd'; + $bind_values[] = ($condition["approved"] == 1) ? 1 : 0; + } + + return array( join(" ", $catCondition), $bind_types, $bind_values); + } + +} 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 @@ +path_config) { + //$path_config_file = APP_DIR . '/config/routing.php'; + //$this->path_config = require $path_config_file; + //} + } + + // url: admin/abc/product.php?para1=value1 + public function getRouting() { + $parsed = Url::parse($_SERVER['REQUEST_URI']); //abc/product?param1=12¶m2=value2 + // print_r($parsed); + + // home + if($parsed['path'] == '/') { + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('module', 'home')), + 'view' => preg_replace("/[^a-z0-9_\-]/i","", getRequest('view', 'home')), + 'view_id'=> 0, + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + } + + // check match pattern in $this->path_config + /*foreach ($this->path_config as $_config => $_route ) { + if(preg_match("{^".$_config."$}", $parsed['path'], $match )) { + + if(isset($_route['query']) && is_array($_route['query'])) { + $_route['query'] = array_merge($_route['query'], $parsed['query']); + }else{ + $_route['query'] = $parsed['query']; + } + + return array_merge([ + 'path' => $parsed['path'], + 'match' => $match, + ], $_route); + } + }*/ + + // auto parse path base on convention: admin/module/view/view_id + $ele = explode("/", trim($parsed['path'], "/")); + + $module = $ele[0] ?? 'home'; + $view = $ele[1] ?? getRequest('view', 'home'); + $view_id = $ele[2] ?? getRequest('id', 'view_id'); + + // else error + return [ + 'module' => preg_replace("/[^a-z0-9_\-]/i","", $module ) , + 'view' => preg_replace("/[^a-z0-9_\-]/i","", $view ) , + 'view_id' => preg_replace("/[^a-z0-9_]/i","", $view_id ), + 'query' => $parsed['query'], + 'url' => $_SERVER['REQUEST_URI'], + ]; + } + +} diff --git a/inc/Hura8/System/APIClient.php b/inc/Hura8/System/APIClient.php new file mode 100644 index 0000000..805fc29 --- /dev/null +++ b/inc/Hura8/System/APIClient.php @@ -0,0 +1,152 @@ +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 (', + ]); + +Will output: + + Hello, Mx. Alex + Your comment was: <script>alert();</script> + +Note that automatic escaping is not a standard Liquid feature: use with care. + +Similarly, the following snippet will parse and render `templates/home.liquid` while storing parsing results in a class-local cache: + + \Liquid\Liquid::set('INCLUDE_PREFIX', ''); + + $template = new \Liquid\Template(__DIR__ . '/protected/templates'); + $template->setCache(new \Liquid\Cache\Local()); + echo $template->parseFile('home')->render(); + +If you render the same template over and over for at least a dozen of times, the class-local cache will give you a slight speed up in range of some milliseconds per render depending on a complexity of your template. + +You should probably extend `Liquid\Template` to initialize everything you do with `Liquid::set` in one place. + +### Custom filters + +Adding filters has never been easier. + + $template = new Template(); + $template->registerFilter('absolute_url', function ($arg) { + return "https://www.example.com$arg"; + }); + $template->parse("{{ my_url | absolute_url }}"); + echo $template->render(array( + 'my_url' => '/test' + )); + // expect: https://www.example.com/test + +## Requirements + + * PHP 7.0+ + +Some earlier versions could be used with PHP 5.3/5.4/5.5/5.6, though they're not supported anymore. + +## Issues + +Have a bug? Please create an issue here on GitHub! + +[https://github.com/kalimatas/php-liquid/issues](https://github.com/kalimatas/php-liquid/issues) + +## Fork notes + +This fork is based on [php-liquid](https://github.com/harrydeluxe/php-liquid) by Harald Hanek. + +It contains several improvements: + + * namespaces + * installing via composer + * new standard filters + * `raw` tag added + +Any help is appreciated! diff --git a/package/vendor/liquid/liquid/composer.json b/package/vendor/liquid/liquid/composer.json new file mode 100644 index 0000000..a453643 --- /dev/null +++ b/package/vendor/liquid/liquid/composer.json @@ -0,0 +1,58 @@ +{ + "name": "liquid/liquid", + "type": "library", + "description": "Liquid template engine for PHP", + "keywords": [ + "liquid", + "template" + ], + "homepage": "https://github.com/kalimatas/php-liquid", + "license": "MIT", + "authors": [ + { + "name": "Guz Alexander", + "email": "kalimatas@gmail.com", + "homepage": "http://guzalexander.com" + }, + { + "name": "Harald Hanek" + }, + { + "name": "Mateo Murphy" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com/" + } + ], + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^2.16.4", + "infection/infection": ">=0.17.6", + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.2.6" + }, + "config": { + "sort-packages": true, + "allow-plugins": true + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Liquid\\": "src/Liquid" + } + }, + "autoload-dev": { + "psr-4": { + "Liquid\\": "tests/Liquid" + } + } +} diff --git a/package/vendor/liquid/liquid/examples/advanced.php b/package/vendor/liquid/liquid/examples/advanced.php new file mode 100644 index 0000000..5b89f3b --- /dev/null +++ b/package/vendor/liquid/liquid/examples/advanced.php @@ -0,0 +1,32 @@ +parse("Hello, {% include 'honorific.html' %}{{ plain-html | raw }} {{ comment-with-xss }}\n"); +$template->setCache(new Local()); + +echo $template->render([ + 'name' => 'Alex', + 'plain-html' => 'Your comment was:', + 'comment-with-xss' => '', +]); diff --git a/package/vendor/liquid/liquid/examples/block.php b/package/vendor/liquid/liquid/examples/block.php new file mode 100644 index 0000000..b7832d0 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/block.php @@ -0,0 +1,39 @@ + 'file', 'cache_dir' => $protectedPath . 'cache' . DIRECTORY_SEPARATOR); +// or if you have APC installed +//$cache = array('cache' => 'apc'); +//$liquid->setCache($cache); + +$liquid->parse(file_get_contents($protectedPath . 'templates' . DIRECTORY_SEPARATOR . 'child.tpl')); + +$assigns = array( + 'document' => array( + 'title' => 'This is php-liquid', + 'content' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.', + 'copyright' => '© Copyright 2014 Guz Alexander - All rights reserved.', + ), +); + +echo $liquid->render($assigns); diff --git a/package/vendor/liquid/liquid/examples/filters.php b/package/vendor/liquid/liquid/examples/filters.php new file mode 100644 index 0000000..95c80c2 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/filters.php @@ -0,0 +1,25 @@ +registerFilter('absolute_url', function ($arg) { + return "https://www.example.com$arg"; +}); +$template->parse("{{ my_url | absolute_url }}"); +echo $template->render(array( + 'my_url' => '/test' +)); +// expect: https://www.example.com/test diff --git a/package/vendor/liquid/liquid/examples/index.php b/package/vendor/liquid/liquid/examples/index.php new file mode 100644 index 0000000..850fe42 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/index.php @@ -0,0 +1,70 @@ + 'file', 'cache_dir' => $protectedPath . 'cache' . DIRECTORY_SEPARATOR); +// or if you have APC installed +//$cache = array('cache' => 'apc'); +//$liquid->setCache($cache); + +$liquid->parse(file_get_contents($protectedPath . 'templates' . DIRECTORY_SEPARATOR . 'index.tpl')); + +$assigns = array( + 'document' => array( + 'title' => 'This is php-liquid', + 'content' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.', + 'copyright' => 'Guz Alexander - All rights reserved.', + ), + 'blog' => array( + array( + 'title' => 'Blog Title 1', + 'content' => 'Nunc putamus parum claram', + 'tags' => array('claram', 'parum'), + 'comments' => array( + array( + 'title' => 'First Comment', + 'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr', + ), + ), + ), + array( + 'title' => 'Blog Title 2', + 'content' => 'Nunc putamus parum claram', + 'tags' => array('claram', 'parum', 'freestyle'), + 'comments' => array( + array( + 'title' => 'First Comment', + 'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr', + ), + array( + 'title' => 'Second Comment', + 'message' => 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr', + ), + ), + ), + + ), + 'array' => array('one', 'two', 'three', 'four'), +); + +echo $liquid->render($assigns); diff --git a/package/vendor/liquid/liquid/examples/protected/cache/.gitignore b/package/vendor/liquid/liquid/examples/protected/cache/.gitignore new file mode 100644 index 0000000..acb5c9f --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/cache/.gitignore @@ -0,0 +1,2 @@ +!.gitignore +liquid_* diff --git a/package/vendor/liquid/liquid/examples/protected/templates/base.tpl b/package/vendor/liquid/liquid/examples/protected/templates/base.tpl new file mode 100644 index 0000000..36ff6cc --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/base.tpl @@ -0,0 +1,15 @@ +{% comment %} This is the base template. {% endcomment %} + + + + {% include 'header.tpl' %} + + +

{% block content %}{% endblock %}
+ + + diff --git a/package/vendor/liquid/liquid/examples/protected/templates/blocks/child.tpl b/package/vendor/liquid/liquid/examples/protected/templates/blocks/child.tpl new file mode 100644 index 0000000..b4012cb --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/blocks/child.tpl @@ -0,0 +1,6 @@ +{% comment %} This is the child template. {% endcomment %} +{% extends "base.tpl" %} + +{% block footer %} + {{ document.copyright }} +{% endblock %} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/examples/protected/templates/child.tpl b/package/vendor/liquid/liquid/examples/protected/templates/child.tpl new file mode 100644 index 0000000..feab5e8 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/child.tpl @@ -0,0 +1,7 @@ +{% comment %} This is the child template. {% endcomment %} +{% extends "blocks/child.tpl" %} + +{% block content %} +

Entry one

+

This is my first entry.

+{% endblock %} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/examples/protected/templates/footer.tpl b/package/vendor/liquid/liquid/examples/protected/templates/footer.tpl new file mode 100644 index 0000000..68d063b --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/footer.tpl @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/package/vendor/liquid/liquid/examples/protected/templates/header.tpl b/package/vendor/liquid/liquid/examples/protected/templates/header.tpl new file mode 100644 index 0000000..85856d9 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/header.tpl @@ -0,0 +1,2 @@ + +{{ document.title }} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/examples/protected/templates/honorific.html b/package/vendor/liquid/liquid/examples/protected/templates/honorific.html new file mode 100644 index 0000000..a935dd2 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/honorific.html @@ -0,0 +1 @@ +Mx. {{ name }} diff --git a/package/vendor/liquid/liquid/examples/protected/templates/index.tpl b/package/vendor/liquid/liquid/examples/protected/templates/index.tpl new file mode 100644 index 0000000..a1375a2 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/index.tpl @@ -0,0 +1,44 @@ +{% comment %} + +This is a comment block +(c) 2014 Guz Alexander + +{% endcomment %} + + + + {% include 'header' %} + + +

{{ document.title }}

+

{{ document.content }}

+

Link to simple.php

+ {% if blog %} + Total Blogentrys: {{ blog | size }} + + {% endif %} + + {% include 'footer' %} + + diff --git a/package/vendor/liquid/liquid/examples/protected/templates/subfooter.tpl b/package/vendor/liquid/liquid/examples/protected/templates/subfooter.tpl new file mode 100644 index 0000000..829a268 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/protected/templates/subfooter.tpl @@ -0,0 +1 @@ +{{ document.copyright }} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/examples/simple.php b/package/vendor/liquid/liquid/examples/simple.php new file mode 100644 index 0000000..7cea203 --- /dev/null +++ b/package/vendor/liquid/liquid/examples/simple.php @@ -0,0 +1,23 @@ +parse('{{ hello }} {{ goback }}'); + +echo $liquid->render(array('hello' => 'hello world', 'goback' => 'index')); diff --git a/package/vendor/liquid/liquid/infection.json.dist b/package/vendor/liquid/liquid/infection.json.dist new file mode 100644 index 0000000..51839ef --- /dev/null +++ b/package/vendor/liquid/liquid/infection.json.dist @@ -0,0 +1,8 @@ +{ + "timeout": 2, + "source": { + "directories": [ + "src" + ] + } +} diff --git a/package/vendor/liquid/liquid/phpunit.xml.dist b/package/vendor/liquid/liquid/phpunit.xml.dist new file mode 100644 index 0000000..84562a6 --- /dev/null +++ b/package/vendor/liquid/liquid/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + + tests/ + + + + + + src/ + + + diff --git a/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php b/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php new file mode 100644 index 0000000..4ca05b2 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/AbstractBlock.php @@ -0,0 +1,264 @@ +nodelist; + } + + /** + * Parses the given tokens + * + * @param array $tokens + * + * @throws \Liquid\LiquidException + * @return void + */ + public function parse(array &$tokens) + { + $startRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '/'); + $tagRegexp = new Regexp('/^' . Liquid::get('TAG_START') . Liquid::get('WHITESPACE_CONTROL') . '?\s*(\w+)\s*(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('TAG_END') . '$/s'); + $variableStartRegexp = new Regexp('/^' . Liquid::get('VARIABLE_START') . '/'); + + $this->nodelist = array(); + + $tags = Template::getTags(); + + while (count($tokens)) { + $token = array_shift($tokens); + + if ($startRegexp->match($token)) { + $this->whitespaceHandler($token); + if ($tagRegexp->match($token)) { + // If we found the proper block delimitor just end parsing here and let the outer block proceed + if ($tagRegexp->matches[1] == $this->blockDelimiter()) { + $this->endTag(); + return; + } + + $tagName = null; + if (array_key_exists($tagRegexp->matches[1], $tags)) { + $tagName = $tags[$tagRegexp->matches[1]]; + } else { + $tagName = self::TAG_PREFIX . ucwords($tagRegexp->matches[1]); + $tagName = (class_exists($tagName) === true) ? $tagName : null; + } + + if ($tagName !== null) { + $this->nodelist[] = new $tagName($tagRegexp->matches[2], $tokens, $this->fileSystem); + if ($tagRegexp->matches[1] == 'extends') { + return; + } + } else { + $this->unknownTag($tagRegexp->matches[1], $tagRegexp->matches[2], $tokens); + } + } else { + throw new ParseException("Tag $token was not properly terminated (won't match $tagRegexp)"); + } + } elseif ($variableStartRegexp->match($token)) { + $this->whitespaceHandler($token); + $this->nodelist[] = $this->createVariable($token); + } else { + // This is neither a tag or a variable, proceed with an ltrim + if (self::$trimWhitespace) { + $token = ltrim($token); + } + + self::$trimWhitespace = false; + $this->nodelist[] = $token; + } + } + + $this->assertMissingDelimitation(); + } + + /** + * Handle the whitespace. + * + * @param string $token + */ + protected function whitespaceHandler($token) + { + /* + * This assumes that TAG_START is always '{%', and a whitespace control indicator + * is exactly one character long, on a third position. + */ + if (mb_substr($token, 2, 1) === Liquid::get('WHITESPACE_CONTROL')) { + $previousToken = end($this->nodelist); + if (is_string($previousToken)) { // this can also be a tag or a variable + $this->nodelist[key($this->nodelist)] = rtrim($previousToken); + } + } + + /* + * This assumes that TAG_END is always '%}', and a whitespace control indicator + * is exactly one character long, on a third position from the end. + */ + self::$trimWhitespace = mb_substr($token, -3, 1) === Liquid::get('WHITESPACE_CONTROL'); + } + + /** + * Render the block. + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + return $this->renderAll($this->nodelist, $context); + } + + /** + * Renders all the given nodelist's nodes + * + * @param array $list + * @param Context $context + * + * @return string + */ + protected function renderAll(array $list, Context $context) + { + $result = ''; + + foreach ($list as $token) { + if (is_object($token) && method_exists($token, 'render')) { + $value = $token->render($context); + } else { + $value = $token; + } + + if (is_array($value)) { + $value = htmlspecialchars(print_r($value, true)); + } + + $result .= $value; + + if (isset($context->registers['break'])) { + break; + } + if (isset($context->registers['continue'])) { + break; + } + + $context->tick(); + } + + return $result; + } + + /** + * An action to execute when the end tag is reached + */ + protected function endTag() + { + // Do nothing by default + } + + /** + * Handler for unknown tags + * + * @param string $tag + * @param string $params + * @param array $tokens + * + * @throws \Liquid\Exception\ParseException + */ + protected function unknownTag($tag, $params, array $tokens) + { + switch ($tag) { + case 'else': + throw new ParseException($this->blockName() . " does not expect else tag"); + case 'end': + throw new ParseException("'end' is not a valid delimiter for " . $this->blockName() . " tags. Use " . $this->blockDelimiter()); + default: + throw new ParseException("Unknown tag $tag"); + } + } + + /** + * This method is called at the end of parsing, and will throw an error unless + * this method is subclassed, like it is for Document + * + * @throws \Liquid\Exception\ParseException + * @return bool + */ + protected function assertMissingDelimitation() + { + throw new ParseException($this->blockName() . " tag was never closed"); + } + + /** + * Returns the string that delimits the end of the block + * + * @return string + */ + protected function blockDelimiter() + { + return "end" . $this->blockName(); + } + + /** + * Returns the name of the block + * + * @return string + */ + private function blockName() + { + $reflection = new \ReflectionClass($this); + return str_replace('tag', '', strtolower($reflection->getShortName())); + } + + /** + * Create a variable for the given token + * + * @param string $token + * + * @throws \Liquid\Exception\ParseException + * @return Variable + */ + private function createVariable($token) + { + $variableRegexp = new Regexp('/^' . Liquid::get('VARIABLE_START') . Liquid::get('WHITESPACE_CONTROL') . '?(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('VARIABLE_END') . '$/s'); + if ($variableRegexp->match($token)) { + return new Variable($variableRegexp->matches[1]); + } + + throw new ParseException("Variable $token was not properly terminated"); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php b/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php new file mode 100644 index 0000000..6b38267 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/AbstractTag.php @@ -0,0 +1,100 @@ +markup = $markup; + $this->fileSystem = $fileSystem; + $this->parse($tokens); + } + + /** + * Parse the given tokens. + * + * @param array $tokens + */ + public function parse(array &$tokens) + { + // Do nothing by default + } + + /** + * Render the tag with the given context. + * + * @param Context $context + * + * @return string + */ + abstract public function render(Context $context); + + /** + * Extracts tag attributes from a markup string. + * + * @param string $markup + */ + protected function extractAttributes($markup) + { + $this->attributes = array(); + + $attributeRegexp = new Regexp(Liquid::get('TAG_ATTRIBUTES')); + + $matches = $attributeRegexp->scan($markup); + + foreach ($matches as $match) { + $this->attributes[$match[0]] = $match[1]; + } + } + + /** + * Returns the name of the tag. + * + * @return string + */ + protected function name() + { + return strtolower(get_class($this)); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache.php b/package/vendor/liquid/liquid/src/Liquid/Cache.php new file mode 100644 index 0000000..28749a3 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache.php @@ -0,0 +1,78 @@ +expire = $options['cache_expire']; + } + + if (isset($options['cache_prefix'])) { + $this->prefix = $options['cache_prefix']; + } + } + + /** + * Retrieves a value from cache with a specified key. + * + * @param string $key a unique key identifying the cached value + * @param bool $unserialize + * + * @return mixed|boolean the value stored in cache, false if the value is not in the cache or expired. + */ + abstract public function read($key, $unserialize = true); + + /** + * Check if specified key exists in cache. + * + * @param string $key a unique key identifying the cached value + * + * @return boolean true if the key is in cache, false otherwise + */ + abstract public function exists($key); + + /** + * Stores a value identified by a key in cache. + * + * @param string $key the key identifying the value to be cached + * @param mixed $value the value to be cached + * @param bool $serialize + * + * @return boolean true if the value is successfully stored into cache, false otherwise + */ + abstract public function write($key, $value, $serialize = true); + + /** + * Deletes all values from cache. + * + * @param bool $expiredOnly + * + * @return boolean whether the flush operation was successful. + */ + abstract public function flush($expiredOnly = false); +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php b/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php new file mode 100644 index 0000000..b52d909 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/Apc.php @@ -0,0 +1,74 @@ +prefix . $key); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + apc_fetch($this->prefix . $key, $success); + return (bool) $success; + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + return apc_store($this->prefix . $key, $value, $this->expire); + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + return apc_clear_cache('user'); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/File.php b/package/vendor/liquid/liquid/src/Liquid/Cache/File.php new file mode 100644 index 0000000..ac0358e --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/File.php @@ -0,0 +1,106 @@ +path = realpath($options['cache_dir']) . DIRECTORY_SEPARATOR; + } else { + throw new NotFoundException('Cachedir not exists or not writable'); + } + } + + /** + * {@inheritdoc} + */ + public function read($key, $unserialize = true) + { + if (!$this->exists($key)) { + return false; + } + + if ($unserialize) { + return unserialize(file_get_contents($this->path . $this->prefix . $key)); + } + + return file_get_contents($this->path . $this->prefix . $key); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + $cacheFile = $this->path . $this->prefix . $key; + + if (!file_exists($cacheFile) || filemtime($cacheFile) + $this->expire < time()) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + $bytes = file_put_contents($this->path . $this->prefix . $key, $serialize ? serialize($value) : $value); + $this->gc(); + + return $bytes !== false; + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + foreach (glob($this->path . $this->prefix . '*') as $file) { + if ($expiredOnly) { + if (filemtime($file) + $this->expire < time()) { + unlink($file); + } + } else { + unlink($file); + } + } + } + + /** + * {@inheritdoc} + */ + protected function gc() + { + $this->flush(true); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php b/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php new file mode 100644 index 0000000..f48415e --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Cache/Local.php @@ -0,0 +1,60 @@ +cache[$key])) { + return $this->cache[$key]; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return isset($this->cache[$key]); + } + + /** + * {@inheritdoc} + */ + public function write($key, $value, $serialize = true) + { + $this->cache[$key] = $value; + return true; + } + + /** + * {@inheritdoc} + */ + public function flush($expiredOnly = false) + { + $this->cache = array(); + return true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Context.php b/package/vendor/liquid/liquid/src/Liquid/Context.php new file mode 100644 index 0000000..251faa0 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Context.php @@ -0,0 +1,458 @@ +assigns = array($assigns); + $this->registers = $registers; + $this->filterbank = new Filterbank($this); + + // first empty array serves as source for overrides, e.g. as in TagDecrement + $this->environments = array(array(), array()); + + if (Liquid::get('EXPOSE_SERVER')) { + $this->environments[1] = $_SERVER; + } else { + $this->environments[1] = array_filter( + $_SERVER, + function ($key) { + return in_array( + $key, + (array)Liquid::get('SERVER_SUPERGLOBAL_WHITELIST') + ); + }, + ARRAY_FILTER_USE_KEY + ); + } + } + + /** + * Sets a tick function, this function is called sometimes while liquid is rendering a template. + * + * @param callable $tickFunction + */ + public function setTickFunction(callable $tickFunction) + { + $this->tickFunction = $tickFunction; + } + + /** + * Add a filter to the context + * + * @param mixed $filter + */ + public function addFilters($filter, callable $callback = null) + { + $this->filterbank->addFilter($filter, $callback); + } + + /** + * Invoke the filter that matches given name + * + * @param string $name The name of the filter + * @param mixed $value The value to filter + * @param array $args Additional arguments for the filter + * + * @return string + */ + public function invoke($name, $value, array $args = array()) + { + try { + return $this->filterbank->invoke($name, $value, $args); + } catch (\TypeError $typeError) { + throw new LiquidException($typeError->getMessage(), 0, $typeError); + } + } + + /** + * Merges the given assigns into the current assigns + * + * @param array $newAssigns + */ + public function merge($newAssigns) + { + $this->assigns[0] = array_merge($this->assigns[0], $newAssigns); + } + + /** + * Push new local scope on the stack. + * + * @return bool + */ + public function push() + { + array_unshift($this->assigns, array()); + return true; + } + + /** + * Pops the current scope from the stack. + * + * @throws LiquidException + * @return bool + */ + public function pop() + { + if (count($this->assigns) == 1) { + throw new LiquidException('No elements to pop'); + } + + array_shift($this->assigns); + } + + /** + * Replaces [] + * + * @param string + * @param mixed $key + * + * @return mixed + */ + public function get($key) + { + return $this->resolve($key); + } + + /** + * Replaces []= + * + * @param string $key + * @param mixed $value + * @param bool $global + */ + public function set($key, $value, $global = false) + { + if ($global) { + for ($i = 0; $i < count($this->assigns); $i++) { + $this->assigns[$i][$key] = $value; + } + } else { + $this->assigns[0][$key] = $value; + } + } + + /** + * Returns true if the given key will properly resolve + * + * @param string $key + * + * @return bool + */ + public function hasKey($key) + { + return (!is_null($this->resolve($key))); + } + + /** + * Resolve a key by either returning the appropriate literal or by looking up the appropriate variable + * + * Test for empty has been moved to interpret condition, in Decision + * + * @param string $key + * + * @throws LiquidException + * @return mixed + */ + private function resolve($key) + { + // This shouldn't happen + if (is_array($key)) { + throw new LiquidException("Cannot resolve arrays as key"); + } + + if (is_null($key) || $key == 'null') { + return null; + } + + if ($key == 'true') { + return true; + } + + if ($key == 'false') { + return false; + } + + if (preg_match('/^\'(.*)\'$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^"(.*)"$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^(-?\d+)$/', $key, $matches)) { + return $matches[1]; + } + + if (preg_match('/^(-?\d[\d\.]+)$/', $key, $matches)) { + return $matches[1]; + } + + return $this->variable($key); + } + + /** + * Fetches the current key in all the scopes + * + * @param string $key + * + * @return mixed + */ + private function fetch($key) + { + // TagDecrement depends on environments being checked before assigns + foreach ($this->environments as $environment) { + if (array_key_exists($key, $environment)) { + return $environment[$key]; + } + } + + foreach ($this->assigns as $scope) { + if (array_key_exists($key, $scope)) { + $obj = $scope[$key]; + + if ($obj instanceof Drop) { + $obj->setContext($this); + } + + return $obj; + } + } + + return null; + } + + /** + * Resolved the namespaced queries gracefully. + * + * @param string $key + * + * @see Decision::stringValue + * @see AbstractBlock::renderAll + * + * @throws LiquidException + * @return mixed + */ + private function variable($key) + { + // Support numeric and variable array indicies + if (preg_match("|\[[0-9]+\]|", $key)) { + $key = preg_replace("|\[([0-9]+)\]|", ".$1", $key); + } elseif (preg_match("|\[[0-9a-z._]+\]|", $key, $matches)) { + $index = $this->get(str_replace(array("[", "]"), "", $matches[0])); + if (strlen($index)) { + $key = preg_replace("|\[([0-9a-z._]+)\]|", ".$index", $key); + } + } + + $parts = explode(Liquid::get('VARIABLE_ATTRIBUTE_SEPARATOR'), $key); + + $object = $this->fetch(array_shift($parts)); + + while (count($parts) > 0) { + // since we still have a part to consider + // and since we can't dig deeper into plain values + // it can be thought as if it has a property with a null value + if (!is_object($object) && !is_array($object) && !is_string($object)) { + return null; + } + + // first try to cast an object to an array or value + if (is_object($object)) { + if (method_exists($object, 'toLiquid')) { + $object = $object->toLiquid(); + } elseif (method_exists($object, 'toArray')) { + $object = $object->toArray(); + } + } + + if (is_null($object)) { + return null; + } + + if ($object instanceof Drop) { + $object->setContext($this); + } + + $nextPartName = array_shift($parts); + + if (is_string($object)) { + if ($nextPartName == 'size') { + // if the last part of the context variable is .size we return the string length + return mb_strlen($object); + } + + // no other special properties for strings, yet + return null; + } + + if (is_array($object)) { + // if the last part of the context variable is .first we return the first array element + if ($nextPartName == 'first' && count($parts) == 0 && !array_key_exists('first', $object)) { + return StandardFilters::first($object); + } + + // if the last part of the context variable is .last we return the last array element + if ($nextPartName == 'last' && count($parts) == 0 && !array_key_exists('last', $object)) { + return StandardFilters::last($object); + } + + // if the last part of the context variable is .size we just return the count + if ($nextPartName == 'size' && count($parts) == 0 && !array_key_exists('size', $object)) { + return count($object); + } + + // no key - no value + if (!array_key_exists($nextPartName, $object)) { + return null; + } + + $object = $object[$nextPartName]; + continue; + } + + if (!is_object($object)) { + // we got plain value, yet asked to resolve a part + // think plain values have a null part with any name + return null; + } + + if ($object instanceof \Countable) { + // if the last part of the context variable is .size we just return the count + if ($nextPartName == 'size' && count($parts) == 0) { + return count($object); + } + } + + if ($object instanceof Drop) { + // if the object is a drop, make sure it supports the given method + if (!$object->hasKey($nextPartName)) { + return null; + } + + $object = $object->invokeDrop($nextPartName); + continue; + } + + // if it has `get` or `field_exists` methods + if (method_exists($object, Liquid::get('HAS_PROPERTY_METHOD'))) { + if (!call_user_func(array($object, Liquid::get('HAS_PROPERTY_METHOD')), $nextPartName)) { + return null; + } + + $object = call_user_func(array($object, Liquid::get('GET_PROPERTY_METHOD')), $nextPartName); + continue; + } + + // if it's just a regular object, attempt to access a public method + if (is_callable(array($object, $nextPartName))) { + $object = call_user_func(array($object, $nextPartName)); + continue; + } + + // if a magic accessor method present... + if (is_object($object) && method_exists($object, '__get')) { + $object = $object->$nextPartName; + continue; + } + + // Inexistent property is a null, PHP-speak + if (!property_exists($object, $nextPartName)) { + return null; + } + + // then try a property (independent of accessibility) + if (property_exists($object, $nextPartName)) { + $object = $object->$nextPartName; + continue; + } + + // we'll try casting this object in the next iteration + } + + // lastly, try to get an embedded value of an object + // value could be of any type, not just string, so we have to do this + // conversion here, not later in AbstractBlock::renderAll + if (is_object($object) && method_exists($object, 'toLiquid')) { + $object = $object->toLiquid(); + } + + /* + * Before here were checks for object types and object to string conversion. + * + * Now we just return what we have: + * - Traversable objects are taken care of inside filters + * - Object-to-string conversion is handled at the last moment in Decision::stringValue, and in AbstractBlock::renderAll + * + * This way complex objects could be passed between templates and to filters + */ + + return $object; + } + + public function tick() + { + if ($this->tickFunction === null) { + return; + } + + $tickFunction = $this->tickFunction; + $tickFunction($this); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php b/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php new file mode 100644 index 0000000..de524a0 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/CustomFilters.php @@ -0,0 +1,32 @@ +valid(); + } + + // toLiquid is handled in Context::variable + $class = get_class($value); + throw new RenderException("Value of type $class has no `toLiquid` nor `__toString` methods"); + } + + // Arrays simply return true + if (is_array($value)) { + return $value; + } + + return $value; + } + + /** + * Check to see if to variables are equal in a given context + * + * @param string $left + * @param string $right + * @param Context $context + * + * @return bool + */ + protected function equalVariables($left, $right, Context $context) + { + $left = $this->stringValue($context->get($left)); + $right = $this->stringValue($context->get($right)); + + return ($left == $right); + } + + /** + * Interpret a comparison + * + * @param string $left + * @param string $right + * @param string $op + * @param Context $context + * + * @throws \Liquid\Exception\RenderException + * @return bool + */ + protected function interpretCondition($left, $right, $op, Context $context) + { + if (is_null($op)) { + $value = $this->stringValue($context->get($left)); + return $value; + } + + // values of 'empty' have a special meaning in array comparisons + if ($right == 'empty' && is_array($context->get($left))) { + $left = count($context->get($left)); + $right = 0; + } elseif ($left == 'empty' && is_array($context->get($right))) { + $right = count($context->get($right)); + $left = 0; + } else { + $left = $context->get($left); + $right = $context->get($right); + + $left = $this->stringValue($left); + $right = $this->stringValue($right); + } + + // special rules for null values + if (is_null($left) || is_null($right)) { + // null == null returns true + if ($op == '==' && is_null($left) && is_null($right)) { + return true; + } + + // null != anything other than null return true + if ($op == '!=' && (!is_null($left) || !is_null($right))) { + return true; + } + + // everything else, return false; + return false; + } + + // regular rules + switch ($op) { + case '==': + return ($left == $right); + + case '!=': + return ($left != $right); + + case '>': + return ($left > $right); + + case '<': + return ($left < $right); + + case '>=': + return ($left >= $right); + + case '<=': + return ($left <= $right); + + case 'contains': + return is_array($left) ? in_array($right, $left) : (strpos($left, $right) !== false); + + default: + throw new RenderException("Error in tag '" . $this->name() . "' - Unknown operator $op"); + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Document.php b/package/vendor/liquid/liquid/src/Liquid/Document.php new file mode 100644 index 0000000..ec7e350 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Document.php @@ -0,0 +1,95 @@ +fileSystem = $fileSystem; + $this->parse($tokens); + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see \Liquid\Tag\TagInclude::hasIncludes() + * @see \Liquid\Tag\TagExtends::hasIncludes() + * @return bool if need to discard cache + */ + public function hasIncludes() + { + $seenExtends = false; + $seenBlock = false; + + foreach ($this->nodelist as $token) { + if ($token instanceof TagExtends) { + $seenExtends = true; + } elseif ($token instanceof TagBlock) { + $seenBlock = true; + } + } + + /* + * We try to keep the base templates in cache (that not extend anything). + * + * At the same time if we re-render all other blocks we see, we avoid most + * if not all related caching quirks. This may be suboptimal. + */ + if ($seenBlock && !$seenExtends) { + return true; + } + + foreach ($this->nodelist as $token) { + // check any of the tokens for includes + if ($token instanceof TagInclude && $token->hasIncludes()) { + return true; + } + + if ($token instanceof TagExtends && $token->hasIncludes()) { + return true; + } + } + + return false; + } + + /** + * There isn't a real delimiter + * + * @return string + */ + protected function blockDelimiter() + { + return ''; + } + + /** + * Document blocks don't need to be terminated since they are not actually opened + */ + protected function assertMissingDelimitation() + { + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Drop.php b/package/vendor/liquid/liquid/src/Liquid/Drop.php new file mode 100644 index 0000000..7adbf62 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Drop.php @@ -0,0 +1,107 @@ + 'sales', 'limit' => 10 )); + * } + * } + * + * tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) + * tmpl.render('product' => ProductDrop.new ) // will invoke topSales query. + * + * Your drop can either implement the methods sans any parameters or implement the beforeMethod(name) method which is a + * catch all. + */ +abstract class Drop +{ + /** + * @var Context + */ + protected $context; + + /** + * Catch all method that is invoked before a specific method + * + * @param string $method + * + * @return null + */ + protected function beforeMethod($method) + { + return null; + } + + /** + * @param Context $context + */ + public function setContext(Context $context) + { + $this->context = $context; + } + + /** + * Invoke a specific method + * + * @param string $method + * + * @return mixed + */ + public function invokeDrop($method) + { + $result = $this->beforeMethod($method); + + if (is_null($result) && is_callable(array($this, $method))) { + $result = $this->$method(); + } + + return $result; + } + + /** + * Returns true if the drop supports the given method + * + * @param string $name + * + * @return bool + */ + public function hasKey($name) + { + return true; + } + + /** + * @return Drop + */ + public function toLiquid() + { + return $this; + } + + /** + * @return string + */ + public function __toString() + { + return get_class($this); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php b/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php new file mode 100644 index 0000000..4ccb6b7 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Exception/CacheException.php @@ -0,0 +1,21 @@ +root = $root; + } + + /** + * Retrieve a template file + * + * @param string $templatePath + * + * @return string template content + */ + public function readTemplateFile($templatePath) + { + return file_get_contents($this->fullPath($templatePath)); + } + + /** + * Resolves a given path to a full template file path, making sure it's valid + * + * @param string $templatePath + * + * @throws \Liquid\Exception\ParseException + * @throws \Liquid\Exception\NotFoundException + * @return string + */ + public function fullPath($templatePath) + { + if (empty($templatePath)) { + throw new ParseException("Empty template name"); + } + + $nameRegex = Liquid::get('INCLUDE_ALLOW_EXT') + ? new Regexp('/^[^.\/][a-zA-Z0-9_\.\/-]+$/') + : new Regexp('/^[^.\/][a-zA-Z0-9_\/-]+$/'); + + if (!$nameRegex->match($templatePath)) { + throw new ParseException("Illegal template name '$templatePath'"); + } + + $templateDir = dirname($templatePath); + $templateFile = basename($templatePath); + + if (!Liquid::get('INCLUDE_ALLOW_EXT')) { + $templateFile = Liquid::get('INCLUDE_PREFIX') . $templateFile . '.' . Liquid::get('INCLUDE_SUFFIX'); + } + + $fullPath = join(DIRECTORY_SEPARATOR, array($this->root, $templateDir, $templateFile)); + + $realFullPath = realpath($fullPath); + if ($realFullPath === false) { + throw new NotFoundException("File not found: $fullPath"); + } + + if (strpos($realFullPath, $this->root) !== 0) { + throw new NotFoundException("Illegal template full path: {$realFullPath} not under {$this->root}"); + } + + return $realFullPath; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php b/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php new file mode 100644 index 0000000..3ceda2f --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/FileSystem/Virtual.php @@ -0,0 +1,64 @@ +callback = $callback; + } + + /** + * Retrieve a template file + * + * @param string $templatePath + * + * @return string template content + */ + public function readTemplateFile($templatePath) + { + return call_user_func($this->callback, $templatePath); + } + + public function __sleep() + { + // we cannot serialize a closure + if ($this->callback instanceof \Closure) { + throw new FilesystemException("Virtual file system with a Closure as a callback cannot be used with a serializing cache"); + } + + return array_keys(get_object_vars($this)); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Filterbank.php b/package/vendor/liquid/liquid/src/Liquid/Filterbank.php new file mode 100644 index 0000000..53775c6 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Filterbank.php @@ -0,0 +1,153 @@ +context = $context; + + $this->addFilter(\Liquid\StandardFilters::class); + $this->addFilter(\Liquid\CustomFilters::class); + } + + /** + * Adds a filter to the bank + * + * @param mixed $filter Can either be an object, the name of a class (in which case the + * filters will be called statically) or the name of a function. + * + * @throws \Liquid\Exception\WrongArgumentException + * @return bool + */ + public function addFilter($filter, callable $callback = null) + { + // If it is a callback, save it as it is + if (is_string($filter) && $callback) { + $this->methodMap[$filter] = $callback; + return true; + } + + // If the filter is a class, register all its static methods + if (is_string($filter) && class_exists($filter)) { + $reflection = new \ReflectionClass($filter); + foreach ($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) { + $this->methodMap[$method->name] = $method->class; + } + + return true; + } + + // If it's a global function, register it simply + if (is_string($filter) && function_exists($filter)) { + $this->methodMap[$filter] = false; + return true; + } + + // If it isn't an object an isn't a string either, it's a bad parameter + if (!is_object($filter)) { + throw new WrongArgumentException("Parameter passed to addFilter must be an object or a string"); + } + + // If the passed filter was an object, store the object for future reference. + $filter->context = $this->context; + $className = get_class($filter); + $this->filters[$className] = $filter; + + // Then register all public static and not methods as filters + foreach (get_class_methods($filter) as $method) { + if (strtolower($method) === '__construct') { + continue; + } + $this->methodMap[$method] = $className; + } + + return true; + } + + /** + * Invokes the filter with the given name + * + * @param string $name The name of the filter + * @param string $value The value to filter + * @param array $args The additional arguments for the filter + * + * @return string + */ + public function invoke($name, $value, array $args = array()) + { + // workaround for a single standard filter being a reserved keyword - we can't use overloading for static calls + if ($name == 'default') { + $name = '_default'; + } + + array_unshift($args, $value); + + // Consult the mapping + if (!isset($this->methodMap[$name])) { + return $value; + } + + $class = $this->methodMap[$name]; + + // If we have a callback + if (is_callable($class)) { + return call_user_func_array($class, $args); + } + + // If we have a registered object for the class, use that instead + if (isset($this->filters[$class])) { + $class = $this->filters[$class]; + } + + // If we're calling a function + if ($class === false) { + return call_user_func_array($name, $args); + } + + // Call a class or an instance method + return call_user_func_array(array($class, $name), $args); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Liquid.php b/package/vendor/liquid/liquid/src/Liquid/Liquid.php new file mode 100644 index 0000000..6f872ab --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Liquid.php @@ -0,0 +1,170 @@ + 'field_exists', + + // This method is called on object when resolving variables when + // a given property exists. + 'GET_PROPERTY_METHOD' => 'get', + + // Separator between filters. + 'FILTER_SEPARATOR' => '\|', + + // Separator for arguments. + 'ARGUMENT_SEPARATOR' => ',', + + // Separator for argument names and values. + 'FILTER_ARGUMENT_SEPARATOR' => ':', + + // Separator for variable attributes. + 'VARIABLE_ATTRIBUTE_SEPARATOR' => '.', + + // Allow template names with extension in include and extends tags. + 'INCLUDE_ALLOW_EXT' => false, + + // Suffix for include files. + 'INCLUDE_SUFFIX' => 'liquid', + + // Prefix for include files. + 'INCLUDE_PREFIX' => '_', + + // Whitespace control. + 'WHITESPACE_CONTROL' => '-', + + // Tag start. + 'TAG_START' => '{%', + + // Tag end. + 'TAG_END' => '%}', + + // Variable start. + 'VARIABLE_START' => '{{', + + // Variable end. + 'VARIABLE_END' => '}}', + + // Variable name. + 'VARIABLE_NAME' => '[a-zA-Z_][a-zA-Z_0-9.-]*', + + 'QUOTED_STRING' => '(?:"[^"]*"|\'[^\']*\')', + 'QUOTED_STRING_FILTER_ARGUMENT' => '"[^"]*"|\'[^\']*\'', + + // Automatically escape any variables unless told otherwise by a "raw" filter + 'ESCAPE_BY_DEFAULT' => false, + + // The name of the key to use when building pagination query strings e.g. ?page=1 + 'PAGINATION_REQUEST_KEY' => 'page', + + // The name of the context key used to denote the current page number + 'PAGINATION_CONTEXT_KEY' => 'page', + + // Whenever variables from $_SERVER should be directly available to templates + 'EXPOSE_SERVER' => false, + + // $_SERVER variables whitelist - exposed even when EXPOSE_SERVER is false + 'SERVER_SUPERGLOBAL_WHITELIST' => [ + 'HTTP_ACCEPT', + 'HTTP_ACCEPT_CHARSET', + 'HTTP_ACCEPT_ENCODING', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_CONNECTION', + 'HTTP_HOST', + 'HTTP_REFERER', + 'HTTP_USER_AGENT', + 'HTTPS', + 'REQUEST_METHOD', + 'REQUEST_URI', + 'SERVER_NAME', + ], + ); + + /** + * Get a configuration setting. + * + * @param string $key setting key + * + * @return string + */ + public static function get($key) + { + // backward compatibility + if ($key === 'ALLOWED_VARIABLE_CHARS') { + return substr(self::$config['VARIABLE_NAME'], 0, -1); + } + if (array_key_exists($key, self::$config)) { + return self::$config[$key]; + } + // This case is needed for compound settings + switch ($key) { + case 'QUOTED_FRAGMENT': + return '(?:' . self::get('QUOTED_STRING') . '|(?:[^\s,\|\'"]|' . self::get('QUOTED_STRING') . ')+)'; + case 'TAG_ATTRIBUTES': + return '/(\w+)\s*\:\s*(' . self::get('QUOTED_FRAGMENT') . ')/'; + case 'TOKENIZATION_REGEXP': + return '/(' . self::$config['TAG_START'] . '.*?' . self::$config['TAG_END'] . '|' . self::$config['VARIABLE_START'] . '.*?' . self::$config['VARIABLE_END'] . ')/s'; + default: + return null; + } + } + + /** + * Changes/creates a setting. + * + * @param string $key + * @param string $value + */ + public static function set($key, $value) + { + // backward compatibility + if ($key === 'ALLOWED_VARIABLE_CHARS') { + $key = 'VARIABLE_NAME'; + $value .= '+'; + } + self::$config[$key] = $value; + } + + /** + * Flatten a multidimensional array into a single array. Does not maintain keys. + * + * @param array $array + * + * @return array + */ + public static function arrayFlatten($array) + { + $return = array(); + + foreach ($array as $element) { + if (is_array($element)) { + $return = array_merge($return, self::arrayFlatten($element)); + } else { + $return[] = $element; + } + } + return $return; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/LiquidException.php b/package/vendor/liquid/liquid/src/Liquid/LiquidException.php new file mode 100644 index 0000000..402c69d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/LiquidException.php @@ -0,0 +1,19 @@ +pattern = (substr($pattern, 0, 1) != '/') + ? '/' . $this->quote($pattern) . '/' + : $pattern; + } + + /** + * Quotes regular expression characters + * + * @param string $string + * + * @return string + */ + public function quote($string) + { + return preg_quote($string, '/'); + } + + /** + * Returns an array of matches for the string in the same way as Ruby's scan method + * + * @param string $string + * + * @return array + */ + public function scan($string) + { + preg_match_all($this->pattern, $string, $matches); + + if (count($matches) == 1) { + return $matches[0]; + } + + array_shift($matches); + + $result = array(); + + foreach ($matches as $matchKey => $subMatches) { + foreach ($subMatches as $subMatchKey => $subMatch) { + $result[$subMatchKey][$matchKey] = $subMatch; + } + } + + return $result; + } + + /** + * Matches the given string. Only matches once. + * + * @param string $string + * + * @return int 1 if there was a match, 0 if there wasn't + */ + public function match($string) + { + return preg_match($this->pattern, $string, $this->matches); + } + + /** + * Matches the given string. Matches all. + * + * @param string $string + * + * @return int The number of matches + */ + public function matchAll($string) + { + return preg_match_all($this->pattern, $string, $this->matches); + } + + /** + * Splits the given string + * + * @param string $string + * @param int $limit Limits the amount of results returned + * + * @return array + */ + public function split($string, $limit = -1) + { + return preg_split($this->pattern, $string, $limit); + } + + /** + * Returns the original pattern primarily for debugging purposes + * + * @return string + */ + public function __toString() + { + return $this->pattern; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php b/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php new file mode 100644 index 0000000..fa02247 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/StandardFilters.php @@ -0,0 +1,727 @@ +rewind(); + return $input->current(); + } + return is_array($input) ? reset($input) : $input; + } + + + /** + * @param mixed $input number + * + * @return int + */ + public static function floor($input) + { + return (int) floor((float)$input); + } + + + /** + * Joins elements of an array with a given character between them + * + * @param array|\Traversable $input + * @param string $glue + * + * @return string + */ + public static function join($input, $glue = ' ') + { + if ($input instanceof \Traversable) { + $str = ''; + foreach ($input as $elem) { + if ($str) { + $str .= $glue; + } + $str .= $elem; + } + return $str; + } + return is_array($input) ? implode($glue, $input) : $input; + } + + + /** + * Returns the last element of an array + * + * @param array|\Traversable $input + * + * @return mixed + */ + public static function last($input) + { + if ($input instanceof \Traversable) { + $last = null; + foreach ($input as $elem) { + $last = $elem; + } + return $last; + } + return is_array($input) ? end($input) : $input; + } + + + /** + * @param string $input + * + * @return string + */ + public static function lstrip($input) + { + return ltrim($input); + } + + + /** + * Map/collect on a given property + * + * @param array|\Traversable $input + * @param string $property + * + * @return string + */ + public static function map($input, $property) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + if (!is_array($input)) { + return $input; + } + return array_map(function ($elem) use ($property) { + if (is_callable($elem)) { + return $elem(); + } elseif (is_array($elem) && array_key_exists($property, $elem)) { + return $elem[$property]; + } + return null; + }, $input); + } + + + /** + * subtraction + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function minus($input, $operand) + { + return (float)$input - (float)$operand; + } + + + /** + * modulo + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function modulo($input, $operand) + { + return fmod((float)$input, (float)$operand); + } + + + /** + * Replace each newline (\n) with html break + * + * @param string $input + * + * @return string + */ + public static function newline_to_br($input) + { + return is_string($input) ? str_replace("\n", "
\n", $input) : $input; + } + + + /** + * addition + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function plus($input, $operand) + { + return (float)$input + (float)$operand; + } + + + /** + * Prepend a string to another + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function prepend($input, $string) + { + return $string . $input; + } + + + /** + * Remove a substring + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function remove($input, $string) + { + return str_replace($string, '', $input); + } + + + /** + * Remove the first occurrences of a substring + * + * @param string $input + * @param string $string + * + * @return string + */ + public static function remove_first($input, $string) + { + if (($pos = strpos($input, $string)) !== false) { + $input = substr_replace($input, '', $pos, strlen($string)); + } + + return $input; + } + + + /** + * Replace occurrences of a string with another + * + * @param string $input + * @param string $string + * @param string $replacement + * + * @return string + */ + public static function replace($input, $string, $replacement = '') + { + return str_replace($string, $replacement, $input); + } + + + /** + * Replace the first occurrences of a string with another + * + * @param string $input + * @param string $string + * @param string $replacement + * + * @return string + */ + public static function replace_first($input, $string, $replacement = '') + { + if (($pos = strpos($input, $string)) !== false) { + $input = substr_replace($input, $replacement, $pos, strlen($string)); + } + + return $input; + } + + + /** + * Reverse the elements of an array + * + * @param array|\Traversable $input + * + * @return array + */ + public static function reverse($input) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + return array_reverse($input); + } + + + /** + * Round a number + * + * @param float $input + * @param int $n precision + * + * @return float + */ + public static function round($input, $n = 0) + { + return round((float)$input, (int)$n); + } + + + /** + * @param string $input + * + * @return string + */ + public static function rstrip($input) + { + return rtrim($input); + } + + + /** + * Return the size of an array or of an string + * + * @param mixed $input + * @throws RenderException + * @return int + */ + public static function size($input) + { + if ($input instanceof \Iterator) { + return iterator_count($input); + } + + if (is_array($input)) { + return count($input); + } + + if (is_object($input)) { + if (method_exists($input, 'size')) { + return $input->size(); + } + + if (!method_exists($input, '__toString')) { + $class = get_class($input); + throw new RenderException("Size of $class cannot be estimated: it has no method 'size' nor can be converted to a string"); + } + } + + // only plain values and stringable objects left at this point + return strlen($input); + } + + + /** + * @param array|\Iterator|string $input + * @param int $offset + * @param int $length + * + * @return array|\Iterator|string + */ + public static function slice($input, $offset, $length = null) + { + if ($input instanceof \Iterator) { + $input = iterator_to_array($input); + } + if (is_array($input)) { + $input = array_slice($input, $offset, $length); + } elseif (is_string($input)) { + $input = mb_substr($input, $offset, $length); + } + + return $input; + } + + + /** + * Sort the elements of an array + * + * @param array|\Traversable $input + * @param string $property use this property of an array element + * + * @return array + */ + public static function sort($input, $property = null) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + if ($property === null) { + asort($input); + } else { + $first = reset($input); + if ($first !== false && is_array($first) && array_key_exists($property, $first)) { + uasort($input, function ($a, $b) use ($property) { + if ($a[$property] == $b[$property]) { + return 0; + } + + return $a[$property] < $b[$property] ? -1 : 1; + }); + } + } + + return $input; + } + + /** + * Explicit string conversion. + * + * @param mixed $input + * + * @return string + */ + public static function string($input) + { + return strval($input); + } + + /** + * Split input string into an array of substrings separated by given pattern. + * + * @param string $input + * @param string $pattern + * + * @return array + */ + public static function split($input, $pattern) + { + if ($input === '' || $input === null) { + return []; + } + + return explode($pattern, $input); + } + + + /** + * @param string $input + * + * @return string + */ + public static function strip($input) + { + return trim($input); + } + + + /** + * Removes html tags from text + * + * @param string $input + * + * @return string + */ + public static function strip_html($input) + { + return is_string($input) ? strip_tags($input) : $input; + } + + + /** + * Strip all newlines (\n, \r) from string + * + * @param string $input + * + * @return string + */ + public static function strip_newlines($input) + { + return is_string($input) ? str_replace(array( + "\n", "\r" + ), '', $input) : $input; + } + + + /** + * multiplication + * + * @param float $input + * @param float $operand + * + * @return float + */ + public static function times($input, $operand) + { + return (float)$input * (float)$operand; + } + + + /** + * Truncate a string down to x characters + * + * @param string $input + * @param int $characters + * @param string $ending string to append if truncated + * + * @return string + */ + public static function truncate($input, $characters = 100, $ending = '...') + { + if (is_string($input) || is_numeric($input)) { + if (strlen($input) > $characters) { + return mb_substr($input, 0, $characters) . $ending; + } + } + + return $input; + } + + + /** + * Truncate string down to x words + * + * @param string $input + * @param int $words + * @param string $ending string to append if truncated + * + * @return string + */ + public static function truncatewords($input, $words = 3, $ending = '...') + { + if (is_string($input)) { + $wordlist = explode(" ", $input); + + if (count($wordlist) > $words) { + return implode(" ", array_slice($wordlist, 0, $words)) . $ending; + } + } + + return $input; + } + + + /** + * Remove duplicate elements from an array + * + * @param array|\Traversable $input + * + * @return array + */ + public static function uniq($input) + { + if ($input instanceof \Traversable) { + $input = iterator_to_array($input); + } + return array_unique($input); + } + + + /** + * Convert an input to uppercase + * + * @param string $input + * + * @return string + */ + public static function upcase($input) + { + return is_string($input) ? mb_strtoupper($input) : $input; + } + + + /** + * URL encodes a string + * + * @param string $input + * + * @return string + */ + public static function url_encode($input) + { + return urlencode($input); + } + + /** + * Decodes a URL-encoded string + * + * @param string $input + * + * @return string + */ + public static function url_decode($input) + { + return urldecode($input); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php new file mode 100644 index 0000000..76dabda --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagAssign.php @@ -0,0 +1,76 @@ +match($markup)) { + $this->to = $syntaxRegexp->matches[1]; + $this->from = new Variable($syntaxRegexp->matches[2]); + } else { + throw new ParseException("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + $output = $this->from->render($context); + + $context->set($this->to, $output, true); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php new file mode 100644 index 0000000..c1f63cb --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBlock.php @@ -0,0 +1,56 @@ +match($markup)) { + $this->block = $syntaxRegexp->matches[1]; + parent::__construct($markup, $tokens, $fileSystem); + } else { + throw new ParseException("Syntax Error in 'block' - Valid syntax: block [name]"); + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php new file mode 100644 index 0000000..5a16828 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagBreak.php @@ -0,0 +1,42 @@ +registers['break'] = true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php new file mode 100644 index 0000000..1995d66 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCapture.php @@ -0,0 +1,71 @@ +match($markup)) { + $this->to = $syntaxRegexp->matches[1]; + parent::__construct($markup, $tokens, $fileSystem); + } else { + throw new ParseException("Syntax Error in 'capture' - Valid syntax: capture [var] [value]"); + } + } + + /** + * Renders the block + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $output = parent::render($context); + + $context->set($this->to, $output, true); + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php new file mode 100644 index 0000000..9ed29a3 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCase.php @@ -0,0 +1,171 @@ +nodelists = array(); + $this->elseNodelist = array(); + + parent::__construct($markup, $tokens, $fileSystem); + + $syntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/'); + + if ($syntaxRegexp->match($markup)) { + $this->left = $syntaxRegexp->matches[0]; + } else { + throw new ParseException("Syntax Error in tag 'case' - Valid syntax: case [condition]"); // harry + } + } + + /** + * Pushes the last nodelist onto the stack + */ + public function endTag() + { + $this->pushNodelist(); + } + + /** + * Unknown tag handler + * + * @param string $tag + * @param string $params + * @param array $tokens + * + * @throws \Liquid\Exception\ParseException + */ + public function unknownTag($tag, $params, array $tokens) + { + $whenSyntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/'); + + switch ($tag) { + case 'when': + // push the current nodelist onto the stack and prepare for a new one + if ($whenSyntaxRegexp->match($params)) { + $this->pushNodelist(); + $this->right = $whenSyntaxRegexp->matches[0]; + $this->nodelist = array(); + } else { + throw new ParseException("Syntax Error in tag 'case' - Valid when condition: when [condition]"); // harry + } + break; + + case 'else': + // push the last nodelist onto the stack and prepare to receive the else nodes + $this->pushNodelist(); + $this->right = null; + $this->elseNodelist = &$this->nodelist; + $this->nodelist = array(); + break; + + default: + parent::unknownTag($tag, $params, $tokens); + } + } + + /** + * Pushes the current right value and nodelist into the nodelist stack + */ + public function pushNodelist() + { + if (!is_null($this->right)) { + $this->nodelists[] = array($this->right, $this->nodelist); + } + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $output = ''; // array(); + $runElseBlock = true; + + foreach ($this->nodelists as $data) { + list($right, $nodelist) = $data; + + if ($this->equalVariables($this->left, $right, $context)) { + $runElseBlock = false; + + $context->push(); + $output .= $this->renderAll($nodelist, $context); + $context->pop(); + } + } + + if ($runElseBlock) { + $context->push(); + $output .= $this->renderAll($this->elseNodelist, $context); + $context->pop(); + } + + return $output; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php new file mode 100644 index 0000000..e41cc06 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagComment.php @@ -0,0 +1,37 @@ +registers['continue'] = true; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php new file mode 100644 index 0000000..2140847 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagCycle.php @@ -0,0 +1,130 @@ +match($markup)) { + $this->variables = $this->variablesFromString($namedSyntax->matches[2]); + $this->name = $namedSyntax->matches[1]; + } elseif ($simpleSyntax->match($markup)) { + $this->variables = $this->variablesFromString($markup); + $this->name = "'" . implode($this->variables) . "'"; + } else { + throw new ParseException("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"); + } + } + + /** + * Renders the tag + * + * @var Context $context + * @return string + */ + public function render(Context $context) + { + $context->push(); + + $key = $context->get($this->name); + + if (isset($context->registers['cycle'][$key])) { + $iteration = $context->registers['cycle'][$key]; + } else { + $iteration = 0; + } + + $result = $context->get($this->variables[$iteration]); + + $iteration += 1; + + if ($iteration >= count($this->variables)) { + $iteration = 0; + } + + $context->registers['cycle'][$key] = $iteration; + + $context->pop(); + + return $result; + } + + /** + * Extract variables from a string of markup + * + * @param string $markup + * + * @return array; + */ + private function variablesFromString($markup) + { + $regexp = new Regexp('/\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*/'); + $parts = explode(',', $markup); + $result = array(); + + foreach ($parts as $part) { + $regexp->match($part); + + if (!empty($regexp->matches[1])) { + $result[] = $regexp->matches[1]; + } + } + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php new file mode 100644 index 0000000..2a76887 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagDecrement.php @@ -0,0 +1,83 @@ +match($markup)) { + $this->toDecrement = $syntax->matches[0]; + } else { + throw new ParseException("Syntax Error in 'decrement' - Valid syntax: decrement [var]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + // if the value is not set in the environment check to see if it + // exists in the context, and if not set it to 0 + if (!isset($context->environments[0][$this->toDecrement])) { + // check for a context value + $fromContext = $context->get($this->toDecrement); + + // we already have a value in the context + $context->environments[0][$this->toDecrement] = (null !== $fromContext) ? $fromContext : 0; + } + + // decrement the environment value + $context->environments[0][$this->toDecrement]--; + + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php new file mode 100644 index 0000000..8c88a29 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagExtends.php @@ -0,0 +1,214 @@ +match($markup) && isset($regex->matches[1])) { + $this->templateName = substr($regex->matches[1], 1, strlen($regex->matches[1]) - 2); + } else { + throw new ParseException("Error in tag 'extends' - Valid syntax: extends '[template name]'"); + } + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * @param array $tokens + * + * @return array + */ + private function findBlocks(array $tokens) + { + $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/'); + $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/'); + + $b = array(); + $name = null; + + foreach ($tokens as $token) { + if ($blockstartRegexp->match($token)) { + $name = $blockstartRegexp->matches[1]; + $b[$name] = array(); + } elseif ($blockendRegexp->match($token)) { + $name = null; + } else { + if ($name !== null) { + array_push($b[$name], $token); + } + } + } + + return $b; + } + + /** + * Parses the tokens + * + * @param array $tokens + * + * @throws \Liquid\Exception\MissingFilesystemException + */ + public function parse(array &$tokens) + { + if ($this->fileSystem === null) { + throw new MissingFilesystemException("No file system"); + } + + // read the source of the template and create a new sub document + $source = $this->fileSystem->readTemplateFile($this->templateName); + + // tokens in this new document + $maintokens = Template::tokenize($source); + + $eRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*extends (.*)?' . Liquid::get('TAG_END') . '$/'); + foreach ($maintokens as $maintoken) { + if ($eRegexp->match($maintoken)) { + $m = $eRegexp->matches[1]; + break; + } + } + + if (isset($m)) { + $rest = array_merge($maintokens, $tokens); + } else { + $childtokens = $this->findBlocks($tokens); + + $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/'); + $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/'); + + $name = null; + + $rest = array(); + $keep = false; + + for ($i = 0; $i < count($maintokens); $i++) { + if ($blockstartRegexp->match($maintokens[$i])) { + $name = $blockstartRegexp->matches[1]; + + if (isset($childtokens[$name])) { + $keep = true; + array_push($rest, $maintokens[$i]); + foreach ($childtokens[$name] as $item) { + array_push($rest, $item); + } + } + } + if (!$keep) { + array_push($rest, $maintokens[$i]); + } + + if ($blockendRegexp->match($maintokens[$i]) && $keep === true) { + $keep = false; + array_push($rest, $maintokens[$i]); + } + } + } + + $cache = Template::getCache(); + + if (!$cache) { + $this->document = new Document($rest, $this->fileSystem); + return; + } + + $this->hash = md5($source); + + $this->document = $cache->read($this->hash); + + if ($this->document == false || $this->document->hasIncludes() == true) { + $this->document = new Document($rest, $this->fileSystem); + $cache->write($this->hash, $this->document); + } + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see Document::hasIncludes() + * @return boolean + */ + public function hasIncludes() + { + if ($this->document->hasIncludes() == true) { + return true; + } + + $source = $this->fileSystem->readTemplateFile($this->templateName); + + if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) { + return false; + } + + return true; + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $context->push(); + $result = $this->document->render($context); + $context->pop(); + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php new file mode 100644 index 0000000..e146887 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagFor.php @@ -0,0 +1,236 @@ +match($markup)) { + $this->variableName = $syntaxRegexp->matches[1]; + $this->collectionName = $syntaxRegexp->matches[2]; + $this->name = $syntaxRegexp->matches[1] . '-' . $syntaxRegexp->matches[2]; + $this->extractAttributes($markup); + } else { + $syntaxRegexp = new Regexp('/(\w+)\s+in\s+\((\d+|' . Liquid::get('VARIABLE_NAME') . ')\s*\.\.\s*(\d+|' . Liquid::get('VARIABLE_NAME') . ')\)/'); + if ($syntaxRegexp->match($markup)) { + $this->type = 'digit'; + $this->variableName = $syntaxRegexp->matches[1]; + $this->start = $syntaxRegexp->matches[2]; + $this->collectionName = $syntaxRegexp->matches[3]; + $this->name = $syntaxRegexp->matches[1].'-digit'; + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"); + } + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return null|string + */ + public function render(Context $context) + { + if (!isset($context->registers['for'])) { + $context->registers['for'] = array(); + } + + if ($this->type == 'digit') { + return $this->renderDigit($context); + } + + // that's the default + return $this->renderCollection($context); + } + + private function renderCollection(Context $context) + { + $collection = $context->get($this->collectionName); + + if ($collection instanceof \Generator && !$collection->valid()) { + return ''; + } + + if ($collection instanceof \Traversable) { + $collection = iterator_to_array($collection); + } + + if (is_null($collection) || !is_array($collection) || count($collection) == 0) { + return ''; + } + + $range = array(0, count($collection)); + + if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) { + $offset = 0; + + if (isset($this->attributes['offset'])) { + $offset = ($this->attributes['offset'] == 'continue') ? $context->registers['for'][$this->name] : $context->get($this->attributes['offset']); + } + + $limit = (isset($this->attributes['limit'])) ? $context->get($this->attributes['limit']) : null; + $rangeEnd = $limit ? $limit : count($collection) - $offset; + $range = array($offset, $rangeEnd); + + $context->registers['for'][$this->name] = $rangeEnd + $offset; + } + + $result = ''; + $segment = array_slice($collection, $range[0], $range[1]); + if (!count($segment)) { + return null; + } + + $context->push(); + $length = count($segment); + + $index = 0; + foreach ($segment as $key => $item) { + $value = is_numeric($key) ? $item : array($key, $item); + $context->set($this->variableName, $value); + $context->set('forloop', array( + 'name' => $this->name, + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1) + )); + + $result .= $this->renderAll($this->nodelist, $context); + + $index++; + + if (isset($context->registers['break'])) { + unset($context->registers['break']); + break; + } + if (isset($context->registers['continue'])) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + return $result; + } + + private function renderDigit(Context $context) + { + $start = $this->start; + if (!is_integer($this->start)) { + $start = $context->get($this->start); + } + + $end = $this->collectionName; + if (!is_integer($this->collectionName)) { + $end = $context->get($this->collectionName); + } + + $range = array($start, $end); + + $context->push(); + $result = ''; + $index = 0; + $length = $range[1] - $range[0]; + for ($i = $range[0]; $i <= $range[1]; $i++) { + $context->set($this->variableName, $i); + $context->set('forloop', array( + 'name' => $this->name, + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1) + )); + + $result .= $this->renderAll($this->nodelist, $context); + + $index++; + + if (isset($context->registers['break'])) { + unset($context->registers['break']); + break; + } + if (isset($context->registers['continue'])) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php new file mode 100644 index 0000000..01904a7 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIf.php @@ -0,0 +1,168 @@ +nodelist = & $this->nodelistHolders[count($this->blocks)]; + + array_push($this->blocks, array('if', $markup, &$this->nodelist)); + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * Handler for unknown tags, handle else tags + * + * @param string $tag + * @param array $params + * @param array $tokens + */ + public function unknownTag($tag, $params, array $tokens) + { + if ($tag == 'else' || $tag == 'elsif') { + // Update reference to nodelistHolder for this block + $this->nodelist = & $this->nodelistHolders[count($this->blocks) + 1]; + $this->nodelistHolders[count($this->blocks) + 1] = array(); + + array_push($this->blocks, array($tag, $params, &$this->nodelist)); + } else { + parent::unknownTag($tag, $params, $tokens); + } + } + + /** + * Render the tag + * + * @param Context $context + * + * @throws \Liquid\Exception\ParseException + * @return string + */ + public function render(Context $context) + { + $context->push(); + + $logicalRegex = new Regexp('/\s+(and|or)\s+/'); + $conditionalRegex = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*([=!<>a-z_]+)?\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')?/'); + + $result = ''; + foreach ($this->blocks as $block) { + if ($block[0] == 'else') { + $result = $this->renderAll($block[2], $context); + + break; + } + + if ($block[0] == 'if' || $block[0] == 'elsif') { + // Extract logical operators + $logicalRegex->matchAll($block[1]); + + $logicalOperators = $logicalRegex->matches; + $logicalOperators = $logicalOperators[1]; + // Extract individual conditions + $temp = $logicalRegex->split($block[1]); + + $conditions = array(); + + foreach ($temp as $condition) { + if ($conditionalRegex->match($condition)) { + $left = (isset($conditionalRegex->matches[1])) ? $conditionalRegex->matches[1] : null; + $operator = (isset($conditionalRegex->matches[2])) ? $conditionalRegex->matches[2] : null; + $right = (isset($conditionalRegex->matches[3])) ? $conditionalRegex->matches[3] : null; + + array_push($conditions, array( + 'left' => $left, + 'operator' => $operator, + 'right' => $right + )); + } else { + throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]"); + } + } + if (count($logicalOperators)) { + // If statement contains and/or + $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); + foreach ($logicalOperators as $k => $logicalOperator) { + if ($logicalOperator == 'and') { + $display = ($display && $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); + } else { + $display = ($display || $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context)); + } + } + } else { + // If statement is a single condition + $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context); + } + + // hook for unless tag + $display = $this->negateIfUnless($display); + + if ($display) { + $result = $this->renderAll($block[2], $context); + + break; + } + } + } + + $context->pop(); + + return $result; + } + + protected function negateIfUnless($display) + { + // no need to negate a condition in a regular `if` tag (will do that in `unless` tag) + return $display; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php new file mode 100644 index 0000000..ae4cec1 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIfchanged.php @@ -0,0 +1,61 @@ +lastValue == $output) { + return ''; + } + $this->lastValue = $output; + return $this->lastValue; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php new file mode 100644 index 0000000..481cdba --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagInclude.php @@ -0,0 +1,200 @@ +match($markup)) { + throw new ParseException("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"); + } + + $unquoted = (strpos($regex->matches[1], '"') === false && strpos($regex->matches[1], "'") === false); + + $start = 1; + $len = strlen($regex->matches[1]) - 2; + + if ($unquoted) { + $start = 0; + $len = strlen($regex->matches[1]); + } + + $this->templateName = substr($regex->matches[1], $start, $len); + + if (isset($regex->matches[1])) { + $this->collection = (isset($regex->matches[3])) ? ($regex->matches[3] == "for") : null; + $this->variable = (isset($regex->matches[4])) ? $regex->matches[4] : null; + } + + $this->extractAttributes($markup); + + parent::__construct($markup, $tokens, $fileSystem); + } + + /** + * Parses the tokens + * + * @param array $tokens + * + * @throws \Liquid\Exception\MissingFilesystemException + */ + public function parse(array &$tokens) + { + if ($this->fileSystem === null) { + throw new MissingFilesystemException("No file system"); + } + + // read the source of the template and create a new sub document + $source = $this->fileSystem->readTemplateFile($this->templateName); + + $cache = Template::getCache(); + + if (!$cache) { + // tokens in this new document + $templateTokens = Template::tokenize($source); + $this->document = new Document($templateTokens, $this->fileSystem); + return; + } + + $this->hash = md5($source); + $this->document = $cache->read($this->hash); + + if ($this->document == false || $this->document->hasIncludes() == true) { + $templateTokens = Template::tokenize($source); + $this->document = new Document($templateTokens, $this->fileSystem); + $cache->write($this->hash, $this->document); + } + } + + /** + * Check for cached includes; if there are - do not use cache + * + * @see Document::hasIncludes() + * @return boolean + */ + public function hasIncludes() + { + if ($this->document->hasIncludes() == true) { + return true; + } + + $source = $this->fileSystem->readTemplateFile($this->templateName); + + if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) { + return false; + } + + return true; + } + + /** + * Renders the node + * + * @param Context $context + * + * @return string + */ + public function render(Context $context) + { + $result = ''; + $variable = $context->get($this->variable); + + $context->push(); + + foreach ($this->attributes as $key => $value) { + $context->set($key, $context->get($value)); + } + + if ($this->collection) { + foreach ($variable as $item) { + $context->set($this->templateName, $item); + $result .= $this->document->render($context); + } + } else { + if (!is_null($this->variable)) { + $context->set($this->templateName, $variable); + } + + $result .= $this->document->render($context); + } + + $context->pop(); + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php new file mode 100644 index 0000000..ed428c8 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagIncrement.php @@ -0,0 +1,83 @@ +match($markup)) { + $this->toIncrement = $syntax->matches[0]; + } else { + throw new ParseException("Syntax Error in 'increment' - Valid syntax: increment [var]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string|void + */ + public function render(Context $context) + { + // If the value is not set in the environment check to see if it + // exists in the context, and if not set it to -1 + if (!isset($context->environments[0][$this->toIncrement])) { + // check for a context value + $from_context = $context->get($this->toIncrement); + + // we already have a value in the context + $context->environments[0][$this->toIncrement] = (null !== $from_context) ? $from_context : -1; + } + + // Increment the value + $context->environments[0][$this->toIncrement]++; + + return ''; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php new file mode 100644 index 0000000..afafaef --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagPaginate.php @@ -0,0 +1,206 @@ + + * {% endfor %} + * {% endpaginate %} + * + */ + +class TagPaginate extends AbstractBlock +{ + /** + * @var array The collection to paginate + */ + private $collectionName; + + /** + * @var array The collection object + */ + private $collection; + + /** + * @var int The size of the collection + */ + private $collectionSize; + + /** + * @var int The number of items to paginate by + */ + private $numberItems; + + /** + * @var int The current page + */ + private $currentPage; + + /** + * @var int The current offset (no of pages times no of items) + */ + private $currentOffset; + + /** + * @var int Total pages + */ + private $totalPages; + + + /** + * Constructor + * + * @param string $markup + * @param array $tokens + * @param FileSystem $fileSystem + * + * @throws \Liquid\Exception\ParseException + * + */ + public function __construct($markup, array &$tokens, FileSystem $fileSystem = null) + { + parent::__construct($markup, $tokens, $fileSystem); + + $syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')\s+by\s+(\w+)/'); + + if ($syntax->match($markup)) { + $this->collectionName = $syntax->matches[1]; + $this->numberItems = $syntax->matches[2]; + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error - Valid syntax: paginate [collection] by [items]"); + } + } + + /** + * Renders the tag + * + * @param Context $context + * + * @return string + * + */ + public function render(Context $context) + { + $this->collection = $context->get($this->collectionName); + + if ($this->collection instanceof \Traversable) { + $this->collection = iterator_to_array($this->collection); + } + + if (!is_array($this->collection)) { + // TODO do not throw up if error mode allows, see #83 + throw new RenderException("Missing collection with name '{$this->collectionName}'"); + } + + // How many pages are there? + $this->collectionSize = count($this->collection); + $this->totalPages = ceil($this->collectionSize / $this->numberItems); + + // Whatever there is in the context, we need a number + $this->currentPage = intval($context->get(Liquid::get('PAGINATION_CONTEXT_KEY'))); + + // Page number can only be between 1 and a number of pages + $this->currentPage = max(1, min($this->currentPage, $this->totalPages)); + + // Find the offset and select that part + $this->currentOffset = ($this->currentPage - 1) * $this->numberItems; + $paginatedCollection = array_slice($this->collection, $this->currentOffset, $this->numberItems); + + // We must work in a new scope so we won't pollute a global scope + $context->push(); + + // Sets the collection if it's a key of another collection (ie search.results, collection.products, blog.articles) + $segments = explode('.', $this->collectionName); + if (count($segments) == 2) { + $context->set($segments[0], array($segments[1] => $paginatedCollection)); + } else { + $context->set($this->collectionName, $paginatedCollection); + } + + $paginate = array( + 'page_size' => $this->numberItems, + 'current_page' => $this->currentPage, + 'current_offset' => $this->currentOffset, + 'pages' => $this->totalPages, + 'items' => $this->collectionSize + ); + + // Get the name of the request field to use in URLs + $pageRequestKey = Liquid::get('PAGINATION_REQUEST_KEY'); + + if ($this->currentPage > 1) { + $paginate['previous']['title'] = 'Previous'; + $paginate['previous']['url'] = $this->currentUrl($context, [ + $pageRequestKey => $this->currentPage - 1, + ]); + } + + if ($this->currentPage < $this->totalPages) { + $paginate['next']['title'] = 'Next'; + $paginate['next']['url'] = $this->currentUrl($context, [ + $pageRequestKey => $this->currentPage + 1, + ]); + } + + $context->set('paginate', $paginate); + + $result = parent::render($context); + + $context->pop(); + + return $result; + } + + /** + * Returns the current page URL + * + * @param Context $context + * @param array $queryPart + * + * @return string + * + */ + public function currentUrl($context, $queryPart = []) + { + // From here we have $url->path and $url->query + $url = (object) parse_url($context->get('REQUEST_URI')); + + // Let's merge the query part + if (isset($url->query)) { + parse_str($url->query, $url->query); + $url->query = array_merge($url->query, $queryPart); + } else { + $url->query = $queryPart; + } + + $url->query = http_build_query($url->query); + + $scheme = $context->get('HTTPS') == 'on' ? 'https' : 'http'; + + return "$scheme://{$context->get('HTTP_HOST')}{$url->path}?{$url->query}"; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php new file mode 100644 index 0000000..36310d7 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagRaw.php @@ -0,0 +1,52 @@ +nodelist = array(); + + while (count($tokens)) { + $token = array_shift($tokens); + + if ($tagRegexp->match($token)) { + // If we found the proper block delimiter just end parsing here and let the outer block proceed + if ($tagRegexp->matches[1] == $this->blockDelimiter()) { + break; + } + } + + $this->nodelist[] = $token; + } + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php new file mode 100644 index 0000000..13cf6e3 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagTablerow.php @@ -0,0 +1,152 @@ +match($markup)) { + $this->variableName = $syntax->matches[1]; + $this->collectionName = $syntax->matches[2]; + + $this->extractAttributes($markup); + } else { + throw new ParseException("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols:3"); + } + } + + /** + * Renders the current node + * + * @param Context $context + * @throws \Liquid\Exception\RenderException + * @return string + */ + public function render(Context $context) + { + $collection = $context->get($this->collectionName); + + if ($collection instanceof \Traversable) { + $collection = iterator_to_array($collection); + } + + if (!is_array($collection)) { + throw new RenderException("Not an array"); + } + + // discard keys + $collection = array_values($collection); + + if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) { + $limit = $context->get($this->attributes['limit']); + $offset = $context->get($this->attributes['offset']); + $collection = array_slice($collection, $offset, $limit); + } + + $length = count($collection); + + $cols = isset($this->attributes['cols']) ? $context->get($this->attributes['cols']) : PHP_INT_MAX; + + $row = 1; + $col = 0; + + $result = "\n"; + + $context->push(); + + foreach ($collection as $index => $item) { + $context->set($this->variableName, $item); + $context->set('tablerowloop', array( + 'length' => $length, + 'index' => $index + 1, + 'index0' => $index, + 'rindex' => $length - $index, + 'rindex0' => $length - $index - 1, + 'first' => (int)($index == 0), + 'last' => (int)($index == $length - 1) + )); + + $text = $this->renderAll($this->nodelist, $context); + $break = isset($context->registers['break']); + $continue = isset($context->registers['continue']); + + if ((!$break && !$continue) || strlen(trim($text)) > 0) { + $result .= "$text"; + } + + if ($col == $cols && !($index == $length - 1)) { + $col = 0; + $result .= "\n\n"; + } + + if ($break) { + unset($context->registers['break']); + break; + } + if ($continue) { + unset($context->registers['continue']); + } + } + + $context->pop(); + + $result .= "\n"; + + return $result; + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php b/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php new file mode 100644 index 0000000..7d1f94d --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Tag/TagUnless.php @@ -0,0 +1,31 @@ +parse(template_source); + * $tpl->render(array('foo'=>1, 'bar'=>2); + */ +class Template +{ + const CLASS_PREFIX = '\Liquid\Cache\\'; + + /** + * @var Document The root of the node tree + */ + private $root; + + /** + * @var FileSystem The file system to use for includes + */ + private $fileSystem; + + /** + * @var array Globally included filters + */ + private $filters = array(); + + /** + * @var callable|null Called "sometimes" while rendering. For example to abort the execution of a rendering. + */ + private $tickFunction = null; + + /** + * @var array Custom tags + */ + private static $tags = array(); + + /** + * @var Cache + */ + private static $cache; + + /** + * Constructor. + * + * @param string $path + * @param array|Cache $cache + * + * @return Template + */ + public function __construct($path = null, $cache = null) + { + $this->fileSystem = $path !== null + ? new LocalFileSystem($path) + : null; + + $this->setCache($cache); + } + + /** + * @param FileSystem $fileSystem + */ + public function setFileSystem(FileSystem $fileSystem) + { + $this->fileSystem = $fileSystem; + } + + /** + * @param array|Cache $cache + * + * @throws \Liquid\Exception\CacheException + */ + public static function setCache($cache) + { + if (is_array($cache)) { + if (isset($cache['cache']) && class_exists($classname = self::CLASS_PREFIX . ucwords($cache['cache']))) { + self::$cache = new $classname($cache); + } else { + throw new CacheException('Invalid cache options!'); + } + } + + if ($cache instanceof Cache) { + self::$cache = $cache; + } + + if (is_null($cache)) { + self::$cache = null; + } + } + + /** + * @return Cache + */ + public static function getCache() + { + return self::$cache; + } + + /** + * @return Document + */ + public function getRoot() + { + return $this->root; + } + + /** + * Register custom Tags + * + * @param string $name + * @param string $class + */ + public static function registerTag($name, $class) + { + self::$tags[$name] = $class; + } + + /** + * @return array + */ + public static function getTags() + { + return self::$tags; + } + + /** + * Register the filter + * + * @param string $filter + */ + public function registerFilter($filter, callable $callback = null) + { + // Store callback for later use + if ($callback) { + $this->filters[] = [$filter, $callback]; + } else { + $this->filters[] = $filter; + } + } + + public function setTickFunction(callable $tickFunction) + { + $this->tickFunction = $tickFunction; + } + + /** + * Tokenizes the given source string + * + * @param string $source + * + * @return array + */ + public static function tokenize($source) + { + return empty($source) + ? array() + : preg_split(Liquid::get('TOKENIZATION_REGEXP'), $source, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Parses the given source string + * + * @param string $source + * + * @return Template + */ + public function parse($source) + { + if (!self::$cache) { + return $this->parseAlways($source); + } + + $hash = md5($source); + $this->root = self::$cache->read($hash); + + // if no cached version exists, or if it checks for includes + if ($this->root == false || $this->root->hasIncludes() == true) { + $this->parseAlways($source); + self::$cache->write($hash, $this->root); + } + + return $this; + } + + /** + * Parses the given source string regardless of caching + * + * @param string $source + * + * @return Template + */ + private function parseAlways($source) + { + $tokens = Template::tokenize($source); + $this->root = new Document($tokens, $this->fileSystem); + + return $this; + } + + /** + * Parses the given template file + * + * @param string $templatePath + * @throws \Liquid\Exception\MissingFilesystemException + * @return Template + */ + public function parseFile($templatePath) + { + if (!$this->fileSystem) { + throw new MissingFilesystemException("Could not load a template without an initialized file system"); + } + + return $this->parse($this->fileSystem->readTemplateFile($templatePath)); + } + + /** + * Renders the current template + * + * @param array $assigns an array of values for the template + * @param array $filters additional filters for the template + * @param array $registers additional registers for the template + * + * @return string + */ + public function render(array $assigns = array(), $filters = null, array $registers = array()) + { + $context = new Context($assigns, $registers); + + if ($this->tickFunction) { + $context->setTickFunction($this->tickFunction); + } + + if (!is_null($filters)) { + if (is_array($filters)) { + $this->filters = array_merge($this->filters, $filters); + } else { + $this->filters[] = $filters; + } + } + + foreach ($this->filters as $filter) { + if (is_array($filter)) { + // Unpack a callback saved as second argument + $context->addFilters(...$filter); + } else { + $context->addFilters($filter); + } + } + + return $this->root->render($context); + } +} diff --git a/package/vendor/liquid/liquid/src/Liquid/Variable.php b/package/vendor/liquid/liquid/src/Liquid/Variable.php new file mode 100644 index 0000000..6afeb05 --- /dev/null +++ b/package/vendor/liquid/liquid/src/Liquid/Variable.php @@ -0,0 +1,173 @@ +markup = $markup; + + $filterSep = new Regexp('/' . Liquid::get('FILTER_SEPARATOR') . '\s*(.*)/m'); + $syntaxParser = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')(.*)/ms'); + $filterParser = new Regexp('/(?:\s+|' . Liquid::get('QUOTED_FRAGMENT') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')+/'); + $filterArgsRegex = new Regexp('/(?:' . Liquid::get('FILTER_ARGUMENT_SEPARATOR') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')\s*((?:\w+\s*\:\s*)?' . Liquid::get('QUOTED_FRAGMENT') . ')/'); + + $this->filters = []; + if ($syntaxParser->match($markup)) { + $nameMarkup = $syntaxParser->matches[1]; + $this->name = $nameMarkup; + $filterMarkup = $syntaxParser->matches[2]; + + if ($filterSep->match($filterMarkup)) { + $filterParser->matchAll($filterSep->matches[1]); + + foreach ($filterParser->matches[0] as $filter) { + $filter = trim($filter); + if (preg_match('/\w+/', $filter, $matches)) { + $filterName = $matches[0]; + $filterArgsRegex->matchAll($filter); + $matches = Liquid::arrayFlatten($filterArgsRegex->matches[1]); + $this->filters[] = $this->parseFilterExpressions($filterName, $matches); + } + } + } + } + + if (Liquid::get('ESCAPE_BY_DEFAULT')) { + // if auto_escape is enabled, and + // - there's no raw filter, and + // - no escape filter + // - no other standard html-adding filter + // then + // - add a mandatory escape filter + + $addEscapeFilter = true; + + foreach ($this->filters as $filter) { + // with empty filters set we would just move along + if (in_array($filter[0], array('escape', 'escape_once', 'raw', 'newline_to_br'))) { + // if we have any raw-like filter, stop + $addEscapeFilter = false; + break; + } + } + + if ($addEscapeFilter) { + $this->filters[] = array('escape', array()); + } + } + } + + /** + * @param string $filterName + * @param array $unparsedArgs + * @return array + */ + private static function parseFilterExpressions($filterName, array $unparsedArgs) + { + $filterArgs = array(); + $keywordArgs = array(); + + $justTagAttributes = new Regexp('/\A' . trim(Liquid::get('TAG_ATTRIBUTES'), '/') . '\z/'); + + foreach ($unparsedArgs as $a) { + if ($justTagAttributes->match($a)) { + $keywordArgs[$justTagAttributes->matches[1]] = $justTagAttributes->matches[2]; + } else { + $filterArgs[] = $a; + } + } + + if (count($keywordArgs)) { + $filterArgs[] = $keywordArgs; + } + + return array($filterName, $filterArgs); + } + + /** + * Gets the variable name + * + * @return string The name of the variable + */ + public function getName() + { + return $this->name; + } + + /** + * Gets all Filters + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } + + /** + * Renders the variable with the data in the context + * + * @param Context $context + * + * @return mixed|string + */ + public function render(Context $context) + { + $output = $context->get($this->name); + foreach ($this->filters as $filter) { + list($filtername, $filterArgKeys) = $filter; + + $filterArgValues = array(); + $keywordArgValues = array(); + + foreach ($filterArgKeys as $arg_key) { + if (is_array($arg_key)) { + foreach ($arg_key as $keywordArgName => $keywordArgKey) { + $keywordArgValues[$keywordArgName] = $context->get($keywordArgKey); + } + + $filterArgValues[] = $keywordArgValues; + } else { + $filterArgValues[] = $context->get($arg_key); + } + } + + $output = $context->invoke($filtername, $output, $filterArgValues); + } + return $output; + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/AbstractBlockTest.php b/package/vendor/liquid/liquid/tests/Liquid/AbstractBlockTest.php new file mode 100644 index 0000000..fef3ac6 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/AbstractBlockTest.php @@ -0,0 +1,38 @@ +expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% block }'); + } + + public function testWhitespaceHandler() + { + $this->assertTemplateResult('foo', '{% if true %}foo{% endif %}'); + $this->assertTemplateResult(' foo ', '{% if true %} foo {% endif %}'); + $this->assertTemplateResult(' foo ', ' {% if true %} foo {% endif %} '); + $this->assertTemplateResult('foo ', '{% if true -%} foo {% endif %}'); + $this->assertTemplateResult('foo', '{% if true -%} foo {%- endif %}'); + $this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif %}'); + $this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif -%} '); + $this->assertTemplateResult('foo', ' {%- if true -%} foo {%- endif -%} {%- if false -%} bar {%- endif -%} '); + $this->assertTemplateResult('foobar', ' {%- if true -%} foo {%- endif -%} {%- if true -%} bar {%- endif -%} '); + $this->assertTemplateResult('-> foo', '{% if true %}-> {% endif %} {%- if true -%} foo {%- endif -%}'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Cache/ApcTest.php b/package/vendor/liquid/liquid/tests/Liquid/Cache/ApcTest.php new file mode 100644 index 0000000..2b66116 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Cache/ApcTest.php @@ -0,0 +1,53 @@ +markTestSkipped("Alternative PHP Cache (APC) not available"); + } + + if (!ini_get('apc.enable_cli')) { + $this->markTestSkipped("APC not enabled with cli. Run with: php -d apc.enable_cli=1"); + } + + $this->cache = new Apc(); + } + + public function testNotExists() + { + $this->assertFalse($this->cache->exists('no_such_key')); + } + + public function testReadNotExisting() + { + $this->assertFalse($this->cache->read('no_such_key')); + } + + public function testSetGetFlush() + { + $this->assertTrue($this->cache->write('test', 'example'), "Failed to set value."); + $this->assertSame('example', $this->cache->read('test')); + $this->assertTrue($this->cache->flush()); + $this->assertFalse($this->cache->read('test')); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Cache/FileTest.php b/package/vendor/liquid/liquid/tests/Liquid/Cache/FileTest.php new file mode 100644 index 0000000..bcee1a6 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Cache/FileTest.php @@ -0,0 +1,173 @@ +cacheDir = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'cache_dir'; + + // Remove tmp cache files because they may remain after a failed test run + $this->removeOldCachedFiles(); + + $this->cache = new File(array( + 'cache_dir' => $this->cacheDir, + 'cache_expire' => 3600, + 'cache_prefix' => 'liquid_', + )); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->removeOldCachedFiles(); + } + + private function removeOldCachedFiles(): void + { + if ($files = glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')) { + array_map('unlink', $files); + } + } + + public function testConstructInvalidOptions() + { + $this->expectException(\Liquid\Exception\FilesystemException::class); + + new File(); + } + + public function testConstructNoSuchDirOrNotWritable() + { + $this->expectException(\Liquid\Exception\FilesystemException::class); + + new File(array('cache_dir' => '/no/such/dir/liquid/cache')); + } + + public function testGetExistsNoFile() + { + $this->assertFalse($this->cache->exists('no_key')); + } + + public function testGetExistsExpired() + { + $key = 'test'; + $cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key; + touch($cacheFile, time() - 1000000); // long ago + $this->assertFalse($this->cache->exists($key)); + } + + public function testGetExistsNotExpired() + { + $key = 'test'; + $cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key; + touch($cacheFile); + $this->assertTrue($this->cache->exists($key)); + } + + public function testFlushAll() + { + touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test'); + touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two'); + + $this->assertGreaterThanOrEqual(2, count(glob($this->cacheDir . DIRECTORY_SEPARATOR . '*'))); + + $this->cache->flush(); + + $this->assertCount(0, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')); + } + + public function testFlushExpired() + { + touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test'); + touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two', time() - 1000000); + + $files = join(', ', glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')); + + $this->assertGreaterThanOrEqual(2, count(glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')), "Found more than two files: $files"); + + $this->cache->flush(true); + + $this->assertCount(1, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')); + } + + public function testWriteNoSerialize() + { + $key = 'test'; + $value = 'test_value'; + + $this->assertTrue($this->cache->write($key, $value, false)); + + $this->assertEquals($value, file_get_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key)); + } + + public function testWriteSerialized() + { + $key = 'test'; + $value = 'test_value'; + + $this->assertTrue($this->cache->write($key, $value)); + + $this->assertEquals(serialize($value), file_get_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key)); + } + + /** + * @depends testWriteSerialized + */ + public function testWriteGc() + { + $key = 'test'; + $value = 'test_value'; + + // This cache file must be removed by GC + touch($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_test_two', time() - 1000000); + + $this->assertTrue($this->cache->write($key, $value, false)); + + $this->assertCount(1, glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')); + } + + public function testReadNonExisting() + { + $this->assertFalse($this->cache->read('no_such_key')); + } + + public function testReadNoUnserialize() + { + $key = 'test'; + $value = 'test_value'; + + file_put_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key, $value); + + $this->assertSame($value, $this->cache->read($key, false)); + } + + public function testReadSerialize() + { + $key = 'test'; + $value = 'test_value'; + + file_put_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'liquid_' . $key, serialize($value)); + + $this->assertSame($value, $this->cache->read($key)); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Cache/LocalTest.php b/package/vendor/liquid/liquid/tests/Liquid/Cache/LocalTest.php new file mode 100644 index 0000000..a72cbf9 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Cache/LocalTest.php @@ -0,0 +1,45 @@ +cache = new Local(); + } + + public function testNotExists() + { + $this->assertFalse($this->cache->exists('no_such_key')); + } + + public function testReadNotExisting() + { + $this->assertFalse($this->cache->read('no_such_key')); + } + + public function testSetGetFlush() + { + $this->assertTrue($this->cache->write('test', 'example')); + $this->assertSame('example', $this->cache->read('test')); + $this->assertTrue($this->cache->flush()); + $this->assertFalse($this->cache->read('test')); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/ContextTest.php b/package/vendor/liquid/liquid/tests/Liquid/ContextTest.php new file mode 100644 index 0000000..b95abea --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/ContextTest.php @@ -0,0 +1,503 @@ +value; + } +} + +class NestedObject +{ + public $property; + public $value = -1; + + public function toLiquid() + { + // we intentionally made the value different so + // that we could see where it is coming from + return array( + 'property' => $this->property, + 'value' => 42, + ); + } +} + +class CountableObject implements \Countable +{ + public function count() + { + return 2; + } +} + +class ToArrayObject +{ + public $property; + public $value = -1; + + public function toArray() + { + // we intentionally made the value different so + // that we could see where it is coming from + return array( + 'property' => $this->property, + 'value' => 42, + ); + } +} + +class GetSetObject +{ + public function field_exists($name) + { + return $name == 'answer'; + } + + public function get($prop) + { + if ($prop == 'answer') { + return 42; + } + } +} + +class GetSetMagic +{ + public function __get($prop) + { + if ($prop == 'prime') { + return 2; + } + } +} + + +class HiFilter +{ + public function hi($value) + { + return $value . ' hi!'; + } +} + +class GlobalFilter +{ + public function notice($value) + { + return "Global $value"; + } +} + +class LocalFilter +{ + public function notice($value) + { + return "Local $value"; + } +} + +class ContextTest extends TestCase +{ + /** @var Context */ + public $context; + + protected function setUp(): void + { + parent::setUp(); + + $this->context = new Context(); + } + + public function testScoping() + { + $this->context->push(); + $this->assertNull($this->context->pop()); + } + + /** + */ + public function testNoScopeToPop() + { + $this->expectException(\Liquid\LiquidException::class); + + $this->context->pop(); + } + + /** + */ + public function testGetArray() + { + $this->expectException(\Liquid\LiquidException::class); + + $this->context->get(array()); + } + + public function testGetNotVariable() + { + $data = array( + null => null, + 'null' => null, + 'true' => true, + 'false' => false, + "'quoted_string'" => 'quoted_string', + '"double_quoted_string"' => "double_quoted_string", + ); + + foreach ($data as $key => $expected) { + $this->assertEquals($expected, $this->context->get($key)); + } + + $this->assertEquals(42.00, $this->context->get(42.00)); + } + + public function testVariablesNotExisting() + { + $this->assertNull($this->context->get('test')); + } + + public function testVariableIsObjectWithNoToLiquid() + { + $this->context->set('test', new NoToLiquid()); + $this->assertEquals(42, $this->context->get('test.answer')); + $this->assertEquals(1, $this->context->get('test.count')); + $this->assertNull($this->context->get('test.invalid')); + $this->assertEquals("forty two", $this->context->get('test')); + $this->assertEquals("example", $this->context->get('test.name')); + } + + public function testToLiquidNull() + { + $object = new ToLiquidWrapper(); + $this->context->set('object', $object); + $this->assertNull($this->context->get('object.key')); + } + + public function testToLiquidStringKeyMustBeNull() + { + $object = new ToLiquidWrapper(); + $object->value = 'foo'; + $this->context->set('object', $object); + $this->assertNull($this->context->get('object.foo')); + $this->assertNull($this->context->get('object.foo.bar')); + } + + public function testNestedObject() + { + $object = new NestedObject(); + $object->property = new NestedObject(); + $this->context->set('object', $object); + $this->assertEquals(42, $this->context->get('object.value')); + $this->assertEquals(42, $this->context->get('object.property.value')); + $this->assertNull($this->context->get('object.property.value.invalid')); + } + + public function testToArrayObject() + { + $object = new ToArrayObject(); + $object->property = new ToArrayObject(); + $this->context->set('object', $object); + $this->assertEquals(42, $this->context->get('object.value')); + $this->assertEquals(42, $this->context->get('object.property.value')); + $this->assertNull($this->context->get('object.property.value.invalid')); + } + + public function testGetSetObject() + { + $this->context->set('object', new GetSetObject()); + $this->assertEquals(42, $this->context->get('object.answer')); + $this->assertNull($this->context->get('object.invalid')); + } + + public function testGetSetMagic() + { + $this->context->set('object', new GetSetMagic()); + $this->assertEquals(2, $this->context->get('object.prime')); + $this->assertNull($this->context->get('object.invalid')); + } + + public function testFinalVariableCanBeObject() + { + $this->context->set('test', (object) array('value' => (object) array())); + $this->assertInstanceOf(\stdClass::class, $this->context->get('test.value')); + } + + public function testVariables() + { + $this->context->set('test', 'test'); + $this->assertTrue($this->context->hasKey('test')); + $this->assertFalse($this->context->hasKey('test.foo')); + $this->assertEquals('test', $this->context->get('test')); + + // We add this text to make sure we can return values that evaluate to false properly + $this->context->set('test_0', 0); + $this->assertEquals('0', $this->context->get('test_0')); + } + + public function testLengthQuery() + { + $this->context->set('numbers', array(1, 2, 3, 4)); + $this->assertEquals(4, $this->context->get('numbers.size')); + } + + public function testStringLength() + { + $this->context->set('name', 'Foo Bar'); + $this->assertEquals(7, $this->context->get('name.size')); + + $this->context->set('name', 'テスト'); + $this->assertEquals(3, $this->context->get('name.size')); + } + + public function testCountableLength() + { + $this->context->set('countable', new CountableObject()); + $this->assertEquals(2, $this->context->get('countable.size')); + } + + public function testOverrideSize() + { + $this->context->set('hash', array('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'size' => '5000')); + $this->assertEquals(5000, $this->context->get('hash.size')); + } + + public function testArrayFirst() + { + $this->context->set('array', array(11, 'jack', 43, 74, 5, 'tom')); + $this->assertEquals(11, $this->context->get('array.first')); + } + + public function testOverrideFirst() + { + $this->context->set('array', array(11, 'jack', 43, 'first' => 74, 5, 'tom')); + $this->assertEquals(74, $this->context->get('array.first')); + } + + public function testArrayLast() + { + $this->context->set('array', array(11, 'jack', 43, 74, 5, 'tom')); + $this->assertEquals('tom', $this->context->get('array.last')); + } + + public function testOverrideLast() + { + $this->context->set('array', array(11, 'jack', 43, 'last' => 74, 5, 'tom')); + $this->assertEquals(74, $this->context->get('array.last')); + } + + public function testDeepValueNotObject() + { + $this->context->set('example', array('foo' => new ToLiquidNotObject())); + $this->assertNull($this->context->get('example.foo.bar')); + } + + public function testHierchalData() + { + $this->context->set('hash', array('name' => 'tobi')); + $this->assertEquals('tobi', $this->context->get('hash.name')); + } + + public function testHierchalDataNoKey() + { + $this->context->set('hash', array('name' => 'tobi')); + $this->assertNull($this->context->get('hash.no_key')); + } + + public function testAddFilter() + { + $context = new Context(); + $context->addFilters(new HiFilter()); + $this->assertEquals('hi? hi!', $context->invoke('hi', 'hi?')); + + $context = new Context(); + $this->assertEquals('hi?', $context->invoke('hi', 'hi?')); + + $context->addFilters(new HiFilter()); + $this->assertEquals('hi? hi!', $context->invoke('hi', 'hi?')); + } + + public function testOverrideGlobalFilter() + { + $template = new Template(); + $template->registerFilter(new GlobalFilter()); + + $template->parse("{{'test' | notice }}"); + $this->assertEquals('Global test', $template->render()); + $this->assertEquals('Local test', $template->render(array(), new LocalFilter())); + } + + public function testCallbackFilter() + { + $template = new Template(); + $template->registerFilter('foo', function ($arg) { + return "Foo $arg"; + }); + + $template->parse("{{'test' | foo }}"); + $this->assertEquals('Foo test', $template->render()); + } + + public function testAddItemInOuterScope() + { + $this->context->set('test', 'test'); + $this->context->push(); + $this->assertEquals('test', $this->context->get('test')); + $this->context->pop(); + $this->assertEquals('test', $this->context->get('test')); + } + + public function testAddItemInInnerScope() + { + $this->context->push(); + $this->context->set('test', 'test'); + $this->assertEquals('test', $this->context->get('test')); + $this->context->pop(); + $this->assertNull($this->context->get('test')); + } + + public function testMerge() + { + $this->context->merge(array('test' => 'test')); + $this->assertEquals('test', $this->context->get('test')); + + $this->context->merge(array('test' => 'newvalue', 'foo' => 'bar')); + $this->assertEquals('newvalue', $this->context->get('test')); + $this->assertEquals('bar', $this->context->get('foo')); + } + + public function testCents() + { + $this->context->merge(array('cents' => new HundredCentes())); + $this->assertEquals(100, $this->context->get('cents')); + } + + public function testNestedCents() + { + $this->context->merge(array('cents' => array('amount' => new HundredCentes()))); + $this->assertEquals(100, $this->context->get('cents.amount')); + + $this->context->merge(array('cents' => array('cents' => array('amount' => new HundredCentes())))); + $this->assertEquals(100, $this->context->get('cents.cents.amount')); + } + + public function testCentsThroughDrop() + { + $this->context->merge(array('cents' => new CentsDrop())); + $this->assertEquals(100, $this->context->get('cents.amount')); + } + + public function testCentsThroughDropNestedly() + { + $this->context->merge(array('cents' => array('cents' => new CentsDrop()))); + $this->assertEquals(100, $this->context->get('cents.cents.amount')); + + $this->context->merge(array('cents' => array('cents' => array('cents' => new CentsDrop())))); + $this->assertEquals(100, $this->context->get('cents.cents.cents.amount')); + } + + public function testGetNoOverride() + { + $_GET['test'] = ''; + // Previously $_GET would override directly set values + // It happend during class construction - we need to create a brand new instance right here + $context = new Context(); + $context->set('test', 'test'); + $this->assertEquals('test', $context->get('test')); + } + + public function testServerOnlyExposeWhitelistByDefault() + { + $_SERVER['AWS_SECRET_ACCESS_KEY'] = 'super_secret'; + + $context = new Context(); + $this->assertNull($context->get('AWS_SECRET_ACCESS_KEY')); + + $context->set('AWS_SECRET_ACCESS_KEY', 'test'); + $this->assertEquals('test', $context->get('AWS_SECRET_ACCESS_KEY')); + + $_SERVER['FOO'] = 'foo'; + $_SERVER['BAR'] = 'bar'; + + Liquid::set('SERVER_SUPERGLOBAL_WHITELIST', ['FOO']); + + $context = new Context(); + $this->assertEquals('foo', $context->get('FOO')); + $this->assertNull($context->get('BAR')); + + $context->set('BAR', 'bar'); + $this->assertEquals('bar', $context->get('BAR')); + } + + public function testServerExposedWhenRequested() + { + Liquid::set('EXPOSE_SERVER', true); + + $_SERVER['AWS_SECRET_ACCESS_KEY'] = 'super_secret'; + + $context = new Context(); + $this->assertEquals('super_secret', $context->get('AWS_SECRET_ACCESS_KEY')); + + $context->set('AWS_SECRET_ACCESS_KEY', 'test'); + $this->assertEquals('super_secret', $context->get('AWS_SECRET_ACCESS_KEY'), '$_SERVER should take precedence in this case'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/CustomFiltersTest.php b/package/vendor/liquid/liquid/tests/Liquid/CustomFiltersTest.php new file mode 100644 index 0000000..597bdf4 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/CustomFiltersTest.php @@ -0,0 +1,47 @@ +context = new Context(); + } + + public function testSortKey() + { + $data = array( + array( + array(), + array(), + ), + array( + array('b' => 1, 'c' => 5, 'a' => 3, 'z' => 4, 'h' => 2), + array('a' => 3, 'b' => 1, 'c' => 5, 'h' => 2, 'z' => 4), + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], CustomFilters::sort_key($item[0])); + } + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/CustomTagTest.php b/package/vendor/liquid/liquid/tests/Liquid/CustomTagTest.php new file mode 100644 index 0000000..ceb255a --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/CustomTagTest.php @@ -0,0 +1,47 @@ +getTags())) { + $this->markTestIncomplete("Test tag already registered. Are you missing @depends?"); + } + + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('Unknown tag foo'); + + $template->parse('[ba{% foo %} Comment {% endfoo %}r]'); + } + + /** + * @depends testUnknownTag + */ + public function testCustomTag() + { + $template = new Template(); + $template->registerTag('foo', TagFoo::class); + + $template->parse('[ba{% foo %} Comment {% endfoo %}r]'); + $this->assertEquals('[bar]', $template->render()); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/DropTest.php b/package/vendor/liquid/liquid/tests/Liquid/DropTest.php new file mode 100644 index 0000000..5699755 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/DropTest.php @@ -0,0 +1,140 @@ +context->get($method); + } +} + +class TextDrop extends Drop +{ + public function get_array() + { + return array('text1', 'text2'); + } + + public function text() + { + return 'text1'; + } +} + +class CatchallDrop extends Drop +{ + public function beforeMethod($method) + { + return 'method: ' . $method; + } +} + +class ProductDrop extends Drop +{ + public function top_sales() + { + throw new \Exception("worked"); + } + + public function texts() + { + return new TextDrop(); + } + + public function catchall() + { + return new CatchallDrop(); + } + + public function context() + { + return new ContextDrop(); + } + + public function callmenot() + { + return "protected"; + } + + public function hasKey($name) + { + return $name != 'unknown' && $name != 'false'; + } +} + +class DropTest extends TestCase +{ + /** + */ + public function testProductDrop() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('worked'); + + $template = new Template(); + $template->parse(' {{ product.top_sales }} '); + $template->render(array('product' => new ProductDrop)); + } + + public function testNoKeyDrop() + { + $template = new Template(); + $template->parse(' {{ product.invalid.unknown }}{{ product.false }} '); + $output = $template->render(array('product' => new ProductDrop)); + $this->assertEquals(' ', $output); + } + + public function testTextDrop() + { + $template = new Template(); + $template->parse(' {{ product.texts.text }} '); + $output = $template->render(array('product' => new ProductDrop())); + $this->assertEquals(' text1 ', $output); + + $template = new Template(); + $template->parse(' {{ product.catchall.unknown }} '); + $output = $template->render(array('product' => new ProductDrop())); + $this->assertEquals(' method: unknown ', $output); + } + + public function testTextArrayDrop() + { + $template = new Template(); + $template->parse('{% for text in product.texts.get_array %} {{text}} {% endfor %}'); + $output = $template->render(array('product' => new ProductDrop())); + + $this->assertEquals(' text1 text2 ', $output); + } + + public function testContextDrop() + { + $template = new Template(); + $template->parse(' {{ context.bar }} '); + $output = $template->render(array('context' => new ContextDrop(), 'bar' => 'carrot')); + $this->assertEquals(' carrot ', $output); + } + + public function testNestedContextDrop() + { + $template = new Template(); + $template->parse(' {{ product.context.foo }} '); + $output = $template->render(array('product' => new ProductDrop(), 'foo' => 'monkey')); + $this->assertEquals(' monkey ', $output); + } + + public function testToString() + { + $this->assertEquals(ProductDrop::class, strval(new ProductDrop())); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/EscapeByDefaultTest.php b/package/vendor/liquid/liquid/tests/Liquid/EscapeByDefaultTest.php new file mode 100644 index 0000000..54eed9b --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/EscapeByDefaultTest.php @@ -0,0 +1,127 @@ +string = $string; + } + + public function __toString() + { + return $this->string; + } +} + +class EscapeByDefaultTest extends TestCase +{ + const XSS = ""; + const XSS_FAILED = "<script>alert()</script>"; + + protected $assigns = array(); + + protected function setUp(): void + { + parent::setUp(); + + $this->assigns = array( + 'xss' => self::XSS, + ); + } + + public function testUnescaped() + { + $text = "{{ xss }}"; + $expected = self::XSS; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testEscapedManually() + { + $text = "{{ xss | escape }}"; + $expected = self::XSS_FAILED; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testRawWithoutAutoEscape() + { + $text = "{{ xss | raw }}"; + $expected = self::XSS; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testEscapedAutomatically() + { + Liquid::set('ESCAPE_BY_DEFAULT', true); + + $text = "{{ xss }}"; + $expected = self::XSS_FAILED; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testEscapedManuallyInAutoMode() + { + Liquid::set('ESCAPE_BY_DEFAULT', true); + + // text should only be escaped once + $text = "{{ xss | escape }}"; + $expected = self::XSS_FAILED; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testRawInAutoMode() + { + Liquid::set('ESCAPE_BY_DEFAULT', true); + + $text = "{{ xss | raw }}"; + $expected = self::XSS; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testNlToBr() + { + Liquid::set('ESCAPE_BY_DEFAULT', true); + $text = "{{ xss | newline_to_br }}"; + $expected = self::XSS."
\n".self::XSS; + $this->assertTemplateResult($expected, $text, array('xss' => self::XSS."\n".self::XSS)); + } + + public function testToStringEscape() + { + $this->assertTemplateResult(self::XSS_FAILED, "{{ xss | escape }}", array('xss' => new ObjectWithToString(self::XSS))); + } + + public function testToStringEscapeDefault() + { + Liquid::set('ESCAPE_BY_DEFAULT', true); + $this->assertTemplateResult(self::XSS_FAILED, "{{ xss }}", array('xss' => new ObjectWithToString(self::XSS))); + } + + /** System default value for the escape flag */ + private static $escapeDefault; + + public static function setUpBeforeClass(): void + { + // save system default value for the escape flag before all tests + self::$escapeDefault = Liquid::get('ESCAPE_BY_DEFAULT'); + } + + protected function tearDown(): void + { + // reset to the default after each test + Liquid::set('ESCAPE_BY_DEFAULT', self::$escapeDefault); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/FilterbankTest.php b/package/vendor/liquid/liquid/tests/Liquid/FilterbankTest.php new file mode 100644 index 0000000..9786583 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/FilterbankTest.php @@ -0,0 +1,244 @@ +variable = 'set'; + return 'set'; + } + + public function instance_test_two() + { + return $this->variable; + } +} + +} // global namespace + +namespace Liquid { + +use Liquid\Cache\File; + +class NamespacedClassFilter +{ + public static function static_test2($var) + { + return "good {$var}"; + } +} + +class FilterbankTest extends TestCase +{ + /** @var FilterBank */ + private $filterBank; + + /** @var Context */ + private $context; + + protected function setUp(): void + { + parent::setUp(); + + $this->context = new Context(); + $this->filterBank = new FilterBank($this->context); + } + + protected function tearDown(): void + { + // have to destroy these else PHP goes nuts + unset($this->context); + unset($this->filterBank); + } + + /** + */ + public function testAddFilterNotObjectAndString() + { + $this->expectException(\Liquid\Exception\WrongArgumentException::class); + + $this->filterBank->addFilter(array()); + } + + /** + */ + public function testAddFilterNoFunctionOrClass() + { + $this->expectException(\Liquid\Exception\WrongArgumentException::class); + + $this->filterBank->addFilter('no_such_function_or_class'); + } + + public function testTypeErrorExceptionAndCallDateFilterWithoutArguments() + { + if (\PHP_VERSION_ID < 70100) { + $this->markTestSkipped('TypeError is not thrown in PHP 7.0'); + } + + $var = new Variable('var | date'); + $this->context->set('var', 1000); + + $this->expectException(\Liquid\LiquidException::class); + $var->render($this->context); + } + + public function testInvokeNoFilter() + { + $value = 'value'; + $this->assertEquals($value, $this->filterBank->invoke('non_existing_filter', $value)); + } + + /** + * Test using a simple function + */ + public function testFunctionFilter() + { + $var = new Variable('var | functionFilter'); + $this->context->set('var', 1000); + $this->context->addFilters('functionFilter'); + $this->assertEquals('worked', $var->render($this->context)); + } + + /** + * Test using a namespaced static class + */ + public function testNamespacedStaticClassFilter() + { + $var = new Variable('var | static_test2'); + $this->context->set('var', 1000); + $this->context->addFilters(NamespacedClassFilter::class); + $this->assertEquals('good 1000', $var->render($this->context)); + } + + /** + * Test using a static class + */ + public function testStaticClassFilter() + { + $var = new Variable('var | static_test'); + $this->context->set('var', 1000); + $this->context->addFilters(\ClassFilter::class); + $this->assertEquals('worked', $var->render($this->context)); + } + + /** + * Test with instance method on a static class + */ + public function testStaticMixedClassFilter() + { + $var = new Variable('var | instance_test_one'); + $this->context->set('var', 'foo'); + $this->context->addFilters(\ClassFilter::class); + $this->assertEquals('foo', $var->render($this->context)); + } + + /** + * Test using an object as a filter; an object fiter will retain its state + * between calls to its filters. + */ + public function testObjectFilter() + { + $var = new Variable('var | instance_test_one'); + $this->context->set('var', 1000); + $this->context->addFilters(new \ClassFilter()); + $this->assertEquals('set', $var->render($this->context)); + + $var = new Variable('var | instance_test_two'); + $this->assertEquals('set', $var->render($this->context)); + + $var = new Variable('var | static_test'); + $this->assertEquals('worked', $var->render($this->context)); + + $var = new Variable('var | __construct'); + $this->assertEquals('1000', $var->render($this->context)); + } + + public function testObjectFilterDontCallConstruct() + { + $this->context->set('var', 1000); + $this->context->addFilters(new \ClassFilter()); + + $filterbankReflectionClass = new \ReflectionClass(Context::class); + $methodMapProperty = $filterbankReflectionClass->getProperty('filterbank'); + $methodMapProperty->setAccessible(true); + $filterbank = $methodMapProperty->getValue($this->context); + + $filterbankReflectionClass = new \ReflectionClass(Filterbank::class); + $methodMapProperty = $filterbankReflectionClass->getProperty('methodMap'); + $methodMapProperty->setAccessible(true); + $methodMap = $methodMapProperty->getValue($filterbank); + + $this->assertArrayNotHasKey('__construct', $methodMap); + + $var = new Variable('var | __construct'); + $this->assertEquals('1000', $var->render($this->context)); + } + + public function testCallbackFilter() + { + $var = new Variable('var | my_callback'); + $this->context->set('var', 1000); + $this->context->addFilters('my_callback', function ($var) { + return $var * 2; + }); + $this->assertEquals('2000', $var->render($this->context)); + } + + /** + * Closures are not to be serialized. Let's check that. + */ + public function testWithSerializingCache() + { + $template = new Template(); + $template->registerFilter('foo', function ($arg) { + return "Foo $arg"; + }); + $template->setCache(new File(array( + 'cache_dir' => __DIR__.'/cache_dir/', + ))); + $template->parse("{{'test' | foo }}"); + $this->assertEquals('Foo test', $template->render()); + + $template->parse("{{'bar' | foo }}"); + $this->assertEquals('Foo bar', $template->render()); + } +} + +} // Liquid namespace diff --git a/package/vendor/liquid/liquid/tests/Liquid/FixturesTest.php b/package/vendor/liquid/liquid/tests/Liquid/FixturesTest.php new file mode 100644 index 0000000..07e112b --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/FixturesTest.php @@ -0,0 +1,50 @@ +setFileSystem(new Virtual(function ($filename) { + if (is_file(__DIR__.'/fixtures/'.$filename)) { + return file_get_contents(__DIR__.'/fixtures/'.$filename); + } + })); + + $template->parse(file_get_contents($liquid)); + $result = $template->render(include $data); + + if (getenv('GOLDEN') !== false) { + file_put_contents($expected, $result); + $this->markTestIncomplete("Saved golden fixture"); + } + + $this->assertEquals(file_get_contents($expected), $result); + } + + public function fixtures() + { + foreach (array_map(null, glob(__DIR__.'/fixtures/*.liquid'), glob(__DIR__.'/fixtures/*.php'), glob(__DIR__.'/fixtures/*.html')) as $files) { + yield basename($files[0], '.liquid') => $files; + }; + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/LiquidTest.php b/package/vendor/liquid/liquid/tests/Liquid/LiquidTest.php new file mode 100644 index 0000000..3934e51 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/LiquidTest.php @@ -0,0 +1,88 @@ +assertNull(Liquid::get('no_such_value')); + } + + public function testSetProperty() + { + $key = 'test_key'; + $value = 'test_value'; + Liquid::set($key, $value); + $this->assertSame($value, Liquid::get($key)); + } + + public function testGetSetAllowedChars() + { + Liquid::set('ALLOWED_VARIABLE_CHARS', 'abc'); + $this->assertSame('abc', Liquid::get('ALLOWED_VARIABLE_CHARS')); + $this->assertSame('abc+', Liquid::get('VARIABLE_NAME')); + } + + public function testArrayFlattenEmptyArray() + { + $this->assertSame(array(), Liquid::arrayFlatten(array())); + } + + public function testArrayFlattenFlatArray() + { + $object = new \stdClass(); + + // Method does not maintain keys. + $original = array( + 'one' => 'one_value', + 42, + $object, + ); + + $expected = array( + 'one_value', + 42, + $object + ); + + $this->assertEquals($expected, Liquid::arrayFlatten($original)); + } + + public function testArrayFlattenNestedArray() + { + $object = new \stdClass(); + + // Method does not maintain keys. + $original = array( + 'one' => 'one_value', + 42 => array( + 'one_value', + array( + 'two_value', + 10 + ), + ), + $object, + ); + + $expected = array( + 'one_value', + 'one_value', + 'two_value', + 10, + $object + ); + + $this->assertEquals($expected, Liquid::arrayFlatten($original)); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/LocalFileSystemTest.php b/package/vendor/liquid/liquid/tests/Liquid/LocalFileSystemTest.php new file mode 100644 index 0000000..4e38e7d --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/LocalFileSystemTest.php @@ -0,0 +1,169 @@ +root = __DIR__ . DIRECTORY_SEPARATOR . self::TEMPLATES_DIR . DIRECTORY_SEPARATOR; + // reset to defaults + Liquid::set('INCLUDE_ALLOW_EXT', false); + } + + /** + */ + public function testIllegalTemplateNameEmpty() + { + $this->expectException(\Liquid\LiquidException::class); + + $fileSystem = new Local(''); + $fileSystem->fullPath(''); + } + + /** + */ + public function testIllegalRootPath() + { + $this->expectException(\Liquid\LiquidException::class); + + $fileSystem = new Local('invalid/not/found'); + $fileSystem->fullPath(''); + } + + /** + */ + public function testIllegalTemplateNameIncludeExtension() + { + $this->expectException(\Liquid\LiquidException::class); + + Liquid::set('INCLUDE_ALLOW_EXT', false); + + $fileSystem = new Local(''); + $fileSystem->fullPath('has_extension.ext'); + } + + /** + */ + public function testIllegalTemplateNameNotIncludeExtension() + { + $this->expectException(\Liquid\LiquidException::class); + + Liquid::set('INCLUDE_ALLOW_EXT', true); + + $fileSystem = new Local(''); + $fileSystem->fullPath('has_extension'); + } + + /** + */ + public function testIllegalTemplatePathNoRoot() + { + $this->expectException(\Liquid\LiquidException::class); + + $fileSystem = new Local(''); + $fileSystem->fullPath('mypartial'); + } + + /** + */ + public function testIllegalTemplatePathNoFileExists() + { + $this->expectException(\Liquid\LiquidException::class); + + $fileSystem = new Local(dirname(__DIR__)); + $fileSystem->fullPath('no_such_file_exists'); + } + + /** + */ + public function testIllegalTemplatePathNotUnderTemplateRoot() + { + $this->expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('not under'); + + Liquid::set('INCLUDE_ALLOW_EXT', true); + $fileSystem = new Local(dirname($this->root)); + // find any fail under deeper under the root, so all other checks would pass + $filesUnderCurrentDir = array_map('basename', glob(dirname(__DIR__).'/../*')); + // path relative to root; we can't start it with a dot since it isn't allowed anyway + $fileSystem->fullPath(self::TEMPLATES_DIR."/../../../{$filesUnderCurrentDir[0]}"); + } + + public function testValidPathWithDefaultExtension() + { + $templateName = 'mypartial'; + + $fileSystem = new Local($this->root); + $this->assertEquals($this->root . Liquid::get('INCLUDE_PREFIX') . $templateName . '.' . Liquid::get('INCLUDE_SUFFIX'), $fileSystem->fullPath($templateName)); + } + + public function testValidPathWithCustomExtension() + { + Liquid::set('INCLUDE_PREFIX', ''); + Liquid::set('INCLUDE_SUFFIX', 'tpl'); + + $templateName = 'mypartial'; + + $fileSystem = new Local($this->root); + $this->assertEquals($this->root . Liquid::get('INCLUDE_PREFIX') . $templateName . '.' . Liquid::get('INCLUDE_SUFFIX'), $fileSystem->fullPath($templateName)); + } + + /** + */ + public function testReadIllegalTemplatePathNoFileExists() + { + $this->expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('File not found'); + + $fileSystem = new Local(dirname(__DIR__)); + $fileSystem->readTemplateFile('no_such_file_exists'); + } + + public function testReadTemplateFile() + { + Liquid::set('INCLUDE_PREFIX', ''); + Liquid::set('INCLUDE_SUFFIX', 'tpl'); + + $fileSystem = new Local($this->root); + $this->assertEquals('test content', trim($fileSystem->readTemplateFile('mypartial'))); + } + + public function testDeprecatedLocalFileSystemExists() + { + $this->assertInstanceOf(Local::class, new LocalFileSystem($this->root)); + } + + public function testParseTemplateFile() + { + Liquid::set('INCLUDE_PREFIX', ''); + Liquid::set('INCLUDE_SUFFIX', 'tpl'); + + $template = new Template($this->root); + $this->assertEquals("test content\n", $template->parseFile('mypartial')->render()); + } + + /** + */ + public function testParseTemplateFileError() + { + $this->expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('Could not load a template'); + + $template = new Template(); + $template->parseFile('mypartial'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/OutputTest.php b/package/vendor/liquid/liquid/tests/Liquid/OutputTest.php new file mode 100644 index 0000000..a18b048 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/OutputTest.php @@ -0,0 +1,194 @@ +" . $input . ""; + } + + public function paragraph($input) + { + return "

" . $input . "

"; + } + + public function link_to($name, $url, $protocol) + { + return "" . $name . ""; + } + + public function str_replace($input, $data) + { + foreach ($data as $k => $v) { + $input = str_replace("[" . $k . "]", $v, $input); + } + return $input; + } + + public function img_url($input, $size, $opts = null) + { + $output = "image_" . $size; + if (isset($opts['crop'])) { + $output .= "_cropped_" . $opts['crop']; + } + if (isset($opts['scale'])) { + $output .= "@" . $opts['scale'] . 'x'; + } + return $output . ".png"; + } +} + +class OutputTest extends TestCase +{ + protected $assigns = array(); + + protected function setUp(): void + { + parent::setUp(); + + $this->assigns = array( + 'best_cars' => 'bmw', + 'car' => array('bmw' => 'good', 'gm' => 'bad') + ); + + $this->filters = new FunnyFilter(); + } + + public function testVariable() + { + $text = " {{best_cars}} "; + $expected = " bmw "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariableTrasversing() + { + $text = " {{car.bmw}} {{car.gm}} {{car.bmw}} "; + + $expected = " good bad good "; + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePiping() + { + $text = " {{ car.gm | make_funny }} "; + $expected = " LOL "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithInput() + { + $text = " {{ car.gm | cite_funny }} "; + $expected = " LOL: bad "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithArgs() + { + $text = " {{ car.gm | add_smiley : '=(' }} "; + $expected = " bad =( "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function textVariablePipingWithNoArgs() + { + $text = " {{ car.gm | add_smile }} "; + $expected = " bad =( "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testMultipleVariablePipingWithArgs() + { + $text = " {{ car.gm | add_smiley : '=(' | add_smiley : '=('}} "; + $expected = " bad =( =( "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithTwoArgs() + { + $text = " {{ car.gm | add_tag : 'span', 'bar'}} "; + $expected = " bad "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithVariableArgs() + { + $text = " {{ car.gm | add_tag : 'span', car.bmw}} "; + $expected = " bad "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithKeywordArg() + { + $text = " {{ 'Welcome, [name]' | str_replace: name: 'Santa' }} "; + $expected = " Welcome, Santa "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testVariablePipingWithArgsAndKeywordArgs() + { + $text = " {{ car.gm | img_url: '450x450', crop: 'center', scale: 2 }} "; + $expected = " image_450x450_cropped_center@2x.png "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testMultiplePipings() + { + $text = " {{ best_cars | cite_funny | paragraph }} "; + $expected = "

LOL: bmw

"; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + public function testLinkTo() + { + $text = " {{ 'Typo' | link_to: 'typo.leetsoft.com':'http' }} "; + $expected = " Typo "; + + $this->assertTemplateResult($expected, $text, $this->assigns); + } + + /** + */ + public function testVariableWithANewLine() + { + $text = "{{ aaa\n }}"; + $this->assertTemplateResult('', $text, $this->assigns); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/ParsingQuirksTest.php b/package/vendor/liquid/liquid/tests/Liquid/ParsingQuirksTest.php new file mode 100644 index 0000000..ce0db79 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/ParsingQuirksTest.php @@ -0,0 +1,27 @@ +parse($text); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertEquals($text, $template->render()); + $this->assertIsString($nodelist[0]); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/RegexpTest.php b/package/vendor/liquid/liquid/tests/Liquid/RegexpTest.php new file mode 100644 index 0000000..b7e6f45 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/RegexpTest.php @@ -0,0 +1,69 @@ +regexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/'); + } + + public function testEmpty() + { + $this->assertEquals(array(), $this->regexp->scan('')); + } + + public function testQuote() + { + $this->assertEquals(array('"arg 1"'), $this->regexp->scan('"arg 1"')); + } + + public function testWords() + { + $this->assertEquals(array('arg1', 'arg2'), $this->regexp->scan('arg1 arg2')); + } + + public function testQuotedWords() + { + $this->assertEquals(array('arg1', 'arg2', '"arg 3"'), $this->regexp->scan('arg1 arg2 "arg 3"')); + } + + public function testQuotedWords2() + { + $this->assertEquals(array('arg1', 'arg2', "'arg 3'"), $this->regexp->scan('arg1 arg2 \'arg 3\'')); + } + + public function testQuotedWordsInTheMiddle() + { + $this->assertEquals(array('arg1', 'arg2', '"arg 3"', 'arg4'), $this->regexp->scan('arg1 arg2 "arg 3" arg4 ')); + } + + public function testPregQuote() + { + $this->assertEquals('', $this->regexp->quote('')); + $this->assertEquals('abc', $this->regexp->quote('abc')); + $this->assertEquals('\/\(\{\}\)\/', $this->regexp->quote('/({})/')); + } + + public function testNoDelimiter() + { + $regexp = new Regexp('(example)'); + $this->assertEquals(array('(example)'), $regexp->scan('(example)')); + $this->assertEquals(array(), $regexp->scan('nothing')); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/StandardFiltersTest.php b/package/vendor/liquid/liquid/tests/Liquid/StandardFiltersTest.php new file mode 100644 index 0000000..e4d725e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/StandardFiltersTest.php @@ -0,0 +1,1144 @@ +context = new Context(); + } + + public function testSize() + { + $data = array( + 4 => 1000, + 3 => 100, + 2 => array('one', 'two'), + 1 => new \ArrayIterator(array('one')), + SizeClass::SIZE => new SizeClass(), + ); + + foreach ($data as $expected => $element) { + $this->assertEquals($expected, StandardFilters::size($element)); + } + } + + /** + */ + public function testSizeObject() + { + $this->expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('cannot be estimated'); + + StandardFilters::size((object) array()); + } + + public function testDowncase() + { + $data = array( + 'UpperCaseMiXed' => 'uppercasemixed', + 3 => 3, + // UTF-8 + 'Владимир' => 'владимир' + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::downcase($element)); + } + } + + public function testUpcase() + { + $data = array( + 'UpperCaseMiXed' => 'UPPERCASEMIXED', + 3 => 3, + // UTF-8 + 'владимир' => 'ВЛАДИМИР' + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::upcase($element)); + } + } + + public function testCapitalize() + { + $data = array( + 'one Word not' => 'One Word Not', + '1test' => '1Test', + '' => '', + // UTF-8 + 'владимир владимирович' => 'Владимир Владимирович' + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::capitalize($element)); + } + } + + public function testUrlEncode() + { + $data = array( + 'nothing' => 'nothing', + '%#&^' => '%25%23%26%5E', + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::url_encode($element)); + } + } + + + public function testUrlDecode() + { + $data = array( + '%25%23%26%5E' => '%#&^', + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::url_decode($element)); + } + } + + + public function testRaw() + { + $data = array( + "Anything" => "Anything", + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::raw($element)); + } + } + + public function testEscape() + { + $data = array( + "one Word's not" => "one Word's not", + "&><\"'" => "&><"'", + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::escape($element)); + } + + $this->assertSame(array(1), StandardFilters::escape(array(1))); + } + + public function testEscapeOnce() + { + $data = array( + "" => "<b><script>alert()</script>", + "a < b & c" => "a < b & c", + "a < b & c" => "a < b & c", + "<\">" => "<">", + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::escape_once($element)); + } + + $this->assertSame(array(1), StandardFilters::escape_once(array(1))); + } + + public function testStripNewLines() + { + $data = array( + "one Word\r\n not\r\n\r\n" => "one Word not", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::strip_newlines($element)); + } + } + + public function testNewLineToBr() + { + $data = array( + "one Word\n not\n" => "one Word
\n not
\n", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::newline_to_br($element)); + } + } + + public function testReplace() + { + // Replace for empty string + $data = array( + "one Word not Word" => "one not ", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::replace($element, 'Word')); + } + + // Replace for "Hello" string + $data = array( + "one Word not Word" => "one Hello not Hello", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::replace($element, 'Word', 'Hello')); + } + } + + public function testReplaceFirst() + { + // Replace for empty string + $data = array( + "one Word not Word" => "one not Word", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::replace_first($element, 'Word')); + } + + // Replace for "Hello" string + $data = array( + "one Word not Word" => "one Hello not Word", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::replace_first($element, 'Word', 'Hello')); + } + } + + public function testRemove() + { + $data = array( + "one Word not Word" => "one not ", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::remove($element, 'Word')); + } + } + + public function testRemoveFirst() + { + $data = array( + "one Word not Word" => "one not Word", + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::remove_first($element, 'Word')); + } + } + + public function testAppend() + { + $data = array( + "one Word not Word" => "one Word not Word appended", + '' => ' appended', + 3 => '3 appended', + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::append($element, ' appended')); + } + } + + public function testPrepend() + { + $data = array( + "one Word not Word" => "prepended one Word not Word", + '' => 'prepended ', + 3 => 'prepended 3', + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::prepend($element, 'prepended ')); + } + } + + public function testSlice() + { + // Slice up to the end + $data = array( + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + '', + '', + ), + array( + array(1, 2, 3, 4, 5), + array(3, 4, 5), + ), + array( + new \ArrayIterator(array(1, 2, 3, 4, 5)), + array(3, 4, 5), + ), + array( + '12345', + '345' + ), + array( + 100, + 100 + ), + ); + + foreach ($data as $item) { + $actual = StandardFilters::slice($item[0], 2); + if ($actual instanceof \Traversable) { + $actual = iterator_to_array($actual); + } + $this->assertEquals($item[1], $actual); + } + + // Slice a few elements + $data = array( + array( + null, + null, + ), + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + '', + '', + ), + array( + array(1, 2, 3, 4, 5), + array(3, 4), + ), + array( + new \ArrayIterator(array(1, 2, 3, 4, 5)), + array(3, 4), + ), + array( + '12345', + '34' + ), + array( + 100, + 100 + ), + ); + + foreach ($data as $item) { + $actual = StandardFilters::slice($item[0], 2, 2); + if ($actual instanceof \Traversable) { + $actual = iterator_to_array($actual); + } + $this->assertEquals($item[1], $actual); + } + + $this->assertEquals('Владимир', StandardFilters::slice('Владимир Владимирович', 0, 8)); + } + + public function testTruncate() + { + // Truncate with default ending + $data = array( + '' => '', + str_repeat('a', 150) => str_repeat('a', 100) . '...', + 'test' => 'test', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::truncate($element)); + } + + // Custom length + $this->assertEquals('abc...', StandardFilters::truncate('abcdef', 3)); + + // Custom ending + $this->assertEquals('abcend', StandardFilters::truncate('abcdef', 3, 'end')); + + // UTF-8 + $this->assertEquals('Влад...', StandardFilters::truncate('Владимир Владимирович', 4)); + } + + public function testTruncateWords() + { + // Truncate with default ending + $data = array( + '' => '', + str_repeat('abc ', 10) => rtrim(str_repeat('abc ', 3)) . '...', + 'test two' => 'test two', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::truncatewords($element)); + } + + // Custom length + $this->assertEquals('hello...', StandardFilters::truncatewords('hello from string', 1)); + + // Custom ending + $this->assertEquals('helloend', StandardFilters::truncatewords('hello from string', 1, 'end')); + } + + public function testStripHtml() + { + $data = array( + '' => '', + 'test no html tags' => 'test no html tags', + 'test

paragraph

hello' => 'test paragraph hello', + 3 => 3, + ); + + foreach ($data as $element => $expected) { + $this->assertEquals($expected, StandardFilters::strip_html($element)); + } + } + + public function testJoin() + { + $data = array( + array( + array(), + '', + ), + array( + new \ArrayIterator(array()), + '' + ), + array( + '', + '', + ), + array( + array(1, 2, 3, 4, 5), + '1 2 3 4 5' + ), + array( + new \ArrayIterator(array(1, 2, 3, 4, 5)), + '1 2 3 4 5' + ), + array( + 100, + 100 + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::join($item[0])); + } + + // Custom glue + $this->assertEquals('1-2-3', StandardFilters::join(array(1, 2, 3), '-')); + $this->assertEquals('1-2-3', StandardFilters::join(new \ArrayIterator(array(1, 2, 3)), '-')); + } + + public function testSort() + { + $data = array( + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + array(1, 5, 3, 4, 2), + array(1, 2, 3, 4, 5), + ), + array( + new \ArrayIterator(array(1, 5, 3, 4, 2)), + array(1, 2, 3, 4, 5), + ), + ); + + foreach ($data as $key => $item) { + $this->assertEquals(array_values($item[1]), array_values(StandardFilters::sort($item[0])), "Sort failed for case #{$key}"); + } + + // Sort by inner key + $original = array( + array('a' => 20, 'b' => 10), + array('a' => 45, 'b' => 5), + array('a' => 40, 'b' => 6), + array('a' => 30, 'b' => 48), + ); + $expected = array( + array('a' => 45, 'b' => 5), + array('a' => 40, 'b' => 6), + array('a' => 20, 'b' => 10), + array('a' => 30, 'b' => 48), + ); + + $this->assertEquals($expected, array_values(StandardFilters::sort($original, 'b'))); + $this->assertEquals($expected, array_values(StandardFilters::sort(new \ArrayIterator($original), 'b'))); + } + + /* + + I've commented this out as its not one of the Ruby Standard Filters + + public function testSortKey() { + $data = array( + array( + array(), + array(), + ), + array( + array('b' => 1, 'c' => 5, 'a' => 3, 'z' => 4, 'h' => 2), + array('a' => 3, 'b' => 1, 'c' => 5, 'h' => 2, 'z' => 4), + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::sort_key($item[0])); + } + } + */ + + public function testDefault() + { + $this->assertEquals('hello', StandardFilters::_default('', 'hello')); + $this->assertEquals('world', StandardFilters::_default('world', 'hello')); + // check that our workaround for 'default' works as it should + $this->assertTemplateResult('something', '{{ nothing | default: "something" }}'); + } + + public function testUnique() + { + $data = array( + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + array(1, 1, 5, 3, 4, 2, 5, 2), + array(1, 5, 3, 4, 2), + ), + array( + new \ArrayIterator(array(1, 1, 5, 3, 4, 2, 5, 2)), + array(1, 5, 3, 4, 2), + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], array_values(StandardFilters::uniq($item[0]))); + } + } + + public function testReverse() + { + $data = array( + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + array(1, 1, 5, 3, 4, 2, 5, 2), + array(2, 5, 2, 4, 3, 5, 1, 1), + ), + array( + new \ArrayIterator(array(1, 1, 5, 3, 4, 2, 5, 2)), + array(2, 5, 2, 4, 3, 5, 1, 1), + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::reverse($item[0]), '', 0, 10, true); + } + } + + public function testMap() + { + $data = array( + array( + array(), + array(), + ), + array( + new \ArrayIterator(array()), + array(), + ), + array( + array( + function () { + return 'from function '; + }, + array( + 'b' => 10, + 'attr' => 'value ', + ), + array( + 'a' => 20, + 'no_attr' => 'another value ' + ), + ), + array('from function ', 'value ', null), + ), + array( + new \ArrayIterator(array( + function () { + return 'from function '; + }, + array( + 'b' => 10, + 'attr' => 'value ', + ), + array( + 'a' => 20, + 'no_attr' => 'another value ' + ), + )), + array('from function ', 'value ', null), + ), + array( + 0, + 0 + ) + ); + + foreach ($data as $item) { + $actual = StandardFilters::map($item[0], 'attr'); + if ($actual instanceof \Traversable) { + $actual = iterator_to_array($actual); + } + $this->assertEquals($item[1], $actual); + } + } + + public function testFirst() + { + $data = array( + array( + array(), + false, + ), + array( + new \ArrayIterator(array()), + false, + ), + array( + array('two', 'one', 'three'), + 'two', + ), + array( + new \ArrayIterator(array('two', 'one', 'three')), + 'two', + ), + array( + array(100, 400, 200), + 100, + ), + array( + new \ArrayIterator(array(100, 400, 200)), + 100, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::first($item[0])); + } + } + + public function testLast() + { + $data = array( + array( + array(), + false, + ), + array( + new \ArrayIterator(array()), + false, + ), + array( + array('two', 'one', 'three'), + 'three', + ), + array( + new \ArrayIterator(array('two', 'one', 'three')), + 'three', + ), + array( + array(100, 400, 200), + 200, + ), + array( + new \ArrayIterator(array(100, 400, 200)), + 200, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::last($item[0])); + } + } + + public function testString() + { + $data = array( + array( + 1, + '1', + ), + array( + new SizeClass(), + "forty two", + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::string($item[0])); + } + } + + public function testSplit() + { + $data = array( + array( + '', + array(), + ), + array( + null, + array(), + ), + array( + 'two-one-three', + array('two', 'one', 'three'), + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::split($item[0], '-')); + } + } + + public function testStrip() + { + $data = array( + array( + '', + '', + ), + array( + ' hello ', + 'hello', + ), + array( + 1, + 1, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::strip($item[0])); + } + } + + public function testLStrip() + { + $data = array( + array( + '', + '', + ), + array( + ' hello ', + 'hello ', + ), + array( + 1, + 1, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::lstrip($item[0])); + } + } + + public function testRStrip() + { + $data = array( + array( + '', + '', + ), + array( + ' hello ', + ' hello', + ), + array( + 1, + 1, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[1], StandardFilters::rstrip($item[0])); + } + } + + public function testPlus() + { + $data = array( + array( + '', + '', + 0, + ), + array( + 10, + 20, + 30, + ), + array( + 1.5, + 2.7, + 4.2, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[2], StandardFilters::plus($item[0], $item[1]), '', 0.00001); + } + } + + public function testMinus() + { + $data = array( + array( + '', + '', + 0, + ), + array( + 10, + 20, + -10, + ), + array( + 1.5, + 2.7, + -1.2, + ), + array( + 3.1, + 3.1, + 0 + ) + ); + + foreach ($data as $item) { + $this->assertEquals($item[2], StandardFilters::minus($item[0], $item[1]), '', 0.00001); + } + } + + public function testTimes() + { + $data = array( + array( + '', + '', + 0, + ), + array( + 10, + 20, + 200, + ), + array( + 1.5, + 2.7, + 4.05, + ), + array( + 7.5, + 0, + 0 + ) + ); + + foreach ($data as $item) { + $this->assertEquals($item[2], StandardFilters::times($item[0], $item[1]), '', 0.00001); + } + } + + public function testDivideBy() + { + $data = array( + array( + '20', + 10, + 2, + ), + array( + 10, + 20, + 0.5, + ), + array( + 0, + 200, + 0, + ), + array( + 10, + 0.5, + 20, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[2], StandardFilters::divided_by($item[0], $item[1]), '', 0.00001); + } + } + + public function testModulo() + { + $data = array( + array( + '20', + 10, + 0, + ), + array( + 10, + 20, + 10, + ), + array( + 8, + 3, + 2, + ), + array( + 8.9, + 3.5, + 1.9, + ), + array( + 183.357, + 12, + 3.357, + ), + ); + + foreach ($data as $item) { + $this->assertEquals($item[2], StandardFilters::modulo($item[0], $item[1]), '', 0.00001); + } + } + + public function testRound() + { + $data = array( + array( + '20.003', + 2, + 20.00, + ), + array( + 10, + 3, + 10.000, + ), + array( + 8, + 0, + 8.0, + ), + ); + + foreach ($data as $item) { + $this->assertSame($item[2], StandardFilters::round($item[0], $item[1])); + } + } + + public function testCeil() + { + $data = array( + array( + '20.003', + 21, + ), + array( + 10, + 10, + ), + array( + 0.42, + 1, + ), + ); + + foreach ($data as $item) { + $this->assertSame($item[1], StandardFilters::ceil($item[0])); + } + } + + public function testFloor() + { + $data = array( + array( + '20.003', + 20, + ), + array( + 10, + 10, + ), + array( + 0.42, + 0, + ), + array( + 2.5, + 2, + ) + ); + + foreach ($data as $item) { + $this->assertSame($item[1], StandardFilters::floor($item[0])); + } + } + + public function testLocalFilter() + { + $var = new Variable('var | money'); + $this->context->set('var', 1000); + $this->context->addFilters(new MoneyFilter()); + $this->assertEquals(' 1000$ ', $var->render($this->context)); + } + + public function testUnderscoreInFilterName() + { + $var = new Variable('var | money_with_underscore '); + $this->context->set('var', 1000); + $this->context->addFilters(new MoneyFilter()); + $this->assertEquals(' 1000$ ', $var->render($this->context)); + } + + public function testSecondFilterOverwritesFirst() + { + $var = new Variable('var | money '); + $this->context->set('var', 1000); + $this->context->addFilters(new MoneyFilter()); + $this->context->addFilters(new CanadianMoneyFilter()); + $this->assertEquals(' 1000$ CAD ', $var->render($this->context)); + } + + public function testDate() + { + $var = new Variable('var | date, "%Y"'); + $this->context->set('var', '2017-07-01 21:00:00'); + $this->assertEquals('2017', $var->render($this->context)); + + $var = new Variable("var | date: '%d/%m/%Y %l:%M %p'"); + $this->context->set('var', '2017-07-01 21:00:00'); + $this->assertEquals('01/07/2017 9:00 PM', $var->render($this->context)); + + $var = new Variable('var | date, ""'); + $this->context->set('var', '2017-07-01 21:00:00'); + $this->assertEquals('', $var->render($this->context)); + + $var = new Variable('var | date, "r"'); + $this->context->set('var', 1000000000); + $this->assertEquals(date('r', 1000000000), $var->render($this->context)); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/NoTransformTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/NoTransformTest.php new file mode 100644 index 0000000..7fe98b7 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/NoTransformTest.php @@ -0,0 +1,35 @@ +assertTemplateResult( + 'this text should come out of the template without change...', + 'this text should come out of the template without change...' + ); + + $this->assertTemplateResult('blah', 'blah'); + $this->assertTemplateResult('', ''); + $this->assertTemplateResult('|,.:', '|,.:'); + $this->assertTemplateResult('', ''); + + $text = "this shouldnt see any transformation either but has multiple lines + as you can clearly see here ..."; + + $this->assertTemplateResult($text, $text); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagAssignTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagAssignTest.php new file mode 100644 index 0000000..74c83b6 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagAssignTest.php @@ -0,0 +1,98 @@ +expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + + $template->parse('{% assign test %}'); + } + + /** + * Tests a simple assignment with no filters + */ + public function testSimpleAssign() + { + $template = new Template(); + + $template->parse('{% assign test = "hello" %}{{ test }}'); + $this->assertTrue($template->render() === 'hello'); + } + + /** + * Tests filtered value assignment + */ + public function testAssignWithFilters() + { + $template = new Template(); + + $template->parse('{% assign test = "hello" | upcase %}{{ test }}'); + $this->assertTrue($template->render() === 'HELLO'); + + $template->parse('{% assign test = "hello" | upcase | downcase | capitalize %}{{ test }}'); + $this->assertTrue($template->render() === 'Hello'); + + $template->parse('{% assign test = var1 | first | upcase %}{{ test }}'); + $this->assertTrue($template->render(array('var1' => array('a', 'b', 'c'))) === 'A'); + + $template->parse('{% assign test = var1 | last | upcase %}{{ test }}'); + $this->assertTrue($template->render(array('var1' => array('a', 'b', 'c'))) === 'C'); + + $template->parse('{% assign test = var1 | join %}{{ test }}'); + $this->assertTrue($template->render(array('var1' => array('a', 'b', 'c'))) === 'a b c'); + + $template->parse('{% assign test = var1 | join : "." %}{{ test }}'); + $this->assertTrue($template->render(array('var1' => array('a', 'b', 'c'))) === 'a.b.c'); + } + + /** + * Tests filtered value assignment with separators + */ + public function testTagAssignWithSplit() + { + $template = new Template(); + + $template->parse('{% assign rows = "one|two|three,one|two|three" | upcase | split: "," %}{% for row in rows %}{% assign cols = row | split: "|" %}{% for col in cols %} {{col}}{%endfor%}{% endfor %}'); + $this->assertEquals($template->render(), ' ONE TWO THREE ONE TWO THREE'); + + $template->parse('{% assign issue_numbers = "1339|1338|1321" | split: "|" %}{% for issue in issue_numbers %} {{ issue }}{% endfor %}'); + $this->assertEquals($template->render(), ' 1339 1338 1321'); + } + + /** + * Tests a simple assignment with numbers + */ + public function testNumbersAssign() + { + $this->assertTemplateResult('42', '{% assign i = 42 %}{{ i }}'); + $this->assertTemplateResult('3.14', '{% assign i = 3.14 %}{{ i }}'); + $this->assertTemplateResult('-100', '{% assign i = -100 %}{{ i }}'); + $this->assertTemplateResult('-10.0', '{% assign i = -10.0 %}{{ i }}'); + $this->assertTemplateResult('-10.5', '{% assign i = -10.5 %}{{ i }}'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBlockTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBlockTest.php new file mode 100644 index 0000000..70aba90 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBlockTest.php @@ -0,0 +1,31 @@ +expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% block %}'); + } + + public function testCreateBlock() + { + $this->assertTemplateResult('block content', '{% block foo %}block content{% endblock %}'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBreakTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBreakTest.php new file mode 100644 index 0000000..d31b882 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagBreakTest.php @@ -0,0 +1,50 @@ +assertTemplateResult(' ', '{%for item in array%} {%break%} yo {%endfor%}', array('array' => array(1, 2, 3, 4))); + $this->assertTemplateResult(' yo ', '{%for item in array%} yo {%break%} {%endfor%}', array('array' => array(1, 2, 3, 4))); + $this->assertTemplateResult(' 1 2 ', '{%for item in array%} {%if item == 3%} {%break%} {%endif%} {{ item }} {%endfor%}', array('array' => array(1, 2, 3, 4))); + } + + public function testRange() + { + $this->assertTemplateResult(' ', '{%for item in (3..6)%} {%break%} yo {%endfor%}'); + $this->assertTemplateResult(' yo ', '{%for item in (3..6)%} yo {%break%} {%endfor%}'); + $this->assertTemplateResult(' 3 4 ', '{%for item in (3..6)%} {%if item == 5%} {%break%} {%endif%} {{ item }} {%endfor%}'); + } + + public function testTablerow() + { + $this->assertTemplateResult( + "\n\n", + '{%tablerow item in array%} {%break%} yo {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + $this->assertTemplateResult( + "\n yo \n", + '{%tablerow item in array%} yo {%break%} {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + $this->assertTemplateResult( + "\n 1 2 \n", + '{%tablerow item in array%} {%if item == 3%} {%break%} {%endif%} {{ item }} {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaptureTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaptureTest.php new file mode 100644 index 0000000..9752265 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaptureTest.php @@ -0,0 +1,34 @@ +expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + $template->parse("{% capture %} hello"); + } + + public function testCapture() + { + $assigns = array('var' => 'content'); + $this->assertTemplateResult('content foo content foo ', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', $assigns); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaseTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaseTest.php new file mode 100644 index 0000000..8e99ec9 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCaseTest.php @@ -0,0 +1,106 @@ + 2); + $this->assertTemplateResult(' its 2 ', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', $assigns); + + $assigns = array('condition' => 1); + $this->assertTemplateResult(' its 1 ', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', $assigns); + + $assigns = array('condition' => 3); + $this->assertTemplateResult('', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', $assigns); + + $assigns = array('condition' => "string here"); + $this->assertTemplateResult(' hit ', '{% case condition %}{% when "string here" %} hit {% endcase %}', $assigns); + + $assigns = array('condition' => "bad string here"); + $this->assertTemplateResult('', '{% case condition %}{% when "string here" %} hit {% endcase %}', $assigns); + } + + public function testCaseWithElse() + { + $assigns = array('condition' => 5); + $this->assertTemplateResult(' hit ', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', $assigns); + + $assigns = array('condition' => 6); + $this->assertTemplateResult(' else ', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', $assigns); + } + + /** + */ + public function testSyntaxErrorCase() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% case %}{% when 5 %}{% endcase %}'); + } + + /** + */ + public function testSyntaxErrorWhen() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% case condition %}{% when %}{% endcase %}'); + } + + /** + */ + public function testSyntaxErrorEnd() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% case condition %}{% end %}'); + } + + /** + */ + public function testObject() + { + $this->expectException(\Liquid\Exception\RenderException::class); + + $this->assertTemplateResult('', '{% case variable %}{% when 5 %}{% endcase %}', array('variable' => (object) array())); + } + + public function testStringable() + { + $this->assertTemplateResult('hit', '{% case variable %}{% when 100 %}hit{% endcase %}', array('variable' => new Stringable())); + } + + public function testToLiquid() + { + $this->assertTemplateResult('hit', '{% case variable %}{% when 100 %}hit{% endcase %}', array('variable' => new HasToLiquid())); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCommentTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCommentTest.php new file mode 100644 index 0000000..b942cae --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCommentTest.php @@ -0,0 +1,43 @@ +assertTemplateResult( + "the comment block should be removed .. right?", + "the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?" + ); + + $this->assertTemplateResult('', '{%comment%}{%endcomment%}'); + $this->assertTemplateResult('', '{%comment%}{% endcomment %}'); + $this->assertTemplateResult('', '{% comment %}{%endcomment%}'); + $this->assertTemplateResult('', '{% comment %}{% endcomment %}'); + $this->assertTemplateResult('', '{%comment%}comment{%endcomment%}'); + $this->assertTemplateResult('', '{% comment %}comment{% endcomment %}'); + + $this->assertTemplateResult('foobar', 'foo{%comment%}comment{%endcomment%}bar'); + $this->assertTemplateResult('foobar', 'foo{% comment %}comment{% endcomment %}bar'); + $this->assertTemplateResult('foobar', 'foo{%comment%} comment {%endcomment%}bar'); + $this->assertTemplateResult('foobar', 'foo{% comment %} comment {% endcomment %}bar'); + + $this->assertTemplateResult('foo bar', 'foo {%comment%} {%endcomment%} bar'); + $this->assertTemplateResult('foo bar', 'foo {%comment%}comment{%endcomment%} bar'); + $this->assertTemplateResult('foo bar', 'foo {%comment%} comment {%endcomment%} bar'); + + $this->assertTemplateResult('foobar', 'foo{%comment%} {%endcomment%}bar'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagContinueTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagContinueTest.php new file mode 100644 index 0000000..cd7f875 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagContinueTest.php @@ -0,0 +1,50 @@ +assertTemplateResult(' ', '{%for item in array%} {%continue%} yo {%endfor%}', array('array' => array(1, 2, 3, 4))); + $this->assertTemplateResult(' yo yo yo yo ', '{%for item in array%} yo {%continue%} {%endfor%}', array('array' => array(1, 2, 3, 4))); + $this->assertTemplateResult(' 1 2 4 ', '{%for item in array%} {%if item == 3%} {%continue%} {%endif%} {{ item }} {%endfor%}', array('array' => array(1, 2, 3, 4))); + } + + public function testRange() + { + $this->assertTemplateResult(' ', '{%for item in (3..6)%} {%continue%} yo {%endfor%}'); + $this->assertTemplateResult(' yo yo yo yo ', '{%for item in (3..6)%} yo {%continue%} {%endfor%}'); + $this->assertTemplateResult(' 3 4 6 ', '{%for item in (3..6)%} {%if item == 5%} {%continue%} {%endif%} {{ item }} {%endfor%}'); + } + + public function testTablerow() + { + $this->assertTemplateResult( + "\n\n", + '{%tablerow item in array%} {%continue%} yo {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + $this->assertTemplateResult( + "\n yo yo yo yo \n", + '{%tablerow item in array%} yo {%continue%} {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + $this->assertTemplateResult( + "\n 1 2 4 \n", + '{%tablerow item in array%} {%if item == 3%} {%continue%} {%endif%} {{ item }} {%endtablerow%}', + array('array' => array(1, 2, 3, 4)) + ); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCycleTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCycleTest.php new file mode 100644 index 0000000..857a59a --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagCycleTest.php @@ -0,0 +1,51 @@ +expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + $template->parse("{% cycle %}"); + } + + public function testCycle() + { + $this->assertTemplateResult('one', '{%cycle "one", "two"%}'); + $this->assertTemplateResult('one two', '{%cycle "one", "two"%} {%cycle "one", "two"%}'); + $this->assertTemplateResult('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}'); + } + + public function testMultipleCycles() + { + $this->assertTemplateResult('1 2 1 1 2 3 1', '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}'); + } + + public function testMultipleNamedCycles() + { + $this->assertTemplateResult('one one two two one one', '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}'); + } + + public function testMultipleNamedCyclesWithNamesFromContext() + { + $assigns = array("var1" => 1, "var2" => 2); + $this->assertTemplateResult('one one two two one one', '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', $assigns); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagDecrementTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagDecrementTest.php new file mode 100644 index 0000000..6d2def4 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagDecrementTest.php @@ -0,0 +1,49 @@ +expectException(\Liquid\LiquidException::class); + + $this->assertTemplateResult('', '{% decrement %}'); + } + + /** + * Undefined variable will become -1 + */ + public function testDecrementNonExistingVariable() + { + $this->assertTemplateResult(-1, '{% decrement no_such_var %}{{ no_such_var }}'); + } + + public function testDecrementVariable() + { + $this->assertTemplateResult(42, '{% decrement var %}{{ var }}', array('var' => 43)); + } + + public function testDecrementNestedVariable() + { + $this->assertTemplateResult(42, '{% for var in vars %}{% decrement var %}{{ var }}{% endfor %}', array('vars' => array(43))); + } + + public function testVariableNameContainingNumber() + { + $this->assertTemplateResult(42, '{% decrement var123 %}{{ var123 }}', array('var123' => 43)); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagExtendsTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagExtendsTest.php new file mode 100644 index 0000000..fe1252a --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagExtendsTest.php @@ -0,0 +1,215 @@ +fs = TestFileSystem::fromArray(array( + 'base' => "{% block content %}{% endblock %}{% block footer %}{% endblock %}", + 'sub-base' => "{% extends 'base' %}{% block content %}{% endblock %}{% block footer %} Boo! {% endblock %}", + )); + } + + protected function tearDown(): void + { + // PHP goes nuts unless we unset it + unset($this->fs); + } + + public function testBasicExtends() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% extends 'base' %}{% block content %}{{ hello }}{% endblock %}"); + $output = $template->render(array("hello" => "Hello!")); + $this->assertEquals("Hello!", $output); + } + + public function testDefaultContentExtends() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% block content %}{{ hello }}{% endblock %}\n{% extends 'sub-base' %}"); + $output = $template->render(array("hello" => "Hello!")); + $this->assertEquals("Hello!\n Boo! ", $output); + } + + public function testDeepExtends() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse('{% extends "sub-base" %}{% block content %}{{ hello }}{% endblock %}{% block footer %} I am a footer.{% endblock %}'); + + $output = $template->render(array("hello" => "Hello!")); + $this->assertEquals("Hello! I am a footer.", $output); + } + + public function testWithCache() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->setCache(new Local()); + + foreach (array("Before cache", "With cache") as $type) { + $template->parse("{% extends 'base' %}{% block content %}{{ hello }}{% endblock %}"); + $output = $template->render(array("hello" => "$type")); + $this->assertEquals($type, $output); + } + + $template->setCache(null); + } + + /** + * Render calls in this test will give different results (and fail the test) with cache enabled + */ + public function testExtendsReplaceContentWithCache() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'outer' => "{% block content %}Content for outer block{% endblock %} / {% block footer %}Footer for outer block{% endblock %}", + 'inner' => "{% extends 'outer' %}{% block content %}Content for inner block{% endblock %}", + ))); + + $contentsWithoutCache = $template->parseFile('inner')->render(); + + $template->setCache(new Local()); + $template->parseFile('outer'); + + $this->assertEquals($contentsWithoutCache, $template->parseFile('inner')->render()); + } + + public function testExtendsReplaceContentWithVariables() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'outer' => "{% block content %}Outer{{ a }}{% endblock %}Spacer{{ a }}{% block footer %}Footer{{ a }}{% endblock %}", + 'middle' => "{% extends 'outer' %}{% block content %}Middle{{ a }}{% endblock %}", + 'inner' => "{% extends 'middle' %}{% block content %}Inner{{ a }}{% endblock %}", + ))); + + $template->setCache(new Local()); + + $template->parseFile('outer')->render(['a' => '0']); + $template->parseFile('middle')->render(['a' => '1']); + $template->parseFile('middle')->render(['a' => '2']); + $this->assertEquals('Middle3Spacer3Footer3', $template->parseFile('middle')->render(['a' => '3'])); + $this->assertEquals('Inner4Spacer4Footer4', $template->parseFile('inner')->render(['a' => '4'])); + } + + public function testExtendsWithEmptyDefaultContent() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'base' => "
{% block content %}{% endblock %}
", + 'extends' => "{% extends 'base' %}{% block content %}{{ test }}{% endblock %}", + ))); + + $template->setCache(new Local()); + + $template->parseFile('base')->render(); + $template->parseFile('extends')->render(['test' => 'Foo']); + $template->parseFile('extends')->render(['test' => 'Bar']); + $this->assertEquals('
Baz
', $template->parseFile('extends')->render(['test' => 'Baz'])); + $this->assertEquals('
', $template->parseFile('base')->render()); + } + + public function testCacheDiscardedIfFileChanges() + { + $template = new Template(); + $template->setCache(new Local()); + + $content = "[{{ name }}]"; + $template->setFileSystem(TestFileSystem::fromArray(array( + 'outer' => &$content, + 'inner' => "{% extends 'outer' %}" + ))); + + $template->parseFile('inner'); + $output = $template->render(array("name" => "Example")); + $this->assertEquals("[Example]", $output); + + // this should go from cache + $template->parse("{% extends 'outer' %}"); + $output = $template->render(array("name" => "Example")); + $this->assertEquals("[Example]", $output); + + // content change should trigger re-render + $content = "<{{ name }}>"; + $template->parseFile('inner'); + $output = $template->render(array("name" => "Example")); + $this->assertEquals("", $output); + } + + /** + */ + public function testInvalidSyntaxNoTemplateName() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + $template->parse("{% extends %}"); + } + + /** + */ + public function testInvalidSyntaxNotQuotedTemplateName() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('Error in tag'); + + $template = new Template(); + $template->parse("{% extends base %}"); + } + + /** + */ + public function testMissingFilesystem() + { + $this->expectException(\Liquid\Exception\MissingFilesystemException::class); + $this->expectExceptionMessage('No file system'); + + $template = new Template(); + $template->parse("{% extends 'base' %}"); + } + + /** + */ + public function testInvalidSyntaxEmptyTemplateName() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% extends '' %}"); + } + + public function testInvalidSyntaxInvalidKeyword() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% extends 'base' nothing-should-be-here %}"); + + $this->markTestIncomplete("Exception is expected here"); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagForTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagForTest.php new file mode 100644 index 0000000..72ec2c6 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagForTest.php @@ -0,0 +1,224 @@ +expectException(\Liquid\Exception\ParseException::class); + + $template = new Template(); + $template->parse("{% for elem %}{% endfor %}"); + } + + public function testFor() + { + $this->assertTemplateResult('', '{%for item in array%} yo {%endfor%}', array('array' => array())); + $this->assertTemplateResult(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', array('array' => array(1, 2, 3, 4))); + $this->assertTemplateResult(' boo boo boo boo ', '{%for item in array%} boo {%endfor%}', array('array' => new \ArrayIterator(array(1, 2, 3, 4)))); + $this->assertTemplateResult('yoyo', '{%for item in array%}yo{%endfor%}', array('array' => array(1, 2))); + $this->assertTemplateResult(' yo ', '{%for item in array%} yo {%endfor%}', array('array' => array(1))); + $this->assertTemplateResult('', '{%for item in array%}{%endfor%}', array('array' => array(1, 2))); + + $expected = <<assertTemplateResult($expected, $template, array('array' => array(1, 2, 3))); + } + + public function testForWithVariable() + { + $this->assertTemplateResult(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', array('array' => array(1, 2, 3))); + $this->assertTemplateResult('123', '{%for item in array%}{{item}}{%endfor%}', array('array' => array(1, 2, 3))); + $this->assertTemplateResult('123', '{% for item in array %}{{item}}{% endfor %}', array('array' => array(1, 2, 3))); + $this->assertTemplateResult('abcd', '{%for item in array%}{{item}}{%endfor%}', array('array' => array('a', 'b', 'c', 'd'))); + $this->assertTemplateResult('a b c', '{%for item in array%}{{item}}{%endfor%}', array('array' => array('a', ' ', 'b', ' ', 'c'))); + $this->assertTemplateResult('abc', '{%for item in array%}{{item}}{%endfor%}', array('array' => array('a', '', 'b', '', 'c'))); + $this->assertTemplateResult(' a ', "{%\nfor item in array%} {{item}} {%endfor%}", array('array' => array('a'))); + } + + public function testForWithHash() + { + $this->assertTemplateResult('a=b c=d e=f ', '{%for item in array%}{{item[0]}}={{item[1]}} {%endfor%}', array('array' => array('a' => 'b', 'c' => 'd', 'e' => 'f'))); + } + + public function testForHelpers() + { + $assigns = array('array' => array(1, 2, 3)); + + $this->assertTemplateResult(' 1/3 2/3 3/3 ', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 1 0 0 ', '{%for item in array%} {{forloop.first}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 0 0 1 ', '{%for item in array%} {{forloop.last}} {%endfor%}', $assigns); + } + + public function testForHelpersWithOffsetAndLimit() + { + $assigns = array('array' => array(0, 1, 2, 3, 4)); + + $this->assertTemplateResult(' 1/3 2/3 3/3 ', '{%for item in array offset:1 limit:3%} {{forloop.index}}/{{forloop.length}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 1 2 3 ', '{%for item in array offset:1 limit:3%} {{forloop.index}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 0 1 2 ', '{%for item in array offset:1 limit:3%} {{forloop.index0}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 2 1 0 ', '{%for item in array offset:1 limit:3%} {{forloop.rindex0}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 3 2 1 ', '{%for item in array offset:1 limit:3%} {{forloop.rindex}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 1 0 0 ', '{%for item in array offset:1 limit:3%} {{forloop.first}} {%endfor%}', $assigns); + $this->assertTemplateResult(' 0 0 1 ', '{%for item in array offset:1 limit:3%} {{forloop.last}} {%endfor%}', $assigns); + } + + public function testForAndIf() + { + $assigns = array('array' => array(1, 2, 3)); + $this->assertTemplateResult(' yay ', '{%for item in array%} {% if forloop.first %}yay{% endif %} {%endfor%}', $assigns); + $this->assertTemplateResult(' yay boo boo ', '{%for item in array%} {% if forloop.first %}yay{% else %}boo{% endif %} {%endfor%}', $assigns); + $this->assertTemplateResult(' boo boo ', '{%for item in array%} {% if forloop.first %}{% else %}boo{% endif %} {%endfor%}', $assigns); + } + + public function testLimiting() + { + $assigns = array('array' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)); + $this->assertTemplateResult('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', $assigns); + $this->assertTemplateResult('1234567890', '{%for i in array limit:20 %}{{ i }}{%endfor%}', $assigns); + $this->assertTemplateResult('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', $assigns); + $this->assertTemplateResult('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', $assigns); + $this->assertTemplateResult('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', $assigns); + + $assigns['limit'] = 2; + $assigns['offset'] = 2; + $this->assertTemplateResult('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', $assigns); + } + + public function testNestedFor() + { + $assigns = array('array' => array(array(1, 2), array(3, 4), array(5, 6))); + $this->assertTemplateResult('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', $assigns); + } + + public function testOffsetOnly() + { + $assigns = array('array' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)); + $this->assertTemplateResult('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', $assigns); + } + + public function testPauseResume() + { + $assigns = array('array' => array('items' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0))); + + $markup = <<assertTemplateResult($expected, $markup, $assigns); + } + + public function testPauseResumeLimit() + { + $assigns = array('array' => array('items' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0))); + + $markup = <<assertTemplateResult($expected, $markup, $assigns); + } + + public function testPauseResumeBIGLimit() + { + $assigns = array('array' => array('items' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0))); + + $markup = <<assertTemplateResult($expected, $markup, $assigns); + } + + public function testPauseResumeBIGOffset() + { + $assigns = array('array' => array('items' => array(1, 2, 3, 4, 5, 6, 7, 8, 9, 0))); + + $markup = <<assertTemplateResult($expected, $markup, $assigns); + } + + public function testForWithRanges() + { + $this->assertTemplateResult('123456789', '{%for i in (1..9)%}{{i}}{%endfor%}'); + $this->assertTemplateResult(' 9 10 11', '{%for i in (9..11)%} {{i}}{%endfor%}'); + $this->assertTemplateResult('9991000', '{%for i in (999..1000)%}{{i}}{%endfor%}'); + + $assigns = array('variable' => 100); + $this->assertTemplateResult('9596979899100', '{%for i in (95..variable)%}{{i}}{%endfor%}', $assigns); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfTest.php new file mode 100644 index 0000000..31482f7 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfTest.php @@ -0,0 +1,289 @@ +assertTemplateResult($expected, $text); + } + + public function testTrueNotEqlTrue() + { + $text = " {% if true != true %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text); + } + + public function testTrueLqTrue() + { + $text = " {% if 0 > 0 %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text); + } + + public function testOneLqZero() + { + $text = " {% if 1 > 0 %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testZeroLqOne() + { + $text = " {% if 0 < 1 %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testZeroLqOrEqualOne() + { + $text = " {% if 0 <= 0 %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testZeroLqOrEqualOneInvolvingNil() + { + $text = " {% if null <= 0 %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text); + + + $text = " {% if 0 <= null %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text); + } + + public function testZeroLqqOrEqualOne() + { + $text = " {% if 0 >= 0 %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testStrings() + { + $text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testStringsNotEqual() + { + $text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text); + } + + public function testVarStringsEqual() + { + $text = " {% if var == \"hello there!\" %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 'hello there!')); + } + + public function testVarStringsAreNotEqual() + { + $text = " {% if \"hello there!\" == var %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 'hello there!')); + } + + public function testVarAndLongStringAreEqual() + { + $text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 'hello there!')); + } + + public function testVarAndLongStringAreEqualBackwards() + { + $text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 'hello there!')); + } + + public function testIsCollectionEmpty() + { + $text = " {% if array == empty %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('array' => array())); + + $text = " {% if empty == array %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('array' => array())); + } + + public function testIsNotCollectionEmpty() + { + $text = " {% if array == empty %} true {% else %} false {% endif %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text, array('array' => array(1, 2, 3))); + } + + public function testNil() + { + $text = " {% if var == null %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => null)); + + $text = " {% if var == null %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => null)); + } + + public function testNotNil() + { + $text = " {% if var != null %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 1)); + + $text = " {% if var != null %} true {% else %} false {% endif %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text, array('var' => 1)); + } + + public function testNotNilWhitespaceControlEdgeCase() + { + $this->assertTemplateResult("true", "{% if 1 -%}true{% endif %}"); + $this->assertTemplateResult("true", "{% if 1 -%} true{% endif %}"); + } + + public function testIfFromVariable() + { + $this->assertTemplateResult('', '{% if var %} NO {% endif %}', array('var' => false)); + $this->assertTemplateResult('', '{% if var %} NO {% endif %}', array('var' => null)); + $this->assertTemplateResult('', '{% if foo.bar %} NO {% endif %}', array('foo' => array('bar' => false))); + $this->assertTemplateResult('', '{% if foo.bar %} NO {% endif %}', array('foo' => array())); + $this->assertTemplateResult('', '{% if foo.bar %} NO {% endif %}', array('foo' => null)); + + $this->assertTemplateResult(' YES ', '{% if var %} YES {% endif %}', array('var' => "text")); + $this->assertTemplateResult(' YES ', '{% if var %} YES {% endif %}', array('var' => true)); + $this->assertTemplateResult(' YES ', '{% if var %} YES {% endif %}', array('var' => 1)); + $this->assertTemplateResult(' YES ', '{% if "foo" %} YES {% endif %}'); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} YES {% endif %}', array('foo' => array('bar' => true))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} YES {% endif %}', array('foo' => array('bar' => "text"))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} YES {% endif %}', array('foo' => array('bar' => 1))); + + $this->assertTemplateResult(' YES ', '{% if var %} NO {% else %} YES {% endif %}', array('var' => false)); + $this->assertTemplateResult(' YES ', '{% if var %} NO {% else %} YES {% endif %}', array('var' => null)); + $this->assertTemplateResult(' YES ', '{% if var %} YES {% else %} NO {% endif %}', array('var' => true)); + $this->assertTemplateResult(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', array('var' => "text")); + + $this->assertTemplateResult(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', array('foo' => array('bar' => false))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', array('foo' => array('bar' => true))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', array('foo' => array('bar' => "text"))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', array('foo' => array('notbar' => true))); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', array('foo' => array())); + $this->assertTemplateResult(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', array('notfoo' => array('bar' => true))); + } + + public function testNestedIf() + { + $this->assertTemplateResult('', '{% if false %}{% if false %} NO {% endif %}{% endif %}'); + $this->assertTemplateResult('', '{% if false %}{% if true %} NO {% endif %}{% endif %}'); + $this->assertTemplateResult('', '{% if true %}{% if false %} NO {% endif %}{% endif %}'); + $this->assertTemplateResult(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}'); + + $this->assertTemplateResult(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}'); + $this->assertTemplateResult(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}'); + $this->assertTemplateResult(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}'); + } + + public function testComplexConditions() + { + $this->assertTemplateResult('true', '{% if 10 == 10 and "h" == "h" %}true{% else %}false{% endif %}'); + $this->assertTemplateResult('true', '{% if 8 == 10 or "h" == "h" %}true{% else %}false{% endif %}'); + $this->assertTemplateResult('false', '{% if 8 == 10 and "h" == "h" %}true{% else %}false{% endif %}'); + $this->assertTemplateResult('true', '{% if 10 == 10 or "h" == "k" or "k" == "k" %}true{% else %}false{% endif %}'); + } + + public function testContains() + { + $this->assertTemplateResult('true', '{% if foo contains "h" %}true{% else %}false{% endif %}', array('foo' => array('k', 'h', 'z'))); + $this->assertTemplateResult('false', '{% if foo contains "y" %}true{% else %}false{% endif %}', array('foo' => array('k', 'h', 'z'))); + $this->assertTemplateResult('true', '{% if foo contains "e" %}true{% else %}false{% endif %}', array('foo' => 'abcedf')); + $this->assertTemplateResult('true', '{% if foo contains "e" %}true{% else %}false{% endif %}', array('foo' => 'e')); + $this->assertTemplateResult('false', '{% if foo contains "y" %}true{% else %}false{% endif %}', array('foo' => 'abcedf')); + } + + /** + */ + public function testSyntaxErrorNotClosed() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('if tag was never closed'); + + $this->assertTemplateResult('', '{% if jerry == 1 %}'); + } + + public function testSyntaxErrorNotClosedLineBreak() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('if tag was never closed'); + + $this->assertTemplateResult('', "{% if jerry\n == 1 %}"); + } + + /** + */ + public function testSyntaxErrorEnd() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% if jerry == 1 %}{% end %}'); + } + + /** + */ + public function testInvalidOperator() + { + $this->expectException(\Liquid\Exception\RenderException::class); + + $this->assertTemplateResult('', '{% if foo === y %}true{% else %}false{% endif %}', array('foo' => true, 'y' => true)); + } + + /** + */ + public function testIncomparable() + { + $this->expectException(\Liquid\Exception\RenderException::class); + + $this->assertTemplateResult('', '{% if foo == 1 %}true{% endif %}', array('foo' => (object) array())); + } + + /** + */ + public function testSyntaxErrorElse() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('does not expect else tag'); + + $this->assertTemplateResult('', '{% if foo == 1 %}{% endif %}{% else %}'); + } + + /** + */ + public function testSyntaxErrorUnknown() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('Unknown tag'); + + $this->assertTemplateResult('', '{% unknown-tag %}'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfchangedTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfchangedTest.php new file mode 100644 index 0000000..0358731 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIfchangedTest.php @@ -0,0 +1,31 @@ +assertTemplateResult($expected, $text, array('array' => array(1, 2, 3))); + } + + public function testFails() + { + $text = "{% for i in array %}{% ifchanged %} {{ i }} {% endifchanged %}{% endfor %}"; + $expected = " 1 2 1 "; + $this->assertTemplateResult($expected, $text, array('array' => array(1, 2, 2, 1))); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncludeTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncludeTest.php new file mode 100644 index 0000000..3d8a4cf --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncludeTest.php @@ -0,0 +1,263 @@ +fs = TestFileSystem::fromArray(array( + 'a' => "{% include 'b' %}", + 'b' => "{% include 'c' %}", + 'c' => "{% include 'd' %}", + 'd' => '({{ inner }})', + 'inner' => "Inner: {{ inner }}{{ other }}", + 'example' => "Example: {% include 'inner' %}", + )); + } + + protected function tearDown(): void + { + // PHP goes nuts unless we unset it + unset($this->fs); + } + + /** + */ + public function testInvalidSyntaxNoTemplateName() + { + $this->expectException(\Liquid\Exception\ParseException::class); + $this->expectExceptionMessage('Error in tag'); + + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% include %}"); + } + + /** + */ + public function testMissingFilesystem() + { + $this->expectException(\Liquid\Exception\MissingFilesystemException::class); + $this->expectExceptionMessage('No file system'); + + $template = new Template(); + $template->parse("{% include 'hello' %}"); + } + + public function testInvalidSyntaxInvalidKeyword() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% include 'hello' no_keyword %}"); + + $this->markTestIncomplete("Exception is expected here"); + } + + public function testInvalidSyntaxNoObjectCollection() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->parse("{% include 'hello' with %}"); + + $this->markTestIncomplete("Exception is expected here"); + } + + public function testIncludeTag() + { + $template = new Template(); + $template->setFileSystem($this->fs); + + $template->parse("Outer-{% include 'inner' with 'value' other:23 %}-Outer{% include 'inner' for var other:'loop' %}"); + + $output = $template->render(array("var" => array(1, 2, 3))); + + $this->assertEquals("Outer-Inner: value23-OuterInner: 1loopInner: 2loopInner: 3loop", $output); + } + + public function testIncludeTagNoWith() + { + $template = new Template(); + $template->setFileSystem($this->fs); + + $template->parse("Outer-{% include 'inner' %}-Outer-{% include 'inner' other:'23' %}"); + + $output = $template->render(array("inner" => "orig", "var" => array(1, 2, 3))); + + $this->assertEquals("Outer-Inner: orig-Outer-Inner: orig23", $output); + } + + /** + * @depends testInvalidSyntaxNoObjectCollection + */ + public function testWithCache() + { + $template = new Template(); + $template->setFileSystem($this->fs); + $template->setCache(new Local()); + + foreach (array("Before cache:", "With cache:") as $type) { + $template->parse("{{ type }} {% for item in list %}{% include 'example' inner:item %} {% endfor %}{% include 'a' %}"); + $template->render(array("inner" => "foo", "list" => array(1, 2, 3)), array()); + $this->assertEquals("$type Example: Inner: 1 Example: Inner: 2 (bar)", $template->render(array("type" => $type, "inner" => "bar", "list" => array(1, 2)))); + } + + $template->setCache(null); + } + + public function testIncludeTemplateFile() + { + Liquid::set('INCLUDE_PREFIX', ''); + Liquid::set('INCLUDE_SUFFIX', 'tpl'); + + $template = new Template(dirname(__DIR__).DIRECTORY_SEPARATOR.self::TEMPLATES_DIR); + $template->parse("{% include 'mypartial' %}"); + // template include inserts a new line + $this->assertEquals("test content\n", $template->render()); + } + + public function testIncludePassPlainValue() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'inner' => "[{{ other }}]", + 'example' => "({% include 'inner' other:var %})", + ))); + + $template->parse("{% include 'example' %}"); + + $output = $template->render(array("var" => "test")); + $this->assertEquals("([test])", $output); + } + + /** + */ + public function testIncludePassArrayWithoutIndex() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'inner' => "[{{ other }}]", + 'example' => "({% include 'inner' other:var %})", + ))); + + $template->parse("{% include 'example' %}"); + + $output = $template->render(array("var" => array("a", "b", "c"))); + $expectedOutput = htmlspecialchars(print_r(array("a", "b", "c"), true)); + $this->assertEquals("([$expectedOutput])", $output); + } + + public function testIncludePassArrayWithIndex() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'inner' => "[{{ other[0] }}]", + 'example' => "({% include 'inner' other:var %})", + ))); + + $template->parse("{% include 'example' %}"); + + $output = $template->render(array("var" => array("a", "b", "c"))); + $this->assertEquals("([a])", $output); + } + + public function testIncludePassObjectValue() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'inner' => "[{{ other.a }}]", + 'example' => "({% include 'inner' other:var %})", + ))); + + $template->parse("{% include 'example' %}"); + + $output = $template->render(array("var" => (object) array('a' => 'b'))); + $this->assertEquals("([b])", $output); + } + + public function testIncludeWithoutQuotes() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'inner' => "[{{ other }}]", + 'example' => "{%include inner other:var %} ({{var}})", + ))); + + $template->parse("{% include example other:var %}"); + + $output = $template->render(array("var" => "test")); + $this->assertEquals("[test] (test)", $output); + + $template->parse("{% include inner %}"); + + $output = $template->render(array("other" => "test")); + $this->assertEquals("[test]", $output); + } + + /** + * Render calls in this test shall give same results with cache enabled + */ + public function testIncludeWithExtends() + { + $template = new Template(); + $template->setFileSystem(TestFileSystem::fromArray(array( + 'outer' => "{% block content %}Content for outer block{% endblock %} / {% block footer %}Footer for outer block{% endblock %}", + 'content' => 'Content for {{ name }} block', + 'middle' => "{% extends 'outer' %}{% block content %}{% include 'content' name:'middle' %}{% endblock %}", + 'main' => "Main: {% extends 'middle' %}{% block footer %}{% include 'footer-top' hello:message %}{% endblock %}", + 'footer-bottom' => "{{ name }} with message: {{ hello }}", + 'footer-top' => "Footer top and {% include 'footer-bottom' name:'bottom' %}", + ))); + + $template->setCache(new Local()); + + foreach (array("Before cache", "With cache") as $type) { + $this->assertEquals("Block with message: $type", $template->parseFile('footer-bottom')->render(array("name" => "Block", "hello" => $type))); + $this->assertEquals('Content for middle block / Footer for outer block', $template->parseFile('middle')->render()); + $this->assertEquals("Main: Content for middle block / Footer top and bottom with message: $type", $template->parseFile('main')->render(array("message" => $type))); + + $template->parse("{% include 'main' hello:message %}"); + $output = $template->render(array("message" => $type)); + $this->assertEquals("Main: Content for middle block / Footer top and bottom with message: $type", $output); + } + + $template->setCache(null); + } + + public function testCacheDiscardedIfFileChanges() + { + $template = new Template(); + $template->setCache(new Local()); + + $content = "[{{ name }}]"; + $template->setFileSystem(TestFileSystem::fromArray(array( + 'example' => &$content, + ))); + + $template->parse("{% include 'example' %}"); + $output = $template->render(array("name" => "Example")); + $this->assertEquals("[Example]", $output); + + $content = "<{{ name }}>"; + $template->parse("{% include 'example' %}"); + $output = $template->render(array("name" => "Example")); + $this->assertEquals("", $output); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncrementTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncrementTest.php new file mode 100644 index 0000000..4a49bde --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagIncrementTest.php @@ -0,0 +1,44 @@ +expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% increment %}'); + } + + /** + * Undefined variable will become 0 + */ + public function testIncrementNonExistingVariable() + { + $this->assertTemplateResult(0, '{% increment no_such_var %}{{ no_such_var }}'); + } + + public function testIncrementVariable() + { + $this->assertTemplateResult(42, '{% increment var %}{{ var }}', array('var' => 41)); + } + + public function testIncrementNestedVariable() + { + $this->assertTemplateResult(42, '{% for var in vars %}{% increment var %}{{ var }}{% endfor %}', array('vars' => array(41))); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagPaginateTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagPaginateTest.php new file mode 100644 index 0000000..30c0311 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagPaginateTest.php @@ -0,0 +1,210 @@ +assertTemplateResult($expected, $text, array('products' => array(array('id' => 1), array('id' => 2), array('id' => 3), array('id' => 4), array('id' => 5)))); + } + + public function testVariables() + { + $text = " {% paginate search.products by 3 %}{{ paginate.page_size }} {{ paginate.current_page }} {{ paginate.current_offset }} {{ paginate.pages }} {{ paginate.items }} {{ paginate.next.url }}{% endpaginate %}"; + $expected = " 3 1 0 2 5 http://?page=2"; + $this->assertTemplateResult($expected, $text, array('search' => array('products' => new \ArrayIterator(array(array('id' => 1), array('id' => 2), array('id' => 3), array('id' => 4), array('id' => 5)))))); + } + + public function testNextPage() + { + $text = '{% paginate products by 1 %}{% for product in products %} {{ product.id }} {% endfor %}{{ paginate.next.title }}{% endpaginate %}'; + $expected = ' 2 Next'; + $this->assertTemplateResult($expected, $text, array('HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/products', 'HTTPS' => 'on', 'page' => 2, 'products' => array(array('id' => 1), array('id' => 2), array('id' => 3), array('id' => 4), array('id' => 5)))); + } + + /** + */ + public function testSyntaxErrorCase() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{% paginate products %}{% endpaginate %}'); + } + + /** + */ + public function testNoCollection() + { + $this->expectException(\Liquid\Exception\RenderException::class); + $this->expectExceptionMessage('Missing collection'); + + $this->assertTemplateResult('', '{% paginate products by 1 %}{% for product in products %}{{ product.id }}{% endfor %}{% endpaginate %}'); + } + + const PAGINATION_ASSIGNS = array( + 'HTTP_HOST' => 'example.com', + 'HTTPS' => 'on', + 'page' => 1, + 'articles' => array(array('title' => 1), array('title' => 2), array('title' => 3)), + ); + + public function testPaginationForRepeatedCalls() + { + $text = '{% for article in articles %}{{ article.title }},{% endfor %}'; + $expected = '1,2,3,'; + $this->assertTemplateResult($expected, $text, self::PAGINATION_ASSIGNS); + + $text = '{% paginate articles by 2 %}{% for article in articles %}{{ article.title }},{% endfor %}{% endpaginate %} '.$text; + $expected = '1,2, 1,2,3,'; + $this->assertTemplateResult($expected, $text, self::PAGINATION_ASSIGNS); + } + + public function testPaginationDoesntIncludePreviousIfFirst() + { + $assigns = self::PAGINATION_ASSIGNS; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '1 , Next,https://example.com?page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateDoesntIncludeNextIfLast() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['page'] = 3; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '3 Previous,https://example.com?page=2 ,'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUsingDifferentRequestParameterName() + { + Liquid::set('PAGINATION_REQUEST_KEY', 'pagina'); + + $assigns = self::PAGINATION_ASSIGNS; + $assigns['page'] = 2; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '2 Previous,https://example.com?pagina=1 Next,https://example.com?pagina=3'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUsingDifferentContextParameter() + { + Liquid::set('PAGINATION_CONTEXT_KEY', 'the_current_page'); + + $assigns = self::PAGINATION_ASSIGNS; + $assigns['the_current_page'] = 2; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '2 Previous,https://example.com?page=1 Next,https://example.com?page=3'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUrlGenerationPreservesParams() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['REQUEST_URI'] = '/testfile.php?someparam=1'; + + $text = '{% paginate articles by 1 %}{{ paginate.next.url }}{% endpaginate %}'; + $expected = 'https://example.com/testfile.php?someparam=1&page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUrlGenerationReplacesPageKey() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['REQUEST_URI'] = '/testfile.php?someparam=1&page=1'; + + $text = '{% paginate articles by 1 %}{{ paginate.next.url }}{% endpaginate %}'; + $expected = 'https://example.com/testfile.php?someparam=1&page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUrlGenerationRespectsPageParameterKey() + { + Liquid::set('PAGINATION_REQUEST_KEY', 'pagina'); + + $assigns = self::PAGINATION_ASSIGNS; + $assigns['REQUEST_URI'] = '/testfile.php?someparam=1&page=hello&pagina=1'; + + $text = '{% paginate articles by 1 %}{{ paginate.next.url }}{% endpaginate %}'; + $expected = 'https://example.com/testfile.php?someparam=1&page=hello&pagina=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateUrlGenerationWithoutHTTPS() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['REQUEST_URI'] = '/'; + $assigns['HTTPS'] = ''; + + $text = '{% paginate articles by 1 %}{{ paginate.next.url }}{% endpaginate %}'; + $expected = 'http://example.com/?page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateDoesntIncludeNextIfAfterLast() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['page'] = 42; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '3 Previous,https://example.com?page=2 ,'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateDoesntIncludePreviousIfBeforeFirst() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['page'] = 0; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '1 , Next,https://example.com?page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } + + public function testPaginateIgnoresNonNumbers() + { + $assigns = self::PAGINATION_ASSIGNS; + $assigns['page'] = 'foo'; + + $text = '{% paginate articles by 1 %}{% for article in articles %}{{article.title}}{% endfor %} {{paginate.previous.title}},{{paginate.previous.url}} {{paginate.next.title}},{{paginate.next.url}}{% endpaginate %}'; + $expected = '1 , Next,https://example.com?page=2'; + $this->assertTemplateResult($expected, $text, $assigns); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagRawTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagRawTest.php new file mode 100644 index 0000000..21a90d9 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagRawTest.php @@ -0,0 +1,28 @@ +assertTemplateResult( + '{{ y | plus: x }}{% if %} is equal to 11.', + '{% raw %}{{ y | plus: x }}{% if %}{% endraw %} is equal to 11.', + array('x' => 5, 'y' => 6) + ); + + $this->assertTemplateResult('', '{% raw %}{% endraw %}'); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagTablerowTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagTablerowTest.php new file mode 100644 index 0000000..c5e2b18 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagTablerowTest.php @@ -0,0 +1,66 @@ +assertTemplateResult( + ''."\n".' yo yo yo yo '."\n", + '{% tablerow item in array %} yo {% endtablerow %}', + array('array' => array(1, 2, 3, 4)) + ); + + $this->assertTemplateResult( + ' + item 1 + + item 2 +', + '{% tablerow item in array cols:1 %} item {{ item }} {% endtablerow %}', + array('array' => array(1, 2)) + ); + + $this->assertTemplateResult( + ''."\n".' 2 3 '."\n", + '{% tablerow item in array limit:2 offset:1 %} {{ item }} {% endtablerow %}', + array('array' => array(1, 2, 3, 4)) + ); + + $this->assertTemplateResult( + ''."\n".' yo yo '."\n", + '{%tablerow item in array%} yo {%endtablerow%}', + array('array' => new \ArrayIterator(array(1, 2))) + ); + } + + /** + */ + public function testInvalidSyntax() + { + $this->expectException(\Liquid\Exception\ParseException::class); + + $this->assertTemplateResult('', '{%tablerow item array%} yo {%endtablerow%}', array()); + } + + /** + */ + public function testNotArray() + { + $this->expectException(\Liquid\Exception\RenderException::class); + + $this->assertTemplateResult('', '{%tablerow item in array%} yo {%endtablerow%}', array('array' => true)); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/Tag/TagUnlessTest.php b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagUnlessTest.php new file mode 100644 index 0000000..1f034d9 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/Tag/TagUnlessTest.php @@ -0,0 +1,45 @@ +assertTemplateResult($expected, $text); + } + + public function testTrueNotEqlTrue() + { + $text = " {% unless true != true %} true {% else %} false {% endunless %} "; + $expected = " true "; + $this->assertTemplateResult($expected, $text); + } + + public function testWithVariable() + { + $text = " {% unless variable %} true {% else %} false {% endunless %} "; + $expected = " false "; + $this->assertTemplateResult($expected, $text, array('variable' => true)); + } + + public function testForAndUnless() + { + $this->assertTemplateResult('0=>yay 0=>yay 1=> ', '{% for item in array %}{{ forloop.last }}=>{% unless forloop.last %}yay{% endunless %} {% endfor %}', array('array' => array(1, 2, 3))); + $this->assertTemplateResult('1=> 0=>yay 0=>yay ', '{% for item in array %}{{ forloop.first }}=>{% unless forloop.first %}yay{% endunless %} {% endfor %}', array('array' => array(1, 2, 3))); + $this->assertTemplateResult('0=> 0=> 1=>yay ', '{% for item in array %}{{ forloop.last }}=>{% if forloop.last %}yay{% endif %} {% endfor %}', array('array' => array(1, 2, 3))); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/TemplateTest.php b/package/vendor/liquid/liquid/tests/Liquid/TemplateTest.php new file mode 100644 index 0000000..2f372ca --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/TemplateTest.php @@ -0,0 +1,169 @@ +cacheDir = __DIR__ . DIRECTORY_SEPARATOR . self::CACHE_DIR; + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Remove tmp cache files + array_map('unlink', glob($this->cacheDir . DIRECTORY_SEPARATOR . '*')); + } + + /** + */ + public function testSetCacheInvalidKey() + { + $this->expectException(\Liquid\LiquidException::class); + + $template = new Template(); + $template->setCache(array()); + } + + /** + */ + public function testSetCacheInvalidClass() + { + $this->expectException(\Liquid\LiquidException::class); + + $template = new Template(); + $template->setCache(array('cache' => 'no_such_class')); + } + + public function testSetCacheThroughArray() + { + $template = new Template(); + $template->setCache(array('cache' => 'file', 'cache_dir' => $this->cacheDir)); + $this->assertInstanceOf(\Liquid\Cache\File::class, $template::getCache()); + } + + public function testSetCacheThroughCacheObject() + { + $template = new Template(); + $cache = new Cache\File(array('cache_dir' => $this->cacheDir)); + $template->setCache($cache); + $this->assertEquals($cache, $template::getCache()); + } + + public function testTokenizeStrings() + { + $this->assertEquals(array(' '), Template::tokenize(' ')); + $this->assertEquals(array('hello world'), Template::tokenize('hello world')); + } + + public function testTokenizeVariables() + { + $this->assertEquals(array('{{funk}}'), Template::tokenize('{{funk}}')); + $this->assertEquals(array(' ', '{{funk}}', ' '), Template::tokenize(' {{funk}} ')); + $this->assertEquals(array(' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '), Template::tokenize(' {{funk}} {{so}} {{brother}} ')); + $this->assertEquals(array(' ', '{{ funk }}', ' '), Template::tokenize(' {{ funk }} ')); + } + + public function testTokenizeBlocks() + { + $this->assertEquals(array('{%comment%}'), Template::tokenize('{%comment%}')); + $this->assertEquals(array(' ', '{%comment%}', ' '), Template::tokenize(' {%comment%} ')); + $this->assertEquals(array(' ', '{%comment%}', ' ', '{%endcomment%}', ' '), Template::tokenize(' {%comment%} {%endcomment%} ')); + $this->assertEquals(array(' ', '{% comment %}', ' ', '{% endcomment %}', ' '), Template::tokenize(" {% comment %} {% endcomment %} ")); + } + + public function testBlackspace() + { + $template = new Template(); + $template->parse(' '); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertEquals(array(' '), $nodelist); + } + + public function testVariableBeginning() + { + $template = new Template(); + $template->parse('{{funk}} '); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertCount(2, $nodelist); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[0]); + $this->assertIsString($nodelist[1]); + } + + public function testVariableEnd() + { + $template = new Template(); + $template->parse(' {{funk}}'); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertCount(2, $nodelist); + $this->assertIsString($nodelist[0]); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[1]); + } + + public function testVariableMiddle() + { + $template = new Template(); + $template->parse(' {{funk}} '); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertCount(3, $nodelist); + $this->assertIsString($nodelist[0]); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[1]); + $this->assertIsString($nodelist[2]); + } + + public function testVariableManyEmbeddedFragments() + { + $template = new Template(); + $template->parse(' {{funk}} {{soul}} {{brother}} '); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertCount(7, $nodelist); + $this->assertIsString($nodelist[0]); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[1]); + $this->assertIsString($nodelist[2]); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[3]); + $this->assertIsString($nodelist[4]); + $this->assertInstanceOf(\Liquid\Variable::class, $nodelist[5]); + $this->assertIsString($nodelist[6]); + } + + public function testWithBlock() + { + $template = new Template(); + $template->parse(' {% comment %} {% endcomment %} '); + + $nodelist = $template->getRoot()->getNodelist(); + + $this->assertCount(3, $nodelist); + $this->assertIsString($nodelist[0]); + $this->assertInstanceOf(\Liquid\Tag\TagComment::class, $nodelist[1]); + $this->assertIsString($nodelist[2]); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/TestCase.php b/package/vendor/liquid/liquid/tests/Liquid/TestCase.php new file mode 100644 index 0000000..4cbb1a9 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/TestCase.php @@ -0,0 +1,63 @@ + 'field_exists', + 'GET_PROPERTY_METHOD' => 'get', + 'FILTER_SEPARATOR' => '\|', + 'ARGUMENT_SEPARATOR' => ',', + 'FILTER_ARGUMENT_SEPARATOR' => ':', + 'VARIABLE_ATTRIBUTE_SEPARATOR' => '.', + 'INCLUDE_ALLOW_EXT' => false, + 'INCLUDE_SUFFIX' => 'liquid', + 'INCLUDE_PREFIX' => '_', + 'VARIABLE_START' => '{{', + 'VARIABLE_END' => '}}', + 'VARIABLE_NAME' => '[a-zA-Z_][a-zA-Z0-9_.-]*', + 'EXPOSE_SERVER' => false, + ); + + foreach ($defaultConfig as $configKey => $configValue) { + Liquid::set($configKey, $configValue); + } + + Template::setCache(null); + } + + /** + * @param mixed $expected + * @param string $templateString + * @param array $assigns + * @param string $message + */ + public function assertTemplateResult($expected, $templateString, array $assigns = array(), $message = "%s") + { + $template = new Template(); + $template->parse($templateString); + + $this->assertEquals($expected, $template->render($assigns, $this->filters), $message); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/TestFileSystem.php b/package/vendor/liquid/liquid/tests/Liquid/TestFileSystem.php new file mode 100644 index 0000000..e38bba5 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/TestFileSystem.php @@ -0,0 +1,29 @@ +parse($source); + $template->setTickFunction(function (Context $context) use (&$ticks) { + $ticks++; + }); + + $template->render(); + + $this->assertGreaterThanOrEqual($min, $ticks); + $this->assertLessThanOrEqual($max, $ticks); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/VariableResolutionTest.php b/package/vendor/liquid/liquid/tests/Liquid/VariableResolutionTest.php new file mode 100644 index 0000000..28a3f07 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/VariableResolutionTest.php @@ -0,0 +1,63 @@ +parse("{{test}}"); + $this->assertEquals('worked', $template->render(array('test' => 'worked'))); + } + + public function testSimpleWithWhitespaces() + { + $template = new Template(); + + $template->parse(' {{ test }} '); + $this->assertEquals(' worked ', $template->render(array('test' => 'worked'))); + $this->assertEquals(' worked wonderfully ', $template->render(array('test' => 'worked wonderfully'))); + } + + public function testIgnoreUnknown() + { + $template = new Template(); + + $template->parse('{{ test }}'); + $this->assertEquals('', $template->render()); + } + + public function testLineBreak() + { + $template = new Template(); + + $template->parse("{{ test |\n strip_html }}"); + $this->assertEquals('worked', $template->render(array('test' => 'worked'))); + } + + public function testArrayScoping() + { + $template = new Template(); + + $template->parse('{{ test.test }}'); + $this->assertEquals('worked', $template->render(array('test' => array('test' => 'worked')))); + } + + public function testVariableArrayIndices() + { + $template = new Template(); + + $template->parse("{% assign days = 'Mon,Tue,Wed,Thu,Fri,Sat,Sun' | split: ',' %}{% for i in (0..6) %}{{ days[i] }} {% endfor %}"); + $this->assertEquals('Mon Tue Wed Thu Fri Sat Sun ', $template->render()); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/VariableTest.php b/package/vendor/liquid/liquid/tests/Liquid/VariableTest.php new file mode 100644 index 0000000..697f399 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/VariableTest.php @@ -0,0 +1,122 @@ +assertEquals('hello', $var->getName()); + } + + public function testFilters() + { + $var = new Variable('hello | textileze'); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('textileze', array())), $var->getFilters()); + + $var = new Variable('hello | textileze | paragraph'); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('textileze', array()), array('paragraph', array())), $var->getFilters()); + + $var = new Variable(" hello | strftime: '%Y'"); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('strftime', array("'%Y'"))), $var->getFilters()); + + $var = new Variable(" 'typo' | link_to: 'Typo', true "); + $this->assertEquals("'typo'", $var->getName()); + $this->assertEquals(array(array('link_to', array("'Typo'", "true"))), $var->getFilters()); + + $var = new Variable(" 'typo' | link_to: 'Typo', false "); + $this->assertEquals("'typo'", $var->getName()); + $this->assertEquals(array(array('link_to', array("'Typo'", "false"))), $var->getFilters()); + + $var = new Variable(" 'foo' | repeat: 3 "); + $this->assertEquals("'foo'", $var->getName()); + $this->assertEquals(array(array('repeat', array("3"))), $var->getFilters()); + + $var = new Variable(" 'foo' | repeat: 3, 3"); + $this->assertEquals("'foo'", $var->getName()); + $this->assertEquals(array(array('repeat', array("3", "3"))), $var->getFilters()); + + $var = new Variable(" 'foo' | repeat: 3, 3, 3 "); + $this->assertEquals("'foo'", $var->getName()); + $this->assertEquals(array(array('repeat', array("3", "3", "3"))), $var->getFilters()); + + $var = new Variable(" hello | strftime: '%Y, okay?'"); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('strftime', array("'%Y, okay?'"))), $var->getFilters()); + + $var = new Variable(" hello | things: \"%Y, okay?\", 'the other one'"); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('things', array('"%Y, okay?"', "'the other one'"))), $var->getFilters()); + + $var = new Variable(" product.featured_image | img_url: '450x450', crop: 'center', scale: 2 "); + $this->assertEquals("product.featured_image", $var->getName()); + $this->assertEquals(array(array('img_url', array("'450x450'", array("crop" => "'center'", "scale" => "2")))), $var->getFilters()); + } + + public function testFiltersWithoutWhitespace() + { + $var = new Variable('hello | textileze | paragraph'); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('textileze', array()), array('paragraph', array())), $var->getFilters()); + + $var = new Variable('hello|textileze|paragraph'); + $this->assertEquals('hello', $var->getName()); + $this->assertEquals(array(array('textileze', array()), array('paragraph', array())), $var->getFilters()); + } + + public function testSymbol() + { + $var = new Variable("http://disney.com/logo.gif | image: 'med' "); + $this->assertEquals('http://disney.com/logo.gif', $var->getName()); + $this->assertEquals(array(array('image', array("'med'"))), $var->getFilters()); + } + + public function testStringSingleQuoted() + { + $var = new Variable(' "hello" '); + $this->assertEquals('"hello"', $var->getName()); + } + + public function testStringDoubleQuoted() + { + $var = new Variable(" 'hello' "); + $this->assertEquals("'hello'", $var->getName()); + } + + public function testInteger() + { + $var = new Variable(' 1000 '); + $this->assertEquals('1000', $var->getName()); + } + + public function testFloat() + { + $var = new Variable(' 1000.01 '); + $this->assertEquals('1000.01', $var->getName()); + } + + public function testStringWithSpecialChars() + { + $var = new Variable("'hello! $!@.;\"ddasd\" ' "); + $this->assertEquals("'hello! $!@.;\"ddasd\" '", $var->getName()); + } + + public function testStringDot() + { + $var = new Variable(" test.test "); + $this->assertEquals('test.test', $var->getName()); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/VirtualFileSystemTest.php b/package/vendor/liquid/liquid/tests/Liquid/VirtualFileSystemTest.php new file mode 100644 index 0000000..5b096ab --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/VirtualFileSystemTest.php @@ -0,0 +1,81 @@ +expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('Not a callback'); + + new Virtual(''); + } + + public function testReadTemplateFile() + { + $fs = new Virtual(function ($templatePath) { + if ($templatePath == 'foo') { + return "Contents of foo"; + } + + if ($templatePath == 'bar') { + return "Bar"; + } + + return ''; + }); + + $this->assertEquals('Contents of foo', $fs->readTemplateFile('foo')); + $this->assertEquals('Bar', $fs->readTemplateFile('bar')); + $this->assertEquals('', $fs->readTemplateFile('nothing')); + } + + /** + */ + public function testWithFileCache() + { + $this->expectException(\Liquid\LiquidException::class); + $this->expectExceptionMessage('cannot be used with a serializing cache'); + + $template = new Template(); + $template->setFileSystem(new Virtual(function ($templatePath) { + return ''; + })); + $template->setCache(new File(array( + 'cache_dir' => __DIR__, + ))); + $template->parse("Hello"); + } + + public function virtualFileSystemCallback($templatePath) + { + return 'OK'; + } + + public function testWithRegularCallback() + { + $template = new Template(); + $template->setFileSystem(new Virtual(array($this, 'virtualFileSystemCallback'), true)); + $template->setCache(new File(array( + 'cache_dir' => __DIR__.'/cache_dir/', + ))); + + $template->parse("Test: {% include 'hello' %}"); + $this->assertEquals('Test: OK', $template->render()); + } +} diff --git a/package/vendor/liquid/liquid/tests/Liquid/cache_dir/.gitignore b/package/vendor/liquid/liquid/tests/Liquid/cache_dir/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/cache_dir/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.html new file mode 100644 index 0000000..bc8b413 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.html @@ -0,0 +1,20 @@ + + +test 0 == test 0 + + + +test 1 == test 1 + + + +test 2 == test 2 + + + + + +test 3 == test 3 (inside an if) + + +test 3 == test 3 (outside of an if) \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.liquid new file mode 100644 index 0000000..7d2bd24 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.liquid @@ -0,0 +1,24 @@ +{% assign my_variable = 'test 0' %} + +{{ my_variable }} == test 0 + +{% capture my_variable %}test 1{% endcapture %} + +{{ my_variable }} == test 1 + +{% capture my_variable %}test 2{% endcapture %} + +{{ my_variable }} == test 2 + + +{% if true %} +{% assign my_variable = 'test 0' %} +{% capture my_variable %}test 3{% endcapture %} +{{ my_variable }} == test 3 (inside an if) +{% else %} +{% capture my_variable %}test 4{% endcapture %} +{{ my_variable }} + +{% endif %} + +{{ my_variable }} == test 3 (outside of an if) \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.php new file mode 100644 index 0000000..9890e4e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign-capture.php @@ -0,0 +1,12 @@ +' %} +{{ descriptions[0] }} +{{ descriptions[1] }} diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign.php new file mode 100644 index 0000000..b488a8a --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/assign.php @@ -0,0 +1,16 @@ + array( + 'description' => 'FirstSecond', + ), +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.html new file mode 100644 index 0000000..5f23814 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.html @@ -0,0 +1,22 @@ + + + hit 1 + + + + hit 2 or 3 + + + + ... else ... + + + + ... else ... + + + + ... else ... + + + diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.liquid new file mode 100644 index 0000000..1fcbcd4 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.liquid @@ -0,0 +1,11 @@ +{% for i in (1..max) %} + {% case i %} + {% when 1 %} + hit 1 + {% when 2 or 3 %} + hit 2 or 3 + {% else %} + ... else ... + {% endcase %} +{% endfor %} + diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.php new file mode 100644 index 0000000..c0ffa72 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/case.php @@ -0,0 +1,14 @@ + 5, +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.html new file mode 100644 index 0000000..9bdb7c0 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.html @@ -0,0 +1 @@ +We made 1 million dollars this year \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.liquid new file mode 100644 index 0000000..83f8d6e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.liquid @@ -0,0 +1 @@ +We made 1 million dollars {% comment %} in losses {% endcomment %} this year \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.php new file mode 100644 index 0000000..a1ea689 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/comment.php @@ -0,0 +1,12 @@ + 5 +2 #=> 2 +0 #=> 0, not as is in the docs +2 #=> 2 +0 #=> 'foofoofoofoo' +20 #=> 20 +4 #=> 4 +4 #=> 4 +5 #=> 5 +<div>Read more</div> &rarr; +<div>Read more</div> → +Read more → +
Read more
→ +a%3Db%26c%3Dd +a=&b #=> 'a=&b' +Foo +[ Foo] +[Foo ] +OK +z +z, g, n +n +g-n-z +n-g-z +Foo
+Bar +FooBar +foobar +barbar #=> 'barbar' +foobar #=> 'foobar' +barbar #=> 'barbar' +bar #=> 'bar' +test... #=> 'test...' +Foo bar... #=> 'Foo bar...' +Foo #=> 'Foo' +Foo bar #=> 'Foo bar' + + + + + BazBar + + FooBar + diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.liquid new file mode 100644 index 0000000..7307cab --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.liquid @@ -0,0 +1,50 @@ +Hello {{ 'tobi' | upcase }} +Hello tobi has {{ 'tobi' | size }} letters! +Array has {{ array | size }} elements. +Second element is: {{ array | slice: 2 | first }} +{{ '*tobi*' | textilize | upcase }} +{{ '1971-01-01' | date: "%Y %h" }} +{{ 'foo' | append:'bar' }} +{{ 'tobi' | capitalize }} +{{ 'Tobi' | downcase }} +{{ 10 | divided_by:2 }} #=> 5 +{{ 4 | minus:2 }} #=> 2 +{{ 'a' | plus:'b' }} #=> 0, not as is in the docs +{{ 1 | plus:1 }} #=> 2 +{{ 'foo' | times:4 }} #=> 'foofoofoofoo' +{{ 5 | times:4 }} #=> 20 +{{ 4.3 | round }} #=> 4 +{{ 4.8 | floor }} #=> 4 +{{ 4.3 | ceil }} #=> 5 +{{ '
Read more
→' | escape }} +{{ '
Read more
→' | escape_once }} +{{ '
Read more
→' | strip_html }} +{{ '
Read more
→' | raw }} +{{ 'a=b&c=d' | url_encode }} +{{ 'a%3D%26b' | url_decode }} #=> 'a=&b' +{{ ' Foo ' | strip }} +[{{ ' Foo ' | rstrip }}] +[{{ ' Foo ' | lstrip }}] +{{ '' | default: "OK" }} +{{ array | first }} +{{ array | join:', ' }} +{{ array | last }} +{{ array | sort | join:'-' }} +{{ array | reverse | join:'-' }} +{{ foobar | newline_to_br }} +{{ foobar | strip_newlines }} +{{ 'bar' | prepend:'foo' }} +{{ 'foofoo' | replace:'foo','bar' }} #=> 'barbar' +{{ 'barbar' | replace_first:'bar','foo' }} #=> 'foobar' +{{ 'foobarfoobar' | remove:'foo' }} #=> 'barbar' +{{ 'barbar' | remove_first:'bar' }} #=> 'bar' +{{ 'tests' | truncate:4 }} #=> 'test...' +{{ 'Foo bar bar' | truncatewords:2 }} #=> 'Foo bar...' +{{ 'Foo bar bar' | split: " " | first }} #=> 'Foo' +{{ 'Foo bar bar' | split: " " | uniq | join: " " }} #=> 'Foo bar' + + +{% assign titles = posts | sort: "id" | map: "title" %} +{% for title in titles %} + {{ title }} +{% endfor %} diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.php new file mode 100644 index 0000000..00b6a92 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/filters.php @@ -0,0 +1,19 @@ + array('z', 'g', 'n'), + 'foobar' => "Foo\nBar", + 'posts' => array( + array('id' => 2, 'title'=> 'FooBar', 'tags' => array('foo', 'bar')), + array('id' => 1, 'title'=> 'BazBar', 'tags' => array('baz', 'bar')), + ), +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.html new file mode 100644 index 0000000..1852f6c --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.html @@ -0,0 +1,104 @@ +Helper variables: + + 1 => current item + 5 => length of the entire for loop + 1 => index of the current iteration + 0 => index of the current iteration (zero based) + 5 => how many items are still left? + 4 => how many items are still left? (zero based) + 1 => is this the first iteration? + 0 => is this the last iternation? + + 2 => current item + 5 => length of the entire for loop + 2 => index of the current iteration + 1 => index of the current iteration (zero based) + 4 => how many items are still left? + 3 => how many items are still left? (zero based) + 0 => is this the first iteration? + 0 => is this the last iternation? + + 3 => current item + 5 => length of the entire for loop + 3 => index of the current iteration + 2 => index of the current iteration (zero based) + 3 => how many items are still left? + 2 => how many items are still left? (zero based) + 0 => is this the first iteration? + 0 => is this the last iternation? + + 4 => current item + 5 => length of the entire for loop + 4 => index of the current iteration + 3 => index of the current iteration (zero based) + 2 => how many items are still left? + 1 => how many items are still left? (zero based) + 0 => is this the first iteration? + 0 => is this the last iternation? + + 5 => current item + 5 => length of the entire for loop + 5 => index of the current iteration + 4 => index of the current iteration (zero based) + 1 => how many items are still left? + 0 => how many items are still left? (zero based) + 0 => is this the first iteration? + 1 => is this the last iternation? + + +Limit and offset: + + 3 + + 4 + + +Reversing the loop: + + 1 + + 2 + + 3 + + 4 + + 5 + + +Range loop: + + 1 + + 2 + + 3 + + 4 + + 5 + + +Regular loop: + + 1 + + 2 + + 3 + + +Within a pagination: + + 1 + + 2 + + +Should not be affected by a pagination: + + 1 + + 2 + + 3 diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.liquid new file mode 100644 index 0000000..487b7e0 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.liquid @@ -0,0 +1,41 @@ +Helper variables: +{% for item in array %} + {{ item }} => current item + {{ forloop.length }} => length of the entire for loop + {{ forloop.index }} => index of the current iteration + {{ forloop.index0 }} => index of the current iteration (zero based) + {{ forloop.rindex }} => how many items are still left? + {{ forloop.rindex0 }} => how many items are still left? (zero based) + {{ forloop.first }} => is this the first iteration? + {{ forloop.last }} => is this the last iternation? +{% endfor %} + +Limit and offset: +{% for item in array limit:2 offset:2 %} + {{ item }} +{% endfor %} + +Reversing the loop: +{% for item in array reversed %} + {{ item }} +{% endfor %} + +Range loop: +{% for i in (1..item.quantity) %} + {{ i }} +{% endfor %} + +Regular loop: +{% for article in articles %} + {{ article.title }} +{% endfor %} + +Within a pagination: +{% paginate articles by 2 %}{% for article in articles %} + {{ article.title }} +{% endfor %}{% endpaginate %} + +Should not be affected by a pagination: +{% for article in articles %} + {{ article.title }} +{% endfor %} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.php new file mode 100644 index 0000000..0f2246e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/for.php @@ -0,0 +1,22 @@ + range(1, 5), + 'item' => array( + 'quantity' => 5, + ), + 'articles' => array( + array('title' => 1), + array('title' => 2), + array('title' => 3), + ), +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.html new file mode 100644 index 0000000..686b9a0 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.html @@ -0,0 +1,2 @@ +We made 1 million dollars this year +We made 1 million dollars this year \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.liquid new file mode 100644 index 0000000..482422e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.liquid @@ -0,0 +1,2 @@ +{% include comment.liquid %} +{% include comment.liquid %} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.php new file mode 100644 index 0000000..a1ea689 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/include.php @@ -0,0 +1,12 @@ + call_user_func(function () { + yield 'a'; + yield 'b'; + yield 'c'; + }), +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.html new file mode 100644 index 0000000..6de8f81 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.html @@ -0,0 +1,4 @@ +Hello Harald +Hello DELACAP +Hello Superuser +Hello HARALD \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.liquid new file mode 100644 index 0000000..1cad24e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.liquid @@ -0,0 +1,4 @@ +Hello {{ name }} +Hello {{ company }} +Hello {{ user.name }} +Hello {{ 'Harald' | upcase }} \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.php new file mode 100644 index 0000000..ed4bf72 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/output.php @@ -0,0 +1,18 @@ + 'Harald', + 'company' => 'DELACAP', + 'user' => array( + 'name' => 'Superuser' + ) +); diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.html b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.html new file mode 100644 index 0000000..8629bdf --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.html @@ -0,0 +1,7 @@ +-- + + + Wow, + John G. Chalmers-Smith, you have a long name! + +--Wow,John G. Chalmers-Smith, you have a long name!-- \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.liquid b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.liquid new file mode 100644 index 0000000..f53ae1e --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.liquid @@ -0,0 +1,17 @@ +-- +{% assign username = "John G. Chalmers-Smith" %} +{% if username and username.size > 10 %} + Wow, + {{ username }}, you have a long name! +{% else %} + Hello there! +{% endif %} +-- +{%- assign username = "John G. Chalmers-Smith" -%} +{%- if username and username.size > 10 -%} + Wow, + {{- username -}}, you have a long name! +{%- else -%} + Hello there! +{%- endif -%} +-- \ No newline at end of file diff --git a/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.php b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.php new file mode 100644 index 0000000..a1ea689 --- /dev/null +++ b/package/vendor/liquid/liquid/tests/Liquid/fixtures/whitespace-control.php @@ -0,0 +1,12 @@ + + +
+
+ +

Chuyển đổi

+ +

Tạo thanh toán nhanh chóng và đơn + giản

+

Thu hút thêm nhiều khách hàng hơn trong + khi tiết + kiệm tiền phí + cho nhà cung
+ cấp dịch vụ + thanh toán

+ Bắt + đầu dùng + thử miễn phí + +
+
+
+ chuyendoi +
+
+ Tỷ lệ chuyển đổi thanh toán 61,9% +

Khách hàng thấy tỷ lệ chuyển đổi thanh toán là 61,9% (cao hơn 20% so + với mức trung bình + của ngành) trên xStore khi sử + dụng cấu hình thanh toán hàng đầu

+
+
+
+ +
+
+
+

Tìm hiểu nền tảng của chúng tôi cho phép + bạn

+
+ + + +
+

Tỷ lệ chuyển đổi + cao hơn giúp + doanh nghiệp của bạn
phát triển + nhanh hơn

+

Thanh toán một trang có thể tùy chỉnh hoàn toàn mang lại khả + năng chuyển đổi + thanh toán
tốt nhất + có thể để đảm bảo doanh + số không bị mất do trục trặc khi thanh toán

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ + +
+

Thanh toán nhanh + chóng, đơn giản mà khách hàng
+ của bạn sẽ thích

+

Trải nghiệm thanh toán hiện đại và dễ dàng mà khách hàng hiện + mong đợi khi sử dụng các công cụ như
ví điện tử (Google, + PayPal, Apple, v.v.) và tính năng tự động hoàn thành tích hợp của Google

+ +
+
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ + + +
+ + +
+ +
+

Giá cả cạnh tranh + nhất để tiết kiệm chi phí

+

Các đối tác thanh toán hàng đầu của chúng tôi cung cấp mức giá + cạnh tranh ngay từ đầu và chúng tôi có thể
giúp bạn kiếm + được mức giá tốt hơn khi bạn mở rộng quy mô

+ +
+
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+ +
+ +
+

Không có phí giao + dịch bổ sung cho thanh toán hoặc các
tính năng cốt lõi

+

Chọn nhà cung cấp dịch vụ thanh toán mà không phải trả thêm phí + giao dịch/hình phạt so với mức phí lên tới 2%
trên các + nền tảng khác — hãy đưa ra quyết định dựa trên nhu cầu kinh doanh của bạn, không phải của chúng + tôi.

+ +
+
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+ +
+ + +
+
+

+ Hãy + chuyển sang + nền tảng + xStore
+ ngay hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+

Tìm hiểu thêm về cách xStore trao quyền cho + khách hàng + của chúng tôi để

+ +
+
+ + +
+
+ + \ No newline at end of file diff --git a/template/features/create.html b/template/features/create.html new file mode 100644 index 0000000..77f2fc3 --- /dev/null +++ b/template/features/create.html @@ -0,0 +1,279 @@ +
+ +
+
+ + +

Tạo mới

+ +

Thiết kế trang web thân thiện với
+ người dùng để thúc đẩy chuyển đổi

+

Có thể tự do tạo gian hàng đẹp nhất thể hiện + câu chuyện thương hiệu của bạn

+ Bắt + đầu dùng + thử miễn phí + + +
+
+
+ chuyendoi +
+
+ +42% chuyển đổi từ lượt truy cập thành đơn hàng +

Các trang web doanh nghiệp nhỏ trên xStore chỉ sử dụng trải nghiệm thanh + toán + cốt lõi của chúng tôi có tỷ lệ chuyển đổi + từ lượt truy cập thành đơn hàng cao hơn 42% so với nền tảng khác

+
+
+
+ + +
+
+ + +
+

Tìm hiểu nền tảng của chúng tôi cho phép + bạn

+
+ + + +
+

Khởi chạy nhanh chóng + bằng Page Builder để tạo trang web của bạn

+

Trình chỉnh sửa trực quan Page Builder cho phép xây dựng và ra mắt + trang nhanh chóng,
không cần mã với những lợi ích của + một nền tảng mạnh mẽ

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ + +
+

Xây dựng thương hiệu + của bạn bằng cách sử dụng các
mẫu chuyên nghiệp

+

Các chủ đề được tối ưu hóa và tùy chỉnh hoàn toàn với khả năng phản + hồi trên thiết bị
di động ngay khi xuất xưởng

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Phát triển linh hoạt + trên nền tảng có khả năng mở rộng
+ theo cửa hàng của bạn

+

Xây dựng doanh nghiệp trực tuyến của bạn một cách nhanh chóng, với + sự tự do phát triển thành các giải pháp
trang web tùy + chỉnh hoặc không có giao diện bằng các công cụ như Next.js và React

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Giữ nguyên trang web + hiện tại của bạn
+ bằng cách kết nối WordPress trực tiếp với xStore

+

Kết nối trang web WordPress của bạn với công cụ mạnh mẽ của chúng + tôi để truy cập vào
các chức năng thương mại điện tử + mà không phải bỏ nội dung của bạn

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+
+

+ Hãy + chuyển sang + nền tảng + xStore
+ ngay hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+

Tìm hiểu thêm về cách xStore trao quyền cho + khách hàng + của chúng tôi để

+ +
+
+ + +
+ +
+
\ No newline at end of file diff --git a/template/features/grow.html b/template/features/grow.html new file mode 100644 index 0000000..ae03327 --- /dev/null +++ b/template/features/grow.html @@ -0,0 +1,268 @@ +
+ +
+
+ + +

Vận hành

+ +

Mở rộng thương hiệu của bạn để thúc + đẩy doanh số tốt hơn

+

Phát triển không giới hạn bằng cách nhanh + chóng tiếp cận các tính năng, khả
năng mới và tăng nguồn doanh thu

+ Bắt + đầu dùng + thử miễn phí + + +
+
+
+ chuyendoi +
+
+ +26% Doanh thu +

Khách hàng doanh nghiệp nhỏ sẽ có doanh thu tăng thêm 26% khi thêm nguồn + doanh thu mới thông qua nhiều cửa hàng.*

+
+
+
+ + +
+
+ +
+

Chọn một nền tảng cung cấp cho bạn

+
+ + +
+

Kết hợp trực tuyến và + ngoại tuyến để thu hút khách hàng
theo cách họ muốn

+

Kết nối liền mạch điểm bán hàng truyền thống với cửa hàng trực tuyến + của bạn để khách
hàng có thể Mua hàng trực tuyến, + Nhận hàng tại cửa hàng

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Thêm nguồn doanh thu + mới để mở rộng
doanh nghiệp của bạn.

+

Mở rộng để phục vụ cả khách hàng bán lẻ và bán buôn hoặc thêm các + thương hiệu mới
mà không tốn kém và rắc rối khi quản + lý nhiều nền tảng

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+
+

Phát triển linh hoạt + trên nền tảng có khả năng mở rộng
+ theo cửa hàng của bạn

+

Xây dựng doanh nghiệp trực tuyến của bạn một cách nhanh chóng, với + sự tự do phát triển thành các giải pháp
trang web tùy + chỉnh hoặc không có giao diện bằng các công cụ như Next.js và React

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Khởi nghiệp kinh + doanh mới nhanh chóng mà +
không cần phải học nền tảng mới +

+

Ra mắt thêm các doanh nghiệp với trang web và trải nghiệm thương mại + điện tử riêng biệt
mà không cần phải bắt đầu từ đầu + hoặc chia nhỏ phân tích trên nhiều nền tảng

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+
+

+ Hãy + chuyển sang + nền tảng + xStore
+ ngay hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+

Tìm hiểu thêm về cách xStore trao quyền cho + khách hàng + của chúng tôi để

+ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/template/features/market.html b/template/features/market.html new file mode 100644 index 0000000..97d72ed --- /dev/null +++ b/template/features/market.html @@ -0,0 +1,234 @@ +
+ +
+
+ + +

Thị trường

+ +

Tiếp thị tới bất kỳ ai, ở bất kỳ đâu +

+

Tiếp cận nhiều người mua sắm hơn trên các kênh + thị trường, mạng xã hội, tìm
kiếm và quảng cáo hàng đầu

+ Bắt + đầu dùng + thử miễn phí + + +
+
+
+ chuyendoi +
+
+ X2 Doanh thu +

Các doanh nghiệp nhỏ thêm một thị trường sẽ tăng doanh thu 38%, thêm hai + thị trường sẽ tăng doanh thu 120% và thêm ba + kênh quảng cáo trở lên sẽ tăng mức độ tương tác 250%.

+
+
+
+ + +
+
+ +
+

Chọn một nền tảng cung cấp cho bạn

+
+ + +
+

Quảng cáo ở mọi nơi + thông qua
+ nhiều kênh hơn

+

Tích hợp trực tiếp và tối ưu hóa hiệu suất vào các kênh tìm kiếm, + nền tảng truyền thông xã hội và kênh hiển thị hàng
đầu + giúp bạn thu hút nhiều khách hàng hơn và đạt được khả năng hiển thị thương hiệu (Google, Instagram, + TikTok, v.v.)

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Đặt sản phẩm ở khắp + mọi nơi
+ của thị trường

+

Tích hợp với các thị trường lớn nhất (Facebook, Tiktok, Shopee,...) + giúp mở rộng phạm vi tiếp cận cửa hàng
của bạn, với + các tùy chọn để tối ưu hóa nguồn cấp dữ liệu sản phẩm trên nhiều kênh.

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+
+

Đứng đầu trang tìm + kiếm
+ với các công cụ SEO mạnh mẽ

+

Các công cụ SEO mạnh mẽ, sẵn dùng giúp tăng lưu lượng truy cập để + đảm bảo
+ bạn có thứ hạng cao hơn đối thủ cạnh tranh

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+
+

+ Hãy + chuyển sang + nền tảng + xStore
+ ngay hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+

Tìm hiểu thêm về cách xStore trao quyền cho + khách hàng + của chúng tôi để

+ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/template/features/operate.html b/template/features/operate.html new file mode 100644 index 0000000..5495aff --- /dev/null +++ b/template/features/operate.html @@ -0,0 +1,283 @@ +
+ +
+
+ + +

Vận hành

+ +

Điều hành doanh nghiệp của bạn hiệu + quả hơn

+

Giảm chi phí vận hành của bạn với một nền tảng + đáng tin cậy
+ và một đội ngũ hỗ trợ bạn

+ Bắt + đầu dùng + thử miễn phí + + +
+
+
+ chuyendoi +
+
+ Thời gian hoạt động 99,99% +

Giảm thiểu phiền phức, tối đa hóa tăng trưởng với thời gian hoạt động + 99,99%, thời gian chờ hỗ trợ trung bình <2 phút,> + 85% cuộc gọi hỗ trợ đầu tiên được giải quyết, hơn 20 triệu biến thể sản phẩm, hơn 2 + triệu SKU sản phẩm và tổng chi + phí sở hữu tốt nhất.

+
+
+
+ + +
+
+ +
+

Chọn một nền tảng cung cấp cho bạn

+
+ +
+

Hiệu suất đáng tin + cậy
+ để tránh gián đoạn kinh doanh

+

Thời gian hoạt động 99,99%, trang tải nhanh và bảo trì được xử lý + cho bạn, bao gồm bảo mật,
lưu trữ, quản lý tuân thủ và + cập nhật

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+

Giảm chi phí khi bạn + phát triển để hỗ trợ
việc mở rộng quy mô

+

Nền tảng của bạn cần được xây dựng để mở rộng quy mô, nhưng chi phí + của bạn không nhất thiết phải tăng theo. Hãy xây + dựng
doanh nghiệp của bạn trên một nền tảng có tổng chi phí sở hữu thấp nhất, bao gồm cả việc + giảm + tỷ lệ thanh toán, khi + bạn phát triển.

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+
+

Đối tác đáng tin cậy + cho mọi bước phát triển của bạn

+

Yên tâm hơn khi biết rằng bạn chỉ cần gọi điện thoại là có thể nhận + được hỗ trợ trực tiếp 24/7 từ nhân viên
xStore + tại Hà nội, cùng các dịch vụ hỗ trợ thành công, ra mắt hoặc di chuyển trước, trong và sau khi ra + mắt +

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+
+

Hiểu biết sâu sắc về + kinh doanh và sự nhanh nhẹn thúc đẩy
việc ra quyết định thông minh

+

Với các công cụ phân tích mạnh mẽ được tích hợp sẵn trong mỗi gói, + bạn có thể theo dõi hoạt động kinh doanh và điều + chỉnh
để tăng tốc độ phát triển. Ngoài ra, bộ công cụ AI ngày càng phát triển của chúng tôi có + thể + tăng tốc các tác vụ + hàng ngày và giảm
thiểu công sức vận hành doanh nghiệp. +

+ +
+ +
+
+ hacom +
+
+
+
+
+ Khám phá thêm nhiều tính năng + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+ +
+
+ +
+
+

+ Hãy + chuyển sang + nền tảng + xStore
+ ngay hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+

Tìm hiểu thêm về cách xStore trao quyền cho + khách hàng + của chúng tôi để

+ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/template/free-trial/home.html b/template/free-trial/home.html new file mode 100644 index 0000000..f8e4f7b --- /dev/null +++ b/template/free-trial/home.html @@ -0,0 +1,327 @@ +
+
+
+ + +

Bắt đầu dùng thử miễn phí

+

Khám phá lý do tại sao
+ các thương hiệu phát triển nhanh lại chọn xStore

+ +
+
+ +
+
+
+
+
+ Ra mắt cửa hàng của bạn nhanh chóng +

+ Với Trình tạo trang kéo và thả, chủ đề tương thích với thiết bị di động, thanh toán một + trang + gốc và các công cụ SEO + mạnh mẽ, bạn có thể dễ dàng — và nhanh chóng — tạo một cửa hàng trực tuyến đẹp mắt có khả + năng + chuyển đổi.

+ Tiết kiệm chi phí thanh toán +

Hãy chọn nhà cung cấp dịch vụ thanh toán phù hợp nhất với doanh + nghiệp của bạn — yên tâm rằng + chúng tôi sẽ không tính + thêm phí. Hơn nữa, bạn còn tiết kiệm hơn nữa với các đối tác ưu tiên cung cấp mức giá ưu đãi + đã + được thương lượng.

+ Trải nghiệm độ tin cậy thực sự +

Nền tảng này không chỉ đáng tin cậy (thời gian hoạt động 99,99% + và nhiều chứng chỉ bảo mật + Tier + 1), mà đội ngũ của chúng + tôi cũng vậy. Chúng tôi luôn sẵn sàng hỗ trợ 24/7 qua điện thoại, trò chuyện và email để + giải + đáp mọi thắc mắc của bạn.

+ + trial + xStore mang đến trải nghiệm mà khách hàng chưa + từng thấy được ở + các nền tảng + khác +
+
+
+

Tạo cửa hàng trực tuyến của bạn

+

Hãy bắt đầu ngay hôm nay với bản dùng thử + miễn phí 15 + ngày của + chúng tôi

+ +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+

Để đảm bảo hiệu suất và tốc độ tối ưu cho cửa hàng + của bạn, hãy chọn khu vực gần nhất + với khách hàng tiềm năng. +

+ + + +

+ Bằng cách cung cấp email của bạn, bạn đồng ý với các điều khoản dịch vụ + của chúng + tôi.

+

+ Trang web này được bảo vệ bởi reCAPTCHA Enterprise và chính sách bảo + mật cũng như + điều khoản dịch vụ của Google được + áp + dụng. +

+ +
+ + +
+
+
+
+
+
+ + +
+

Câu hỏi thường gặp

+
+
+
+
+ Cho tôi xin bảng giá dịch vụ của sXtore? + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+
+ Nền tảng xStore phù hợp với các doanh nghiệp + nào? + +
+
+ Phù hợp từ cửa hàng nhỏ đến doanh nghiệp vừa và lớn, đặc biệt là ngành bán lẻ, + thời trang, mỹ phẩm và điện tử. +
+ +
+
+
+ Tại sao tôi nên chọn xStore thay vì các nền + tảng thương mại điện tử khác? + +
+
+
    +
  • Hiệu năng tốt, tối ưu SEO.
  • +
  • Nhiều tích hợp thanh toán/vận chuyển.
  • +
  • Dễ mở rộng giao diện với Tailwind.
  • +
+
+ +
+
+
+ Cách sử dụng nền tảng này như thế nào + vậy? + +
+
+ Đăng ký tài khoản, chọn gói, thêm sản phẩm và bật cổng thanh toán. Có tài liệu + hướng dẫn chi tiết từng bước. +
+ +
+
+ +

Xem tất cả

+ +
+
+
+
\ No newline at end of file diff --git a/template/home/home.html b/template/home/home.html new file mode 100644 index 0000000..6e4d43a --- /dev/null +++ b/template/home/home.html @@ -0,0 +1,391 @@ +
+ + +
+
+

Bạn có thể + nhận được những gì khi + sử dụng nền tảng xStore?

+
+
+
+ +
+ Tăng doanh thu nhanh hơn +

Các công cụ tích hợp mạnh mẽ giúp tăng tốc độ phát + triển thương mại điện + tử của bạn.

+
+
+
+ +
+ Thu hút nhiều khách hàng hơn. +

Quy trình thanh toán hàng đầu trong ngành giúp + biến người truy cập trang + web thành khách hàng

+
+
+
+ +
+ Tiết kiệm chi phí +

Cung cấp dịch vụ thanh toán ưu tiên mức giá cạnh + tranh

+
+
+
+ +
+ Xây dựng theo cách của bạn +

Tính linh hoạt của nền tảng có nghĩa là bạn có thể + tự do tùy chỉnh.

+
+
+
+ +
+ Tăng doanh số bán hàng +

Các tính năng đảm bảo bạn có thể mở rộng quy mô + bán hàng trực tuyến.

+
+
+
+ +
+ Đội ngũ hỗ trợ tin cậy +

Hỗ trợ trực tiếp 24/7 giải quyết mọi vấn đề của + khách hàng

+
+
+
+
+ +
+
+
+

Nền tảng hiệu suất cao có thể Tuỳ chỉnh

+ +
+
+

Tạo mới

+

Thiết kế một trang web có khả năng chuyển đổi

+
    +
  • Trình tạo trang kéo và thả
  • +
  • Chủ đề có thể tùy chỉnh
  • +
  • Khả năng phản hồi di động tích hợp
  • +
  • Kết nối Wordpress liền mạch
  • +
+ + Xem thêm + + +
+
+ + mygear + +
+
+ + +
+
+ + mygear + +
+
+

Chuyển đổi

+

Tùy chỉnh hoàn toàn quá trình thanh toán của bạn

+
    +
  • Thanh toán một trang được tối ưu hóa được thiết kế + để chuyển + đổi người mua sắm
  • +
  • Chọn đối tác thanh toán ưa thích của bạn để nhận + được mức giá + tốt nhất có thể — + không bị phạt
  • +
  • Ví điện tử và giải pháp mua ngay, trả tiền sau
  • +
  • Tùy chọn thanh toán một cú nhấp chuột không cần mật + khẩu
  • +
+ + Xem thêm + + +
+
+ + +
+
+

Thị trường

+

Thu hút nhiều người mua sắm hơn

+
    +
  • Bán trên các kênh truyền thông xã hội như Meta và TikTok
  • +
  • Quảng cáo trên các nền tảng như Google, Youtube
  • +
  • Đưa sản phẩm lên các thị trường như Shopee và Lazada
  • +
  • Tối đa hóa lưu lượng truy cập trang web bằng các công cụ SEO mạnh mẽ
  • +
+ + Xem thêm + + +
+
+ + mygear + +
+
+ +
+
+ + mygear + +
+
+

Phát triển

+

Mở rộng thương hiệu của bạn

+
    +
  • Bán lẻ và bán buôn với các tính năng như nhóm khách + hàng
  • +
  • Quản lý nhiều trang web với Multi-Storefront
  • +
  • Kết nối ngoại tuyến và trực tuyến với Mua trực + tuyến, Nhận tại cửa hàng
  • +
  • Nhanh chóng tạo ra một thương hiệu mới với cùng một + nền tảng
  • +
+ + Xem thêm + + +
+
+ +
+
+

vận hành

+

Hoạt động hiệu quả hơn

+
    +
  • Thời gian hoạt động tốt nhất trong phân khúc 99,99%
  • +
  • Thời gian hoạt động 100% trong Tuần lễ An ninh mạng 10 năm liên tiếp Tổng chi phí sở + hữu thấp hơn Tuân thủ nhiều tiêu + chuẩn bảo mật Cấp 1 bao gồm ISO, PCI, SOC, GDPR và CCPA
  • +
+ + Xem thêm + + +
+
+ + mygear + +
+
+ +
+
+
+ + +
+
+
+
+ + + +
+
+

Bán hàng nhiều hơn, nhanh hơn với sự trợ giúp từ + chuyên gia + tư + vấn xStore

+

Các tư vấn viên hướng dẫn chuyên môn về cách thiết lập cửa hàng + XStore — + khách hàng mới đăng + ký gói Pro hoặc Plus hàng + năm có thể tiết kiệm 50% cho dịch vụ này.

+ +
+
+
+
+ + +
+
+
+

Tìm một đối + tác để + biến tầm nhìn
+ của + bạn thành + hiện thực +

+ +
+
+
+ hura8 + Hura8 +

+ Phần mềm tạo website TMĐT chạy nên nền tảng Cloud dành cho doanh nghiệp lớn +

+ + Xem thêm + + +
+
+ Chatngay + Chatngay +

+ Phần mềm hỗ trợ, cskh tích hợp website giúp khách hàng tương tác nhanh chóng và + thuận tiện. +

+ + Xem thêm + + +
+
+ +
+

Câu hỏi thường gặp

+ +
+
+
+ Cho tôi xin bảng giá dịch vụ của sXtore? + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+
+ Nền tảng xStore phù hợp với các doanh nghiệp + nào? + +
+
+ Phù hợp từ cửa hàng nhỏ đến doanh nghiệp vừa và lớn, đặc biệt là ngành bán lẻ, + thời trang, mỹ phẩm và điện tử. +
+ +
+
+
+ Tại sao tôi nên chọn xStore thay vì các nền + tảng thương mại điện tử khác? + +
+
+
    +
  • Hiệu năng tốt, tối ưu SEO.
  • +
  • Nhiều tích hợp thanh toán/vận chuyển.
  • +
  • Dễ mở rộng giao diện với Tailwind.
  • +
+
+ +
+
+
+ Cách sử dụng nền tảng này như thế nào + vậy? + +
+
+ Đăng ký tài khoản, chọn gói, thêm sản phẩm và bật cổng thanh toán. Có tài liệu + hướng dẫn chi tiết từng bước. +
+ +
+
+ + +

Xem tất cả

+ +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/template/login/home.html b/template/login/home.html new file mode 100644 index 0000000..f641c2e --- /dev/null +++ b/template/login/home.html @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/template/other/footer.html b/template/other/footer.html new file mode 100644 index 0000000..f14e3d5 --- /dev/null +++ b/template/other/footer.html @@ -0,0 +1,62 @@ + \ No newline at end of file diff --git a/template/other/header.html b/template/other/header.html new file mode 100644 index 0000000..1219436 --- /dev/null +++ b/template/other/header.html @@ -0,0 +1,222 @@ +
+ +
+
+ + +
+
+ +
\ No newline at end of file diff --git a/template/pricing/home.html b/template/pricing/home.html new file mode 100644 index 0000000..0159e24 --- /dev/null +++ b/template/pricing/home.html @@ -0,0 +1,760 @@ +
+
+
+ +

Dùng thử xStore miễn phí trong 15 + ngày trước khi +
đăng ký + sử dụng + nền + tảng +

+ +
+ Trả hàng tháng + + + Trả hàng năm +
+ +
+ +
+ Tiêu chuẩn +

Dành cho cá nhân và thương + hiệu nhỏ

+ +
+
+
900.000đ
+
+ Tiết kiệm 20%
+
+ 1 chi nhánh/ tháng +
+ +
    +
  • + +

    Doanh thu trực tuyến lên tới 100tr

    +
  • +
  • + +

    Phí thanh toán thêm 0% +

    +
  • +
  • + +

    Hỗ trợ qua điện thoại và trò chuyện trực tiếp 24/7 +

    +
  • +
+ + Bắt + đầu dùng + thử miễn phí + +
+ + So sánh các kế hoạch + + + +
+
+ +
+ Nâng cao +

Dành cho cá nhân và thương + hiệu nhỏ có triển vọng phát triển

+ +
+
+
1.200.000đ
+
+ Tiết kiệm 20%
+
+ 1 chi nhánh/ tháng +
+ +
    +
  • + +

    Doanh thu trực tuyến lên tới 100tr

    +
  • +
  • + +

    Phí thanh toán thêm 0% +

    +
  • +
  • + +

    Hỗ trợ qua điện thoại và trò chuyện trực tiếp 24/7 +

    +
  • +
+ + Bắt + đầu dùng + thử miễn phí + +
+ + So sánh các kế hoạch + + + +
+
+ +
+
+ Cao cấp +
+ Phổ biến +
+
+ +

Dành cho cá nhân và thương + hiệu nhỏ có triển vọng phát triển

+ +
+
+
3.000.000đ
+
+ Tiết kiệm 20%
+
+ 1 chi nhánh/ tháng +
+ +
    +
  • + +

    Doanh thu trực tuyến lên tới 100tr

    +
  • +
  • + +

    Phí thanh toán thêm 0% +

    +
  • +
  • + +

    Hỗ trợ qua điện thoại và trò chuyện trực tiếp 24/7 +

    +
  • +
+ + Bắt + đầu dùng + thử miễn phí + +
+ + So sánh các kế hoạch + + + +
+
+
+ Doanh nghiệp lớn +

Dành cho các doanh nghiệp đã + thành lập đang trên đà phát triển lớn mạnh +

+ +
+
+
Liên hệ
+
+ Liên hệ để được tư vấn phù hợp với doanh nghiệp +
+ +
    +
  • + +

    Tối ưu nhất cho các doanh nghiệp

    +
  • +
  • + +

    Phí thanh toán thêm 0% +

    +
  • +
  • + +

    Hỗ trợ qua điện thoại và trò chuyện trực tiếp 24/7 +

    +
  • +
+ + Bắt + đầu với Hura8 + +
+ + So sánh các kế hoạch + + + +
+
+ +
+ + +

Ưu đãi áp dụng cho các gói Standard, Plus và Pro.

+ +
+
+ +
+

Xem tất cả kế hoạch tính năng

+
+
+
+
+ Bao gồm trong tất cả các kế hoạch + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bao gồm trong tất cả các kế hoạchTiêu chuẩnNâng caoCao cấpDoanh nghiệp lớn
Không có phí giao dịch bổ sung 0%0%0%0%
Sản phẩm, lưu trữ tệp và băng thôngKhông giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
Tài khoản nhân viênKhông giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
+
+
+
+
+ Kênh bán hàng + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bao gồm trong tất cả các kế hoạchTiêu chuẩnNâng caoCao cấpDoanh nghiệp lớn
Cửa hàng trực tuyến
Nhiều cửa hàng +

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Liên hệ

+
Điểm bán hàng Không giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
Shopee/Lazada/Tiki
Facebook/Instagram
+
+
+
+
+ Đặc trưng + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bao gồm trong tất cả các kế hoạchTiêu chuẩnNâng caoCao cấpDoanh nghiệp lớn
Cửa hàng trực tuyến
Nhiều cửa hàng +

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Liên hệ

+
Điểm bán hàng Không giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
Shopee/Lazada/Tiki
Facebook/Instagram
+
+
+
+
+ Dịch vụ và Hỗ trợ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bao gồm trong tất cả các kế hoạchTiêu chuẩnNâng caoCao cấpDoanh nghiệp lớn
Cửa hàng trực tuyến
Nhiều cửa hàng +

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Liên hệ

+
Điểm bán hàng Không giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
Shopee/Lazada/Tiki
Facebook/Instagram
+
+
+
+
+ Xử lý thanh toán + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bao gồm trong tất cả các kế hoạchTiêu chuẩnNâng caoCao cấpDoanh nghiệp lớn
Cửa hàng trực tuyến
Nhiều cửa hàng +

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Bổ sung 550k/1 cửa hàng + (Tối đa 3)

+
+

Liên hệ

+
Điểm bán hàng Không giới hạnKhông giới hạnKhông giới hạnKhông giới hạn
Shopee/Lazada/Tiki
Facebook/Instagram
+
+
+
+
+
+ +
+
+
+

Hãy chuyển sang sử dụng xStore ngay + hôm nay

+ Bắt + đầu dùng + thử miễn phí +
+
+
+ +
+

Câu hỏi thường gặp

+
+
+
+
+ Cho tôi xin bảng giá dịch vụ của sXtore? + +
+
+ Bảng giá phụ thuộc gói tính năng, lưu trữ và số lượng sản phẩm. Liên hệ để được + báo giá chi tiết và ưu đãi theo nhu cầu + của bạn. +
+
+
+
+ Nền tảng xStore phù hợp với các doanh nghiệp + nào? + +
+
+ Phù hợp từ cửa hàng nhỏ đến doanh nghiệp vừa và lớn, đặc biệt là ngành bán lẻ, + thời trang, mỹ phẩm và điện tử. +
+ +
+
+
+ Tại sao tôi nên chọn xStore thay vì các nền + tảng thương mại điện tử khác? + +
+
+
    +
  • Hiệu năng tốt, tối ưu SEO.
  • +
  • Nhiều tích hợp thanh toán/vận chuyển.
  • +
  • Dễ mở rộng giao diện với Tailwind.
  • +
+
+ +
+
+
+ Cách sử dụng nền tảng này như thế nào + vậy? + +
+
+ Đăng ký tài khoản, chọn gói, thêm sản phẩm và bật cổng thanh toán. Có tài liệu + hướng dẫn chi tiết từng bước. +
+ +
+
+ +

Xem tất cả

+ +
+
+
+ +
+
+
+
+ + Điều khoản & điều kiện + +

What is Lorem Ipsum? + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been + the + industry's standard + dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it + to + make a type specimen + book. It has survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially + unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem + Ipsum passages, and more + recently with desktop publishing software like Aldus PageMaker including versions of Lorem + Ipsum. +

+ +

+ Why do we use it? + It is a long established fact that a reader will be distracted by the readable content of a page + when looking at its + layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of + letters, + as opposed to using + 'Content here, content here', making it look like readable English. Many desktop publishing + packages + and web page + editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will + uncover + many web sites + still in their infancy. Various versions have evolved over the years, sometimes by accident, + sometimes on purpose + (injected humour and the like).v

+

+ What is Lorem Ipsum? + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been + the + industry's standard + dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it + to + make a type specimen + book. It has survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially + unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem + Ipsum passages, and more + recently with desktop publishing software like Aldus PageMaker including versions of Lorem + Ipsum. +

+
+
+
+
+
\ No newline at end of file diff --git a/template/theme.html b/template/theme.html new file mode 100644 index 0000000..c202bdc --- /dev/null +++ b/template/theme.html @@ -0,0 +1,37 @@ + + + + + + + xstore + + + + + + + + + + + + {% if global.module != 'login' } + {% else %} + {% include other/header %} + {% endif %} + + {{ page_content }} + + {% if global.module != 'login' } + {% else %} + {% include other/footer %} + {% endif %} + + + + + + + \ No newline at end of file