<?php
/**
 * parser.php
 *
 * Parse data from registered hosts to feed the search database.
 * If in a standalone helpers implementation, it must be run on a regular basis
 * (with a cron job) for the search to work.
 *
 * Requires OpenSimulator Search module
 *   [OpenSimSearch](https://github.com/kcozens/OpenSimSearch)
 * Events are fetched from 2do HYPEvents or any other HYPEvents implementation
 *   [2do HYPEvents](https://2do.directory)
 *
 * @package     magicoli/opensim-helpers
 * @author      Gudule Lapointe <gudule@speculoos.world>
 * @link            https://github.com/magicoli/opensim-helpers
 * @license     AGPLv3
 *
 * Includes portions of code from
 *   [OpenSimSearch](https://github.com/kcozens/OpenSimSearch)
 **/

require_once 'includes/config.php';
require_once 'includes/search.php';
dontWait();

$now = time();

function hostCheck( $hostname, $port ) {
	global $SearchDB, $now;

	$failcounter = 0;
	$interval    = 600; // Wait at least 10 minutes before scanning the same host

	$xml = file_get_contents( "http://$hostname:$port/?method=collector" );
	if ( empty( $xml ) ) {
		error_log( "$hostname:$port unreachable" );
		$fails       = $SearchDB->prepareAndExecute(
			'SELECT failcounter FROM hostsregister
      WHERE host = :host AND port = :port',
			array(
				'host' => $hostname,
				'port' => $port,
			)
		);
		$failcounter = $fails->fetch()[0] + 1;
		$interval    = $interval * pow( 2, $failcounter ); // extend scanning interval for inactive hosts
		if ( $failcounter > 10 ) {
			ossearch_hostUnregister( $hostname, $port );
			return;
		}
	}
	$nextcheck = time() + $interval;

	// Update nextcheck time. The next check interval is multiplied by the number
	// of fails to minimize useless requests
	$query = $SearchDB->prepareAndExecute(
		'UPDATE hostsregister
    SET failcounter = :failcounter, nextcheck = :nextcheck, checked = 1
    WHERE host = :host AND port = :port',
		array(
			'failcounter' => $failcounter,
			'nextcheck'   => 0,
			'host'        => $hostname,
			'port'        => $port,
		)
	);

	if ( ! empty( $xml ) ) {
		hostScan( $hostname, $port, $xml );
	}
}

function hostScan( $hostname, $port, $xmlcontent ) {
	global $SearchDB, $now;
	//
	//
	// Search engine sim scanner
	//

	//
	// Load XML doc from URL
	//
	$objDOM                   = new DOMDocument();
	$objDOM->resolveExternals = false;

	// Don't try and scan if XML is invalid or we got an HTML 404 error.
	if ( $objDOM->loadXML( $xmlcontent ) == false ) {
		return;
	}

	$gatekeeperURL = ( ! empty( $objDOM->getElementsByTagName( 'gatekeeperURL' ) ) ) ? $objDOM->getElementsByTagName( 'gatekeeperURL' )->item( 0 )->nodeValue : '';

	//
	// Get the region data to update
	//
	$regiondata = $objDOM->getElementsByTagName( 'regiondata' );

	// If returned length is 0, collector method may have returned an error
	if ( $regiondata->length == 0 ) {
		return;
	}

	$regiondata = $regiondata->item( 0 );

	//
	// Update nextcheck so this host entry won't be checked again until after
	// the DataSnapshot module has generated a new set of data to be scanned.
	//
	$expire = $regiondata->getElementsByTagName( 'expire' )->item( 0 )->nodeValue;
	$next   = $now + $expire;

	$query = $SearchDB->prepare( 'UPDATE hostsregister SET nextcheck = ?, gatekeeperURL = ? WHERE host = ? AND port = ?' );
	$query->execute( array( $next, $gatekeeperURL, $hostname, $port ) );

	//
	// Get the region data to be saved in the database
	//
	$regionlist = $regiondata->getElementsByTagName( 'region' );

	foreach ( $regionlist as $region ) {
		$mature = $region->getAttributeNode( 'category' )->nodeValue;

		//
		// Start reading the Region info
		//
		$info         = $region->getElementsByTagName( 'info' )->item( 0 );
		$regionUUID   = $info->getElementsByTagName( 'uuid' )->item( 0 )->nodeValue;
		$regionname   = $info->getElementsByTagName( 'name' )->item( 0 )->nodeValue;
		$regionhandle = $info->getElementsByTagName( 'handle' )->item( 0 )->nodeValue;
		$url          = $info->getElementsByTagName( 'url' )->item( 0 )->nodeValue;

		/*
		* First, check if we already have a region that is the same
		*/

		$SearchDB->prepareAndExecute( 'DELETE FROM ' . SEARCH_REGION_TABLE . ' WHERE regionUUID = ?', array( $regionUUID ) );
		$SearchDB->prepareAndExecute( 'DELETE FROM parcels WHERE regionUUID = ?', array( $regionUUID ) );
		$SearchDB->prepareAndExecute( 'DELETE FROM allparcels WHERE regionUUID = ?', array( $regionUUID ) );
		$SearchDB->prepareAndExecute( 'DELETE FROM parcelsales WHERE regionUUID = ?', array( $regionUUID ) );
		$SearchDB->prepareAndExecute( 'DELETE FROM objects WHERE regionuuid = ?', array( $regionUUID ) );

		$data         = $region->getElementsByTagName( 'data' )->item( 0 );
		$estate       = $data->getElementsByTagName( 'estate' )->item( 0 );
		$parentestate = $estate->getElementsByTagName( 'id' )->item( 0 )->nodeValue;
		$username     = $estate->getElementsByTagName( 'name' )->item( 0 )->nodeValue;
		$useruuid     = $estate->getElementsByTagName( 'uuid' )->item( 0 )->nodeValue;
		if ( empty( $useruuid ) ) {
			$useruuid = NULL_KEY;
		}

		/*
		* Second, add the new info to the database again
		*/

		$SearchDB->insert(
			SEARCH_REGION_TABLE,
			array(
				'regionname'    => $regionname,
				'regionUUID'    => $regionUUID,
				'regionhandle'  => $regionhandle,
				'url'           => $url,
				'owner'         => $username,
				'ownerUUID'     => $useruuid,
				'gatekeeperURL' => $gatekeeperURL,
			)
		);

		/*
		* Read parcel info
		*/

		$parcels = $data->getElementsByTagName( 'parcel' );
		foreach ( $parcels as $parcel ) {
			$parcelname        = $parcel->getElementsByTagName( 'name' )->item( 0 )->nodeValue;
			$parcelUUID        = $parcel->getElementsByTagName( 'uuid' )->item( 0 )->nodeValue;
			$infoUUID          = $parcel->getElementsByTagName( 'infouuid' )->item( 0 )->nodeValue;
			$landingpoint      = $parcel->getElementsByTagName( 'location' )->item( 0 )->nodeValue;
			$parceldescription = $parcel->getElementsByTagName( 'description' )->item( 0 )->nodeValue;
			$parcelarea        = $parcel->getElementsByTagName( 'area' )->item( 0 )->nodeValue;
			$searchcategory    = $parcel->getAttributeNode( 'category' )->nodeValue;
			$saleprice         = $parcel->getAttributeNode( 'salesprice' )->nodeValue;
			$dwell             = $parcel->getElementsByTagName( 'dwell' )->item( 0 )->nodeValue;

			// The image tag will only exist if the parcel has a snapshot image
			$has_picture = 0;
			$image_node  = $parcel->getElementsByTagName( 'image' );
			if ( $image_node->length > 0 ) {
				$image = $image_node->item( 0 )->nodeValue;
				if ( $image != NULL_KEY ) {
					$has_picture = 1;
				}
			}
			if ( empty( $image ) ) {
				$image = NULL_KEY;
			}

			$owner     = $parcel->getElementsByTagName( 'owner' )->item( 0 );
			$ownerUUID = $owner->getElementsByTagName( 'uuid' )->item( 0 )->nodeValue;

			// Adding support for groups
			$group = $parcel->getElementsByTagName( 'group' )->item( 0 );
			if ( $group ) {
				$groupUUID = $group->getElementsByTagName( 'groupuuid' )->item( 0 )->nodeValue;
			}
			if ( empty( $groupUUID ) ) {
				$groupUUID = NULL_KEY;  // empty group, should it happen?
			}

			//
			// Check bits on Public, Build, Script
			//
			$parcelforsale   = $parcel->getAttributeNode( 'forsale' )->nodeValue;
			$parceldirectory = $parcel->getAttributeNode( 'showinsearch' )->nodeValue;
			$parcelbuild     = $parcel->getAttributeNode( 'build' )->nodeValue;
			$parcelscript    = $parcel->getAttributeNode( 'scripts' )->nodeValue;
			$parcelpublic    = $parcel->getAttributeNode( 'public' )->nodeValue;

			// Prepare for the insert of data in to the popularplaces table. This gets
			// rid of any obsolete data for parcels no longer set to show in search.
			$query = $SearchDB->prepare( 'DELETE FROM popularplaces WHERE parcelUUID = ?' );
			$query->execute( array( $parcelUUID ) );

			/*
			 * Save
			 *
			 * Sometimes, the parcel is inserted more than once, which causes a fatal
			 * issue. Delete it first (quick workaround, should be investigated).
			 */

			$query = $SearchDB->prepare( 'DELETE FROM allparcels WHERE parcelUUID = :parcelUUID' );
			$query->execute( array( 'parcelUUID' => $parcelUUID ) );

			$SearchDB->insert(
				'allparcels',
				array(
					'regionUUID'    => $regionUUID,
					'parcelname'    => $parcelname,
					'ownerUUID'     => $ownerUUID,
					'groupUUID'     => $groupUUID,
					'landingpoint'  => $landingpoint,
					'parcelUUID'    => $parcelUUID,
					'infoUUID'      => $infoUUID,
					'parcelarea'    => $parcelarea,
					'gatekeeperURL' => $gatekeeperURL,
				)
			);

			if ( $parceldirectory == 'true' ) {
				$SearchDB->insert(
					'parcels',
					array(
						'regionUUID'     => $regionUUID,
						'parcelname'     => $parcelname,
						'parcelUUID'     => $parcelUUID,
						'landingpoint'   => $landingpoint,
						'description'    => $parceldescription,
						'searchcategory' => $searchcategory,
						'build'          => $parcelbuild,
						'script'         => $parcelscript,
						'public'         => $parcelpublic,
						'dwell'          => $dwell,
						'infouuid'       => $infoUUID,
						'mature'         => $mature,
						'gatekeeperURL'  => $gatekeeperURL,
						'imageUUID'      => $image,
					)
				);

				  // We don't want land for sale in popular places.
				if ( $parcelforsale == 'false' ) {
					$SearchDB->insert(
						'popularplaces',
						array(
							'parcelUUID'    => $parcelUUID,
							'name'          => $parcelname,
							'dwell'         => $dwell,
							'infoUUID'      => $infoUUID,
							'has_picture'   => $has_picture,
							'mature'        => $mature,
							'gatekeeperURL' => $gatekeeperURL,
						)
					);
				}
			}

			if ( $parcelforsale == 'true' ) {
				$SearchDB->insert(
					'parcelsales',
					array(
						'regionUUID'    => $regionUUID,
						'parcelname'    => $parcelname,
						'parcelUUID'    => $parcelUUID,
						'area'          => $parcelarea,
						'saleprice'     => $saleprice,
						'landingpoint'  => $landingpoint,
						'infoUUID'      => $infoUUID,
						'dwell'         => $dwell,
						'parentestate'  => $parentestate,
						'mature'        => $mature,
						'gatekeeperURL' => $gatekeeperURL,
					)
				);
			}
		}

		//
		// Handle objects
		//
		$objects = $data->getElementsByTagName( 'object' );

		foreach ( $objects as $object ) {
			$objectname = $object->getElementsByTagName( 'title' )->item( 0 )->nodeValue;
			$location   = $object->getElementsByTagName( 'location' )->item( 0 )->nodeValue;
			$parcelUUID = $object->getElementsByTagName( 'parceluuid' )->item( 0 )->nodeValue;
			$regionUUID = $object->getElementsByTagName( 'regionuuid' )->item( 0 )->nodeValue;
			// $flags = $object->getElementsByTagName("flags")->item(0)->nodeValue; // not implemented
			// $image = $object->getElementsByTagName("image")->item(0)->nodeValue; // not implemented
			$SearchDB->insert(
				'objects',
				array(
					'objectuuid'    => $object->getElementsByTagName( 'uuid' )->item( 0 )->nodeValue,
					'parceluuid'    => $parcelUUID,
					'location'      => $location,
					'name'          => $location,
					'description'   => $object->getElementsByTagName( 'description' )->item( 0 )->nodeValue,
					'regionuuid'    => $object->getElementsByTagName( 'regionuuid' )->item( 0 )->nodeValue,
					'gatekeeperURL' => $gatekeeperURL,
				)
			);
		}
	}
}

if ( $SearchDB && $SearchDB->connected ) {
	// $sql = "SELECT host, port FROM hostsregister WHERE nextcheck<$now AND checked=0 AND failcounter<10 LIMIT 0,100";
	$sql = "SELECT host, port FROM hostsregister WHERE nextcheck<$now AND checked=0 LIMIT 0,100";
	// $sql = "SELECT host, port FROM hostsregister WHERE checked=0 LIMIT 0,100";
	$jobsearch = $SearchDB->query( $sql );

	//
	// If the sql query returns no rows, all entries in the hostsregister
	// table have been checked. Reset the checked flag and re-run the
	// query to select the next set of hosts to be checked.
	//

	if ( $jobsearch->rowCount() == 0 ) {
		$jobsearch = $SearchDB->query( 'UPDATE hostsregister SET checked = 0' );
		$jobsearch = $SearchDB->query( $sql );
	}

	while ( $jobs = $jobsearch->fetch( PDO::FETCH_NUM ) ) {
		hostCheck( $jobs[0], $jobs[1] );
	}
}

die();
