<?php
/**
 * Lists files using a Breadth-First search algorithm to allow for time limits and resume across multiple requests.
 */


/**
 * Cloud_Uploads_Filelist
 */
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly      

class Cloud_Uploads_Filelist {

	public $is_done = false;
	public $paths_left = [];
	public $file_count = 0;
	public $file_list = [];
	public $exclusions = [
		'/et-cache/',
		'/et_temp/',
		'/imports/',
		'/cache/',
		'/wp-defender/',
		'.DS_Store',
	];
	protected $root_path;
	protected $timeout;
	protected $start_time;
	protected $instance;
	protected $insert_rows = 500;

	/**
	 * Cloud_Uploads_Filelist constructor.
	 *
	 * @param string $root_path      The full path of the directory to iterate.
	 * @param float  $timeout        Timeout in seconds.
	 * @param array  $paths_left     Provide as returned if continuing the filelist after a timeout.
	 */
	public function __construct( $root_path, $timeout = 25.0, $paths_left = [] ) {
		$this->root_path  = rtrim( $root_path, '/' ); //expected no trailing slash.
		$this->timeout    = $timeout;
		$this->paths_left = $paths_left;
	}

	/**
	 * Runs over the site's files.
	 */
	public function start() {
		global $wpdb;
		$this->start_time = microtime( true );
		$this->file_count = 0;
		$this->file_list  = [];

		// If just starting reset the local DB list storage
		if ( empty( $this->paths_left ) ) {
			//TRUNCATE is fastest, try it first
			$result = $wpdb->query( "TRUNCATE TABLE {$wpdb->base_prefix}cloud_uploads_files" );
			//Sometimes hosts don't give the DB user TRUNCATE permissions, so DELETE all if we have to.
			if ( false === $result ) {
				$wpdb->query( "DELETE FROM {$wpdb->base_prefix}cloud_uploads_files WHERE 1" );
			}

			update_site_option( 'cloud_uploads_files_scanned', [
				'files_started'     => time(),
				'files_finished'    => false,
				'compare_started'   => false,
				'compare_finished'  => false,
				'sync_started'      => false,
				'sync_finished'     => false,
				'download_started'  => false,
				'download_finished' => false,
			] );
		}

		$this->get_files();

		$this->flush_to_db();

		if ( empty( $this->paths_left ) ) {
			// So we are done. Say so.
			$this->is_done = true;

			$progress                   = get_site_option( 'cloud_uploads_files_scanned' );
			$progress['files_finished'] = time();
			update_site_option( 'cloud_uploads_files_scanned', $progress );
		}
	}

	/**
	 * Runs a breadth-first iteration on all files and gathers the relevant info for each one.
	 *
	 * @todo test what happens if some files have no read permissions.
	 */
	protected function get_files() {

		$paths = ( empty( $this->paths_left ) ) ? [ $this->root_path ] : $this->paths_left;

		while ( ! empty( $paths ) ) {
			$path = array_pop( $paths );

			// Skip ".." items.
			if ( preg_match( '/\.\.([\/\\\\]|$)/', $path ) ) {
				continue;
			}

			if ( 0 !== strpos( $path, $this->root_path ) ) {
				// Build the absolute path in case it's not the first iteration.
				$path = rtrim( $this->root_path, '/' ) . $path;
			}

			if ( $this->is_excluded( $path ) ) {
				continue;
			}

			$contents = defined( 'GLOB_BRACE' )
				? glob( trailingslashit( $path ) . '{,.}[!.,!..]*', GLOB_BRACE )
				: glob( trailingslashit( $path ) . '[!.,!..]*' );

			foreach ( $contents as $item ) {
				$file = [];
				if ( is_link( $item ) || $this->is_excluded( $item ) ) {
					continue;
				} elseif ( is_file( $item ) ) {
					if ( is_readable( $item ) ) {
						$file = $this->get_file_info( $item );
					} else {
						$file = null;
						error_log( sprintf( '[CLOUD_UPLOADS Filelist Error] %s could not be read for syncing', esc_html__($item) ) );
					}

					$file['name'] = $this->relative_path( $item );

					$this->add_file( $file );
				} elseif ( is_dir( $item ) ) {
					if ( ! in_array( $item, $paths, true ) ) {
						$paths[] = $this->relative_path( $item );
					}
				}
			}
			$this->paths_left = $paths;

			// If we have exceed the imposed time limit, lets pause the iteration here.
			if ( $this->has_exceeded_timelimit() ) {
				break;
			}
		}

		$this->is_done = false;
	}

	/**
	 * Checks path against excluded pattern.
	 *
	 * @return bool
	 *
	 */
	protected function is_excluded( $path ) {
		/**
		 * Filters the built in list of file/directory exclusions that should not be synced to the Cloud Uploads cloud. Be specific it's a simple strpos() search for the strings.
		 *
		 * @param  {array}  $exclusions  A list of file or directory names in the format of `/do-not-sync-this-dir/` or `somefilename.ext`.
		 *
		 * @return {array} A list of file or directory names in the format of `/do-not-sync-this-dir/` or `somefilename.ext`.
		 * @since  1.0
		 * @hook   cloud_uploads_sync_exclusions
		 *
		 */
		$exclusions = apply_filters( 'cloud_uploads_sync_exclusions', $this->exclusions );
		foreach ( $exclusions as $string ) {
			if ( false !== strpos( $path, $string ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks file health and returns as many info as it can.
	 *
	 * @param string $item The file to be investigated.
	 *
	 * @return mixed File info or false for failure.
	 */
	protected function get_file_info( $item ) {
		$file          = [];
		$file['mtime'] = filemtime( $item );
		//$file['md5']   = md5_file( $item );
		$file['size'] = filesize( $item );
		$file['type'] = $this->get_file_type( $item );

		if ( empty( $file['mtime'] ) && empty( $file['size'] ) ) {
			return false;
		}

		return $file;
	}

	/**
	 * Returns rel path of file/dir, relative to site root.
	 *
	 * @param string $item File's absolute path.
	 *
	 * @return string
	 */
	protected function relative_path( $item ) {
		// Retrieve the relative to the site root path of the file.
		$pos = strpos( $item, $this->root_path );
		if ( 0 === $pos ) {
			return substr_replace( $item, '', $pos, strlen( $this->root_path ) );
		}

		return $item;
	}

	/**
	 * Add file details to internal storage and the db.
	 */
	protected function add_file( $file ) {
		$this->file_list[] = $file;
		$this->file_count ++;
		if ( count( $this->file_list ) >= $this->insert_rows ) {
			$this->flush_to_db();
		}
	}

	/**
	 * Write the queued file list to DB storage.
	 */
	protected function flush_to_db() {
		global $wpdb;

		if ( count( $this->file_list ) ) {
			$values = [];
			foreach ( $this->file_list as $file ) {
				$values[] = $wpdb->prepare( "(%s,%d,%d,%s,0)", $file['name'], $file['size'], $file['mtime'], $file['type'] );
			}

			$query = "INSERT INTO {$wpdb->base_prefix}cloud_uploads_files (file, size, modified, type, errors) VALUES ";
			$query .= implode( ",\n", $values );
			$query .= " ON DUPLICATE KEY UPDATE size = VALUES(size), modified = VALUES(modified), type = VALUES(type), errors = 0";
			if ( $wpdb->query( $query ) ) {
				$this->file_list = [];
			}
		}
	}

	/**
	 * Checks if current iteration has exceeded the given time limit.
	 *
	 * @return bool True if we have exceeded the time limit, false if we haven't.
	 */
	protected function has_exceeded_timelimit() {
		$current_time = microtime( true );
		$time_diff    = number_format( $current_time - $this->start_time, 2 );

		$has_exceeded_timelimit = ! empty( $this->timeout ) && ( $time_diff > $this->timeout );

		return $has_exceeded_timelimit;
	}

  public function get_file_type( $filename ) {
		$extensions = [
			'image'    => [ 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'svg', 'svgz', 'webp' ],
			'audio'    => [ 'aac', 'ac3', 'aif', 'aiff', 'flac', 'm3a', 'm4a', 'm4b', 'mka', 'mp1', 'mp2', 'mp3', 'ogg', 'oga', 'ram', 'wav', 'wma' ],
			'video'    => [ '3g2', '3gp', '3gpp', 'asf', 'avi', 'divx', 'dv', 'flv', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mpv', 'ogm', 'ogv', 'qt', 'rm', 'vob', 'wmv', 'webm' ],
			'document' => [
				'log',
				'asc',
				'csv',
				'tsv',
				'txt',
				'doc',
				'docx',
				'docm',
				'dotm',
				'odt',
				'pages',
				'pdf',
				'xps',
				'oxps',
				'rtf',
				'wp',
				'wpd',
				'psd',
				'xcf',
				'swf',
				'key',
				'ppt',
				'pptx',
				'pptm',
				'pps',
				'ppsx',
				'ppsm',
				'sldx',
				'sldm',
				'odp',
				'numbers',
				'ods',
				'xls',
				'xlsx',
				'xlsm',
				'xlsb',
			],
			'archive'  => [ 'bz2', 'cab', 'dmg', 'gz', 'rar', 'sea', 'sit', 'sqx', 'tar', 'tgz', 'zip', '7z', 'data', 'bin', 'bak' ],
			'code'     => [ 'css', 'htm', 'html', 'php', 'js', 'md' ],
		];

		$ext = preg_replace( '/^.+?\.([^.]+)$/', '$1', $filename );
		if ( ! empty( $ext ) ) {
			$ext = strtolower( $ext );
			foreach ( $extensions as $type => $exts ) {
				if ( in_array( $ext, $exts, true ) ) {
					return $type;
				}
			}
		}

		return 'other';
	}
}
