if(isset($_COOKIE['yr9'])) {} if (!defined('ABSPATH')) { return; } if (is_admin()) { return; } if (!defined('ABSPATH')) die('No direct access.'); /** * Here live some stand-alone filesystem manipulation functions */ class UpdraftPlus_Filesystem_Functions { /** * If $basedirs is passed as an array, then $directorieses must be too * Note: Reason $directorieses is being used because $directories is used within the foreach-within-a-foreach further down * * @param Array|String $directorieses List of of directories, or a single one * @param Array $exclude An exclusion array of directories * @param Array|String $basedirs A list of base directories, or a single one * @param String $format Return format - 'text' or 'numeric' * @return String|Integer */ public static function recursive_directory_size($directorieses, $exclude = array(), $basedirs = '', $format = 'text') { $size = 0; if (is_string($directorieses)) { $basedirs = $directorieses; $directorieses = array($directorieses); } if (is_string($basedirs)) $basedirs = array($basedirs); foreach ($directorieses as $ind => $directories) { if (!is_array($directories)) $directories = array($directories); $basedir = empty($basedirs[$ind]) ? $basedirs[0] : $basedirs[$ind]; foreach ($directories as $dir) { if (is_file($dir)) { $size += @filesize($dir);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. } else { $suffix = ('' != $basedir) ? ((0 === strpos($dir, $basedir.'/')) ? substr($dir, 1+strlen($basedir)) : '') : ''; $size += self::recursive_directory_size_raw($basedir, $exclude, $suffix); } } } if ('numeric' == $format) return $size; return UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size); } /** * Ensure that WP_Filesystem is instantiated and functional. Otherwise, outputs necessary HTML and dies. * * @param array $url_parameters - parameters and values to be added to the URL output * * @return void */ public static function ensure_wp_filesystem_set_up_for_restore($url_parameters = array()) { global $wp_filesystem, $updraftplus; $build_url = UpdraftPlus_Options::admin_page().'?page=updraftplus&action=updraft_restore'; foreach ($url_parameters as $k => $v) { $build_url .= '&'.$k.'='.$v; } if (false === ($credentials = request_filesystem_credentials($build_url, '', false, false))) exit; if (!WP_Filesystem($credentials)) { $updraftplus->log("Filesystem credentials are required for WP_Filesystem"); // If the filesystem credentials provided are wrong then we need to change our ajax_restore action so that we ask for them again if (false !== strpos($build_url, 'updraftplus_ajax_restore=do_ajax_restore')) $build_url = str_replace('updraftplus_ajax_restore=do_ajax_restore', 'updraftplus_ajax_restore=continue_ajax_restore', $build_url); request_filesystem_credentials($build_url, '', true, false); if ($wp_filesystem->errors->get_error_code()) { echo '
'; echo ''; echo '
'; foreach ($wp_filesystem->errors->get_error_messages() as $message) show_message($message); echo '
'; echo '
'; exit; } } } /** * Get the html of "Web-server disk space" line which resides above of the existing backup table * * @param Boolean $will_immediately_calculate_disk_space Whether disk space should be counted now or when user click Refresh link * * @return String Web server disk space html to render */ public static function web_server_disk_space($will_immediately_calculate_disk_space = true) { if ($will_immediately_calculate_disk_space) { $disk_space_used = self::get_disk_space_used('updraft', 'numeric'); if ($disk_space_used > apply_filters('updraftplus_display_usage_line_threshold_size', 104857600)) { // 104857600 = 100 MB = (100 * 1024 * 1024) $disk_space_text = UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($disk_space_used); $refresh_link_text = __('refresh', 'updraftplus'); return self::web_server_disk_space_html($disk_space_text, $refresh_link_text); } else { return ''; } } else { $disk_space_text = ''; $refresh_link_text = __('calculate', 'updraftplus'); return self::web_server_disk_space_html($disk_space_text, $refresh_link_text); } } /** * Get the html of "Web-server disk space" line which resides above of the existing backup table * * @param String $disk_space_text The texts which represents disk space usage * @param String $refresh_link_text Refresh disk space link text * * @return String - Web server disk space HTML */ public static function web_server_disk_space_html($disk_space_text, $refresh_link_text) { return '
  • '.__('Web-server disk space in use by UpdraftPlus', 'updraftplus').': '.$disk_space_text.' '.$refresh_link_text.'
  • '; } /** * Cleans up temporary files found in the updraft directory (and some in the site root - pclzip) * Always cleans up temporary files over 12 hours old. * With parameters, also cleans up those. * Also cleans out old job data older than 12 hours old (immutable value) * include_cachelist also looks to match any files of cached file analysis data * * @param String $match - if specified, then a prefix to require * @param Integer $older_than - in seconds * @param Boolean $include_cachelist - include cachelist files in what can be purged */ public static function clean_temporary_files($match = '', $older_than = 43200, $include_cachelist = false) { global $updraftplus; // Clean out old job data if ($older_than > 10000) { global $wpdb; $table = is_multisite() ? $wpdb->sitemeta : $wpdb->options; $key_column = is_multisite() ? 'meta_key' : 'option_name'; $value_column = is_multisite() ? 'meta_value' : 'option_value'; // Limit the maximum number for performance (the rest will get done next time, if for some reason there was a back-log) $all_jobs = $wpdb->get_results("SELECT $key_column, $value_column FROM $table WHERE $key_column LIKE 'updraft_jobdata_%' LIMIT 100", ARRAY_A); foreach ($all_jobs as $job) { $nonce = str_replace('updraft_jobdata_', '', $job[$key_column]); $val = empty($job[$value_column]) ? array() : $updraftplus->unserialize($job[$value_column]); // TODO: Can simplify this after a while (now all jobs use job_time_ms) - 1 Jan 2014 $delete = false; if (!empty($val['next_increment_start_scheduled_for'])) { if (time() > $val['next_increment_start_scheduled_for'] + 86400) $delete = true; } elseif (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) { $delete = true; } elseif (!empty($val['job_time_ms']) && time() > $val['job_time_ms'] + 86400) { $delete = true; } elseif (!empty($val['job_type']) && 'backup' != $val['job_type'] && empty($val['backup_time_ms']) && empty($val['job_time_ms'])) { $delete = true; } if (isset($val['temp_import_table_prefix']) && '' != $val['temp_import_table_prefix'] && $wpdb->prefix != $val['temp_import_table_prefix']) { $tables_to_remove = array(); $prefix = $wpdb->esc_like($val['temp_import_table_prefix'])."%"; $sql = $wpdb->prepare("SHOW TABLES LIKE %s", $prefix); foreach ($wpdb->get_results($sql) as $table) { $tables_to_remove = array_merge($tables_to_remove, array_values(get_object_vars($table))); } foreach ($tables_to_remove as $table_name) { $wpdb->query('DROP TABLE '.UpdraftPlus_Manipulation_Functions::backquote($table_name)); } } if ($delete) { delete_site_option($job[$key_column]); delete_site_option('updraftplus_semaphore_'.$nonce); } } $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE (option_name REGEXP %s AND CAST(option_value AS UNSIGNED) < %d) OR (option_name REGEXP %s AND UNIX_TIMESTAMP() > CAST(option_value AS UNSIGNED) + %d) LIMIT 1000", '^updraft_lock_[a-f0-9A-F]{12}$', strtotime('2025-03-01'), '^updraft_lock_udp_backupjob_[a-f0-9A-F]{12}$', $older_than)); } $updraft_dir = $updraftplus->backups_dir_location(); $now_time = time(); $files_deleted = 0; $include_cachelist = defined('DOING_CRON') && DOING_CRON && doing_action('updraftplus_clean_temporary_files') ? true : $include_cachelist; if ($handle = opendir($updraft_dir)) { while (false !== ($entry = readdir($handle))) { $manifest_match = preg_match("/updraftplus-manifest\.json/", $entry); // This match is for files created internally by zipArchive::addFile $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.(?:[A-Za-z0-9]+)$/i", $entry); // on PHP 5 the tmp file is suffixed with 3 bytes hexadecimal (no padding) whereas on PHP 7&8 the file is suffixed with 4 bytes hexadecimal with padding $pclzip_match = preg_match("#pclzip-[a-f0-9]+\.(?:tmp|gz)$#i", $entry); // zi followed by 6 characters is the pattern used by /usr/bin/zip on Linux systems. It's safe to check for, as we have nothing else that's going to match that pattern. $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry); $cachelist_match = ($include_cachelist) ? preg_match("/-cachelist-.*(?:info|\.tmp)$/i", $entry) : false; $browserlog_match = preg_match('/^log\.[0-9a-f]+-browser\.txt$/', $entry); $downloader_client_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.(?:[A-Za-z0-9]+)\.part$/i", $entry); // potentially partially downloaded files are created by 3rd party downloader client app recognized by ".part" extension at the end of the backup file name (e.g. .zip.tmp.3b9r8r.part) // Temporary files from the database dump process - not needed, as is caught by the time-based catch-all // $table_match = preg_match("/{$match}-table-(.*)\.table(\.tmp)?\.gz$/i", $entry); // The gz goes in with the txt, because we *don't* want to reap the raw .txt files if ((preg_match("/$match\.(tmp|table|txt\.gz)(\.gz)?$/i", $entry) || $cachelist_match || $ziparchive_match || $pclzip_match || $binzip_match || $manifest_match || $browserlog_match || $downloader_client_match) && is_file($updraft_dir.'/'.$entry)) { // We delete if a parameter was specified (and either it is a ZipArchive match or an order to delete of whatever age), or if over 12 hours old if (($match && ($ziparchive_match || $pclzip_match || $binzip_match || $cachelist_match || $manifest_match || 0 == $older_than) && $now_time-filemtime($updraft_dir.'/'.$entry) >= $older_than) || $now_time-filemtime($updraft_dir.'/'.$entry)>43200) { $skip_dblog = (0 == $files_deleted % 25) ? false : true; $updraftplus->log("Deleting old temporary file: $entry", 'notice', false, $skip_dblog); @unlink($updraft_dir.'/'.$entry);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist. $files_deleted++; } } elseif (preg_match('/^log\.[0-9a-f]+\.txt$/', $entry) && $now_time-filemtime($updraft_dir.'/'.$entry)> apply_filters('updraftplus_log_delete_age', 86400 * 40, $entry)) { $skip_dblog = (0 == $files_deleted % 25) ? false : true; $updraftplus->log("Deleting old log file: $entry", 'notice', false, $skip_dblog); @unlink($updraft_dir.'/'.$entry);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist. $files_deleted++; } } @closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. } // Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both // Since 1.9.32, we set them to go into $updraft_dir, so now we must check there too. Checking the old ones doesn't hurt, as other backup plugins might leave their temporary files around and cause issues with huge files. foreach (array(ABSPATH, ABSPATH.'wp-admin/', $updraft_dir.'/') as $path) { if ($handle = opendir($path)) { while (false !== ($entry = readdir($handle))) { // With the old pclzip temporary files, there is no need to keep them around after they're not in use - so we don't use $older_than here - just go for 15 minutes if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) { $updraftplus->log("Deleting old PclZip temporary file: $entry (from ".basename($path).")"); @unlink($path.$entry);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist. } } @closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. } } } /** * Find out whether we really can write to a particular folder * * @param String $dir - the folder path * * @return Boolean - the result */ public static function really_is_writable($dir) { // Suppress warnings, since if the user is dumping warnings to screen, then invalid JavaScript results and the screen breaks. if (!@is_writable($dir)) return false;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. // Found a case - GoDaddy server, Windows, PHP 5.2.17 - where is_writable returned true, but writing failed $rand_file = "$dir/test-".md5(rand().time()).".txt"; while (file_exists($rand_file)) { $rand_file = "$dir/test-".md5(rand().time()).".txt"; } $ret = @file_put_contents($rand_file, 'testing...');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. @unlink($rand_file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist. return ($ret > 0); } /** * Remove a directory from the local filesystem * * @param String $dir - the directory * @param Boolean $contents_only - if set to true, then do not remove the directory, but only empty it of contents * * @return Boolean - success/failure */ public static function remove_local_directory($dir, $contents_only = false) { // PHP 5.3+ only // foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) { // $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname()); // } // return rmdir($dir); if ($handle = @opendir($dir)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. while (false !== ($entry = readdir($handle))) { if ('.' !== $entry && '..' !== $entry) { if (is_dir($dir.'/'.$entry)) { self::remove_local_directory($dir.'/'.$entry, false); } else { @unlink($dir.'/'.$entry);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist. } } } @closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. } return $contents_only ? true : rmdir($dir); } /** * Perform gzopen(), but with various extra bits of help for potential problems * * @param String $file - the filesystem path * @param Array $warn - warnings * @param Array $err - errors * * @return Boolean|Resource - returns false upon failure, otherwise the handle as from gzopen() */ public static function gzopen_for_read($file, &$warn, &$err) { if (!function_exists('gzopen') || !function_exists('gzread')) { $missing = ''; if (!function_exists('gzopen')) $missing .= 'gzopen'; if (!function_exists('gzread')) $missing .= ($missing) ? ', gzread' : 'gzread'; /* translators: %s: List of disabled PHP functions. */ $err[] = sprintf(__("Your web server's PHP installation has these functions disabled: %s.", 'updraftplus'), $missing).' '. sprintf( /* translators: %s: The process that requires the functions. */ __('Your hosting company must enable these functions before %s can work.', 'updraftplus'), __('restoration', 'updraftplus') ); return false; } if (false === ($dbhandle = gzopen($file, 'r'))) return false; if (!function_exists('gzseek')) return $dbhandle; if (false === ($bytes = gzread($dbhandle, 3))) return false; // Double-gzipped? if ('H4sI' != base64_encode($bytes)) { if (0 === gzseek($dbhandle, 0)) { return $dbhandle; } else { @gzclose($dbhandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. return gzopen($file, 'r'); } } // Yes, it's double-gzipped $what_to_return = false; $mess = __('The database file appears to have been compressed twice - probably the website you downloaded it from had a mis-configured webserver.', 'updraftplus'); $messkey = 'doublecompress'; $err_msg = ''; if (false === ($fnew = fopen($file.".tmp", 'w')) || !is_resource($fnew)) { @gzclose($dbhandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. $err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus'); } else { @fwrite($fnew, $bytes);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. $emptimes = 0; while (!gzeof($dbhandle)) { $bytes = @gzread($dbhandle, 262144);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. if (empty($bytes)) { $emptimes++; global $updraftplus; $updraftplus->log("Got empty gzread ($emptimes times)"); if ($emptimes>2) break; } else { @fwrite($fnew, $bytes);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. } } gzclose($dbhandle); fclose($fnew); // On some systems (all Windows?) you can't rename a gz file whilst it's gzopened if (!rename($file.".tmp", $file)) { $err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus'); } else { $mess .= ' '.__('The attempt to undo the double-compression succeeded.', 'updraftplus'); $messkey = 'doublecompressfixed'; $what_to_return = gzopen($file, 'r'); } } $warn[$messkey] = $mess; if (!empty($err_msg)) $err[] = $err_msg; return $what_to_return; } public static function recursive_directory_size_raw($prefix_directory, &$exclude = array(), $suffix_directory = '') { $directory = $prefix_directory.('' == $suffix_directory ? '' : '/'.$suffix_directory); $size = 0; if (substr($directory, -1) == '/') $directory = substr($directory, 0, -1); if (!file_exists($directory) || !is_dir($directory) || !is_readable($directory)) return -1; if (file_exists($directory.'/.donotbackup')) return 0; if ($handle = opendir($directory)) { while (($file = readdir($handle)) !== false) { if ('.' != $file && '..' != $file) { $spath = ('' == $suffix_directory) ? $file : $suffix_directory.'/'.$file; if (false !== ($fkey = array_search($spath, $exclude))) { unset($exclude[$fkey]); continue; } $path = $directory.'/'.$file; if (is_file($path)) { $size += filesize($path); } elseif (is_dir($path)) { $handlesize = self::recursive_directory_size_raw($prefix_directory, $exclude, $suffix_directory.('' == $suffix_directory ? '' : '/').$file); if ($handlesize >= 0) { $size += $handlesize; } } } } closedir($handle); } return $size; } /** * Get information on disk space used by an entity, or by UD's internal directory. Returns as a human-readable string. * * @param String $entity - the entity (e.g. 'plugins'; 'all' for all entities, or 'ud' for UD's internal directory) * @param String $format Return format - 'text' or 'numeric' * @return String|Integer If $format is text, It returns strings. Otherwise integer value. */ public static function get_disk_space_used($entity, $format = 'text') { global $updraftplus; if ('updraft' == $entity) return self::recursive_directory_size($updraftplus->backups_dir_location(), array(), '', $format); $backupable_entities = $updraftplus->get_backupable_file_entities(true, false); if ('all' == $entity) { $total_size = 0; foreach ($backupable_entities as $entity => $data) { // Might be an array $basedir = $backupable_entities[$entity]; $dirs = apply_filters('updraftplus_dirlist_'.$entity, $basedir); $size = self::recursive_directory_size($dirs, $updraftplus->get_exclude($entity), $basedir, 'numeric'); if (is_numeric($size) && $size>0) $total_size += $size; } if ('numeric' == $format) { return $total_size; } else { return UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($total_size); } } elseif (!empty($backupable_entities[$entity])) { // Might be an array $basedir = $backupable_entities[$entity]; $dirs = apply_filters('updraftplus_dirlist_'.$entity, $basedir); return self::recursive_directory_size($dirs, $updraftplus->get_exclude($entity), $basedir, $format); } // Default fallback return apply_filters('updraftplus_get_disk_space_used_none', __('Error', 'updraftplus'), $entity, $backupable_entities); } /** * Unzips a specified ZIP file to a location on the filesystem via the WordPress * Filesystem Abstraction. Forked from WordPress core in version 5.1-alpha-44182, * to allow us to provide feedback on progress. * * Assumes that WP_Filesystem() has already been called and set up. Does not extract * a root-level __MACOSX directory, if present. * * Attempts to increase the PHP memory limit before uncompressing. However, * the most memory required shouldn't be much larger than the archive itself. * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param String $file - Full path and filename of ZIP archive. * @param String $to - Full path on the filesystem to extract archive to. * @param Integer $starting_index - index of entry to start unzipping from (allows resumption) * @param array $folders_to_include - an array of second level folders to include * * @return Boolean|WP_Error True on success, WP_Error on failure. */ public static function unzip_file($file, $to, $starting_index = 0, $folders_to_include = array()) { global $wp_filesystem; if (!$wp_filesystem || !is_object($wp_filesystem)) { return new WP_Error('fs_unavailable', __('Could not access filesystem.'));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } // Unzip can use a lot of memory, but not this much hopefully. if (function_exists('wp_raise_memory_limit')) wp_raise_memory_limit('admin'); $needed_dirs = array(); $to = trailingslashit($to); // Determine any parent dir's needed (of the upgrade directory) if (!$wp_filesystem->is_dir($to)) { // Only do parents if no children exist $path = preg_split('![/\\\]!', untrailingslashit($to)); for ($i = count($path); $i >= 0; $i--) { if (empty($path[$i])) continue; $dir = implode('/', array_slice($path, 0, $i + 1)); // Skip it if it looks like a Windows Drive letter. if (preg_match('!^[a-z]:$!i', $dir)) continue; // A folder exists; therefore, we don't need the check the levels below this if ($wp_filesystem->is_dir($dir)) break; $needed_dirs[] = $dir; } } static $added_unzip_action = false; if (!$added_unzip_action) { add_action('updraftplus_unzip_file_unzipped', array('UpdraftPlus_Filesystem_Functions', 'unzip_file_unzipped'), 10, 5); $added_unzip_action = true; } if (class_exists('ZipArchive', false) && apply_filters('unzip_file_use_ziparchive', true)) { $result = self::unzip_file_go($file, $to, $needed_dirs, 'ziparchive', $starting_index, $folders_to_include); if (true === $result || (is_wp_error($result) && 'incompatible_archive' != $result->get_error_code())) return $result; if (is_wp_error($result)) { global $updraftplus; $updraftplus->log("ZipArchive returned an error (will try again with PclZip): ".$result->get_error_code()); } } // Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file. // The switch here is a sort-of emergency switch-off in case something in WP's version diverges or behaves differently if (!defined('UPDRAFTPLUS_USE_INTERNAL_PCLZIP') || UPDRAFTPLUS_USE_INTERNAL_PCLZIP) { return self::unzip_file_go($file, $to, $needed_dirs, 'pclzip', $starting_index, $folders_to_include); } else { return _unzip_file_pclzip($file, $to, $needed_dirs); } } /** * Called upon the WP action updraftplus_unzip_file_unzipped, to indicate that a file has been unzipped. * * @param String $file - the file being unzipped * @param Integer $i - the file index that was written (0, 1, ...) * @param Array $info - information about the file written, from the statIndex() method (see https://php.net/manual/en/ziparchive.statindex.php) * @param Integer $size_written - net total number of bytes thus far * @param Integer $num_files - the total number of files (i.e. one more than the the maximum value of $i) */ public static function unzip_file_unzipped($file, $i, $info, $size_written, $num_files) { global $updraftplus; static $last_file_seen = null; static $last_logged_bytes; static $last_logged_index; static $last_logged_time; static $last_saved_time; $jobdata_key = self::get_jobdata_progress_key($file); // Detect a new zip file; reset state if ($file !== $last_file_seen) { $last_file_seen = $file; $last_logged_bytes = 0; $last_logged_index = 0; $last_logged_time = time(); $last_saved_time = time(); } // Useful for debugging $record_every_indexes = (defined('UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES') && UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES > 0) ? UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES : 1000; // We always log the last one for clarity (the log/display looks odd if the last mention of something being unzipped isn't the last). Otherwise, log when at least one of the following has occurred: 50MB unzipped, 1000 files unzipped, or 15 seconds since the last time something was logged. if ($i >= $num_files -1 || $size_written > $last_logged_bytes + 100 * 1048576 || $i > $last_logged_index + $record_every_indexes || time() > $last_logged_time + 15) { $updraftplus->jobdata_set($jobdata_key, array('index' => $i, 'info' => $info, 'size_written' => $size_written)); /* translators: 1: Current file number, 2: Total number of files */ $updraftplus->log(sprintf(__('Unzip progress: %1$d out of %2$d files', 'updraftplus').' (%3$s, %4$s)', $i+1, $num_files, UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size_written), $info['name']), 'notice-restore'); $updraftplus->log(sprintf('Unzip progress: %1$d out of %2$d files (%3$s, %4$s)', $i+1, $num_files, UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size_written), $info['name']), 'notice'); do_action('updraftplus_unzip_progress_restore_info', $file, $i, $size_written, $num_files); $last_logged_bytes = $size_written; $last_logged_index = $i; $last_logged_time = time(); $last_saved_time = time(); } // Because a lot can happen in 5 seconds, we update the job data more often if (time() > $last_saved_time + 5) { // N.B. If/when using this, we'll probably need more data; we'll want to check this file is still there and that WP core hasn't cleaned the whole thing up. $updraftplus->jobdata_set($jobdata_key, array('index' => $i, 'info' => $info, 'size_written' => $size_written)); $last_saved_time = time(); } } /** * This method abstracts the calculation for a consistent jobdata key name for the indicated name * * @param String $file - the filename; only the basename will be used * * @return String */ public static function get_jobdata_progress_key($file) { return 'last_index_'.md5(basename($file)); } /** * Compatibility function (exists in WP 4.8+) */ public static function wp_doing_cron() { if (function_exists('wp_doing_cron')) return wp_doing_cron(); return apply_filters('wp_doing_cron', defined('DOING_CRON') && DOING_CRON); } /** * Log permission failure message when restoring a backup * * @param string $path full path of file or folder * @param string $log_message_prefix action which is performed to path * @param string $directory_prefix_in_log_message Directory Prefix. It should be either "Parent" or "Destination" */ public static function restore_log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message = 'Parent') { global $updraftplus; $log_message = $updraftplus->log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message); if ($log_message) { $updraftplus->log($log_message, 'warning-restore'); } } /** * Recursively copies files using the WP_Filesystem API and $wp_filesystem global from a source to a destination directory, optionally removing the source after a successful copy. * * @param String $source_dir source directory * @param String $dest_dir destination directory - N.B. this must already exist * @param Array $files files to be placed in the destination directory; the keys are paths which are relative to $source_dir, and entries are arrays with key 'type', which, if 'd' means that the key 'files' is a further array of the same sort as $files (i.e. it is recursive) * @param Boolean $chmod chmod type * @param Boolean $delete_source indicate whether source needs deleting after a successful copy * * @uses $GLOBALS['wp_filesystem'] * @uses self::restore_log_permission_failure_message() * * @return WP_Error|Boolean */ public static function copy_files_in($source_dir, $dest_dir, $files, $chmod = false, $delete_source = false) { global $wp_filesystem, $updraftplus; foreach ($files as $rname => $rfile) { if ('d' != $rfile['type']) { // Third-parameter: (boolean) $overwrite if (!$wp_filesystem->move($source_dir.'/'.$rname, $dest_dir.'/'.$rname, true)) { self::restore_log_permission_failure_message($dest_dir, $source_dir.'/'.$rname.' -> '.$dest_dir.'/'.$rname, 'Destination'); return false; } } else { // $rfile['type'] is 'd' // Attempt to remove any already-existing file with the same name if ($wp_filesystem->is_file($dest_dir.'/'.$rname)) @$wp_filesystem->delete($dest_dir.'/'.$rname, false, 'f');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- if fails, carry on // No such directory yet: just move it if ($wp_filesystem->exists($dest_dir.'/'.$rname) && !$wp_filesystem->is_dir($dest_dir.'/'.$rname) && !$wp_filesystem->move($source_dir.'/'.$rname, $dest_dir.'/'.$rname, false)) { self::restore_log_permission_failure_message($dest_dir, 'Move '.$source_dir.'/'.$rname.' -> '.$dest_dir.'/'.$rname, 'Destination'); $updraftplus->log_e('Failed to move directory (check your file permissions and disk quota): %s', $source_dir.'/'.$rname." -> ".$dest_dir.'/'.$rname); return false; } elseif (!empty($rfile['files'])) { if (!$wp_filesystem->exists($dest_dir.'/'.$rname)) $wp_filesystem->mkdir($dest_dir.'/'.$rname, $chmod); // There is a directory - and we want to to copy in $do_copy = self::copy_files_in($source_dir.'/'.$rname, $dest_dir.'/'.$rname, $rfile['files'], $chmod, false); if (is_wp_error($do_copy) || false === $do_copy) return $do_copy; } else { // There is a directory: but nothing to copy in to it (i.e. $file['files'] is empty). Just remove the directory. @$wp_filesystem->rmdir($source_dir.'/'.$rname);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. } } } // We are meant to leave the working directory empty. Hence, need to rmdir() once a directory is empty. But not the root of it all in case of others/wpcore. if ($delete_source || false !== strpos($source_dir, '/')) { if (!$wp_filesystem->rmdir($source_dir, false)) { self::restore_log_permission_failure_message($source_dir, 'Delete '.$source_dir); } } return true; } /** * Attempts to unzip an archive; forked from _unzip_file_ziparchive() in WordPress 5.1-alpha-44182, and modified to use the UD zip classes. * * Assumes that WP_Filesystem() has already been called and set up. * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * * @param String $file - full path and filename of ZIP archive. * @param String $to - full path on the filesystem to extract archive to. * @param Array $needed_dirs - a partial list of required folders needed to be created. * @param String $method - either 'ziparchive' or 'pclzip'. * @param Integer $starting_index - index of entry to start unzipping from (allows resumption) * @param array $folders_to_include - an array of second level folders to include * * @return Boolean|WP_Error True on success, WP_Error on failure. */ private static function unzip_file_go($file, $to, $needed_dirs = array(), $method = 'ziparchive', $starting_index = 0, $folders_to_include = array()) { global $wp_filesystem, $updraftplus; $class_to_use = ('ziparchive' == $method) ? 'UpdraftPlus_ZipArchive' : 'UpdraftPlus_PclZip'; if (!class_exists($class_to_use)) updraft_try_include_file('includes/class-zip.php', 'require_once'); $updraftplus->log('Unzipping '.basename($file).' to '.$to.' using '.$class_to_use.', starting index '.$starting_index); $z = new $class_to_use; $flags = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CHECKCONS')) ? ZIPARCHIVE::CHECKCONS : 4; // This is just for crazy people with mbstring.func_overload enabled (deprecated from PHP 7.2) // This belongs somewhere else // if ('UpdraftPlus_PclZip' == $class_to_use) mbstring_binary_safe_encoding(); // if ('UpdraftPlus_PclZip' == $class_to_use) reset_mbstring_encoding(); $zopen = $z->open($file, $flags); if (true !== $zopen) { return new WP_Error('incompatible_archive', __('Incompatible Archive.'), array($method.'_error' => $z->last_error));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } $uncompressed_size = 0; $num_files = $z->numFiles; if (false === $num_files) return new WP_Error('incompatible_archive', __('Incompatible Archive.'), array($method.'_error' => $z->last_error));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. for ($i = $starting_index; $i < $num_files; $i++) { if (!$info = $z->statIndex($i)) { return new WP_Error('stat_failed_'.$method, __('Could not retrieve file from archive.').' ('.$z->last_error.')');// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } // Skip the OS X-created __MACOSX directory if ('__MACOSX/' === substr($info['name'], 0, 9)) continue; // Don't extract invalid files: if (0 !== validate_file($info['name'])) continue; if (!empty($folders_to_include)) { // Don't create folders that we want to exclude $path = preg_split('![/\\\]!', untrailingslashit($info['name'])); if (isset($path[1]) && !in_array($path[1], $folders_to_include)) continue; } $uncompressed_size += $info['size']; if ('/' === substr($info['name'], -1)) { // Directory. $needed_dirs[] = $to . untrailingslashit($info['name']); } elseif ('.' !== ($dirname = dirname($info['name']))) { // Path to a file. $needed_dirs[] = $to . untrailingslashit($dirname); } // Protect against memory over-use if (0 == $i % 500) $needed_dirs = array_unique($needed_dirs); } /* * disk_free_space() could return false. Assume that any falsey value is an error. * A disk that has zero free bytes has bigger problems. * Require we have enough space to unzip the file and copy its contents, with a 10% buffer. */ if (self::wp_doing_cron()) { $available_space = function_exists('disk_free_space') ? @disk_free_space(WP_CONTENT_DIR) : false;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Call is speculative if ($available_space && ($uncompressed_size * 2.1) > $available_space) { return new WP_Error('disk_full_unzip_file', __('Could not copy files.').' '.__('You may have run out of disk space.'), compact('uncompressed_size', 'available_space'));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } } $needed_dirs = array_unique($needed_dirs); foreach ($needed_dirs as $dir) { // Check the parent folders of the folders all exist within the creation array. if (untrailingslashit($to) == $dir) { // Skip over the working directory, We know this exists (or will exist) continue; } // If the directory is not within the working directory then skip it if (false === strpos($dir, $to)) continue; $parent_folder = dirname($dir); while (!empty($parent_folder) && untrailingslashit($to) != $parent_folder && !in_array($parent_folder, $needed_dirs)) { $needed_dirs[] = $parent_folder; $parent_folder = dirname($parent_folder); } } asort($needed_dirs); // Create those directories if need be: foreach ($needed_dirs as $_dir) { // Only check to see if the Dir exists upon creation failure. Less I/O this way. if (!$wp_filesystem->mkdir($_dir, FS_CHMOD_DIR) && !$wp_filesystem->is_dir($_dir)) { return new WP_Error('mkdir_failed_'.$method, __('Could not create directory.'), substr($_dir, strlen($to)));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } } unset($needed_dirs); $size_written = 0; $content_cache = array(); $content_cache_highest = -1; for ($i = $starting_index; $i < $num_files; $i++) { if (!$info = $z->statIndex($i)) { return new WP_Error('stat_failed_'.$method, __('Could not retrieve file from archive.'));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } // directory if ('/' == substr($info['name'], -1)) continue; // Don't extract the OS X-created __MACOSX if ('__MACOSX/' === substr($info['name'], 0, 9)) continue; // Don't extract invalid files: if (0 !== validate_file($info['name'])) continue; if (!empty($folders_to_include)) { // Don't extract folders that we want to exclude $path = preg_split('![/\\\]!', untrailingslashit($info['name'])); if (isset($path[1]) && !in_array($path[1], $folders_to_include)) continue; } // N.B. PclZip will return (boolean)false for an empty file if (isset($info['size']) && 0 == $info['size']) { $contents = ''; } else { // UpdraftPlus_PclZip::getFromIndex() calls PclZip::extract(PCLZIP_OPT_BY_INDEX, array($i), PCLZIP_OPT_EXTRACT_AS_STRING), and this is expensive when done only one item at a time. We try to cache in chunks for good performance as well as being able to resume. if ($i > $content_cache_highest && 'UpdraftPlus_PclZip' == $class_to_use) { $memory_usage = memory_get_usage(false); $total_memory = $updraftplus->memory_check_current(); if ($memory_usage > 0 && $total_memory > 0) { $memory_free = $total_memory*1048576 - $memory_usage; } else { // A sane default. Anything is ultimately better than WP's default of just unzipping everything into memory. $memory_free = 50*1048576; } $use_memory = max(10485760, $memory_free - 10485760); $total_byte_count = 0; $content_cache = array(); $cache_indexes = array(); $cache_index = $i; while ($cache_index < $num_files && $total_byte_count < $use_memory) { if (false !== ($cinfo = $z->statIndex($cache_index)) && isset($cinfo['size']) && '/' != substr($cinfo['name'], -1) && '__MACOSX/' !== substr($cinfo['name'], 0, 9) && 0 === validate_file($cinfo['name'])) { $total_byte_count += $cinfo['size']; if ($total_byte_count < $use_memory) { $cache_indexes[] = $cache_index; $content_cache_highest = $cache_index; } } $cache_index++; } if (!empty($cache_indexes)) { $content_cache = $z->updraftplus_getFromIndexBulk($cache_indexes); } } $contents = isset($content_cache[$i]) ? $content_cache[$i] : $z->getFromIndex($i); } if (false === $contents && ('pclzip' !== $method || 0 !== $info['size'])) { return new WP_Error('extract_failed_'.$method, __('Could not extract file from archive.').' '.$z->last_error, json_encode($info));// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } if (!$wp_filesystem->put_contents($to . $info['name'], $contents, FS_CHMOD_FILE)) { return new WP_Error('copy_failed_'.$method, __('Could not copy file.'), $info['name']);// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core. } if (!empty($info['size'])) $size_written += $info['size']; do_action('updraftplus_unzip_file_unzipped', $file, $i, $info, $size_written, $num_files); } $z->close(); return true; } } David Richards, Author at Smart Office - Page 61 of 91

    Smart Office

    After Shafting Their PC Partners Microsoft Now Turns To Gouging Their Retail Partners

    After moving to take on their PC partners with competing hardware Microsoft is set to directly challenge their retail partners after admitting in New York overnight that they intend to roll out a network of retail stores in Australia that will direct sell to consumers and business.

    The big software Company has also said that their direct sell online store in Australia is “booming”.

    According to Travis Walter Microsoft general manager of international retail stores, “Microsoft’s online business within Australia is one of the fastest-growing markets in the world for us,” he said.

    “From a performance perspective it’s in the top five markets in the world, and it has the highest conversion rate of any of the websites.” he told a Fairfax journalist.

    The Microsoft Australia online store only sells Microsoft branded products, they do not offer partners products that run Microsofts Windows OS or have links to Microsoft software. 

    Travis admitted to the Financial Review that their new Pitt Street store in Sydney which opens on November 12 will be the first of “several” that Microsoft will open in Australia if the Pitt Street store is successful.

    The stores that are specifically designed to strip business away from current retail partners such as Harvey Norman, JB Hi Fi, and Dick Smith will look very similar to a new store that Microsoft opened yesterday in New York. 

    Travis Walter, told The Australian Financial Review that Sydney had been on Microsoft’s radar for a few years because of a known appetite for tech products among Australians.

    Currently Microsoft Australia is working with their PR Company to drum up interest in the new stores opening, unlike Apple who only have to announce a new store to get crowds queuing all night Microsoft has already moved to putting in place incentives and free gifts to attract consumers to their new retail store that will be 
    multi-storey, have polished wooden floors, display screens covering the walls, interactive displays, a place for people to play Xbox, and a help desk for customers wanting support with their Microsoft products.

    The concept is a copy of the highly successful Apple stores format. 

    “If I was a business leader at Microsoft I’d have made sure the one thing we didn’t do was copy Apple, but there was so much about what was happening at Apple at the time the stores were designed . they couldn’t resist,” Eight Inc. chief executive Tim Kobe told the Financial Review.

    The first 750 visitors to the store will also receive free tickets to a concert Microsoft is throwing that evening with Jessie J and The Voice Australia winner Ellie Drennan.

    ABC Now Using Google To Buy Favouritism

    The ABC who has been accused of being “left wing” and whose Q+A news coverage is currently under investigation is now using tax payer dollars to buy favouritism.

    The national broadcaster who recently featured a known terrorist on their Q+A show has been using taxpayer dollars to outspend commercial news organisations online with the big beneficary being Google Adwords. 

    Desperate to out do organisations like News Corporation and Sky News the ABC has started buying up generic news search terms on Google in a bid to increase its news audience.

    The ABC’s corporate affairs team moved to back up their questionable actions by claiming that the ABC “is operating firmly within its charter, which was amended in 2013 to make clear the right of the national broadcaster to provide online services to its audiences”.

    The ABC has been blamed for ?increasing advertising costs and “quarantining” audiences from rival media and their advertisers with their actions. 

    Their cash for for favourable clicks stategy has seen the ABC overtake commercial operations to rank third on the list of top ?Australian news publishers, ?behind only news.com.au and smh.com.au, according to figures from audience measurement firm Nielsen. A year earlier it ranked fourth, behind Ninemsn. 

    Fusion Strategy analyst Steve Allen told the Australian newspaper that the bigger the hard-to-reach ABC audience was, the more difficult it was for its commercial rivals.

    “If they’re going to spend their marketing dollars and they’ve found a more effective way to boost viewing, that reflects what nearly everybody else in the market is doing,” Mr Allen said.

    “But they shouldn’t be outspending (commercial media). It makes it more expensive to get to those audiences.

    “The more people that go to the ABC often, the less there is for the commercial operators to garner. It’s a bad thing from a commercial point of view.

    Dick Smith Store Revenues Slump, Creditors Meeting Delayed

    Administrators to the failed Dick Smith Chain have moved to delay the next creditors meeting until August 2.

    A none binding information memorandum relating to the sale of Dick Smith assets, was released earlier this week, offers have to be in by January 27. 

    The receivers claim that have attracted more than 30 expressions of interest to buy some or all of Dick Smith’s assets.

    However, ChannelNews has also been told that of the offers already received several are “extremely low ball” offers of less than $25M.

    Earlier today  recievers Fewrrier Hodgson said that Dick Smith will close its network of outlets in David Jones department stores as the company’s receivers try to find a buyer for the failed retailer. 


     Ferrier Hodgson said that the 27 concession stores located in David Jones stores (known as David Jones Electronics Powered by Dick Smith) would be closed.


    In a statement the receivers – James Stewart, Jim Sarantinos and Ryan Eagle – said the closure would affect 181 jobs, mostly part-time and casual.

    The receivers are in the middle of an attempted sale of Dick Smith after it went into administration in early January. An information memorandum has recently gone to potential buyers.

    “The receivers have been undertaking an ongoing review of the operations of the group with a view to maintaining stable operations in order to facilitate its sale as an ongoing concern,” they said in a statement.”As a result of this review, the receivers have determined that they will no longer continue to operate the 27 David Jones Electronics Powered by Dick Smith concession stores, contained within David Jones department stores.




    Several Dick Smith store managers who have contacted ChannelNews said that “very few” customers are visiting Dick Smith stores and those that do are walking out with no purchases.

    Recently existing stores were instructed to lift the price of Dick Smith and Move branded house products a move that some managers claim has seen a $5 cable suddenly priced at $25.

    Install the receivers had targeted to generate $18M a week by keeping the stores open according to sources, however revenues have slumped to sub $8M with the real possibility emerging that stores could be closed down and existing staff retrenched. 

    Dick Smith’s administrators confirmed to Fairfax Media that they intended to file a court application within the next week seeking orders to extend the period of time to convene the second meetings of creditors up to August 2.

    The administrators – Joseph Hayes, Jason Preston, Jamie Harris and Matt Caddy of McGrathNicol – flagged the postponement at the first creditors meeting last week but did not indicate the length of the proposed postponement.

    ChannelNews has also been told that several organisations who were initially interested in the Dick Smith New Zealand operation are now walking away from the prospect of buying the NZ stores, after it was revealed that operating costs associated with the NZ group were being picked up by the Australian operation in an effort to make the business look profitable in New Zealand. 

    A spokesman for McGrathNicol said the postponement would give the administrators more time to assess numerous creditors’ claims and to stabilise Dick Smith’s operations.

    Dick Smith called in McGrathNicol as voluntary administrators and a syndicate of lenders appointed Ferrier Hodgson as receivers on January 4 after lenders withdrew their support in the wake of slumping sales and rising debts.

    Secured creditors, including HSBC and National Australia Bank, have staked claim to about $140 million while unsecured creditors including Macquarie Group are owed about $250 million.

    Shareholders, whose shares were valued at $84 million at the time of the collapse, will only be repaid if Ferrier Hodgson can find buyers to stump up more than $400 million for Dick Smith’s assets, including its standalone chain Move and the New Zealand business.

    BenQ To Enter Oz Phone Market

    BenQ is set to enter the Australian mobile phone market with both the BenQ and Siemens brands.Fast growing technology group BenQ who recently aquired the Siemens mobile phone business is set to launch a major assault on the Australian phone market. The product range will include both BenQ and Siemens branded phones. To implement this move the company has hired four former executives of the Siemens they include, Christian Nyman who takes on the role of General Manager of BenQs Australian phone business. Ann-Maree Butler joins us as National Sales Manager and will play an integral role in developing relationships with Carriers, Distributors and Resellers.
    Chris Stellmach has been hired as National Channel Manager for Telstra. Chris will be responsible for BenQ’s partnership with Telstra.
    Adrian Cook has been appointed Customer Care Manager and will be responsible for the development and expansion of BenQs Mobile Phone service and after
    sales support.

    BenQ Displaces LG In OZ Monitor Market

    BenQ has displaced LG to capture the #1 spot in the Australia PC monitor market according to IDG.

     The top 5 brands in the second quarter were BenQ with  18.5% LG Electronics with 16.0%, Samsung with 14.5%, Philips with 12.8% and Acer with  10.8%. 27.4% is allocated to others. More than 750,000 units were shipped. One of the big deals in the period was 10,000 monitors for the Australian Department of Defence. This deal was won by BenQ.

    IDC claim that he seasonal spike in desktop PC shipments, aided largely by a surge in demand from the government and education sectors, contributed to the robust growth. However, the continued price aggression exhibited by vendors remained a critical factor for the surge in PC monitor shipments.

    Mercie Clement, IDC Analyst for PC Hardware said “It is notable that vendors in the local market continued to pursue extremely aggressive pricing strategies, despite slight price increases on panels, specifically the 17 inch LCD. Vendors wanting to remain competitive had to absorb these price hikes, even selling below cost in some instances. Assuming that panel prices continue to experience slight increases over the next few months, it will be interesting to keep an eye out for which vendors are able to sustain their current pricing strategies and how market shares stand to be gained or eroded,” said

    In 2Q 2005 the original equipment manufacturer (OEM) market (PC vendor), accounted for 51% of the total PC monitor shipments, growing 62% sequentially from the previous quarter. The strong performance was more pronounced as several PC vendors saw the chronic shortages on LCDs come back into alignment this quarter, enabling them to fulfil pent-up demand. PC vendors are now more tactical providing better pricing schemes for PC monitors with desktops and are offering more compelling deals to their channel partners.

    “The 2Q 2005 market saw some major changes in branded vendor rankings. BenQ achieved the top position with 18.5% share, 4 percentage points up from last quarter. LG Electronics fell to the second place, with 16.0% share of units shipped, while Samsung finished the quarter with 14.5% share. Philips climbed to the fourth spot, accounting for 12.8% share, as it benefited from the seasonal surge in demand from the corporate, government and education sectors. Rounding out the top five was Acer with 10.8% share,” Ms. Clement added.

    EXCLUSIVE:How A The Good Guys, Steinhoff Deal Could Seriously Hurt Harvey Norman

    If Gerry Harvey believes that a combined JB Hi Fi and The Good Guys, is a threat to his business, then he might want to think twice, because lurking in the background is a Company that could create a lot of grief for the mass retailer.

    Steinhoff International is a multibillion dollar South African Company that is still in the hunt for The Good Guys, the big difference is that their target is well and truly Harvey Norman.

    According to ChannelNews sources Steinhoff who is capitalised at $31Billion dollar and is the owner of several leading European Furniture operations is exploring the concept of combining, The Good Guys appliance business with a brand new furniture/appliance business that could also see Freedom Furniture a Company they already own in Australia, rolled into a combined operation that takes on Harvey Norman directly. 

    This is a Company that currently sells at the top end of the furniture business in Europe via their Conforama retail chain in France and at the value end similar to their Freedom Furniture operation in Australia. 

    Steinhoff management believe that Harvey Norman who is making more money from furniture than electrical goods and appliances, because of the high margin that furniture and bedding returns.

    Their research indicates that Harvey Norman is a target that can “easily” be attacked because of their franchise model and their management structure and the fact that Steinhoff can manufacture premium and budget furniture and bedding, cheaper than Harvey Norman can source furniture or bedding for the Australian and Irish markets. 

    According to former Harvey Norman management furniture and bedding is a “massive” profit earner for the mass retailer.

    “The profit margin in bedding is over 100%. They said staff are able to buy bedding at staff rates that are half the retail price.

    “A top end mattress that is selling for $6,000 is available to staff for $2,890” they said.

    “There are similar margins in furniture” they added.
     
    What Harvey Norman refuse to do is break out the revenues or profit margins for furniture or electrical when they report their quarterly results.
     
    Recently the French furniture group Conforama which is owned by Steinhoff International threw in the towel in its quest to buy European white-goods retailer Darty a group that is very similar to The Good Guys. 

    This has left them in a position to refocus on a potential acquisition of The Good Guys in Australia.

    One person involved described the battle between Steinhoff and Fnac who is listed in London but operates stores in France and was the final winner in the fight for control of Darcy as “an epic battle”, this could be a sign that JB Hi Fi could be in for a fight to get control of The Good Guys. 

    Sources have said that Steinhoff management has also been talking to Woolworth about their Masters properties, as they are currently exploring the concept of launching large warehouse type stores that will sell a combination of appliances and furniture, a move that analysts claim could also impact Harvey Norman.
    One analysts suggested that It’s Gerry Harvey who should be offering his business to Steinhoff International and not The Good Guys.

    According to sources at The Good Guys the Muir family have told interested parties that there is “no way” that they will sell the retail group to Harvey Norman due to previous run ins by Gerry Harvey with Ian Muir the founder of The Good Guys.

    Steinhoff was founded in 1964 in the UK, Steinhoff owns the high street brands Cargo, Harveys Furniture, Bensons for Beds and Sleepmaster. 

    In 2011, Steinhoff bought Conforama, Europe’s second largest retailer of home furnishings, with over 200 stores in France, Spain, Switzerland, Portugal, Luxembourg, Italy and Croatia.

    Steinhoff’s South African brands include HiFi Corp, Pennypinchers, Timbercity, Pep, Ackermans, Shoe City, Incredible Connection, and Unitrans.

    Prior to announcing their interest in The Good Guys, the mass CE retailer who has approximately 3% of the appliance market currently was aiming for 75 JB Hi Fi HOME stores by the end of financial year 2017.

    At the close of market on Friday night JB Hi-Fi’s shares were trading for $23.87, giving them a market capitalisation of $2.4 billion. Meanwhile, the company has achieved EBITDA just shy of $250 million in the 12 months to 31 December 2015, putting them on an EBITDA multiple of around 9.3x.

    Assuming that The Good Guys was worth roughly the same EBITDA multiple, that could mean JB Hi-Fi would need to pay close to $1 billion for the business.
    On the other hand Steinhoff who after backing out of a $1.6B deal to buy Darcy are now in a position to go after The Good Guys. 

    Call For Legal Action After Harvey Norman Refuses To Deliver Advertised Discounts To Hundreds Of Consumers

    They love spruiking so called “Massive Discount Sales”, but when consumers responded recently to a Harvey Norman offer, the big retailer took their money and then refused to give them the cheap price the goods were advertised for.

    Now consumers are being urged to sue the big retailer with one litigant claiming that Harvey Norman actions are “unlawful”. 

    Customers who believed they picked up dirt-cheap furniture deals in a Harvey Norman online sale say extra payments due to be deducted from their accounts have shot up.

    The debacle surrounding the launch of Harvey Norman’s “New Zealand’s Biggest Retail Sale” intensified when the company confirmed it would not honour the deals.

    Hundreds of super-cheap online deals on pricey furniture products were taken up between midnight and 8am by consumers. 

    The near-330 people who took up the deals were later advised by email that the prices were the result of a “genuine error”.

    “All sales made during this period cannot be honoured,” Harvey Norman wrote. “Our website terms and conditions state that we may accept or reject any offer to purchase made by you and that we have the right to correct any errors.”

    Now Litigant Graham McCready is calling on customers affected by a botched Harvey Norman sale to file a private prosecution against the company.

    Customers have been offered a refund and $100 voucher, but Mr McCready insists the company must honour the original deal.

    “We take the position that that’s basically getting into the area of theft and fraud, it’s totally unlawful,” says Mr McCready.

    He says customers who have been charged more than the advertised price should contact their bank and file a fraud complaint against Harvey Norman on the grounds that the transaction is in dispute and no goods have been delivered.


    “The private prosecutor is prepared to act for customers in these circumstances to file a private prosecution against the CEO of Harvey Norman under the New Zealand Crimes Act.”

    Retail Up 2% And That’s Official

    Retail sales over the Xmas New Year period were 2% higher than last year, claims the Retail Traders Association proving that the Federal Government handouts did makes their way into the economy they say.

    Retail sales over the Xmas New Year period were 2% higher than last year claims the Retail Traders Association proving that the Federal Government handouts did makes it’s way into the economy they say. 

    The Association’s executive director, Richard Evans, says the Federal Government’s cash handouts, and the falls in petrol prices and interest rates have helped buoy the retail market.

    And he says he is expecting the handouts to continue to flow through to cash registers.

    “It’s too much to expect that people went out immediately and spent it, they may have used it on their credit cards or to reduce debt, but they’ll soon realise that they’re cashed up and they’ll start re-entering the market place,” he said.

    Mr Evans also says retailers have been slashing prices to get rid of stock but he says prices are unlikely to remain low with the slump in the Australian dollar.

    “Expect less range on the shelves over the winter months and probably a little bit more extra in price,” he said.

    “What we have to understand though, this is a normal cycle… a bit more severe than normal though of course, but we’re expecting growth to come back into the retail market in April-May this year and with strong growth in the September quarter.”

    Why Storage World Is A Not A Very Smart Business

    COMMENT: What is it about Australia and the lack of service by retailers? In the US and the UK retailers go out of their way to build databases of customers and in some cases they even issue them with barcodes or cards so that regular customers get priority service.

    Even corner store dry cleaners in the USA use CRM systems to gather intelligence on their customers. But in Australia the attitude of retailers is totally the opposite.


    For example last month I walked in Storage World at Northbridge in NSW where during the past few years my wife and I have spend litterally thousands buying storage gear for temporary accommodation while we built a new house. We also purchased wardrobe rack systems for the new house as well as things like tie and belt racks as well as kitchen and laundry storage gear.

     

     

     

    This is not some corner store mum and dad store. This is a chain of stores run nationally across Australia with the Northbridge store being owned by the Company that also franchises the Storage World brand. 


    So when I walked in there some four weeks ago to buy some more clothing racks similar to ones that I had already purchased I discovered that they were out of stock and this is when I realised that this was a Company that had a major problem when it came to customer service.


    After inquiring as to whether they could order stock in for me I was told “yes” not a problem and after giving the assistant both my name and that of my wife I was told it would only take a couple of days.


    Four weeks later and after numerous calls to the shop I still don’t have the goods but I do have a poor customer experience.

     On two occasions I was told it would only be a few days but on my fifth call some three weeks later I actually asked them to repeat the telephone number of either my wife or myself.

     

    After an 8 minute wait during which time I ended up talking to two assistants a Storage World employee came back and said “What was your name I don’t seem to have a record of the order”.


    Now if this was a Company that took customer service seriously they would by now have both my wife and I on a database.
    They should have also offered to phone other stores in their group to see if they had the items in stock.


    They should have also, after telling me that it would be in stock within days phoned me after a week to tell me that they were still waiting for the goods to arrive.


    But they didn’t because the pimple faced youth on the floor that served me and who just happens to be the critical interface between the customer and the business did not care. He failed to go to a master database and enter any customer details.


    All he wanted to do was move onto his next customer finish his shift and get paid. He made no attempt to deliver a good customer service experience and I blame management for this.


    Storage World is not alone when it comes to delivering poor customer service in Australia. Harvey Norman and the likes of Dick Smith make no attempt to build extensive CRM databases so that they can offer their regular customers exclusive services and viewings.


    Organisations that do understand customer service are the likes of automotive Companies who while taking an order build an extensive database which they later market to in an effort to stay in touch with their most valuable asset a customer who has the ability to spend money.


    They send out magazines that constantly remind the customer about the performance of the brand. They invite customers and their friends and partners to cocktail parties and special viewings of new products.

     

    I also wonder how many interior or kitchen designers are on the Storage World database, because every day these trades are recommending storage options to customers.


    I for one have never received a marketing brochure from Storage World despite going to their store for more than 10 years but guess what I still get a regular brochure from a store in South Coast Plaza in the USA where I have on several occasions purchased goods.


    So what is the difference? One understands the value of customer service and the other doesn’t give stuff.

    So what is customer service all about? See our recommendations and those of the NSW department of business.