File "tables.php"

Full Path: /home/pylifeesyu/www/wp-content/plugins/wponlinebackup/include/tables.php
File size: 24.15 KB
MIME-type: text/x-php
Charset: utf-8

<?php

/*
WPOnlineBackup_Backup_Tables - Workhouse for the database backup
We pass it the stream we want it to use to store the data
*/

class WPOnlineBackup_Backup_Tables
{
	/*private*/ var $WPOnlineBackup;

	/*private*/ var $bootstrap;
	/*private*/ var $stream;
	/*private*/ var $progress;
	/*private*/ var $job;

	/*private*/ var $internals;

	/*private*/ var $use_wpdb_api = null;
	/*private*/ var $max_block_size;

	/*private*/ var $db_prefix;
	/*private*/ var $multisite_prefix_regex;

	/*public*/ function WPOnlineBackup_Backup_Tables( & $WPOnlineBackup, $db_force_master = '' )
	{
		global $wpdb;

		$this->WPOnlineBackup = & $WPOnlineBackup;

		// Cache multisite regex stuff - the regex must end in # since we use that in the code that references it
		$this->multisite_prefix_regex = '#^(' . preg_quote( $db_prefix, '#' ) . ')[0-9]+_';

		$this->db_prefix = $wpdb->prefix;

		// Generate internal table list
		$this->internals = array(
			$this->db_prefix . 'wponlinebackup_activity_log',
			$this->db_prefix . 'wponlinebackup_event_log',
			$this->db_prefix . 'wponlinebackup_generations',
			$this->db_prefix . 'wponlinebackup_items',
			$this->db_prefix . 'wponlinebackup_status',
			$this->db_prefix . 'wponlinebackup_scan_log',
			$this->db_prefix . 'wponlinebackup_local',
		);

		$this->dump_segment_size = $WPOnlineBackup->Get_Setting( 'dump_segment_size' );
	}

	/*private*/ function _Fetch_Tables()
	{
		// Grab core table list and append prefixes
		$core_list = $this->WPOnlineBackup->Get_Setting( 'core_tables' );

		foreach ( $core_list as $key => $table ) {

			$core_list[ $key ] = $this->db_prefix . $table;

		}

		// Grab site table list and append to the core_list too with our prefix (speeds up the loop below since it skips the preg_replace)
		// But also add into the site list with our prefix (if we're running multisite), which we'll use after a preg_replace to strip the blog ID
		$site_list = $this->WPOnlineBackup->Get_Setting( 'site_tables' );

		foreach ( $site_list as $key => $table ) {

			$core_list[] = $this->db_prefix . $table;

			$site_list[ $key ] = $this->db_prefix . $table;

		}

		return array( $core_list, $site_list );
	}

	/*public*/ function Fetch_Available()
	{
		global $wpdb;

		// Cache multisite status and make our regex for testing for site tables
		if ( $multisite = $this->WPOnlineBackup->multisite )
			$multisite_replace = $this->multisite_prefix_regex . '(.++)$#';

		// Fetch tables
		list ( $core_list, $site_list ) = $this->_Fetch_Tables();

		// Grab table list - if error happens we'll get 0 tables - can be fine
		$tables = $wpdb->get_results( 'SHOW TABLES', ARRAY_N );

		$core = array();
		$custom = array();

		// Loop tables to extract core tables and custom ones
		foreach ( $tables as $entry ) {

			$entry = $entry[0];

			// If table is internal to our plugin, ignore
			if ( in_array( $entry, $this->internals ) )
				continue;

			// If table is a default core wordpress table, add to the core table list
			if ( in_array( $entry, $core_list ) ) {
				$core[] = $entry;
				continue;
			}

			// If table is a multisite table, add to the core table list too
			if ( $multisite && in_array( preg_replace( $multisite_replace, '\1\2', $entry ), $site_list ) ) {
				$core[] = $entry;
				continue;
			}

			// Add to custom table list
			$custom[ $entry ] = $entry;

		}

		// Return the list of tables
		return array( $core, $custom );
	}

	/*public*/ function Initialise( & $bootstrap, & $progress )
	{
		global $wpdb;

		// Cache multisite status and make our regex for testing for site tables
		if ( $multisite = $this->WPOnlineBackup->multisite )
			$multisite_replace = $this->multisite_prefix_regex . '(.++)$#';

		// Fetch tables
		list ( $core_list, $site_list ) = $this->_Fetch_Tables();

		// Grab table list
		$tables = $wpdb->get_results( 'SHOW TABLES', ARRAY_N );

		// SHOW TABLES would never return no rows (empty array), and an error returns no rows - so no rows will always be treated as an error
		if ( count( $tables ) == 0 ) {

			// Failed. Report it to the event log and return the progress tracker unmodified so we can still backup files
			$bootstrap->Log_Event(
				WPONLINEBACKUP_EVENT_ERROR,
				sprintf( __( 'Failed to retrieve the table list from the database: %s.' , 'wponlinebackup' ), WPOnlineBackup::Get_WPDB_Last_Error() )
			);

			return true;

		}

		// Help out memory usage
		$wpdb->flush();

		// Table === true means open stream
		$progress['jobs'][] = array(
			'processor'	=> 'tables',
			'progress'	=> 0,
			'progresslen'	=> 1,
			'table'		=> true,
			'generation'	=> time(),
		);

		// Process table list
		foreach ( $tables as $entry ) {

			$entry = $entry[0];

			// Is the table one of our own? Skip
			if ( in_array( $entry, $this->internals ) )
				continue;
			// Is the table a core table or a site table? Don't check anything
			else if ( in_array( $entry, $core_list ) );
			else if ( $multisite && in_array( preg_replace( $multisite_replace, '\1\2', $entry ), $site_list ) );
			// Depending on the inclusion settings, skip the table or not
			else if ( $this->WPOnlineBackup->Get_Setting( 'selection_method' ) == 'include' && !in_array( $entry, $this->WPOnlineBackup->Get_Setting( 'selection_list' ) ) )
				continue;
			else if ( $this->WPOnlineBackup->Get_Setting( 'selection_method' ) != 'include' && in_array( $entry, $this->WPOnlineBackup->Get_Setting( 'selection_list' ) ) )
				continue;

			// Add the table to the progress tracker
			$job = array(
				'processor'	=> 'tables',
				'progress'	=> 0,
				'progresslen'	=> 1,
				'table'		=> $entry,
				'total'		=> null,
				'done'		=> 0,
				'last_id'	=> array(),
				'primary'	=> array(),
				'fields'	=> array(),
			);

			// Grab indexes from the table
			$indexes = $wpdb->get_results( 'SHOW INDEXES FROM `' . $entry . '`', ARRAY_A );
			$unique = false;

			// SHOW INDEXES might return no rows (empty array), but an error would also return an empty array. get_col_info() is tell-tale sign for errors
			if ( !is_null( $wpdb->get_col_info( 'name', 0 ) ) ) {

				// Find fields that form part of a PRIMARY key. Also look out for a UNIQUE key
				foreach ( $indexes as $index ) {

					if ( $index['Key_name'] == 'PRIMARY' ) {

						// Found part of a PRIMARY key, store the field name, and prepare last_id tracker
						$job['primary'][$index['Seq_in_index']] = $index['Column_name'];
						$job['last_id'][$index['Seq_in_index']] = null;

					} else if ( $unique === false && $index['Non_unique'] == 0 ) {

						// Found a UNIQUE key, store the key name and ignore any other UNIQUE keys as we only need one
						$unique = $index['Key_name'];

					}

				}

			} else {

				// Failed to gather indexes, report it to the event log - MIGHT DUPLICATE ON ERROR!
				$bootstrap->Log_Event(
					WPONLINEBACKUP_EVENT_ERROR,
					sprintf( __( 'Failed to retrieve the index list from table \'%s\': %s.' , 'wponlinebackup' ), $entry, WPOnlineBackup::Get_WPDB_Last_Error() )
				);

			}

			// If we didn't find a PRIMARY key, but we found a UNIQUE key, locate the columns that form that key, and pretend it is a PRIMARY
			// - For what we require, PRIMARY and UNIQUE both provide what we need - a unique key
			if ( count( $job['primary'] ) == 0 && $unique ) {

				foreach ( $indexes as $index ) {

					if ( $index['Key_name'] == $unique ) {

						// Found a column from the UNIQUE key, store the field name, and prepare last_id tracker
						$job['primary'][$index['Seq_in_index']] = $index['Column_name'];
						$job['last_id'][$index['Seq_in_index']] = null;

					}

				}

			}

			// If we have a unique key, sort the columns in order of sequence in the key (which we set as the key!)
			// When pulling fields out, we will use these keys, in that order - resulting in not only fast queries, but allowing us to keep the backup as consistent as is possible
			if ( count( $job['primary'] ) ) {

				ksort( $job['primary'] );
				ksort( $job['last_id'] );

			} else {

				// No key was found! Report it to the event log, and set last_id tracker to null so we know there is none - MIGHT DUPLICATE ON ERROR
				$bootstrap->Log_Event(
					WPONLINEBACKUP_EVENT_WARNING,
					sprintf( __( 'Table \'%s\' has no PRIMARY or UNIQUE key! Backup of this table may be inconsistent; some rows could get missed. See FAQ for more information.' , 'wponlinebackup' ), $entry )
				);

				$job['last_id'] = null;

			}

			// Help out memory usage
			unset( $indexes );
			$wpdb->flush();

			// Grab the field list for use later when generating INSERT statements
			$fields = $wpdb->get_results( 'DESCRIBE `' . $entry . '`', ARRAY_A );

			// DESCRIBE will always return at least one row, so an empty array (no rows) or error (no rows) both are considered an error
			if ( count( $fields ) ) {

				// Store the fields
				foreach ( $fields as $field )
					$job['fields'][] = $field['Field'];

			} else {

				// Report it to the event log - MIGHT DUPLICATE ON ERROR!
				$bootstrap->Log_Event(
					WPONLINEBACKUP_EVENT_ERROR,
					sprintf( __( 'Failed to retrieve field list from table \'%s\': %s. Table will be skipped.' , 'wponlinebackup' ), $entry, WPOnlineBackup::Get_WPDB_Last_Error() )
				);

				continue;

			}

			// Help out memory usage
			unset( $fields );
			$wpdb->flush();

			$progress['jobs'][] = $job;

		}

		// Table === false means end stream, commit and cleanup
		$progress['jobs'][] = array(
			'processor'	=> 'tables',
			'progress'	=> 0,
			'progresslen'	=> 1,
			'table'		=> false,
		);

		// Make some log entries if we're ignoring trash or spam
		if ( $this->WPOnlineBackup->Get_Setting( 'ignore_trash_comments' ) ) 
			$bootstrap->Log_Event(
				WPONLINEBACKUP_EVENT_INFORMATION,
				__( 'Ignoring comments in the trash.' , 'wponlinebackup' )
			);

		if ( $this->WPOnlineBackup->Get_Setting( 'ignore_spam_comments' ) ) 
			$bootstrap->Log_Event(
				WPONLINEBACKUP_EVENT_INFORMATION,
				__( 'Ignoring comments that are marked as spam.' , 'wponlinebackup' )
			);

		// Return the progress tracker
		return true;
	}

	/*public*/ function Save()
	{
	}

	/*public*/ function CleanUp( $ticking = false )
	{
	}

	/*public*/ function Backup( & $bootstrap, & $stream, & $progress, & $job )
	{
		$this->bootstrap = & $bootstrap;
		$this->stream = & $stream;
		$this->progress = & $progress;
		$this->job = & $job;

		// Cache some settings
		if ( is_null( $this->use_wpdb_api ) ) {
			$this->use_wpdb_api = $this->WPOnlineBackup->Get_Setting( 'use_wpdb_api' );
			$this->max_block_size = $this->WPOnlineBackup->Get_Setting( 'max_block_size' );
		}

		if ( $job['table'] === true ) {

			$progress['message'] = __( 'Starting database backup...' , 'wponlinebackup' );

			$progress['rcount']++;

			if ( ( $ret = $stream->Start_Stream(
				WPONLINEBACKUP_BIN_DATABASE,
				'OBFW_Database.sql',
				$size,
				array(
					'item_id'	=> 1,
					'parent_id'	=> 0,
					'mod_time'	=> $job['generation'],
					'backup_time'	=> $job['generation'],
				)
			) ) !== true ) return $this->bootstrap->FSError( __LINE__, __FILE__, '\'Database/OBFW_Database.sql\'', $ret );

			// translators: date() function string when displaying dump time in database backup header
			$date_format = __( 'd-m-Y H.i.s T' , 'wponlinebackup' );

			$header =
				'-- ' . __( 'Online Backup for WordPress', 'wponlinebackup' ) . WPONLINEBACKUP_EOL .
				'-- ' . sprintf( __( 'Version %s', 'wponlinebackup' ), WPONLINEBACKUP_VERSION ) . WPONLINEBACKUP_EOL .
				'-- ' . __( 'http://www.backup-technology.com/free-wordpress-backup/', 'wponlinebackup' ) . WPONLINEBACKUP_EOL .
				'-- ' . WPONLINEBACKUP_EOL .
				'-- ' . sprintf( __( 'Blog: %s', 'wponlinebackup' ), get_bloginfo( 'wpurl' ) ) . WPONLINEBACKUP_EOL .
				'-- ' . sprintf( __( 'Database Dump Time: %s', 'wponlinebackup' ), date( $date_format ) ) . WPONLINEBACKUP_EOL .
				WPONLINEBACKUP_EOL .
				'SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";' . WPONLINEBACKUP_EOL .
				WPONLINEBACKUP_EOL;

			$progress['rsize'] += strlen( $header );

			$ret = $stream->Write_Stream($header);

			$job['progress'] = 100;

		} else if ( $job['table'] === false ) {

			$progress['message'] = __( 'Finalising database backups...' , 'wponlinebackup' );

			if ( $job['progress'] == 0 ) {

				if ( ( $ret = $stream->End_Stream() ) !== true ) return $this->bootstrap->FSError( __LINE__, __FILE__, '\'Database/OBFW_Database.sql\'', $ret );

				$job['progress'] = 33;

				$bootstrap->Tick( false, true );

			}

			if ( $job['progress'] == 33 ) {

				if ( ( $ret = $stream->Commit_Stream() ) !== true ) return $this->bootstrap->FSError( __LINE__, __FILE__, '\'Database/OBFW_Database.sql\'', $ret );

				$job['progress'] = 66;

				$bootstrap->Tick( false, true );

			}

			if ( $job['progress'] == 66 ) {

				$ret = $stream->CleanUp_Stream();

				if ( $ret === true ) {

					// This won't duplicate as we will force an update in bootstrap when we leave this function
					$bootstrap->Log_Event(
						WPONLINEBACKUP_EVENT_INFORMATION,
						__( 'Database backup completed.' , 'wponlinebackup' )
					);

				}

				$job['progress'] = 100;

			}

		} else {

			$ret = $this->Backup_Table();

		}

		return $ret === true ? true : $this->bootstrap->FSError( __LINE__, __FILE__, '\'Database/OBFW_Database.sql\'', $ret );
	}

	/*private*/ function Query( $fields, $table, $where, $extra, $start )
	{
		global $wpdb;

		// Build the query
		$query =
			'SELECT ' . $fields . ' ' .
			'FROM `' . $table . '`' .
			$where .
			$extra . ' ' .
			'LIMIT ' . $start . $this->dump_segment_size;

		if ( $this->use_wpdb_api ) {

			// Use the wacky WPDB API
			$result = $wpdb->get_results( $query, ARRAY_N );

			// Could get back 0 rows (empty array), but error is also an empty array, so lets check get_col_info()
			// My head hurts... We should be failing if there is an error! Not giving 0 rows?! Especially bad during backup!
			if ( is_null( $wpdb->get_col_info( 'name', 0 ) ) ) {

				$this->bootstrap->Log_Event(
					WPONLINEBACKUP_EVENT_ERROR,
					sprintf( __( 'Failed to retrieve rows in table \'%s\' using WPDB API: %s. The table may only be partially backed up.' , 'wponlinebackup' ), $this->job['table'], WPOnlineBackup::Get_WPDB_Last_Error() )
				);

				return false;

			}

			reset($result);

			return $result;

		}

		$unbuffered = false;

		while (42) {

			// Steal the database connection resource from WPDB and work with MySQL client directly - we checked this is possible already
			// This way, we can actually do an unbuffered MySQL query and not worry about memory
			if ( $unbuffered )
				$result = @mysql_unbuffered_query( $query, $wpdb->dbh );
			else
				$result = @mysql_query( $query, $wpdb->dbh );

			// Check for error
			if ( $result === false ) {

				if ( @mysql_errno( $wpdb->dbh ) === 2008 ) {

					if ( !$unbuffered ) {
						// MySQL ran out of memory - switch to unbuffered for this segment
						$unbuffered = true;
						continue;
					}

				}

				$this->bootstrap->Log_Event(
					WPONLINEBACKUP_EVENT_ERROR,
					sprintf( __( 'Failed to retrieve rows in table \'%s\' using MySQL client: %s. The table may only be partially backed up.' , 'wponlinebackup' ), $this->job['table'], @mysql_error( $wpdb->dbh ) )
				);

				return false;

			}

			break;

		}

		return $result;
	}

	/*private*/ function Fetch_Row( & $result )
	{
		if ( $this->use_wpdb_api ) {

			// Try to conserve memory as much as possible by discarding entries as we finish with them
			if ( ( $row = current($result) ) === false ) return false;
			$key = key($result);
			next($result);
			unset($result[$key]);
			return $row;

		}

		// Use MySQL client directly
		return @mysql_fetch_array( $result, MYSQL_NUM );
	}

	/*private*/ function Free_Result( $result )
	{
		global $wpdb;

		if ( $this->use_wpdb_api )
			$wpdb->flush();
		else
			// Only worked with buffered query but we'll run anyway as we don't store what type it was yet (TODO)
			@mysql_free_result( $result );
	}

	/*public*/ function Backup_Table()
	{
		global $wpdb;

		// We backup each table and jump back to bootstrap after each table for a forced update
		// We may improve this to work same as files in future and loop forever in here and decide on our own forced updates
		while ( $this->job['progress'] != 100 ) {

			$where = array();

			if ( count( $this->job['primary'] ) ) {

				// We have a primary or unique key, add an order by clause
				$extra = ' ORDER BY ' . WPOnlineBackup_Backup_Tables::Implode_Backquote( ' ASC, ', $this->job['primary'] ) . ' ASC';

				// Calculate the where clause based on the key if we already have the table information
				// If we don't have the table information yet, we don't give a WHERE so we can get the first set of rows
				if ( !is_null( $this->job['total'] ) ) {

					$previous = array();

					// We search for records with ID higher than the last id.
					// For multi-column, we check for where the first ID is higher, or the first ID is the same and the second ID is higher, and so on
					foreach ( $this->job['primary'] as $index => $column ) {

						$value = $this->job['last_id'][$index];

						// Calls _real_escape if it exists - escape() seems to call _weak_escape() instead
						$wpdb->escape_by_ref( $value );

						if ( count( $previous ) ) $where[] = '(' . implode( ' AND ', $previous ) . ' AND `' . $column . '` > \'' . $value . '\')';
						else $where[] = '`' . $column . '` > \'' . $value . '\'';
						$previous[] = '`' . $column . '` = \'' . $value . '\'';

					}

					if ( count( $where ) > 1 )
						$where = array( '(' . implode( ' OR ', $where ) . ')' );

				}

				// When using a key, we don't need a start offset, as we calculate it based on IDs
				$start = '';

			} else {

				$extra = '';

				// No primary or unique key available, so we failback to setting a start offset
				if ( !is_null( $this->job['total'] ) ) {
					$start = $this->job['done'] . ', ';
				} else $start = '';

			}

			if ( $is_comments = preg_match( $this->multisite_prefix_regex . 'comments$#', $this->job['table'] ) ) {

				if ( $this->WPOnlineBackup->Get_Setting( 'ignore_spam_comments' ) )
					$where[] = '`comment_approved` <> \'spam\'';

				if ( $this->WPOnlineBackup->Get_Setting( 'ignore_trash_comments' ) )
					$where[] = '`comment_approved` <> \'trash\'';

			} else if ( $this->job['table'] == $this->db_prefix . 'options' ) {

				// Remove this option - it will trigger our tables to be created on restore
				$where[] = '`option_name` <> \'wponlinebackup_check_tables\'';

			}

			$where = implode( ' AND ', $where );
			if ( $where ) $where = ' WHERE ' . $where;

			if ( is_null( $this->job['total'] ) ) {

				$this->progress['message'] = sprintf( __( 'Backing up %s...' , 'wponlinebackup' ), $this->job['table'] );

				$drop = 'DROP TABLE IF EXISTS `' . $this->job['table'] . '`;' . WPONLINEBACKUP_EOL . WPONLINEBACKUP_EOL;

				// Table information doesn't exist, so let's gather it, first by getting the CREATE TABLE dump
				$wpdb->query( 'SET sql_quote_show_create = 1' );

				$create = $wpdb->get_var( 'SHOW CREATE TABLE `' . $this->job['table'] . '`', 1 );

				// SHOW CREATE TABLE should always return a row, so 0 rows (null) or error (null) both are considered an error
				if ( is_null( $create ) ) {

					// Failed to gather table information - report, and skip the table
					$this->bootstrap->Log_Event(
						WPONLINEBACKUP_EVENT_ERROR,
						sprintf( __( 'Failed to retrieve information for table \'%s\': %s. The table will be skipped.' , 'wponlinebackup' ), $this->job['table'], WPOnlineBackup::Get_WPDB_Last_Error() )
					);

					$this->job['progress'] = 100;

					// Breaking will throw us all the way out back into bootstrap for a forced update that will prevent the event log duplicating
					break;

				}

				// Normalise line-endings
				$create = preg_replace( '/\\r\\n?|\\n/', WPONLINEBACKUP_EOL, $create );

				$create .= ';' . WPONLINEBACKUP_EOL . WPONLINEBACKUP_EOL;
				$this->progress['rsize'] += strlen( $create );
				if ( ( $ret = $this->stream->Write_Stream( $drop . $create ) ) !== true ) return $ret;

				// Get the total number of rows in the table, so we can provide progress information if required
				$this->job['total'] = $wpdb->get_var( 'SELECT COUNT(*) FROM `' . $this->job['table'] . '`' . $where );

				// SELECT COUNT(*) should always return a row, so 0 rows (null) or error (null) both are considered an error
				if ( is_null( $this->job['total'] ) ) {

					// Failed to gather row count - report, and skip the table
					$this->bootstrap->Log_Event(
						WPONLINEBACKUP_EVENT_ERROR,
						sprintf( __( 'Failed to retrieve row count for table \'%s\': %s. The table will be skipped.' , 'wponlinebackup' ), $this->job['table'], WPOnlineBackup::Get_WPDB_Last_Error() )
					);

					$this->job['progress'] = 100;

					// Breaking will throw us all the way out back into bootstrap for a forced update that will prevent the event log duplicating
					break;

				}

				$this->bootstrap->Tick();

			}

			$fields = WPOnlineBackup_Backup_Tables::Implode_Backquote( ',', $this->job['fields'] );

			// Begin retrieving data
			if ( ( $result = $this->Query( $fields, $this->job['table'], $where, $extra, $start ) ) === false ) {

				// Failure is logged inside Query()
				$this->job['progress'] = 100;

				// Breaking here throws back to bootstrap for a forced update - this will prevent duplicating the event log entries Query() wrote
				break;

			}

			$this->bootstrap->Tick();

			// Create a fully escaped insert statement
			$insert = '';
			$row_count = 0;
			$insert_size = 0;

			while ( false !== ( $next_row = $this->Fetch_Row( $result ) ) ) {

				$row = $next_row;
				$values = array();
				$row_count++;

				foreach ( $row as $index => $value ) {

					$insert_size += strlen( $value );

					// If we're not the first row and our insert has got too big, write the insert and start another
					// This prevents our insert getting rediculously big
					if ( $row_count > 1 && $insert_size > $this->max_block_size ) {
						$row_count--;
						break 2;
					}

					if ( is_null( $value ) ) {
						$value = 'NULL';
					} else if ( !$this->Requires_Quotes( $value ) ) {
					} else {
						// escape_by_ref uses _real_escape - preferred. escape() appears to only use _weak_escape()
						$wpdb->escape_by_ref( $value );
						$value = '\'' . $value . '\'';
					}

					$values[] = $value;

				}

				$insert .=
					( $row_count == 1 ?
						'INSERT INTO `' . $this->job['table'] . '` (' . $fields . ') VALUES' . WPONLINEBACKUP_EOL :
						',' . WPONLINEBACKUP_EOL
					) .
					'(' . implode( ',', $values ) . ')';

			}

			unset( $values );
			unset( $value );

			$this->Free_Result( $result );

			// If 0 rows were returned, we reached the end of the dataset
			// We couldn't use num_rows or anything as we may be an unbuffered query result
			if ( $row_count == 0 ) {

				$this->job['progress'] = 100;

				break;

			}

			// Finish the statement
			$insert .= ';' . WPONLINEBACKUP_EOL . WPONLINEBACKUP_EOL;

			$this->job['done'] += $row_count;

			if ( $this->job['done'] >= $this->job['total'] ) $this->job['progress'] = 99;
			else {
				$this->job['progress'] = floor( ( $this->job['done'] * 99 ) / $this->job['total'] );
				if ( $this->job['progress'] >= 100 ) $this->job['progress'] = 99;
			}

			$this->progress['message'] = sprintf( __( 'Backing up %s; %d of %d rows...' , 'wponlinebackup' ), $this->job['table'], $this->job['done'], $this->job['total'] );

			// If we are tracking using a key, update the last_id fields
			if ( count( $this->job['primary'] ) ) {
				foreach ( $this->job['primary'] as $index => $column ) {
					$this->job['last_id'][$index] = $row[ array_search( $column, $this->job['fields'] ) ];
				}
			}

			// Add to the dump
			$this->progress['rsize'] += strlen( $insert );
			if ( ( $ret = $this->stream->Write_Stream( $insert ) ) !== true ) return $ret;

			$this->bootstrap->Tick();

		}

		return true;
	}

	/*private static*/ function Requires_Quotes( $value )
	{
		// We require quotes if we aren't a number, or if we are a number and we are prefixed with 0s (telephone numbers etc.)
		// We also need quotes if the number is pretty large... we'll just allow 9 digits without quotes to be safe
		// Note: is_numeric is unsafe, it will return true for scientific format doubles like 1234e56
		return !preg_match( '/^[0-9]+$/', $value ) || strlen( $value ) > 9 || preg_match( '/^0+/', $value );
	}

	/*private static*/ function Implode_Backquote( $delimeter, $array )
	{
		reset( $array );
		list ( , $value ) = each( $array );
		$return = '`' . $value . '`';
		while ( list ( , $value ) = each( $array ) ) {
			$return .= $delimeter . '`' . $value . '`';
		}
		return $return;
	}
}

?>