' . $this->get_mysqldump_method() . ')' ) );
214 |
215 | break;
216 |
217 | case 'hmbkp_mysqldump_verify_started' :
218 |
219 | $this->set_status( sprintf( __( 'Verifying Database Dump %s', 'wpremote' ), '(' . $this->get_mysqldump_method() . ')' ) );
220 |
221 | break;
222 |
223 | case 'hmbkp_archive_started' :
224 |
225 | if ( $this->is_using_file_manifest() )
226 | $status = sprintf( __( '%d files remaining to archive %s', 'wpremote' ), $this->file_manifest_remaining, '(' . $this->get_archive_method() . ')' );
227 | else
228 | $status = sprintf( __( 'Creating zip archive %s', 'wpremote' ), '(' . $this->get_archive_method() . ')' );
229 |
230 | $this->set_status( $status );
231 |
232 | break;
233 |
234 | case 'hmbkp_archive_verify_started' :
235 |
236 | $this->set_status( sprintf( __( 'Verifying Zip Archive %s', 'wpremote' ), '(' . $this->get_archive_method() . ')' ) );
237 |
238 | break;
239 |
240 | case 'hmbkp_backup_complete' :
241 |
242 | if ( file_exists( $this->get_schedule_running_path() ) )
243 | unlink( $this->get_schedule_running_path() );
244 |
245 | $this->clear_backup_process_id();
246 |
247 | break;
248 |
249 | case 'hmbkp_error' :
250 |
251 | if ( $this->get_errors() ) {
252 |
253 | $file = $this->get_path() . '/.backup_errors';
254 |
255 | if ( file_exists( $file ) )
256 | unlink( $file );
257 |
258 | if ( ! $handle = @fopen( $file, 'w' ) )
259 | return;
260 |
261 | fwrite( $handle, json_encode( $this->get_errors() ) );
262 |
263 | fclose( $handle );
264 |
265 | }
266 |
267 | break;
268 |
269 | case 'hmbkp_warning' :
270 |
271 | if ( $this->get_warnings() ) {
272 |
273 | $file = $this->get_path() . '/.backup_warnings';
274 |
275 | if ( file_exists( $file ) )
276 | unlink( $file );
277 |
278 | if ( ! $handle = @fopen( $file, 'w' ) )
279 | return;
280 |
281 | fwrite( $handle, json_encode( $this->get_warnings() ) );
282 |
283 | fclose( $handle );
284 |
285 | }
286 |
287 | break;
288 |
289 | endswitch;
290 |
291 | }
292 |
293 | /**
294 | * Get the path to the backups directory
295 | *
296 | * Will try to create it if it doesn't exist
297 | * and will fallback to default if a custom dir
298 | * isn't writable.
299 | *
300 | * @access private
301 | * @see default_path()
302 | * @return string $path
303 | */
304 | private function path() {
305 |
306 | global $is_apache;
307 |
308 | $path = get_option( 'wprp_backup_path' );
309 |
310 | // If the dir doesn't exist or isn't writable then use the default path instead instead
311 | if ( ! $path || ( is_dir( $path ) && ! is_writable( $path ) ) || ( ! is_dir( $path ) && ! is_writable( dirname( $path ) ) ) )
312 | $path = $this->path_default();
313 |
314 | // Create the backups directory if it doesn't exist
315 | if ( ! is_dir( $path ) && is_writable( dirname( $path ) ) )
316 | mkdir( $path, 0755 );
317 |
318 | // If the path has changed then cache it
319 | if ( get_option( 'wprp_backup_path' ) !== $path )
320 | update_option( 'wprp_backup_path', $path );
321 |
322 | // Protect against directory browsing by including a index.html file
323 | $index = $path . '/index.html';
324 |
325 | if ( ! file_exists( $index ) && is_writable( $path ) )
326 | file_put_contents( $index, '' );
327 |
328 | $htaccess = $path . '/.htaccess';
329 |
330 | // Protect the directory with a .htaccess file on Apache servers
331 | if ( $is_apache && function_exists( 'insert_with_markers' ) && ! file_exists( $htaccess ) && is_writable( $path ) ) {
332 |
333 | $contents[] = '# ' . sprintf( __( 'This %s file ensures that other people cannot download your backup files.', 'wpremote' ), '.htaccess' );
334 | $contents[] = '';
335 | $contents[] = '' . $filename . '' );
355 |
356 | $this->archive_filename = strtolower( sanitize_file_name( remove_accents( $filename ) ) );
357 |
358 | }
359 |
360 | /**
361 | * Get the full filepath to the database dump file.
362 | *
363 | * @access public
364 | * @return string
365 | */
366 | public function get_database_dump_filepath() {
367 |
368 | return trailingslashit( $this->get_path() ) . $this->get_database_dump_filename();
369 |
370 | }
371 |
372 | /**
373 | * Get the filename of the database dump file
374 | *
375 | * @access public
376 | * @return string
377 | */
378 | public function get_database_dump_filename() {
379 |
380 | if ( empty( $this->database_dump_filename ) )
381 | $this->set_database_dump_filename( 'database_' . DB_NAME . '.sql' );
382 |
383 | return $this->database_dump_filename;
384 |
385 | }
386 |
387 | /**
388 | * Set the filename of the database dump file
389 | *
390 | * @param string $filename
391 | * @throws Exception
392 | */
393 | public function set_database_dump_filename( $filename ) {
394 |
395 | if ( empty( $filename ) || ! is_string( $filename ) )
396 | throw new Exception( __( 'database dump filename must be a non empty string', 'wpremote' ) );
397 |
398 | if ( pathinfo( $filename, PATHINFO_EXTENSION ) !== 'sql' )
399 | throw new Exception( __( 'invalid file extension for database dump filename', 'wpremote' ) . '' . $filename . '' );
400 |
401 | $this->database_dump_filename = strtolower( sanitize_file_name( remove_accents( $filename ) ) );
402 |
403 | }
404 |
405 | /**
406 | * Get the root directory to backup from
407 | *
408 | * Defaults to the root of the path equivalent of your home_url
409 | *
410 | * @access public
411 | * @return string
412 | */
413 | public function get_root() {
414 |
415 | if ( empty( $this->root ) )
416 | $this->set_root( self::conform_dir( self::get_home_path() ) );
417 |
418 | return $this->root;
419 |
420 | }
421 |
422 | /**
423 | * Set the root directory to backup from
424 | *
425 | * @param string $path
426 | * @throws Exception
427 | */
428 | public function set_root( $path ) {
429 |
430 | if ( empty( $path ) || ! is_string( $path ) || ! is_dir ( $path ) )
431 | throw new Exception( sprintf( __( 'Invalid root path %s must be a valid directory path', 'wpremote' ), '' . $path . '' ) );
432 |
433 | $this->root = self::conform_dir( $path );
434 |
435 | }
436 |
437 | /**
438 | * Get the directory backups are saved to
439 | *
440 | * @access public
441 | * @return string
442 | */
443 | public function get_path() {
444 |
445 | if ( empty( $this->path ) )
446 | $this->set_path( self::conform_dir( self::get_path_default() ) );
447 |
448 | return $this->path;
449 |
450 | }
451 |
452 | /**
453 | * Get default backup path to save to.
454 | *
455 | * @return string
456 | */
457 | protected function get_path_default() {
458 | return WP_CONTENT_DIR . '/backups';
459 | }
460 |
461 | /**
462 | * Set the directory backups are saved to
463 | *
464 | * @param string $path
465 | * @throws Exception
466 | */
467 | public function set_path( $path ) {
468 |
469 | if ( empty( $path ) || ! is_string( $path ) )
470 | throw new Exception( sptrinf( __( 'Invalid backup path %s must be a non empty (string)', wpremote ), '' . $path . '' ) );
471 |
472 | $this->path = self::conform_dir( $path );
473 |
474 | }
475 |
476 | /**
477 | * Get the archive method that was used for the backup
478 | *
479 | * Will be either zip, ZipArchive or PclZip
480 | *
481 | * @access public
482 | */
483 | public function get_archive_method() {
484 | return $this->archive_method;
485 | }
486 |
487 | /**
488 | * Get the database dump method that was used for the backup
489 | *
490 | * Will be either mysqldump or mysqldump_fallback
491 | *
492 | * @access public
493 | */
494 | public function get_mysqldump_method() {
495 | return $this->mysqldump_method;
496 | }
497 |
498 | /**
499 | * Whether or not to use the file manifest
500 | *
501 | * @access public
502 | */
503 | public function is_using_file_manifest() {
504 | return apply_filters( 'hmbkp_use_file_manifest', (bool)$this->using_file_manifest );
505 | }
506 |
507 | /**
508 | * Set whether or not to use the file manifest
509 | *
510 | * @access public
511 | */
512 | public function set_is_using_file_manifest( $val ) {
513 | $this->using_file_manifest = (bool)$val;
514 | }
515 |
516 | /**
517 | * Create a series of file manifests for the backup
518 | *
519 | * @access private
520 | */
521 | private function create_file_manifests() {
522 |
523 | if ( is_dir( $this->get_file_manifest_dirpath() ) )
524 | $this->rmdir_recursive( $this->get_file_manifest_dirpath() );
525 |
526 | mkdir( $this->get_file_manifest_dirpath(), 0755 );
527 |
528 | // Protect against directory browsing by including a index.html file
529 | $index = $this->get_file_manifest_dirpath() . '/index.html';
530 | if ( ! file_exists( $index ) && is_writable( $this->get_file_manifest_dirpath() ) )
531 | file_put_contents( $index, '' );
532 |
533 | $excludes = $this->exclude_string( 'regex' );
534 |
535 | $file_manifest = array();
536 | $this->file_manifest_remaining = 0;
537 | $file_manifest_file_count = 0;
538 | $current_batch = 0;
539 | foreach( $this->get_files() as $file ) {
540 |
541 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
542 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
543 | continue;
544 |
545 | // Skip unreadable files
546 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
547 | continue;
548 |
549 | // Excludes
550 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
551 | continue;
552 |
553 | if ( $file->isDir() )
554 | $line = trailingslashit( str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) );
555 |
556 | elseif ( $file->isFile() )
557 | $line = str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) );
558 |
559 | // File manifest is full
560 | if ( ! empty( $current_file ) && $current_batch >= $this->file_manifest_per_batch ) {
561 |
562 | @fclose( $current_file );
563 | $current_file = false;
564 |
565 | }
566 |
567 | // Create a new file manifest
568 | if ( empty( $current_file ) ) {
569 |
570 | $file_manifest_file_count++;
571 | $file_manifest_filename = str_pad( $file_manifest_file_count, 10, "0", STR_PAD_LEFT );
572 | if ( ! $current_file = @fopen( $this->get_file_manifest_dirpath() . '/' . $file_manifest_filename . '.txt', 'w' ) )
573 | return false;
574 |
575 | $current_batch = 0;
576 | }
577 |
578 | // Write the line to the file manifest if it isn't empty for some reason
579 | if ( ! empty( $line ) ) {
580 | @fwrite( $current_file, $line . PHP_EOL );
581 | unset( $line );
582 | $this->file_manifest_remaining++;
583 | $current_batch++;
584 | }
585 |
586 | }
587 |
588 | @file_put_contents( $this->get_path() . '/.file-manifest-remaining', $this->file_manifest_remaining );
589 |
590 | return true;
591 | }
592 |
593 | /**
594 | * Delete the current file manifest
595 | *
596 | * @access private
597 | */
598 | private function delete_current_file_manifest() {
599 |
600 | if ( ! file_exists( $this->current_file_manifest ) )
601 | return false;
602 |
603 | // Remove the file manifest because it's already been archived
604 | unlink( $this->current_file_manifest );
605 |
606 | // Update the count of remaining files.
607 | $this->file_manifest_remaining = $this->file_manifest_remaining - count( $this->file_manifest_already_archived );
608 | if ( $this->file_manifest_remaining < 0 )
609 | $this->file_manifest_remaining = 0;
610 | file_put_contents( $this->get_path() . '/.file-manifest-remaining', $this->file_manifest_remaining );
611 |
612 | $this->file_manifest_already_archived = array();
613 |
614 | }
615 |
616 |
617 | /**
618 | * Get the path to the file manifest directory
619 | *
620 | * @access private
621 | */
622 | protected function get_file_manifest_dirpath() {
623 | return $this->get_path() . '/.file-manifests';
624 | }
625 |
626 | /**
627 | * Get batch of files to archive from the file manifest
628 | * Ignore any files that already have been archived
629 | *
630 | * @access private
631 | */
632 | private function get_next_files_from_file_manifest() {
633 |
634 | if ( ! is_dir( $this->get_file_manifest_dirpath() ) )
635 | return array();
636 |
637 | $files = glob( $this->get_file_manifest_dirpath() . '/*.txt' );
638 | if ( empty( $files ) )
639 | return array();
640 |
641 | $this->current_file_manifest = array_shift( $files );
642 |
643 | $files = file_get_contents( $this->current_file_manifest );
644 | $files = array_map( 'trim', explode( PHP_EOL, $files ) );
645 | if ( empty( $files ) )
646 | return array();
647 |
648 | $this->file_manifest_remaining = (int)file_get_contents( $this->get_path() . '/.file-manifest-remaining' );
649 |
650 | return $files;
651 | }
652 |
653 | /**
654 | * Get the backup type
655 | *
656 | * Defaults to complete
657 | *
658 | * @access public
659 | */
660 | public function get_type() {
661 |
662 | if ( empty( $this->type ) )
663 | $this->set_type( 'complete' );
664 |
665 | return $this->type;
666 |
667 | }
668 |
669 | /**
670 | * Set the backup type
671 | *
672 | * $type must be one of complete, database or file
673 | * @param string $type
674 | * @throws Exception
675 | */
676 | public function set_type( $type ) {
677 |
678 | if ( ! is_string( $type ) || ! in_array( $type, array( 'file', 'database', 'complete' ) ) )
679 | throw new Exception( sprintf( __( 'Invalid backup type %s must be one of (string) file, database or complete', 'wpremote' ), '' . $type . '' ) );
680 |
681 | $this->type = $type;
682 |
683 | }
684 |
685 | /**
686 | * Get the path to the mysqldump bin
687 | *
688 | * If not explicitly set will attempt to work
689 | * it out by checking common locations
690 | *
691 | * @access public
692 | * @return string
693 | */
694 | public function get_mysqldump_command_path() {
695 |
696 | // Check shell_exec is available
697 | if ( ! self::is_shell_exec_available() )
698 | return '';
699 |
700 | // Return now if it's already been set
701 | if ( isset( $this->mysqldump_command_path ) )
702 | return $this->mysqldump_command_path;
703 |
704 | $this->mysqldump_command_path = '';
705 |
706 | // Does mysqldump work
707 | if ( is_null( shell_exec( 'hash mysqldump 2>&1' ) ) ) {
708 |
709 | // If so store it for later
710 | $this->set_mysqldump_command_path( 'mysqldump' );
711 |
712 | // And return now
713 | return $this->mysqldump_command_path;
714 |
715 | }
716 |
717 | // List of possible mysqldump locations
718 | $mysqldump_locations = array(
719 | '/usr/local/bin/mysqldump',
720 | '/usr/local/mysql/bin/mysqldump',
721 | '/usr/mysql/bin/mysqldump',
722 | '/usr/bin/mysqldump',
723 | '/opt/local/lib/mysql6/bin/mysqldump',
724 | '/opt/local/lib/mysql5/bin/mysqldump',
725 | '/opt/local/lib/mysql4/bin/mysqldump',
726 | '/xampp/mysql/bin/mysqldump',
727 | '/Program Files/xampp/mysql/bin/mysqldump',
728 | '/Program Files/MySQL/MySQL Server 6.0/bin/mysqldump',
729 | '/Program Files/MySQL/MySQL Server 5.5/bin/mysqldump',
730 | '/Program Files/MySQL/MySQL Server 5.4/bin/mysqldump',
731 | '/Program Files/MySQL/MySQL Server 5.1/bin/mysqldump',
732 | '/Program Files/MySQL/MySQL Server 5.0/bin/mysqldump',
733 | '/Program Files/MySQL/MySQL Server 4.1/bin/mysqldump',
734 | '/opt/local/bin/mysqldump',
735 | );
736 |
737 | // Find the one which works
738 | foreach ( $mysqldump_locations as $location )
739 | if ( @is_executable( self::conform_dir( $location ) ) )
740 | $this->set_mysqldump_command_path( $location );
741 |
742 | return $this->mysqldump_command_path;
743 |
744 | }
745 |
746 | /**
747 | * Set the path to the mysqldump bin
748 | *
749 | * Setting the path to false will cause the database
750 | * dump to use the php fallback
751 | *
752 | * @access public
753 | * @param mixed $path
754 | */
755 | public function set_mysqldump_command_path( $path ) {
756 |
757 | $this->mysqldump_command_path = $path;
758 |
759 | }
760 |
761 | /**
762 | * Get the path to the zip bin
763 | *
764 | * If not explicitly set will attempt to work
765 | * it out by checking common locations
766 | *
767 | * @access public
768 | * @return string
769 | */
770 | public function get_zip_command_path() {
771 |
772 | // Check shell_exec is available
773 | if ( ! self::is_shell_exec_available() )
774 | return '';
775 |
776 | // Return now if it's already been set
777 | if ( isset( $this->zip_command_path ) )
778 | return $this->zip_command_path;
779 |
780 | $this->zip_command_path = '';
781 |
782 | // Does zip work
783 | if ( is_null( shell_exec( 'hash zip 2>&1' ) ) ) {
784 |
785 | // If so store it for later
786 | $this->set_zip_command_path( 'zip' );
787 |
788 | // And return now
789 | return $this->zip_command_path;
790 |
791 | }
792 |
793 | // List of possible zip locations
794 | $zip_locations = array(
795 | '/usr/bin/zip',
796 | '/opt/local/bin/zip',
797 | );
798 |
799 | // Find the one which works
800 | foreach ( $zip_locations as $location )
801 | if ( @is_executable( self::conform_dir( $location ) ) )
802 | $this->set_zip_command_path( $location );
803 |
804 | return $this->zip_command_path;
805 |
806 | }
807 |
808 | /**
809 | * Set the path to the zip bin
810 | *
811 | * Setting the path to false will cause the database
812 | * dump to use the php fallback
813 | *
814 | * @access public
815 | * @param mixed $path
816 | */
817 | public function set_zip_command_path( $path ) {
818 |
819 | $this->zip_command_path = $path;
820 |
821 | }
822 |
823 | /**
824 | * Set up the ZipArchive instance if ZipArchive is available
825 | */
826 | protected function &setup_ziparchive() {
827 |
828 | // Instance is already open
829 | if ( ! empty( $this->ziparchive ) ) {
830 | $this->ziparchive->open( $this->get_archive_filepath(), ZIPARCHIVE::CREATE );
831 | return $this->ziparchive;
832 | }
833 |
834 | $ziparchive = new ZipArchive;
835 |
836 | // Try opening ZipArchive
837 | if ( ! file_exists( $this->get_archive_filepath() ) )
838 | $ret = $ziparchive->open( $this->get_archive_filepath(), ZIPARCHIVE::CREATE );
839 | else
840 | $ret = $ziparchive->open( $this->get_archive_filepath() );
841 |
842 | // File couldn't be opened
843 | if ( ! $ret )
844 | return false;
845 |
846 | // Try closing ZipArchive
847 | $ret = $ziparchive->close();
848 |
849 | // File couldn't be closed
850 | if ( ! $ret )
851 | return false;
852 |
853 | // Open it once more
854 | if ( ! file_exists( $this->get_archive_filepath() ) )
855 | $ziparchive->open( $this->get_archive_filepath(), ZIPARCHIVE::CREATE );
856 | else
857 | $ziparchive->open( $this->get_archive_filepath() );
858 |
859 | $this->ziparchive = $ziparchive;
860 | return $this->ziparchive;
861 | }
862 |
863 | /**
864 | * Set up the PclZip instance
865 | *
866 | * @access protected
867 | */
868 | protected function &setup_pclzip() {
869 |
870 | if ( empty( $this->pclzip ) ) {
871 | $this->load_pclzip();
872 | $this->pclzip = new PclZip( $this->get_archive_filepath() );
873 | }
874 | return $this->pclzip;
875 | }
876 |
877 | protected function do_action( $action ) {
878 |
879 | do_action( $action, $this );
880 |
881 | }
882 |
883 | /**
884 | * Kick off a backup
885 | *
886 | * @access public
887 | * @return bool
888 | */
889 | public function backup() {
890 |
891 | $this->do_action( 'hmbkp_backup_started' );
892 |
893 | // Backup database
894 | if ( $this->get_type() !== 'file' )
895 | $this->dump_database();
896 |
897 | // Zip everything up
898 | $this->archive();
899 |
900 | $this->do_action( 'hmbkp_backup_complete' );
901 |
902 | }
903 |
904 | /**
905 | * Create the mysql backup
906 | *
907 | * Uses mysqldump if available, falls back to PHP
908 | * if not.
909 | *
910 | * @access public
911 | */
912 | public function dump_database() {
913 |
914 | if ( $this->get_mysqldump_command_path() )
915 | $this->mysqldump();
916 |
917 | if ( empty( $this->mysqldump_verified ) )
918 | $this->mysqldump_fallback();
919 |
920 | $this->do_action( 'hmbkp_mysqldump_finished' );
921 |
922 | }
923 |
924 | public function mysqldump() {
925 |
926 | $this->mysqldump_method = 'mysqldump';
927 |
928 | $this->do_action( 'hmbkp_mysqldump_started' );
929 |
930 | $host = explode( ':', DB_HOST );
931 |
932 | $host = reset( $host );
933 | $port = strpos( DB_HOST, ':' ) ? end( explode( ':', DB_HOST ) ) : '';
934 |
935 | // Path to the mysqldump executable
936 | $cmd = escapeshellarg( $this->get_mysqldump_command_path() );
937 |
938 | // We don't want to create a new DB
939 | $cmd .= ' --no-create-db';
940 |
941 | // Allow lock-tables to be overridden
942 | if ( ! defined( 'HMBKP_MYSQLDUMP_SINGLE_TRANSACTION' ) || HMBKP_MYSQLDUMP_SINGLE_TRANSACTION !== false )
943 | $cmd .= ' --single-transaction';
944 |
945 | // Make sure binary data is exported properly
946 | $cmd .= ' --hex-blob';
947 |
948 | // Username
949 | $cmd .= ' -u ' . escapeshellarg( DB_USER );
950 |
951 | // Don't pass the password if it's blank
952 | if ( DB_PASSWORD )
953 | $cmd .= ' -p' . escapeshellarg( DB_PASSWORD );
954 |
955 | // Set the host
956 | $cmd .= ' -h ' . escapeshellarg( $host );
957 |
958 | // Set the port if it was set
959 | if ( ! empty( $port ) && is_numeric( $port ) )
960 | $cmd .= ' -P ' . $port;
961 |
962 | // The file we're saving too
963 | $cmd .= ' -r ' . escapeshellarg( $this->get_database_dump_filepath() );
964 |
965 | // The database we're dumping
966 | $cmd .= ' ' . escapeshellarg( DB_NAME );
967 |
968 | // Pipe STDERR to STDOUT
969 | $cmd .= ' 2>&1';
970 |
971 | // Store any returned data in an error
972 | $stderr = shell_exec( $cmd );
973 |
974 | // Skip the new password warning that is output in mysql > 5.6 (@see http://bugs.mysql.com/bug.php?id=66546)
975 | if ( trim( $stderr ) === 'Warning: Using a password on the command line interface can be insecure.' ) {
976 | $stderr = '';
977 | }
978 |
979 | if ( $stderr ) {
980 | $this->error( $this->get_mysqldump_method(), $stderr );
981 | }
982 |
983 | $this->verify_mysqldump();
984 |
985 | }
986 |
987 | /**
988 | * PHP mysqldump fallback functions, exports the database to a .sql file
989 | *
990 | * @access public
991 | */
992 | public function mysqldump_fallback() {
993 |
994 | $this->errors_to_warnings( $this->get_mysqldump_method() );
995 |
996 | $this->mysqldump_method = 'mysqldump_fallback';
997 |
998 | $this->do_action( 'hmbkp_mysqldump_started' );
999 |
1000 | $this->db = @mysqli_connect( 'p:'.DB_HOST, DB_USER, DB_PASSWORD, DB_NAME );
1001 |
1002 | if ( ! $this->db )
1003 | $this->db = mysqli_connect( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME );
1004 |
1005 | if ( ! $this->db )
1006 | return;
1007 |
1008 | // mysql_select_db( DB_NAME, $this->db );
1009 |
1010 | if ( function_exists( 'mysqli_set_charset') )
1011 | mysqli_set_charset( $this->db, DB_CHARSET );
1012 |
1013 | // Begin new backup of MySql
1014 | $tables = mysqli_query( $this->db, 'SHOW TABLES' );
1015 |
1016 | $sql_file = "# WordPress : " . get_bloginfo( 'url' ) . " MySQL database backup\n";
1017 | $sql_file .= "#\n";
1018 | $sql_file .= "# Generated: " . date( 'l j. F Y H:i T' ) . "\n";
1019 | $sql_file .= "# Hostname: " . DB_HOST . "\n";
1020 | $sql_file .= "# Database: " . $this->sql_backquote( DB_NAME ) . "\n";
1021 | $sql_file .= "# --------------------------------------------------------\n";
1022 |
1023 | while ( $row = mysqli_fetch_array($tables) ) {
1024 | $curr_table = $row[0];
1025 |
1026 | // Create the SQL statements
1027 | $sql_file .= "# --------------------------------------------------------\n";
1028 | $sql_file .= "# Table: " . $this->sql_backquote( $curr_table ) . "\n";
1029 | $sql_file .= "# --------------------------------------------------------\n";
1030 |
1031 | $this->make_sql( $sql_file, $curr_table );
1032 |
1033 | }
1034 |
1035 | }
1036 |
1037 | /**
1038 | * Zip up all the files.
1039 | *
1040 | * Attempts to use the shell zip command, if
1041 | * thats not available then it falls back to
1042 | * PHP ZipArchive and finally PclZip.
1043 | *
1044 | * @access public
1045 | */
1046 | public function archive() {
1047 |
1048 | // If using a manifest, perform the backup in chunks
1049 | if ( 'database' !== $this->get_type()
1050 | && $this->is_using_file_manifest()
1051 | && $this->create_file_manifests() ) {
1052 |
1053 | $this->archive_via_file_manifest();
1054 |
1055 | } else {
1056 |
1057 | $this->archive_via_single_request();
1058 |
1059 | }
1060 |
1061 | }
1062 |
1063 | /**
1064 | * Archive with a file manifest
1065 | *
1066 | * @access private
1067 | */
1068 | private function archive_via_file_manifest() {
1069 |
1070 | $errors = array();
1071 |
1072 | // Back up files from the file manifest in chunks
1073 | $next_files = $this->get_next_files_from_file_manifest();
1074 | do {
1075 |
1076 | $this->do_action( 'hmbkp_archive_started' );
1077 |
1078 | // `zip` is the most performant archive method
1079 | if ( $this->get_zip_command_path() ) {
1080 | $this->archive_method = 'zip_files';
1081 | $error = $this->zip_files( $next_files );
1082 | }
1083 |
1084 | // ZipArchive is also pretty fast for chunked backups
1085 | else if ( class_exists( 'ZipArchive' ) && empty( $this->skip_zip_archive ) ) {
1086 | $this->archive_method = 'zip_archive_files';
1087 |
1088 | $ret = $this->zip_archive_files( $next_files );
1089 | if ( ! $ret ) {
1090 | $this->skip_zip_archive = true;
1091 | continue;
1092 | }
1093 | }
1094 |
1095 | // Last opportunity
1096 | else {
1097 | $this->archive_method = 'pcl_zip_files';
1098 | $error = $this->pcl_zip_files( $next_files );
1099 | }
1100 |
1101 | if ( ! empty( $error ) ) {
1102 | $errors[] = $error;
1103 | unset( $error );
1104 | }
1105 |
1106 | // Update the file manifest with these files that were archived
1107 | $this->file_manifest_already_archived = array_merge( $this->file_manifest_already_archived, $next_files );
1108 | $this->delete_current_file_manifest();
1109 |
1110 | // Get the next set of files to archive
1111 | $next_files = $this->get_next_files_from_file_manifest();
1112 |
1113 | } while( ! empty( $next_files ) );
1114 |
1115 | // If the database should be included in the backup, it's included last
1116 | if ( 'file' !== $this->get_type() && file_exists( $this->get_database_dump_filepath() ) ) {
1117 |
1118 | switch ( $this->archive_method ) {
1119 |
1120 | case 'zip_archive_files':
1121 |
1122 | $zip = &$this->setup_ziparchive();
1123 |
1124 | $zip->addFile( $this->get_database_dump_filepath(), $this->get_database_dump_filename() );
1125 |
1126 | $zip->close();
1127 |
1128 | break;
1129 |
1130 | case 'zip_files':
1131 |
1132 | $error = shell_exec( 'cd ' . escapeshellarg( $this->get_path() ) . ' && ' . escapeshellcmd( $this->get_zip_command_path() ) . ' -uq ' . escapeshellarg( $this->get_archive_filepath() ) . ' ' . escapeshellarg( $this->get_database_dump_filename() ) . ' 2>&1' );
1133 |
1134 | break;
1135 |
1136 | case 'pcl_zip_files':
1137 |
1138 | $pclzip = &$this->setup_pclzip();
1139 |
1140 | if ( ! $pclzip->add( $this->get_database_dump_filepath(), PCLZIP_OPT_REMOVE_PATH, $this->get_path() ) )
1141 | $this->warning( $this->get_archive_method(), $pclzip->errorInfo( true ) );
1142 |
1143 | break;
1144 | }
1145 |
1146 | if ( ! empty( $error ) ) {
1147 | $errors[] = $error;
1148 | unset( $error );
1149 | }
1150 | }
1151 |
1152 | // If the methods produced any errors, log them
1153 | if ( ! empty( $errors ) )
1154 | $this->warning( $this->get_archive_method(), implode( ', ', $errors ) );
1155 |
1156 | // ZipArchive has some special reporting requirements
1157 | if ( ! empty( $this->ziparchive ) ) {
1158 |
1159 | if ( $this->ziparchive->status )
1160 | $this->warning( $this->get_archive_method(), $this->ziparchive->status );
1161 |
1162 | if ( $this->ziparchive->statusSys )
1163 | $this->warning( $this->get_archive_method(), $this->ziparchive->statusSys );
1164 |
1165 | }
1166 |
1167 | // Verify and remove if errors
1168 | $this->verify_archive();
1169 |
1170 | // Remove the file manifest
1171 | if ( is_dir( $this->get_file_manifest_dirpath() ) )
1172 | $this->rmdir_recursive( $this->get_file_manifest_dirpath() );
1173 |
1174 | // Delete the database dump file
1175 | if ( file_exists( $this->get_database_dump_filepath() ) )
1176 | unlink( $this->get_database_dump_filepath() );
1177 |
1178 | $this->do_action( 'hmbkp_archive_finished' );
1179 |
1180 | }
1181 |
1182 | /**
1183 | * Archive using our traditional method of one request
1184 | *
1185 | * @access private
1186 | */
1187 | private function archive_via_single_request() {
1188 |
1189 | // Do we have the path to the zip command
1190 | if ( $this->get_zip_command_path() )
1191 | $this->zip();
1192 |
1193 | // If not or if the shell zip failed then use ZipArchive
1194 | if ( empty( $this->archive_verified ) && class_exists( 'ZipArchive' ) && empty( $this->skip_zip_archive ) )
1195 | $this->zip_archive();
1196 |
1197 | // If ZipArchive is unavailable or one of the above failed
1198 | if ( empty( $this->archive_verified ) )
1199 | $this->pcl_zip();
1200 |
1201 | // Delete the database dump file
1202 | if ( file_exists( $this->get_database_dump_filepath() ) )
1203 | unlink( $this->get_database_dump_filepath() );
1204 |
1205 | $this->do_action( 'hmbkp_archive_finished' );
1206 |
1207 | }
1208 |
1209 | /**
1210 | * Restart a failed archive process
1211 | *
1212 | * @access public
1213 | */
1214 | public function restart_archive() {
1215 |
1216 | if ( $this->is_using_file_manifest() ) {
1217 |
1218 | $this->archive_via_file_manifest();
1219 |
1220 | } else {
1221 |
1222 | $this->archive_via_single_request();
1223 |
1224 | }
1225 |
1226 | $this->do_action( 'hmbkp_backup_complete' );
1227 | }
1228 |
1229 | /**
1230 | * Zip using the native zip command
1231 | *
1232 | * @access public
1233 | */
1234 | public function zip() {
1235 |
1236 | $this->archive_method = 'zip';
1237 |
1238 | $this->do_action( 'hmbkp_archive_started' );
1239 |
1240 | // Zip up $this->root with excludes
1241 | if ( $this->get_type() !== 'database' && $this->exclude_string( 'zip' ) ) {
1242 | $stderr = shell_exec( 'cd ' . escapeshellarg( $this->get_root() ) . ' && ' . escapeshellcmd( $this->get_zip_command_path() ) . ' -rq ' . escapeshellarg( $this->get_archive_filepath() ) . ' ./' . ' -x ' . $this->exclude_string( 'zip' ) . ' 2>&1' );
1243 |
1244 | // Zip up $this->root without excludes
1245 | } elseif ( $this->get_type() !== 'database' ) {
1246 | $stderr = shell_exec( 'cd ' . escapeshellarg( $this->get_root() ) . ' && ' . escapeshellcmd( $this->get_zip_command_path() ) . ' -rq ' . escapeshellarg( $this->get_archive_filepath() ) . ' ./' . ' 2>&1' );
1247 |
1248 | }
1249 |
1250 | // Add the database dump to the archive
1251 | if ( $this->get_type() !== 'file' && file_exists( $this->get_database_dump_filepath() ) )
1252 | $stderr = shell_exec( 'cd ' . escapeshellarg( $this->get_path() ) . ' && ' . escapeshellcmd( $this->get_zip_command_path() ) . ' -uq ' . escapeshellarg( $this->get_archive_filepath() ) . ' ' . escapeshellarg( $this->get_database_dump_filename() ) . ' 2>&1' );
1253 |
1254 | if ( ! empty( $stderr ) )
1255 | $this->warning( $this->get_archive_method(), $stderr );
1256 |
1257 | $this->verify_archive();
1258 | }
1259 |
1260 | /**
1261 | * Zip one or more files
1262 | *
1263 | * @access private
1264 | */
1265 | private function zip_files( $files ) {
1266 |
1267 | // Not necessary to include directories when using `zip`
1268 | foreach( $files as $key => $file ) {
1269 |
1270 | if ( ! is_dir( $file ) )
1271 | continue;
1272 |
1273 | unset( $files[$key] );
1274 | }
1275 |
1276 | return shell_exec( 'cd ' . escapeshellarg( $this->get_root() ) . ' && ' . escapeshellcmd( $this->get_zip_command_path() ) . ' ' . escapeshellarg( $this->get_archive_filepath() ) . ' ' . implode( ' ', $files ) . ' -q 2>&1' );
1277 | }
1278 |
1279 | /**
1280 | * Fallback for creating zip archives if zip command is
1281 | * unavailable.
1282 | */
1283 | public function zip_archive() {
1284 |
1285 | $this->errors_to_warnings( $this->get_archive_method() );
1286 | $this->archive_method = 'ziparchive';
1287 |
1288 | $this->do_action( 'hmbkp_archive_started' );
1289 |
1290 | if ( false === ( $zip = &$this->setup_ziparchive() ) )
1291 | return;
1292 |
1293 | $excludes = $this->exclude_string( 'regex' );
1294 |
1295 | if ( $this->get_type() !== 'database' ) {
1296 |
1297 | $files_added = 0;
1298 |
1299 | foreach ( $this->get_files() as $file ) {
1300 |
1301 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1302 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1303 | continue;
1304 |
1305 | // Skip unreadable files
1306 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1307 | continue;
1308 |
1309 | // Excludes
1310 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
1311 | continue;
1312 |
1313 | if ( $file->isDir() )
1314 | $zip->addEmptyDir( trailingslashit( str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) );
1315 |
1316 | elseif ( $file->isFile() )
1317 | $zip->addFile( $file->getPathname(), str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) );
1318 |
1319 | if ( ++$files_added % 500 === 0 )
1320 | if ( ! $zip->close() || ! $zip->open( $this->get_archive_filepath(), ZIPARCHIVE::CREATE ) )
1321 | return;
1322 |
1323 | }
1324 |
1325 | }
1326 |
1327 | // Add the database
1328 | if ( $this->get_type() !== 'file' && file_exists( $this->get_database_dump_filepath() ) )
1329 | $zip->addFile( $this->get_database_dump_filepath(), $this->get_database_dump_filename() );
1330 |
1331 | if ( $zip->status )
1332 | $this->warning( $this->get_archive_method(), $zip->status );
1333 |
1334 | if ( $zip->statusSys )
1335 | $this->warning( $this->get_archive_method(), $zip->statusSys );
1336 |
1337 | $zip->close();
1338 |
1339 | $this->verify_archive();
1340 |
1341 | }
1342 |
1343 | /**
1344 | * Zip Archive one or more files
1345 | *
1346 | * @access private
1347 | */
1348 | private function zip_archive_files( $files ) {
1349 |
1350 | if ( false === ( $zip = &$this->setup_ziparchive() ) )
1351 | return false;
1352 |
1353 | foreach( $files as $file ) {
1354 |
1355 | $full_path = trailingslashit( $this->get_root() ) . $file;
1356 | if ( is_dir( $full_path ) )
1357 | $zip->addEmptyDir( $file );
1358 | else
1359 | $zip->addFile( $full_path, $file );
1360 |
1361 | }
1362 |
1363 | // Avoid limitations in ZipArchive by making sure we save each batch to disk
1364 | $zip->close();
1365 | return true;
1366 | }
1367 |
1368 | /**
1369 | * Fallback for creating zip archives if zip command and ZipArchive are
1370 | * unavailable.
1371 | *
1372 | * Uses the PclZip library that ships with WordPress
1373 | */
1374 | public function pcl_zip() {
1375 |
1376 | $this->errors_to_warnings( $this->get_archive_method() );
1377 | $this->archive_method = 'pclzip';
1378 |
1379 | $this->do_action( 'hmbkp_archive_started' );
1380 |
1381 | global $_wprp_hmbkp_exclude_string;
1382 |
1383 | $_wprp_hmbkp_exclude_string = $this->exclude_string( 'regex' );
1384 |
1385 | $archive = &$this->setup_pclzip();
1386 |
1387 | // Zip up everything
1388 | if ( $this->get_type() !== 'database' )
1389 | if ( ! $archive->add( $this->get_root(), PCLZIP_OPT_REMOVE_PATH, $this->get_root(), PCLZIP_CB_PRE_ADD, 'wprp_hmbkp_pclzip_callback' ) )
1390 | $this->warning( $this->get_archive_method(), $archive->errorInfo( true ) );
1391 |
1392 | // Add the database
1393 | if ( $this->get_type() !== 'file' && file_exists( $this->get_database_dump_filepath() ) )
1394 | if ( ! $archive->add( $this->get_database_dump_filepath(), PCLZIP_OPT_REMOVE_PATH, $this->get_path() ) )
1395 | $this->warning( $this->get_archive_method(), $archive->errorInfo( true ) );
1396 |
1397 | unset( $GLOBALS['_wprp_hmbkp_exclude_string'] );
1398 |
1399 | $this->verify_archive();
1400 |
1401 | }
1402 |
1403 | /**
1404 | * Use PclZip to archive batches of files
1405 | */
1406 | private function pcl_zip_files( $files ) {
1407 |
1408 | $this->errors_to_warnings( $this->get_archive_method() );
1409 |
1410 | $pclzip = &$this->setup_pclzip();
1411 |
1412 | foreach( $files as $file ) {
1413 |
1414 | $full_path = trailingslashit( $this->get_root() ) . $file;
1415 | if ( is_dir( $full_path ) )
1416 | continue;
1417 |
1418 | if ( ! $pclzip->add( $full_path, PCLZIP_OPT_REMOVE_PATH, $this->get_root() ) )
1419 | $this->warning( $this->get_archive_method(), $pclzip->errorInfo( true ) );
1420 |
1421 | }
1422 |
1423 | }
1424 |
1425 | public function verify_mysqldump() {
1426 |
1427 | $this->do_action( 'hmbkp_mysqldump_verify_started' );
1428 |
1429 | // If we've already passed then no need to check again
1430 | if ( ! empty( $this->mysqldump_verified ) )
1431 | return true;
1432 |
1433 | // If there are mysqldump errors delete the database dump file as mysqldump will still have written one
1434 | if ( $this->get_errors( $this->get_mysqldump_method() ) && file_exists( $this->get_database_dump_filepath() ) )
1435 | unlink( $this->get_database_dump_filepath() );
1436 |
1437 | // If we have an empty file delete it
1438 | if ( @filesize( $this->get_database_dump_filepath() ) === 0 )
1439 | unlink( $this->get_database_dump_filepath() );
1440 |
1441 | // If the file still exists then it must be good
1442 | if ( file_exists( $this->get_database_dump_filepath() ) )
1443 | return $this->mysqldump_verified = true;
1444 |
1445 | return false;
1446 |
1447 |
1448 | }
1449 |
1450 | /**
1451 | * Verify that the archive is valid and contains all the files it should contain.
1452 | *
1453 | * @access public
1454 | * @return bool
1455 | */
1456 | public function verify_archive() {
1457 |
1458 | $this->do_action( 'hmbkp_archive_verify_started' );
1459 |
1460 | // If we've already passed then no need to check again
1461 | if ( ! empty( $this->archive_verified ) )
1462 | return true;
1463 |
1464 | // If there are errors delete the backup file.
1465 | if ( $this->get_errors( $this->get_archive_method() ) && file_exists( $this->get_archive_filepath() ) )
1466 | unlink( $this->get_archive_filepath() );
1467 |
1468 | // If the archive file still exists assume it's good
1469 | if ( file_exists( $this->get_archive_filepath() ) )
1470 | return $this->archive_verified = true;
1471 |
1472 | return false;
1473 |
1474 | }
1475 |
1476 | /**
1477 | * Return an array of all files in the filesystem
1478 | *
1479 | * @access public
1480 | * @return array
1481 | */
1482 | public function get_files() {
1483 |
1484 | if ( ! empty( $this->files ) )
1485 | return $this->files;
1486 |
1487 | $this->files = array();
1488 |
1489 | // We only want to use the RecursiveDirectoryIterator if the FOLLOW_SYMLINKS flag is available
1490 | if ( defined( 'RecursiveDirectoryIterator::FOLLOW_SYMLINKS' ) ) {
1491 |
1492 | $this->files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $this->get_root(), RecursiveDirectoryIterator::FOLLOW_SYMLINKS ), RecursiveIteratorIterator::SELF_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD );
1493 |
1494 | // Skip dot files if the SKIP_Dots flag is available
1495 | if ( defined( 'RecursiveDirectoryIterator::SKIP_DOTS' ) )
1496 | $this->files->setFlags( RecursiveDirectoryIterator::SKIP_DOTS + RecursiveDirectoryIterator::FOLLOW_SYMLINKS );
1497 |
1498 |
1499 | // If RecursiveDirectoryIterator::FOLLOW_SYMLINKS isn't available then fallback to a less memory efficient method
1500 | } else {
1501 |
1502 | $this->files = $this->get_files_fallback( $this->get_root() );
1503 |
1504 | }
1505 |
1506 | return $this->files;
1507 |
1508 | }
1509 |
1510 | /**
1511 | * Fallback function for generating a filesystem
1512 | * array
1513 | *
1514 | * Used if RecursiveDirectoryIterator::FOLLOW_SYMLINKS isn't available
1515 | *
1516 | * @access private
1517 | * @param string $dir
1518 | * @param array $files. (default: array())
1519 | * @return array
1520 | */
1521 | private function get_files_fallback( $dir, $files = array() ) {
1522 |
1523 | $handle = opendir( $dir );
1524 |
1525 | $excludes = $this->exclude_string( 'regex' );
1526 |
1527 | while ( $file = readdir( $handle ) ) :
1528 |
1529 | // Ignore current dir and containing dir
1530 | if ( $file === '.' || $file === '..' )
1531 | continue;
1532 |
1533 | $filepath = self::conform_dir( trailingslashit( $dir ) . $file );
1534 | $file = str_ireplace( trailingslashit( $this->get_root() ), '', $filepath );
1535 |
1536 | $files[] = new SplFileInfo( $filepath );
1537 |
1538 | if ( is_dir( $filepath ) )
1539 | $files = $this->get_files_fallback( $filepath, $files );
1540 |
1541 | endwhile;
1542 |
1543 | return $files;
1544 |
1545 | }
1546 |
1547 | /**
1548 | * Returns an array of files that will be included in the backup.
1549 | *
1550 | * @access public
1551 | * @return array
1552 | */
1553 | public function get_included_files() {
1554 |
1555 | if ( ! empty( $this->included_files ) )
1556 | return $this->included_files;
1557 |
1558 | $this->included_files = array();
1559 |
1560 | $excludes = $this->exclude_string( 'regex' );
1561 |
1562 | foreach ( $this->get_files() as $file ) {
1563 |
1564 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1565 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1566 | continue;
1567 |
1568 | // Skip unreadable files
1569 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1570 | continue;
1571 |
1572 | // Excludes
1573 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
1574 | continue;
1575 |
1576 | $this->included_files[] = $file;
1577 |
1578 | }
1579 |
1580 | return $this->included_files;
1581 |
1582 | }
1583 |
1584 | /**
1585 | * Return the number of files included in the backup
1586 | *
1587 | * @access public
1588 | * @return array
1589 | */
1590 | public function get_included_file_count() {
1591 |
1592 | if ( ! empty( $this->included_file_count ) )
1593 | return $this->included_file_count;
1594 |
1595 | $this->included_file_count = 0;
1596 |
1597 | $excludes = $this->exclude_string( 'regex' );
1598 |
1599 | foreach ( $this->get_files() as $file ) {
1600 |
1601 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1602 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1603 | continue;
1604 |
1605 | // Skip unreadable files
1606 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1607 | continue;
1608 |
1609 | // Excludes
1610 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
1611 | continue;
1612 |
1613 | $this->included_file_count++;
1614 |
1615 | }
1616 |
1617 | return $this->included_file_count;
1618 |
1619 | }
1620 |
1621 | /**
1622 | * Returns an array of files that match the exclude rules.
1623 | *
1624 | * @access public
1625 | * @return array
1626 | */
1627 | public function get_excluded_files() {
1628 |
1629 | if ( ! empty( $this->excluded_files ) )
1630 | return $this->excluded_files;
1631 |
1632 | $this->excluded_files = array();
1633 |
1634 | $excludes = $this->exclude_string( 'regex' );
1635 |
1636 | foreach ( $this->get_files() as $file ) {
1637 |
1638 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1639 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1640 | continue;
1641 |
1642 | // Skip unreadable files
1643 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1644 | continue;
1645 |
1646 | // Excludes
1647 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
1648 | $this->excluded_files[] = $file;
1649 |
1650 | }
1651 |
1652 | return $this->excluded_files;
1653 |
1654 | }
1655 |
1656 | /**
1657 | * Return the number of files excluded from the backup
1658 | *
1659 | * @access public
1660 | * @return array
1661 | */
1662 | public function get_excluded_file_count() {
1663 |
1664 | if ( ! empty( $this->excluded_file_count ) )
1665 | return $this->excluded_file_count;
1666 |
1667 | $this->excluded_file_count = 0;
1668 |
1669 | $excludes = $this->exclude_string( 'regex' );
1670 |
1671 | foreach ( $this->get_files() as $file ) {
1672 |
1673 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1674 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1675 | continue;
1676 |
1677 | // Skip unreadable files
1678 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1679 | continue;
1680 |
1681 | // Excludes
1682 | if ( $excludes && preg_match( '(' . $excludes . ')', str_ireplace( trailingslashit( $this->get_root() ), '', self::conform_dir( $file->getPathname() ) ) ) )
1683 | $this->excluded_file_count++;
1684 |
1685 | }
1686 |
1687 | return $this->excluded_file_count;
1688 |
1689 | }
1690 |
1691 | /**
1692 | * Returns an array of unreadable files.
1693 | *
1694 | * @access public
1695 | * @return array
1696 | */
1697 | public function get_unreadable_files() {
1698 |
1699 | if ( ! empty( $this->unreadable_files ) )
1700 | return $this->unreadable_files;
1701 |
1702 | $this->unreadable_files = array();
1703 |
1704 | foreach ( $this->get_files() as $file ) {
1705 |
1706 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1707 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1708 | continue;
1709 |
1710 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1711 | $this->unreadable_files[] = $file;
1712 |
1713 | }
1714 |
1715 | return $this->unreadable_files;
1716 |
1717 | }
1718 |
1719 | /**
1720 | * Return the number of unreadable files.
1721 | *
1722 | * @access public
1723 | * @return array
1724 | */
1725 | public function get_unreadable_file_count() {
1726 |
1727 | if ( ! empty( $this->get_unreadable_file_count ) )
1728 | return $this->get_unreadable_file_count;
1729 |
1730 | $this->get_unreadable_file_count = 0;
1731 |
1732 | foreach ( $this->get_files() as $file ) {
1733 |
1734 | // Skip dot files, they should only exist on versions of PHP between 5.2.11 -> 5.3
1735 | if ( method_exists( $file, 'isDot' ) && $file->isDot() )
1736 | continue;
1737 |
1738 | if ( ! @realpath( $file->getPathname() ) || ! $file->isReadable() )
1739 | $this->get_unreadable_file_count++;
1740 |
1741 | }
1742 |
1743 | return $this->get_unreadable_file_count;
1744 |
1745 | }
1746 |
1747 | private function load_pclzip() {
1748 |
1749 | // Load PclZip
1750 | if ( ! defined( 'PCLZIP_TEMPORARY_DIR' ) )
1751 | define( 'PCLZIP_TEMPORARY_DIR', trailingslashit( $this->get_path() ) );
1752 |
1753 | require_once( ABSPATH . 'wp-admin/includes/class-pclzip.php' );
1754 |
1755 | }
1756 |
1757 | /**
1758 | * Get an array of exclude rules
1759 | *
1760 | * The backup path is automatically excluded
1761 | *
1762 | * @access public
1763 | * @return array
1764 | */
1765 | public function get_excludes() {
1766 |
1767 | $excludes = array();
1768 |
1769 | if ( isset( $this->excludes ) )
1770 | $excludes = $this->excludes;
1771 |
1772 | // If path() is inside root(), exclude it
1773 | if ( strpos( $this->get_path(), $this->get_root() ) !== false )
1774 | array_unshift( $excludes, trailingslashit( $this->get_path() ) );
1775 |
1776 | return array_unique( $excludes );
1777 |
1778 | }
1779 |
1780 | /**
1781 | * Set the excludes, expects and array
1782 | *
1783 | * @access public
1784 | * @param Array $excludes
1785 | * @param Bool $append
1786 | */
1787 | public function set_excludes( $excludes, $append = false ) {
1788 |
1789 | if ( is_string( $excludes ) )
1790 | $excludes = explode( ',', $excludes );
1791 |
1792 | if ( $append )
1793 | $excludes = array_merge( $this->excludes, $excludes );
1794 |
1795 | $this->excludes = array_filter( array_unique( array_map( 'trim', $excludes ) ) );
1796 |
1797 | }
1798 |
1799 | /**
1800 | * Generate the exclude param string for the zip backup
1801 | *
1802 | * Takes the exclude rules and formats them for use with either
1803 | * the shell zip command or pclzip
1804 | *
1805 | * @access public
1806 | * @param string $context. (default: 'zip')
1807 | * @return string
1808 | */
1809 | public function exclude_string( $context = 'zip' ) {
1810 |
1811 | // Return a comma separated list by default
1812 | $separator = ', ';
1813 | $wildcard = '';
1814 |
1815 | // The zip command
1816 | if ( $context === 'zip' ) {
1817 | $wildcard = '*';
1818 | $separator = ' -x ';
1819 |
1820 | // The PclZip fallback library
1821 | } elseif ( $context === 'regex' ) {
1822 | $wildcard = '([\s\S]*?)';
1823 | $separator = '|';
1824 |
1825 | }
1826 |
1827 | $excludes = $this->get_excludes();
1828 |
1829 | foreach( $excludes as $key => &$rule ) {
1830 |
1831 | $file = $absolute = $fragment = false;
1832 |
1833 | // Files don't end with /
1834 | if ( ! in_array( substr( $rule, -1 ), array( '\\', '/' ) ) )
1835 | $file = true;
1836 |
1837 | // If rule starts with a / then treat as absolute path
1838 | elseif ( in_array( substr( $rule, 0, 1 ), array( '\\', '/' ) ) )
1839 | $absolute = true;
1840 |
1841 | // Otherwise treat as dir fragment
1842 | else
1843 | $fragment = true;
1844 |
1845 | // Strip $this->root and conform
1846 | $rule = str_ireplace( $this->get_root(), '', untrailingslashit( self::conform_dir( $rule ) ) );
1847 |
1848 | // Strip the preceeding slash
1849 | if ( in_array( substr( $rule, 0, 1 ), array( '\\', '/' ) ) )
1850 | $rule = substr( $rule, 1 );
1851 |
1852 | // Escape string for regex
1853 | if ( $context === 'regex' )
1854 | $rule = str_replace( '.', '\.', $rule );
1855 |
1856 | // Convert any existing wildcards
1857 | if ( $wildcard !== '*' && strpos( $rule, '*' ) !== false )
1858 | $rule = str_replace( '*', $wildcard, $rule );
1859 |
1860 | // Wrap directory fragments and files in wildcards for zip
1861 | if ( $context === 'zip' && ( $fragment || $file ) )
1862 | $rule = $wildcard . $rule . $wildcard;
1863 |
1864 | // Add a wildcard to the end of absolute url for zips
1865 | if ( $context === 'zip' && $absolute )
1866 | $rule .= $wildcard;
1867 |
1868 | // Add and end carrot to files for pclzip but only if it doesn't end in a wildcard
1869 | if ( $file && $context === 'regex' )
1870 | $rule .= '$';
1871 |
1872 | // Add a start carrot to absolute urls for pclzip
1873 | if ( $absolute && $context === 'regex' )
1874 | $rule = '^' . $rule;
1875 |
1876 | }
1877 |
1878 | // Escape shell args for zip command
1879 | if ( $context === 'zip' )
1880 | $excludes = array_map( 'escapeshellarg', array_unique( $excludes ) );
1881 |
1882 | return implode( $separator, $excludes );
1883 |
1884 | }
1885 |
1886 | /**
1887 | * Add backquotes to tables and db-names in SQL queries. Taken from phpMyAdmin.
1888 | *
1889 | * @param $a_name
1890 | * @return array|string
1891 | */
1892 | private function sql_backquote( $a_name ) {
1893 |
1894 | if ( ! empty( $a_name ) && $a_name !== '*' ) {
1895 |
1896 | if ( is_array( $a_name ) ) {
1897 |
1898 | $result = array();
1899 |
1900 | reset( $a_name );
1901 |
1902 | while ( list( $key, $val ) = each( $a_name ) )
1903 | $result[$key] = '`' . $val . '`';
1904 |
1905 | return $result;
1906 |
1907 | } else {
1908 |
1909 | return '`' . $a_name . '`';
1910 |
1911 | }
1912 |
1913 | } else {
1914 |
1915 | return $a_name;
1916 |
1917 | }
1918 |
1919 | }
1920 |
1921 | /**
1922 | * Reads the Database table in $table and creates
1923 | * SQL Statements for recreating structure and data
1924 | * Taken partially from phpMyAdmin and partially from
1925 | * Alain Wolf, Zurich - Switzerland
1926 | * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1927 | *
1928 | * @access private
1929 | * @param string $sql_file
1930 | * @param string $table
1931 | */
1932 | private function make_sql( $sql_file, $table ) {
1933 |
1934 | // Add SQL statement to drop existing table
1935 | $sql_file .= "\n";
1936 | $sql_file .= "\n";
1937 | $sql_file .= "#\n";
1938 | $sql_file .= "# Delete any existing table " . $this->sql_backquote( $table ) . "\n";
1939 | $sql_file .= "#\n";
1940 | $sql_file .= "\n";
1941 | $sql_file .= "DROP TABLE IF EXISTS " . $this->sql_backquote( $table ) . ";\n";
1942 |
1943 | /* Table Structure */
1944 |
1945 | // Comment in SQL-file
1946 | $sql_file .= "\n";
1947 | $sql_file .= "\n";
1948 | $sql_file .= "#\n";
1949 | $sql_file .= "# Table structure of table " . $this->sql_backquote( $table ) . "\n";
1950 | $sql_file .= "#\n";
1951 | $sql_file .= "\n";
1952 |
1953 | // Get table structure
1954 | $query = 'SHOW CREATE TABLE ' . $this->sql_backquote( $table );
1955 | $result = mysqli_query( $this->db, $query );
1956 |
1957 | if ( $result ) {
1958 |
1959 | if ( mysqli_num_rows( $result ) > 0 ) {
1960 | $sql_create_arr = mysqli_fetch_array( $result );
1961 | $sql_file .= $sql_create_arr[1];
1962 | }
1963 |
1964 | mysqli_free_result( $result );
1965 | $sql_file .= ' ;';
1966 |
1967 | }
1968 |
1969 | /* Table Contents */
1970 |
1971 | // Get table contents
1972 | $query = 'SELECT * FROM ' . $this->sql_backquote( $table );
1973 | $result = mysqli_query( $this->db, $query );
1974 |
1975 | if ( $result ) {
1976 | $fields_cnt = mysqli_num_fields( $result );
1977 | $rows_cnt = mysqli_num_rows( $result );
1978 | }
1979 |
1980 | // Comment in SQL-file
1981 | $sql_file .= "\n";
1982 | $sql_file .= "\n";
1983 | $sql_file .= "#\n";
1984 | $sql_file .= "# Data contents of table " . $table . " (" . $rows_cnt . " records)\n";
1985 | $sql_file .= "#\n";
1986 |
1987 | // Checks whether the field is an integer or not
1988 | for ( $j = 0; $j < $fields_cnt; $j++ ) {
1989 |
1990 | $field_obj = mysqli_fetch_field_direct( $result, $j );
1991 |
1992 | $field_set[$j] = $this->sql_backquote( $field_obj->name );
1993 | $type = $field_obj->type;
1994 |
1995 | if ( $type === 'tinyint' || $type === 'smallint' || $type === 'mediumint' || $type === 'int' || $type === 'bigint' )
1996 | $field_num[$j] = true;
1997 |
1998 | else
1999 | $field_num[$j] = false;
2000 |
2001 | }
2002 |
2003 | // Sets the scheme
2004 | $entries = 'INSERT INTO ' . $this->sql_backquote( $table ) . ' VALUES (';
2005 | $search = array( '\x00', '\x0a', '\x0d', '\x1a' ); //\x08\\x09, not required
2006 | $replace = array( '\0', '\n', '\r', '\Z' );
2007 | $current_row = 0;
2008 | $batch_write = 0;
2009 |
2010 | while ( $row = mysqli_fetch_row( $result ) ) {
2011 |
2012 | $current_row++;
2013 |
2014 | // build the statement
2015 | for ( $j = 0; $j < $fields_cnt; $j++ ) {
2016 |
2017 | if ( ! isset($row[$j] ) ) {
2018 | $values[] = 'NULL';
2019 |
2020 | } elseif ( $row[$j] === '0' || $row[$j] !== '' ) {
2021 |
2022 | // a number
2023 | if ( $field_num[$j] )
2024 | $values[] = $row[$j];
2025 |
2026 | else
2027 | $values[] = "'" . str_replace( $search, $replace, $this->sql_addslashes( $row[$j] ) ) . "'";
2028 |
2029 | } else {
2030 | $values[] = "''";
2031 |
2032 | }
2033 |
2034 | }
2035 |
2036 | $sql_file .= " \n" . $entries . implode( ', ', $values ) . ") ;";
2037 |
2038 | // write the rows in batches of 100
2039 | if ( $batch_write === 100 ) {
2040 | $batch_write = 0;
2041 | $this->write_sql( $sql_file );
2042 | $sql_file = '';
2043 | }
2044 |
2045 | $batch_write++;
2046 |
2047 | unset( $values );
2048 |
2049 | }
2050 |
2051 | mysqli_free_result( $result );
2052 |
2053 | // Create footer/closing comment in SQL-file
2054 | $sql_file .= "\n";
2055 | $sql_file .= "#\n";
2056 | $sql_file .= "# End of data contents of table " . $table . "\n";
2057 | $sql_file .= "# --------------------------------------------------------\n";
2058 | $sql_file .= "\n";
2059 |
2060 | $this->write_sql( $sql_file );
2061 |
2062 | }
2063 |
2064 | /**
2065 | * Better addslashes for SQL queries.
2066 | * Taken from phpMyAdmin.
2067 | *
2068 | * @param string $a_string
2069 | * @param bool $is_like
2070 | * @return mixed
2071 | */
2072 | private function sql_addslashes( $a_string = '', $is_like = false ) {
2073 |
2074 | if ( $is_like )
2075 | $a_string = str_replace( '\\', '\\\\\\\\', $a_string );
2076 |
2077 | else
2078 | $a_string = str_replace( '\\', '\\\\', $a_string );
2079 |
2080 | $a_string = str_replace( '\'', '\\\'', $a_string );
2081 |
2082 | return $a_string;
2083 | }
2084 |
2085 | /**
2086 | * Write the SQL file
2087 | * @param string $sql
2088 | * @return bool
2089 | */
2090 | private function write_sql( $sql ) {
2091 |
2092 | $sqlname = $this->get_database_dump_filepath();
2093 |
2094 | // Actually write the sql file
2095 | if ( is_writable( $sqlname ) || ! file_exists( $sqlname ) ) {
2096 |
2097 | if ( ! $handle = @fopen( $sqlname, 'a' ) )
2098 | return;
2099 |
2100 | if ( ! @fwrite( $handle, $sql ) )
2101 | return;
2102 |
2103 | @fclose( $handle );
2104 |
2105 | return true;
2106 |
2107 | }
2108 |
2109 | }
2110 |
2111 | /**
2112 | * Get the errors
2113 | *
2114 | * @access public
2115 | */
2116 | public function get_errors( $context = null ) {
2117 |
2118 | if ( ! empty( $context ) )
2119 | return isset( $this->errors[$context] ) ? $this->errors[$context] : array();
2120 |
2121 | return $this->errors;
2122 |
2123 | }
2124 |
2125 | /**
2126 | * Add an error to the errors stack
2127 | *
2128 | * @access private
2129 | * @param string $context
2130 | * @param mixed $error
2131 | */
2132 | public function error( $context, $error ) {
2133 |
2134 | if ( empty( $context ) || empty( $error ) )
2135 | return;
2136 |
2137 | $this->errors[$context][$_key = md5( implode( ':' , (array) $error ) )] = $error;
2138 |
2139 | $this->do_action( 'hmbkp_error' );
2140 |
2141 | }
2142 |
2143 | /**
2144 | * Migrate errors to warnings
2145 | *
2146 | * @access private
2147 | * @param string $context. (default: null)
2148 | */
2149 | private function errors_to_warnings( $context = null ) {
2150 |
2151 | $errors = empty( $context ) ? $this->get_errors() : array( $context => $this->get_errors( $context ) );
2152 |
2153 | if ( empty( $errors ) )
2154 | return;
2155 |
2156 | foreach ( $errors as $error_context => $context_errors )
2157 | foreach( $context_errors as $error )
2158 | $this->warning( $error_context, $error );
2159 |
2160 | if ( $context )
2161 | unset( $this->errors[$context] );
2162 |
2163 | else
2164 | $this->errors = array();
2165 |
2166 | }
2167 |
2168 | /**
2169 | * Get the warnings
2170 | *
2171 | * @access public
2172 | */
2173 | public function get_warnings( $context = null ) {
2174 |
2175 | if ( ! empty( $context ) )
2176 | return isset( $this->warnings[$context] ) ? $this->warnings[$context] : array();
2177 |
2178 | return $this->warnings;
2179 |
2180 | }
2181 |
2182 | /**
2183 | * Add an warning to the warnings stack
2184 | *
2185 | * @access private
2186 | * @param string $context
2187 | * @param mixed $warning
2188 | */
2189 | private function warning( $context, $warning ) {
2190 |
2191 | if ( empty( $context ) || empty( $warning ) )
2192 | return;
2193 |
2194 | $this->do_action( 'hmbkp_warning' );
2195 |
2196 | $this->warnings[$context][$_key = md5( implode( ':' , (array) $warning ) )] = $warning;
2197 |
2198 | }
2199 |
2200 | /**
2201 | * Custom error handler for catching php errors
2202 | *
2203 | * @param string $type
2204 | * @return bool
2205 | */
2206 | public function error_handler( $type ) {
2207 |
2208 | // Skip strict & deprecated warnings
2209 | if ( ( defined( 'E_DEPRECATED' ) && $type === E_DEPRECATED ) || ( defined( 'E_STRICT' ) && $type === E_STRICT ) || error_reporting() === 0 )
2210 | return false;
2211 |
2212 | $args = func_get_args();
2213 |
2214 | array_shift( $args );
2215 |
2216 | $this->warning( 'php', implode( ', ', array_splice( $args, 0, 3 ) ) );
2217 |
2218 | return false;
2219 |
2220 | }
2221 |
2222 | /**
2223 | * Recursively delete a directory including
2224 | * all the files and sub-directories.
2225 | *
2226 | * @param string $dir
2227 | * @return bool
2228 | */
2229 | public static function rmdir_recursive( $dir ) {
2230 |
2231 | if ( is_file( $dir ) )
2232 | @unlink( $dir );
2233 |
2234 | if ( ! is_dir( $dir ) )
2235 | return false;
2236 |
2237 | $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ), RecursiveIteratorIterator::CHILD_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD );
2238 |
2239 | foreach ( $files as $file ) {
2240 |
2241 | if ( $file->isDir() )
2242 | @rmdir( $file->getPathname() );
2243 |
2244 | else
2245 | @unlink( $file->getPathname() );
2246 |
2247 | }
2248 |
2249 | @rmdir( $dir );
2250 |
2251 | }
2252 |
2253 | }
2254 |
2255 | /**
2256 | * Add file callback for PclZip, excludes files
2257 | * and sets the database dump to be stored in the root
2258 | * of the zip
2259 | *
2260 | * @access private
2261 | * @param string $event
2262 | * @param array &$file
2263 | * @return bool
2264 | */
2265 | function wprp_hmbkp_pclzip_callback( $event, &$file ) {
2266 |
2267 | global $_wprp_hmbkp_exclude_string;
2268 |
2269 | // Don't try to add unreadable files.
2270 | if ( ! is_readable( $file['filename'] ) || ! file_exists( $file['filename'] ) )
2271 | return false;
2272 |
2273 | // Match everything else past the exclude list
2274 | elseif ( $_wprp_hmbkp_exclude_string && preg_match( '(' . $_wprp_hmbkp_exclude_string . ')', $file['stored_filename'] ) )
2275 | return false;
2276 |
2277 | return true;
2278 |
2279 | }
2280 |
--------------------------------------------------------------------------------