*/
class action_plugin_sql2wiki_sqlite extends \dokuwiki\Extension\ActionPlugin
{
const PLUGIN_SQL2WIKI_EDIT_SUMMARY = 'plugin sql2wiki';
const PLUGIN_SQL2WIKI_EDIT_SUMMARY_INFINITE_LOOP = 'plugin sql2wiki: syntax commented out: query results may depend on page revision';
/** @var array databases that has changed */
protected $queue = [];
protected function queue_put($db, $query_name='') {
if (!isset($this->queue[$db])) {
$this->queue[$db] = [];
}
// if query_name is not specified, update all database queries and don't consider future query_name
if (empty($query_name)) {
$this->queue[$db] = true;
}
// add new query_name if we still did not request all database queries update
if (is_array($this->queue[$db])) {
$this->queue[$db][$query_name] = true;
}
}
protected function queue_filtered($sql2wiki_data) {
// ignore the queries that have not been changed in this request
$queue = $this->queue;
return array_filter($sql2wiki_data, function ($query) use ($queue) {
$db = $query['db'];
$query_name = $query['query_name'];
return isset($queue[$db]) && ($queue[$db] === true || isset($queue[$db][$query_name]));
});
}
protected function queue_clean() {
$this->queue = [];
}
/** @inheritDoc */
public function register(Doku_Event_Handler $controller)
{
$controller->register_hook('PLUGIN_SQLITE_QUERY_EXECUTE', 'AFTER', $this, 'handle_plugin_sqlite_query_execute');
$controller->register_hook('PLUGIN_SQLITE_QUERY_SAVE', 'AFTER', $this, 'handle_plugin_sqlite_query_change');
$controller->register_hook('PLUGIN_SQLITE_QUERY_DELETE', 'AFTER', $this, 'handle_plugin_sqlite_query_change');
// update pages after all saving and metadata updating has happened
$controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'update_pages_content');
// if we have updated the page we are currently viewing, redirect to updated version
// this is why we are using ACTION_HEADERS_SEND event here
$controller->register_hook('ACTION_HEADERS_SEND', 'AFTER', $this, 'check_current_page_for_updates');
// support for struct inline edits
$controller->register_hook('AJAX_CALL_UNKNOWN', 'AFTER', $this, 'update_pages_content');
}
public function check_current_page_for_updates(Doku_Event $event, $param) {
global $ID, $ACT;
if ($ACT != 'show') return; // update the page content only when we viewing it
$sql2wiki_data = p_get_metadata($ID, 'plugin_sql2wiki');
if (!$sql2wiki_data) return;
// check if we have some resutls updates
$something_changed = $this->update_query_results($ID, $sql2wiki_data, 1);
if ($something_changed) {
// update other pages in queue and redirect
$this->update_pages_content($event, [$ID]);
$go = wl($ID, '', true, '&');
send_redirect($go);
}
}
public function update_pages_content(Doku_Event $event, $param) {
global $ID;
$filter_ids = []; // don't update specific pages
if (!is_null($param)) $filter_ids = $param;
$indexer = idx_get_indexer();
$dbs = array_keys($this->queue);
$dbs_pages = $indexer->lookupKey('sql2wiki_db', $dbs);
$pages = array_unique(array_merge(...array_values($dbs_pages)));
foreach ($pages as $page) {
if (in_array($page, $filter_ids)) continue;
$sql2wiki_data = p_get_metadata($page, 'plugin_sql2wiki');
if (!$sql2wiki_data) continue;
$sql2wiki_filtered = $this->queue_filtered($sql2wiki_data);
// the $ID is usually updated in check_current_page_for_updates
// but when $ACT != 'show' the current page might be not updated yet
$sleep = $page == $ID ? 1 : 0;
$this->update_query_results($page, $sql2wiki_filtered, $sleep);
}
$this->queue_clean();
}
public function handle_plugin_sqlite_query_execute(Doku_Event $event, $param)
{
if ($event->data['stmt']->rowCount() == 0) return; // ignore select queries
$db = $event->data['sqlitedb']->getDbName();
$this->queue_put($db);
}
public function handle_plugin_sqlite_query_change(Doku_Event $event, $param)
{
$upstream = $event->data['upstream'];
$query_name = $event->data['name'];
$this->queue_put($upstream, $query_name);
}
protected function get_page_content_with_wrapped_tags($page_content, $sql2wiki_data) {
$offset = 0;
foreach ($sql2wiki_data as $sql2wiki_query) {
$pos = $sql2wiki_query['pos'] - 1;
$match = $sql2wiki_query['match'];
$wrapped_tag = '' . $match . '
';
$updated_content = substr_replace($page_content, $wrapped_tag, $pos + $offset, strlen($match));
$offset = strlen($updated_content) - strlen($page_content);
$page_content = $updated_content;
}
return $page_content;
}
protected function get_updated_page_content($page_content, $page, $sql2wiki_data) {
$offset = 0;
$logger_details = [
'page' => $page,
'before_page_content' => $page_content,
'sql2wiki_data' => $sql2wiki_data,
'results' => []
];
foreach ($sql2wiki_data as $sql2wiki_query) {
$sqliteDb = new SQLiteDB($sql2wiki_query['db'], '');
$querySaver = new QuerySaver($sqliteDb->getDBName());
$query = $querySaver->getQuery($sql2wiki_query['query_name']);
if ($query) {
$params = str_replace([
'$ID$',
'$NS$',
'$PAGE$'
], [
$page,
getNS($page),
noNS($page)
], $sql2wiki_query['args']);
$result = $sqliteDb->queryAll($query, $params);
if (isset($result[0])) { // generate header if any row exists
array_unshift($result, array_keys($result[0]));
}
$query_result_csv = "\n" . Csv::arr2csv($result); // "\n" to wrap the tag
$logger_details['results'][] = $result;
} else { //unknown query - clear the results
$query_result_csv = "";
$logger_details['results'][] = null;
}
$start = $sql2wiki_query['start'];
$end = $sql2wiki_query['end'];
$length = $end - $start;
$updated_content = substr_replace($page_content, $query_result_csv, $start + $offset, $length);
$offset = strlen($updated_content) - strlen($page_content);
$page_content = $updated_content;
}
$before_sql2wiki_opening_tags = substr_count($logger_details['before_page_content'], '');
$after_sql2wiki_opening_tags = substr_count($page_content, '');
if ($before_sql2wiki_opening_tags != $after_sql2wiki_opening_tags ||
$before_sql2wiki_closing_tags != $after_sql2wiki_closing_tags ||
$after_sql2wiki_opening_tags != $after_sql2wiki_closing_tags
) {
$logger_details['after_page_content'] = $page_content;
\dokuwiki\Logger::error('sql2wiki', $logger_details, __FILE__, __LINE__);
}
return $page_content;
}
protected function update_query_results($page, $sql2wiki_data, $sleep=0) {
$page_content = file_get_contents(wikiFN($page));
$updated_content = $this->get_updated_page_content($page_content, $page, $sql2wiki_data);
if ($page_content != $updated_content) {
sleep($sleep); // wait if we are processing currently viewed page
saveWikiText($page, $updated_content, self::PLUGIN_SQL2WIKI_EDIT_SUMMARY);
$next_update = $this->get_updated_page_content($page_content, $page, $sql2wiki_data);
// this may mean that the query results depend on page revisions which leads to infinite loop
if ($updated_content != $next_update) {
// comment out tags to prevent infinite loop
$wrapped_content = $this->get_page_content_with_wrapped_tags($page_content, $sql2wiki_data);
sleep(1); // wait for all types of updates since we have just updated the page
saveWikiText($page, $wrapped_content, self::PLUGIN_SQL2WIKI_EDIT_SUMMARY_INFINITE_LOOP);
}
return true;
}
return false;
}
}