diff --git a/admin/NoidLib/custom/MysqlArkDB.php b/admin/NoidLib/custom/MysqlArkDB.php index 916113f..fdcbd54 100644 --- a/admin/NoidLib/custom/MysqlArkDB.php +++ b/admin/NoidLib/custom/MysqlArkDB.php @@ -149,6 +149,23 @@ class MysqlArkDB implements DatabaseInterface return FALSE; } + /** + * Query + * @param $query + * @return false|string + */ + public function query($query) { + if (!($this->handle instanceof mysqli)) { + return FALSE; + } + $query = str_replace('', $this->db_name, $query); + + if ($res = $this->handle->query($query)) { + return $res->fetch_all(MYSQLI_ASSOC); + } + return FALSE; + } + /** * @param string $key * @param string $value diff --git a/admin/admin.php b/admin/admin.php index 714240e..a682e37 100755 --- a/admin/admin.php +++ b/admin/admin.php @@ -132,6 +132,124 @@ $subheader .= "

"; + + // + // Pipelining function for DataTables. To be used to the `ajax` option of DataTables + // + $.fn.dataTable.pipeline = function ( opts ) { + // Configuration options + var conf = $.extend( { + pages: 5, // number of pages to cache + url: '', // script url + data: null, // function or object with parameters to send to the server + // matching how `ajax.data` works in DataTables + method: 'GET' // Ajax HTTP method + }, opts ); + + // Private variables for storing the cache + var cacheLower = -1; + var cacheUpper = null; + var cacheLastRequest = null; + var cacheLastJson = null; + + return function ( request, drawCallback, settings ) { + var ajax = false; + var requestStart = request.start; + var drawStart = request.start; + var requestLength = request.length; + var requestEnd = requestStart + requestLength; + + if ( settings.clearCache ) { + // API requested that the cache be cleared + ajax = true; + settings.clearCache = false; + } + else if ( cacheLower < 0 || requestStart < cacheLower || requestEnd > cacheUpper ) { + // outside cached data - need to make a request + ajax = true; + } + else if ( JSON.stringify( request.order ) !== JSON.stringify( cacheLastRequest.order ) || + JSON.stringify( request.columns ) !== JSON.stringify( cacheLastRequest.columns ) || + JSON.stringify( request.search ) !== JSON.stringify( cacheLastRequest.search ) + ) { + // properties changed (ordering, columns, searching) + ajax = true; + } + + // Store the request for checking next time around + cacheLastRequest = $.extend( true, {}, request ); + + if ( ajax ) { + // Need data from the server + if ( requestStart < cacheLower ) { + requestStart = requestStart - (requestLength*(conf.pages-1)); + + if ( requestStart < 0 ) { + requestStart = 0; + } + } + + cacheLower = requestStart; + cacheUpper = requestStart + (requestLength * conf.pages); + + request.start = requestStart; + request.length = requestLength*conf.pages; + + // Provide the same `data` options as DataTables. + if ( typeof conf.data === 'function' ) { + // As a function it is executed with the data object as an arg + // for manipulation. If an object is returned, it is used as the + // data object to submit + var d = conf.data( request ); + if ( d ) { + $.extend( request, d ); + } + } + else if ( $.isPlainObject( conf.data ) ) { + // As an object, the data given extends the default + $.extend( request, conf.data ); + } + + return $.ajax( { + "type": conf.method, + "url": conf.url, + "data": request, + "dataType": "json", + "cache": false, + "success": function ( json ) { + cacheLastJson = $.extend(true, {}, json); + + if ( cacheLower != drawStart ) { + json.data.splice( 0, drawStart-cacheLower ); + } + if ( requestLength >= -1 ) { + json.data.splice( requestLength, json.data.length ); + } + + drawCallback( json ); + } + } ); + } + else { + json = $.extend( true, {}, cacheLastJson ); + json.draw = request.draw; // Update the echo for each response + json.data.splice( 0, requestStart-cacheLower ); + json.data.splice( requestLength, json.data.length ); + + drawCallback(json); + } + } + }; + + // Register an API method that will empty the pipelined data, forcing an Ajax + // fetch on the next draw (i.e. `table.clearPipeline().draw()`) + $.fn.dataTable.Api.register( 'clearPipeline()', function () { + return this.iterator( 'table', function ( settings ) { + settings.clearCache = true; + } ); + } ); + + $(document).ready(function () { // To style only selects with the select-ark-id class @@ -174,10 +292,12 @@ $subheader .= "

"; let mintedTable = jQuery('#minted_table').DataTable({ dom: 'lBfrtip', - "ajax": { + "ajax": $.fn.dataTable.pipeline( { "url": "rest.php?db=", - "dataSrc": "" - }, + "pages": 5 // number of pages to cache + }), + processing: true, + serverSide: true, columns: [ {data: 'select'}, {data: '_key'}, @@ -264,10 +384,12 @@ $subheader .= "

"; // Make a Ajax call to Rest api and render data to table let boundTable = jQuery('#bound_table').DataTable({ dom: 'lBfrtip', - "ajax": { + "ajax": $.fn.dataTable.pipeline( { "url": "rest.php?db=", - "dataSrc": "" - }, + "pages": 5 // number of pages to cache + }), + processing: true, + serverSide: true, "initComplete": function (settings, json) { $(".collapse").collapse({ toggle: false @@ -329,7 +451,7 @@ $subheader .= "

"; { "targets": 5, - "data": "metadata", + "data": "metadata", "render": function (data, type, row) { if (data !== undefined && data.indexOf("|") != -1) { var now = row.id; diff --git a/admin/rest.php b/admin/rest.php index 24b03d8..de31081 100644 --- a/admin/rest.php +++ b/admin/rest.php @@ -278,44 +278,131 @@ function selectBound() if (!Database::exist($_GET['db'])) { die(json_encode('Database not found')); } - $columns = json_decode(select()); - //array_push($columns, (object)[]); + + $noid = Database::dbopen($_GET["db"], getcwd() . "/db/", DatabaseInterface::DB_WRITE); + $firstpart = Database::$engine->get(Globals::_RR . "/firstpart"); + + $columnIdx = $_GET['order'][0]['column']; + $sortCol = $_GET['columns'][$columnIdx]; + $sortDir = $_GET['order'][0]['dir'] === 'asc' ? 'ASC' : 'DESC'; + $offset = $_GET['start'] ?? 0; + $limit = $_GET['length'] ?? 50; + $search = $_GET['search']['value']; + + if ($sortCol['data'] === 'redirect') { + $sql = "SELECT arks.* + FROM `` + AS arks + JOIN ( + SELECT bound.id, + COALESCE(redirected._value, 0) + AS _value + FROM ( + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '(\\\\s:\\/c|\\\\sREDIRECT|\\\\sPID|\\\\sLOCAL_ID|\\\\sCOLLECTION)$' + ) AS bound + LEFT JOIN ( + SELECT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id, _value + FROM + WHERE _key LIKE '$firstpart%' AND _key REGEXP '\\\\sREDIRECT$' + ) AS redirected ON bound.id = redirected.id + ORDER BY _value $sortDir + LIMIT $limit + OFFSET $offset + ) AS subquery + ON arks._key LIKE CONCAT(subquery.id, '%') + AND arks._key NOT LIKE '%:\\/c' + ORDER BY arks._key ASC; + "; + $sql_count = "SELECT COUNT(*) as num_filtered + FROM ( + SELECT bound.id, + COALESCE(redirected._value, 0) + AS _value + FROM ( + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '(\\\\s:\\/c|\\\\sREDIRECT|\\\\sPID|\\\\sLOCAL_ID|\\\\sCOLLECTION)$' + ) AS bound + LEFT JOIN ( + SELECT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id, _value + FROM + WHERE _key LIKE '$firstpart%' AND _key REGEXP '\\\\sREDIRECT$' + ) AS redirected ON bound.id = redirected.id + ) AS filtered_ids; + "; + } + else { // Sort on Ark IDs + $sql = "SELECT arks.* + FROM `` + AS arks + JOIN ( + SELECT * FROM ( + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM `` + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '(\\\\s:\\/c|\\\\sREDIRECT|\\\\sPID|\\\\sLOCAL_ID|\\\\sCOLLECTION)$' + INTERSECT + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM `` + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '\\\\s:\\/c' AND (_key LIKE '%$search%' OR _value LIKE '%$search%') + ) AS target + ORDER BY id $sortDir + LIMIT $limit + OFFSET $offset + ) AS subquery + ON arks._key LIKE CONCAT(subquery.id, '%') + AND arks._key NOT LIKE '%:\\/c' + ORDER BY arks._key $sortDir; + "; + $sql_count = "SELECT COUNT(*) as num_filtered + FROM ( + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM `` + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '(\\\\s:\\/c|\\\\sREDIRECT|\\\\sPID|\\\\sLOCAL_ID|\\\\sCOLLECTION)$' + INTERSECT + SELECT DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id + FROM `` + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '\\\\s:\\/c' AND (_key LIKE '%$search%' OR _value LIKE '%$search%') + ) AS filtered_ids; + "; + } + + $rows = Database::$engine->query($sql); + $num_filtered = Database::$engine->query($sql_count)[0]['num_filtered'] ?? 0; + Database::dbclose($noid); + $currentID = null; $result = array(); $r = []; - $countColumns = count($columns); - $index = 1; - $qualifiers = []; - foreach ($columns as $column) { - $column = (array)$column; + foreach ($rows as $row) { + $row = (array)$row; - if (isset($column['_key'])) { - $key_data = preg_split('/\s+/', $column['_key']); - //print_r($column); + if (isset($row['_key'])) { + $key_data = preg_split('/\s+/', $row['_key']); if (!isset($currentID) || ($currentID !== $key_data[0])) { $currentID = $key_data[0]; if (is_array($r) && count($r) > 0) { - if(!array_key_exists('PID', $r)) { - $r['PID'] = " "; - } - if(!array_key_exists('LOCAL_ID', $r)) { - $r['LOCAL_ID'] = " "; - } array_push($result, $r); } - $r = []; + $r = [ + 'select' => ' ', + 'id' => $currentID, + 'PID' => ' ', + 'LOCAL_ID' => ' ', + 'redirect' => 0, + ]; } - $r['select'] = " "; - $r['id'] = $currentID; + if ($key_data[1] == 'PID') - $r['PID'] = (!empty($column['_value'])) ? $column['_value'] : ' '; + $r['PID'] = (!empty($row['_value'])) ? $row['_value'] : ' '; if ($key_data[1] == "LOCAL_ID") - $r['LOCAL_ID'] = (!empty($column['_value'])) ? $column['_value'] : ' '; + $r['LOCAL_ID'] = (!empty($row['_value'])) ? $row['_value'] : ' '; if ($key_data[1] == "REDIRECT") - $r['redirect'] = (!empty($column['_value'])) ? $column['_value'] : ' '; - $r['metadata'] = (!empty($r['metadata']) ? $r['metadata'] . "|" : "") . $key_data[1] .':' .$column['_value']; + $r['redirect'] = (!empty($row['_value'])) ? $row['_value'] : ' '; + $r['metadata'] = (!empty($r['metadata']) ? $r['metadata'] . "|" : "") . $key_data[1] .':' .$row['_value']; // check if server have https://, if not, go with http:// if (empty($_SERVER['HTTPS'])) { @@ -334,25 +421,31 @@ function selectBound() $r['ark_url'] = (array_key_exists("ark_url", $r) && is_array($r['ark_url']) && count($r['ark_url']) > 1) ? $r['ark_url'] : [$ark_url]; // if there is qualifier bound to an Ark ID, establish the link the link - if ($key_data[1] !== "URL" && filter_var($column['_value'], FILTER_VALIDATE_URL)) { + if ($key_data[1] !== "URL" && filter_var($row['_value'], FILTER_VALIDATE_URL)) { array_push($r['ark_url'], strtolower($ark_url. "/". $key_data[1])); } - } + } + + if (!empty($r)) { + array_push($result, $r); + } - // if the loop reach the last pair of elements (for incompleted bind) - if ($index === $countColumns) { - if(!array_key_exists('PID', $r)) { - $r['PID'] = " "; - } - /*if(!array_key_exists('LOCAL_ID', $r)) { - $r['LOCAL_ID'] = " "; - }*/ - array_push($result, $r); - } - $index++; + if ($sortCol['data'] === 'redirect') { + $redirect = array_column($result, "redirect"); + array_multisort($redirect, $sortDir === 'ASC' ? SORT_ASC : SORT_DESC, $result); + } + else { + $id = array_column($result, "id"); + array_multisort($id, $sortDir === 'ASC' ? SORT_ASC : SORT_DESC, $result); } - return json_encode($result); + + return json_encode(array( + "data" => $result, + "draw" => isset ( $_GET['draw'] ) ? intval( $_GET['draw'] ) : 0, + "recordsTotal" => countBoundedArks(), + "recordsFiltered" => $num_filtered, + )); } /** @@ -423,27 +516,79 @@ function getURL($arkID) { */ function getMinted() { - GlobalsArk::$db_type = 'ark_mysql'; - if (!Database::exist($_GET['db'])) { - die(json_encode('Database not found')); - } - $noid = Database::dbopen($_GET["db"], getcwd() . "/db/", DatabaseInterface::DB_WRITE); - $firstpart = Database::$engine->get(Globals::_RR . "/firstpart"); - $result = Database::$engine->select("_key REGEXP '^$firstpart' and _key REGEXP ':/c$'"); - //return json_encode($result); - $json = array(); - foreach ($result as $row) { - $urow = array(); - $urow['select']= ' '; - $urow['_key'] = trim(str_replace(":/c", "", $row['_key'])); + GlobalsArk::$db_type = 'ark_mysql'; + if (!Database::exist($_GET['db'])) { + die(json_encode('Database not found')); + } + $noid = Database::dbopen($_GET["db"], getcwd() . "/db/", DatabaseInterface::DB_WRITE); + $firstpart = Database::$engine->get(Globals::_RR . "/firstpart"); - $metadata = explode('|', $row['_value']); - //$urow['_value'] = date("F j, Y, g:i a", $metadata[2]); - $urow['_value'] = date("F j, Y", $metadata[2]); - array_push($json, (object)$urow); - } - Database::dbclose($noid); - return json_encode($json); + if (isset($_GET['order'][0]['dir'])) { + $sortDir = $_GET['order'][0]['dir'] === 'asc' ? 'ASC' : 'DESC'; + } else { + $sortDir = 'ASC'; + } + $offset = $_GET['start'] ?? 0; + $limit = $_GET['length'] ?? 50; + + $sql = "SELECT REGEXP_SUBSTR(_key, '^([^\\\\s]+)') AS id, _value + FROM `` + WHERE _key LIKE '$firstpart%' AND _key REGEXP '\\\\s:\/c$' + ORDER BY _key $sortDir + LIMIT $limit + OFFSET $offset; + "; + + $result = Database::$engine->query($sql); + Database::dbclose($noid); + + $json = array(); + foreach ($result as $row) { + $urow = array(); + $urow['select'] = ' '; + $urow['_key'] = $row['id']; + + $metadata = explode('|', $row['_value']); + //$urow['_value'] = date("F j, Y, g:i a", $metadata[2]); + $urow['_value'] = date("F j, Y", $metadata[2]); + array_push($json, (object)$urow); + } + + $totalArks = countTotalArks(); + return json_encode(array( + "data" => $json, + "draw" => isset ( $_GET['draw'] ) ? intval( $_GET['draw'] ) : 0, + "recordsTotal" => $totalArks, + "recordsFiltered" => $totalArks, + )); +} + +function countTotalArks() { + GlobalsArk::$db_type = 'ark_mysql'; + if (!Database::exist($_GET['db'])) { + die(json_encode('Database not found')); + } + $noid = Database::dbopen($_GET["db"], getcwd() . "/db/", DatabaseInterface::DB_WRITE); + $firstpart = Database::$engine->get(Globals::_RR . "/firstpart"); + $result = Database::$engine->query("SELECT COUNT(DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)')) AS total FROM `` WHERE _key LIKE '$firstpart%' and _key REGEXP '\\\\s:\\/c$';"); + Database::dbclose($noid); + return $result[0]['total'] ?? 0; +} + +function countBoundedArks() { + GlobalsArk::$db_type = 'ark_mysql'; + if (!Database::exist($_GET['db'])) { + die(json_encode('Database not found')); + } + $noid = Database::dbopen($_GET["db"], getcwd() . "/db/", DatabaseInterface::DB_WRITE); + $firstpart = Database::$engine->get(Globals::_RR . "/firstpart"); + $result = Database::$engine->query("SELECT COUNT( + DISTINCT REGEXP_SUBSTR(_key, '^([^\\\\s]+)')) AS total + FROM `` + WHERE _key LIKE '$firstpart%' AND _key NOT REGEXP '(\\\\s:\\/c|\\\\sREDIRECT|\\\\sPID|\\\\sLOCAL_ID|\\\\sCOLLECTION)$'; + "); + Database::dbclose($noid); + return $result[0]['total'] ?? 0; } /**