diff --git a/composer.json b/composer.json
index 11b39ee6..25a9947d 100644
--- a/composer.json
+++ b/composer.json
@@ -21,6 +21,9 @@
"email": "ngocanhthu20102002@gmail.com"
}
],
+ "config": {
+ "optimize-autoloader": true
+ },
"require": {
"php": "~7.4.0"
},
diff --git a/private/browse.php b/private/browse.php
new file mode 100644
index 00000000..b37b8a8e
--- /dev/null
+++ b/private/browse.php
@@ -0,0 +1,80 @@
+
+
+ $store_name
+
+ ";
+ }
+
+ /**
+ * Retrieve a list of stores and the maximum number of cards that will be display
+ * Display store cards with a number of cards that is <= the maximum number of cards
+ * The cards will be ordered ascending based on its position in the list
+ * @param $stores
+ * @param $max_cards
+ */
+ function each_page($stores, $max_cards) {
+ $min = 0;
+ $max = $max_cards - $min - 1;
+ $page = $_GET["page"];
+ $list_length = count($stores);
+ $min += $max_cards * ($page-1);
+ $max += $max_cards * ($page-1);
+ if ($max > $list_length) {
+ $max = $list_length - 1;
+ }
+ for ($i = $min; $i <= $max; $i++) {
+ if (isset($stores[$i])) {
+ display_store($stores[$i]["store_name"],$stores[$i]["store_id"]);
+ }
+ }
+
+ }
+
+ /**
+ * Use the number of the current page to get the previous page
+ * @return int
+ */
+ function prev_page() {
+ $prev = $_GET["page"] - 1;
+ // page number cannot be lower than 1
+ if ($prev < 1) {
+ $prev = 1;
+ }
+ return $prev;
+ }
+
+ /**
+ * Use the number of the current page to get the next page
+ * @return int
+ */
+ function next_page($list_length, $max_cards) {
+ $next = $_GET["page"] + 1;
+ // calculate the required pages for each browse option
+ if ($list_length % $max_cards != 0) {
+ $max_page = floor($list_length / $max_cards) + 1;
+ } else {
+ $max_page = $list_length / $max_cards;
+ }
+ // page number cannot exceed the maximum number of pages
+ if ($next > $max_page) {
+ $next = $max_page;
+ }
+ return $next;
+ }
\ No newline at end of file
diff --git a/private/classes/Database.php b/private/classes/Database.php
new file mode 100644
index 00000000..38319de6
--- /dev/null
+++ b/private/classes/Database.php
@@ -0,0 +1,82 @@
+type = $type;
+ switch ($this->type) {
+ case self::CATEGORY_DATABASE:
+ $data = read_csv(self::$category_database_path, true);
+ foreach ($data as $entry) {
+ $this->data[] = new DatabaseCategory($entry["id"], $entry["name"]);
+ }
+ break;
+ case self::STORE_DATABASE:
+ $categories = new Database(self::CATEGORY_DATABASE);
+ $data = read_csv(self::$store_database_path, true);
+ foreach ($data as $entry) {
+ $this->data[] = new DatabaseStore(
+ $entry["id"],
+ $entry["name"],
+ $categories->getEntryById($entry["category_id"]),
+ strtotime($entry["created_time"]),
+ boolval($entry["featured"])
+ );
+ }
+ break;
+ case self::PRODUCT_DATABASE:
+ $stores = new Database(self::STORE_DATABASE);
+ $data = read_csv(self::$product_database_path, true);
+ foreach ($data as $entry) {
+ $this->data[] = new DatabaseProduct(
+ $entry["id"],
+ $entry["name"],
+ floatval($entry["price"]),
+ $stores->getEntryById($entry["store_id"]),
+ strtotime($entry["created_time"]),
+ boolval($entry["featured_in_mall"]),
+ boolval($entry["featured_in_store"])
+ );
+ }
+ break;
+ }
+ }
+
+
+ public static function merge(Database $database_1, Database $database_2) {
+ $result = clone $database_1;
+ $result->setAllEntries(array_merge($result->getAllEntries(), $database_2->getAllEntries()));
+ return $result;
+ }
+
+ public function getEntryById(string $id) {
+ foreach ($this->data as $entry) {
+ if ($entry->id === $id) {
+ return $entry;
+ }
+ }
+ return null;
+ }
+
+ public function getAllEntries(): array {
+ return $this->data;
+ }
+
+ private function setAllEntries($data) {
+ $this->data = $data;
+ }
+ }
diff --git a/private/classes/DatabaseCategory.php b/private/classes/DatabaseCategory.php
new file mode 100644
index 00000000..d4dfa11f
--- /dev/null
+++ b/private/classes/DatabaseCategory.php
@@ -0,0 +1,11 @@
+id = $id;
+ $this->name = $name;
+ }
+
+
+ public function isSearchTermMatch(string $search_term, int $levenshtein_match_threshold=0): bool {
+ $name_elements = preg_split("/[\s,]+/", $this->name);
+ foreach ($name_elements as $name_element) {
+ if (levenshtein(strtoupper($name_element), strtoupper($search_term)) <= $levenshtein_match_threshold) {
+ $this->search_relevance++;
+ return true;
+ }
+ }
+ $this->search_relevance--;
+ return false;
+ }
+ }
diff --git a/private/classes/DatabaseProduct.php b/private/classes/DatabaseProduct.php
new file mode 100644
index 00000000..7f86255b
--- /dev/null
+++ b/private/classes/DatabaseProduct.php
@@ -0,0 +1,47 @@
+price = $price;
+ if ($created_time === null) {
+ $this->created_time = time();
+ } else {
+ $this->created_time = $created_time;
+ }
+ $this->store = $store;
+ $this->featured_in_mall = $featured_in_mall;
+ $this->featured_in_store = $featured_in_store;
+
+ self::$count++;
+ }
+
+ public function isSearchTermMatch(string $search_term, int $levenshtein_match_threshold=0): bool {
+ $name_elements = preg_split("/[\s,]+/", $this->name);
+ foreach ($name_elements as $name_element) {
+ if (levenshtein(strtoupper($name_element), strtoupper($search_term), 1, 100, 10000) <= $levenshtein_match_threshold) {
+ $this->search_relevance -= $this->store->search_relevance * 4;
+ return true;
+ }
+ }
+ $this->search_relevance += $this->store->search_relevance;
+ return $this->store->isSearchTermMatch($search_term, $levenshtein_match_threshold);
+ }
+ }
diff --git a/private/classes/DatabaseStore.php b/private/classes/DatabaseStore.php
new file mode 100644
index 00000000..45b1439b
--- /dev/null
+++ b/private/classes/DatabaseStore.php
@@ -0,0 +1,35 @@
+created_time = time();
+ } else {
+ $this->created_time = $created_time;
+ }
+ $this->category = $category;
+ $this->featured = $featured;
+
+ self::$count++;
+ }
+
+ public function isSearchTermMatch(string $search_term, int $levenshtein_match_threshold=0): bool {
+ $name_elements = preg_split("/[\s,]+/", $this->name);
+ foreach ($name_elements as $name_element) {
+ if (levenshtein(strtoupper($name_element), strtoupper($search_term), 1, 10, 100) <= $levenshtein_match_threshold) {
+ $this->search_relevance -= $this->category->search_relevance * 2;
+ return true;
+ }
+ }
+ $this->search_relevance += $this->category->search_relevance;
+ return $this->category->isSearchTermMatch($search_term, $levenshtein_match_threshold);
+ }
+ }
diff --git a/private/classes/Search.php b/private/classes/Search.php
new file mode 100644
index 00000000..7f6ebd6f
--- /dev/null
+++ b/private/classes/Search.php
@@ -0,0 +1,73 @@
+query = $query;
+ $search_terms = preg_split("/[\s,]+/", $this->query);
+
+ if ($filter === "Filter") {
+ $this->filter = self::FILTER_ALL;
+ } else {
+ $this->filter = $filter;
+ }
+ switch ($this->filter) {
+ case self::FILTER_ALL:
+ $this->data = new Database(Database::STORE_DATABASE);
+ $this->data = Database::merge($this->data, new Database(Database::PRODUCT_DATABASE));
+ break;
+ case self::FILTER_PRODUCTS:
+ $this->data = new Database(Database::PRODUCT_DATABASE);
+ break;
+ case self::FILTER_STORES:
+ default:
+ $this->data = new Database(Database::STORE_DATABASE);
+ break;
+ }
+
+ foreach ($search_terms as $search_term) {
+ $this->search_terms[] = new SearchTerm($search_term, $this->data);
+ }
+
+ $this->generateResults();
+ }
+
+
+ public function generateResults() {
+ $match_results = [];
+ foreach ($this->search_terms as $search_term) {
+ foreach ($search_term->getAllMatches() as $match) {
+ if (isset($match_results[$match->id])) {
+ $match_results[$match->id]++;
+ } else {
+ $match_results[$match->id] = 1;
+ }
+ }
+ }
+
+ foreach ($match_results as $match_id => $match_count) {
+ if ($match_count === count($this->search_terms)) {
+ if ($this->filter === self::FILTER_ALL || $this->filter === self::FILTER_STORES || $this->filter === self::FILTER_PRODUCTS) {
+ $this->results[] = $this->data->getEntryById($match_id);
+ } elseif (levenshtein(explode(" ", $this->data->getEntryById($match_id)->category->name)[0], explode(" ", $this->filter)[2]) < 5) { // TODO: This is a temporary workaround. Replace this with an exact match mechanism.
+ $this->results[] = $this->data->getEntryById($match_id);
+ }
+ }
+ }
+
+ usort($this->results, function (DatabaseEntry $entry_1, DatabaseEntry $entry_2) {
+ return $entry_2->search_relevance - $entry_1->search_relevance;
+ });
+ }
+ }
diff --git a/private/classes/SearchTerm.php b/private/classes/SearchTerm.php
new file mode 100644
index 00000000..79dde9b3
--- /dev/null
+++ b/private/classes/SearchTerm.php
@@ -0,0 +1,29 @@
+content = $content;
+ if ($search_data !== null) {
+ $this->generateMatches($search_data);
+ }
+ }
+
+
+ public function generateMatches(Database $search_data) {
+ foreach ($search_data->getAllEntries() as $entry) {
+ if ($entry->isSearchTermMatch($this->content, self::LEVENSHTEIN_MATCH_THRESHOLD)) {
+ $this->matches[] = $entry;
+ }
+ }
+ }
+
+ public function getAllMatches(): array {
+ return $this->matches;
+ }
+ }
diff --git a/private/csv.php b/private/csv.php
index 146cd25a..7754bb4a 100644
--- a/private/csv.php
+++ b/private/csv.php
@@ -124,6 +124,7 @@ function read_csv(string $path, bool $first_line_header=false): array {
function write_csv(string $path, array $data, bool $first_line_header=false): bool {
if (is_csv($path)) {
$file = fopen($path, "w");
+ flock($file, LOCK_EX);
if (!$first_line_header) {
@@ -147,6 +148,7 @@ function write_csv(string $path, array $data, bool $first_line_header=false): bo
}
+ flock($file, LOCK_UN);
fclose($file);
return true;
}
diff --git a/private/database.php b/private/database.php
index 96d5403f..f2d6cbf8 100644
--- a/private/database.php
+++ b/private/database.php
@@ -41,6 +41,27 @@ function list_databases(): array {
}
+ /**
+ * Get the index of a database entry given its id. Only works on databases with header
+ * @param array $database MUST be a database with header. Won't work on header-less database
+ * @param string $id
+ * @return int Return null if database is empty
+ */
+ function get_entry_index_by_id(array $database, string $id): int {
+ $index = null;
+ if (count($database) > 0) {
+ $index = 0;
+ foreach ($database as $entry) {
+ if ($entry["id"] === $id) {
+ break;
+ }
+ $index++;
+ }
+ }
+ return $index;
+ }
+
+
/**
* Convert and echo a given database as an editable HTML table. Uses method=POST to communicate with database processor
* @param string $name the name of the given database. For use with form submission.
@@ -64,7 +85,7 @@ function print_table(string $name, array $database, string $action, $empty=false
foreach ($database[0] as $header => $field) {
echo "
Developer
Chief Secretary
Visit my GitHub page here
" +S-3877562,"Tuong-Minh ""Mike"" Vo","/media/image/team/mike.png","Designer
Project Coordinator
Technical Officer
Visit my GitHub page here
" +S-3878480,"Du Duc Manh","/media/image/team/manh.jpg","Developer
Operation Officer
Visit my GitHub page here
" +S-3879312,"Tran Ngoc Anh Thu","/media/image/team/thu.jpg","Developer
Designer
Content Officer
Visit my GitHub page here
" \ No newline at end of file diff --git a/private/dynamic-display.php b/private/dynamic-display.php new file mode 100644 index 00000000..b68ff026 --- /dev/null +++ b/private/dynamic-display.php @@ -0,0 +1,146 @@ +array containing information + * of all products featured on Mall Home + */ + function check_featured_mall_products(array $products): array { + $featured_mall_products = []; + + foreach ($products as $product) { + if ($product['featured_in_mall'] === "TRUE") { + $featured_mall_products[] = $product; + } + } + return $featured_mall_products; + } + + + /** + * Check if stores are featured on the Mall Home page + * @param array $stores containing information of all stores + * @return array array containing information + * of all stores featured on Mall Home + */ + function check_featured_mall_stores(array $stores): array { + $featured_mall_stores = []; + + foreach ($stores as $store) { + if ($store['featured'] === "TRUE") { + $featured_mall_stores[] = $store; + } + } + return $featured_mall_stores; + } + + + /** + * Check if products are featured on Store Home page + * @param array $products containing information of all products + * @return array array containing information + * of all products featured on Store Home + */ + function check_featured_store_products(array $products): array { + $featured_store_products = []; + + foreach ($products as $product) { + if ($product['featured_in_store'] === "TRUE") { + $featured_store_products[] = $product; + } + } + return $featured_store_products; + } + + + /** + * Compare the dates created of two items (stores, products, etc.). To be used as the handler function for usort() to sort a given group of database items from newest to oldest. + * @param array $item1 first item for comparison + * @param array $item2 second item for comparison + * @return int + */ + function compare_by_time(array $item1, array $item2): int { + return -(strtotime($item1["created_time"]) - strtotime($item2["created_time"])); + } + + + /** + * Get all data of a specific item if the id of that item is retrieved through the link + * @param array $items containing data of all items (stores, products, etc.) + * @return false|mixed + * array containing data of the selected item, + * false otherwise. + */ + function get_item_data(array $items) { + if (isset($_GET["id"])) { + foreach ($items as $item) { + if ($_GET["id"] === $item["id"]) { + return $item; + } + } + } + return false; + } + + + /** + * Get all information of a specific item if the id of that item is + * not retrieved through the link. + * This function is used when we have an item's id from another item's database + * (e.g: store_id from products.csv), to get other data related to that item + * (e.g: need to get store name) + * @param string $item_id + * @param array $items containing information of all items (stores, products, etc.) + * @return false|mixed + * array containing information of the selected item, + * false otherwise. + */ + function get_item_info(string $item_id, array $items) { + foreach ($items as $item) { + if ($item["id"] === $item_id) { + return $item; + } + } + return false; + } + + + /** + * Get products from a specific store + * @param array $products containing products from database + * @param array $store containing data of a specific store + * @return array + * array containing data of products of the selected store + */ + function get_specific_store_products(array $products, array $store): array { + $specific_store_products = []; + + foreach ($products as $product) { + if ($product["store_id"] === $store["id"]) { + $specific_store_products[] = $product; + } + } + return $specific_store_products; + } + + + /** + * Get the store name that correspond to the store id on products.csv + * @param int $id + * @param array $stores + * @return string + * array containing name of the store if store_id on products.csv + * matches id on stores.csv, + * false otherwise. + */ + function get_store_name(int $id, array $stores): string { + foreach ($stores as $store) { + if ((int) $store["id"] === $id) { + return $store["name"]; + } + } + return false; + } + \ No newline at end of file diff --git a/private/initialize.php b/private/initialize.php index 4c270c44..40de8576 100644 --- a/private/initialize.php +++ b/private/initialize.php @@ -24,8 +24,8 @@ // __FILE__ returns the current path to this file // dirname() returns the path to the parent directory define("PRIVATE_PATH", dirname(__FILE__)); - define("PROJECT_PATH", dirname(PRIVATE_PATH)); - define("PUBLIC_PATH", PROJECT_PATH . "/public"); + define("APPLICATION_PATH", dirname(PRIVATE_PATH)); + define("PUBLIC_PATH", APPLICATION_PATH . "/public"); define("SHARED_PATH", PRIVATE_PATH . "/shared"); // Assign the root URL to a PHP constant @@ -33,6 +33,14 @@ // * Use same document root as webserver define("WWW_ROOT", ""); + + // Yabe Custom Class auto-loading + function yabe_autoload($class) { + if (preg_match("/\A\w+\Z/", $class)) { + require_once(PRIVATE_PATH . "/classes/{$class}.php"); + } + } + spl_autoload_register("yabe_autoload"); require_once("functions.php"); $errors = []; diff --git a/private/logsman.php b/private/logsman.php index 67395d20..bba07c3e 100644 --- a/private/logsman.php +++ b/private/logsman.php @@ -18,8 +18,12 @@ */ function new_logs_entry(string $logs_filepath, string $content): void { $file = fopen($logs_filepath, "a"); + flock($file, LOCK_EX); + $date = date("c"); fwrite($file, "[" . $date . "sess_" . session_id() . "] " . $content . "\n"); + + flock($file, LOCK_UN); fclose($file); } @@ -33,11 +37,13 @@ function clear_logs_entry(string $logs_filepath, int $index): void { switch ($index) { case ALL_ENTRIES: $file = fopen($logs_filepath, "w"); + flock($file, LOCK_EX); break; case LATEST_ENTRY: $lines = file($logs_filepath); unset($lines[count($lines) - 1]); $file = fopen($logs_filepath, "w"); + flock($file, LOCK_EX); foreach ($lines as $line) { fwrite($file, $line); } @@ -47,10 +53,12 @@ function clear_logs_entry(string $logs_filepath, int $index): void { unset($lines[$index - 1]); $lines = array_values($lines); $file = fopen($logs_filepath, "w"); + flock($file, LOCK_EX); foreach ($lines as $line) { fwrite($file, $line); } break; } + flock($file, LOCK_UN); fclose($file); }; diff --git a/private/shared/about-us-bio.php b/private/shared/about-us-bio.php new file mode 100644 index 00000000..cd5b4cfd --- /dev/null +++ b/private/shared/about-us-bio.php @@ -0,0 +1 @@ +Kuri Team was founded during semester 2020C at RMIT University Vietnam, Saigon South Campus. As a young organization, our goal is to support each other in our projects, academic success, and personal development. Our last study project is a game prototype, proposed by us in our assignment paper for COSC2083 - Introduction to IT, to be implemented into RMIT wi-fi network. More detail on that project and proposal to RMIT can be found here. The playable prototype can be accessed here.
diff --git a/private/shared/bottom.php b/private/shared/bottom.php index 03567108..5d7a8082 100644 --- a/private/shared/bottom.php +++ b/private/shared/bottom.php @@ -36,10 +36,10 @@