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 13 of 91

    Smart Office

    Clive Peeters Looking For Cash As Sales Slump And Profits Evaporate

    Shares in appliance and consumer electronics retailer, Clive Peeters, have fallen 34% today after the company reported a $4.5 million dollar loss with little hope of future profits from the company who last year got back over $16 million from their former payroll manager.

    The company has also said that they are currently in talks with their bankers about future funding with analysts tipping that the company is set to struggle going forward as the market softens and demand for consumer electronics and appliances slow.

    Currently, the former payroll master of Clive Peeters, Sonia Causer,  is facing theft charges in a Melbourne Court after an extensive investigation revealed that close to $20M dollars was stolen from the Company over a two year period. The theft went undetected by senior management.

    In the three months January to March 2010, the company reported a loss of $4.5 million. This compares to a loss of $0.6 million for the same period in 2009.

    The company is tipped to make a bigger loss in the last quarter of the 2009/2010 trading year after reporting that April sales have deteriorated.

    In a report to the ASX, Clive Peeters said trading conditions at the beginning of H2 2010 for the big ticket discretionary retail sector were challenging and that the receding effect of the Federal Government stimulus packages had begun to affect consumer spending.

     

    The board believes that the cumulative effect of five official interest rate rises in the last seven months appears to be having a significant effect on consumer spending.

    Earlier today the Australian Reserve Bank increased interest rates a further 0.25% to 4.5%. This will put pressure on Clive Peeters borrowing, say analysts.

    Greg Smith, Managing Director said “The combination of very subdued sales and margin pressures will materially impact the trading outlook of the company over H2 2010, despite the company’s successful cost reduction programme, which it implemented over FY 2009 and has maintained over FY 2010 to date.

    The company is still hoping for some improvement in retail conditions over the months of May and June 2010, which are traditionally stronger months in the second half of the financial year. Also the major event of the World Cup football and the progressive release of a host of new home entertainment and technology products, including 3D and Internet TV, are expected to stimulate some sales growth over these two months, and hopefully beyond”.

    Smith added “The reports of a strengthening housing market, an improving unemployment trend and forecast economic growth all provide some grounds for optimism”.

    Harvey Norman To Add $500M In Revenue After Buying Clive Peeters

    Harvey Norman Holdings have snapped up Clive Peeters and WA retail group Rick Hart for the basement price of $55M. The acquisition is tipped to add an additional $500M to Harvey Norman revenues.

    The deal involves approximately 38 stores however speculation is mounting that some of these stores will be closed as part of a rationalisation by Harvey Norman. Six loss making stores were closed last month.
    There is also speculation that Harvey Norman Holdings will trade as Clive Peeters and Rick Hart stores.
    Former Clive Peeters CEO, Greg Smith, a former accountant who was also the largest shareholder in Clive Peeters, will get nothing out of the deal with the bulk of the Harvey Norman payment going to the National Australia Bank who last month appointed receiver Phil Carter from corporate advisory firm, PPB.
    Late on Friday night Harvey Norman Chairman, Gerry Harvey, admitted that the retailer has always had an interest in buying out Clive Peeters, but the high cost of several leases associated with the stores meant that the better option was to let the stores go broke and then bid for the remaining assets with the lease liabilities. 
    Harvey told the Herald Sun in Melbourne: “We were always interested in Clive Peeters but we couldn’t make a bid for it or anything because they had a number of onerous leases,” 
    “So it had to go into receivership before you could actually negotiate some of those leases.”
    Mr Harvey said it was no surprise that the business went bust and he was waiting to make his move on the company.”It’s been common knowledge for a long time that Clive Peeters is on the verge of insolvency,” he said.
    Clive Peeters was forced into voluntary administration on May 19 by the National Australia bank who refused to extend the company’s borrowings. The debts of the retailer were over $160 million.
    On June 11, receiver Phil Carter of PPB Pty Ltd said 75 jobs would be lost from the closure of six underperforming stores.
    In a statement to the ASX on Friday, Harvey Norman said that it had agreed to buy “certain stock and plant and equipment located at certain of the locations, knowhow, intellectual property rights and systems”.
    Harvey Norman is expected to take over about 30 stores under the Clive Peeters and Rick Hart banners. But nothing has been unveiled yet about three warehouses belonging to the company.
    Mr. Carter was appointed as receiver by National Australia Bank in May. He closed six stores and finally the business was put up for sale.
    Carter said that Harvey Norman has assured the receiver that it will be providing employment to the majority of Clive Peeters and Rick Hart employees.

    Spam Levels Fall 50 Percent After Illegal Networks Smashed

    The Australian Federal Police have in part contributed to an International operation that that has seen global spam levels fall by almost 50%.Taking part in an operation that started in August 2010 the AFP has been working closely with several police forces including Scotland Yard and the FBI and a private security Company LastLine as well as various ISP’s, to hurt the networks behind the illegal online operations.

    Research compiled by security firm Symantec show that the amount of junk e-mail messages flowing around the net has dropped 47% in three months. Kaspersky Labs had previously reported falls in September of spam of up to 81.1% of all e-mails following a joint operation by various police forces to cut out International spam.

    The operation against individuals and organisations that was sending botnets and gathering illegal intelligence such as credit card details and personal banking details has seen several organisations shut down.

    Yesterday the Australian Federal Police (AFP) and online security firm Symantec joined forces to promote consumer awareness of cybercrime with a new education program called BLK MKT (Black Market). On hand to support the cause were Stephen Conroy and Attorney-General Robert McClelland, who said that the Government was continuing to push for a “secure, resilient and trusted cyber environment”.

     

    According to Symantec’s 2010 Norton Cybercrime Report, 65 percent of adults worldwide have already fallen victim to cybercrime, while in Australia the statistic is slightly higher, with 69 percent of adults affected.

    According to police sources in Europe, one of the biggest successes of the joint operations was against the Pushdo or Cutwail botnet, which had been in operation since 2007 and was thought to be sending about 10% of global spam.

    The BBC said that an international operation co-ordinated by the security firm LastLine managed to get 20 of the 30 servers controlled by the group shut down. The servers were turned off with the help of the internet service providers unwittingly found to be hosting them.
     
    As a result, many of the “drone” PCs in the huge botnet used to send e-mail were cut off and no longer relayed the junk messages.

    Millions of machines around the world including several in Australia are turned into spam-sending “botnets”.

    Bredolab was another big botnet hit in October thanks to work by the hi-tech division of the national crime squad in the Netherlands. The arrest of an Armenian man thought to be the botnet’s controller led to the closure of the 143 servers linked to Bredolab.

    At its height Bredolab was thought to involve up to 30 million computers around the world and be capable of sending 3.6 billion e-mails every day.

    Discounting Hurting Claims Harvey Norman Director

    One of the most senior executives in the consumer electronics industry has called for manufacturers to stop discounting. He claims that factories making TV’s to notebooks are slashing costs in an effort to support their faltering manufacturing operations and that the move is set to impact CE retailing in Australia in a bad way.One of the most senior executives in the consumer electronics industry has called for manufacturers to stop discounting. He claims that factories making TV’s to notebooks are slashing costs in an effort to support their faltering manufacturing operations and that the move will have a big effect on CE retailing in Australia.
    David Ackery the General Manager  of Electrical at Harvey Norman said “discounting is rampant and in many cases manufacturers subsidiaries in Australia are being forced by factories to discount. This is not good because if we going to sell less we need to have some profit left in a product to survive”.
    “Margin is good not bad because without margin all we are doing is product churning. During the past 12 months we have seen increased sales of notebooks and flat panel TV’s but at the same time margins have been eroded because of rampant discounting”.
    Recently Channel News revealed that the margin in flat panel displays have dropped in price by as much as 30% monitors by 45% and that notebooks have fallen by up to 40% yet despite this Companies like Sony and Panasonic are introducing big price rises due to the fall in the Australian dollar and the rise of the Japanese Yen.
    Len Wallis of Len Wallis Audio in Sydney supported Ackery in his call for more margin in products “Discounting is having a big knock on effect. We cannot buy a TV product  that is under the price that the mass retailers are selling it for example a Sharp large screen LCD TV at Bing Lee is $160 cheaper than I can buy it for. Plus you get a free Chinese dinner set and the chance to win a trip to Hong Kong”.
    He added “The mass retailers must be suffering as they are not making much money selling TV’s even worse is that they will be dependent on rebates and these don’t come through for several weeks so many will be cash flow negative straight after they have made a sale”.
    Several manufacturers contacted by ChannelNews were not available to comment.

    Comment: Has Kogan Online Got Cash Flow Problems?

    COMMENT: The move by Kogan Technologies to solicit consumers to pay upfront before one of his products is manufactured in China smells more like cash flow issues than offering a consumer a big discount benefit.

    This week Ruslan Kogan, founder of online retailer Kogan Technologies, launched  ‘LivePrice’ an incremental pricing scheme, which he says allows customers to purchase a product for a lower price earlier in its production cycle.

    See original story here

    But I supect the issue is more about cash flow than consumers having to wait months to get a product.

    As Gerry Harvey, CEO of Harvey Norman and arch enemy of Ruslan Kogan, was calling on the Federal Government to introduce a 10 percent GST on all online transactions under $1,000, Ruslan Kogan would have been sweating on how much business overseas web sites are sucking out of the Australian economy.

    In reality overseas web sites are more of a threat to Australian online sites than they are to branded retail stores in Australia because a consumer who is prepared to shop online is most likely shopping for the cheapest price.

    Having owned Digital Home, which was an online trading site that we sold to JB Hi fi to become what is today, JB Hi Fi’s online operation, I know firsthand the difficulties of running a local online trading operation.

    By majority, local online operators are running web sites that are nothing more than a marketing front. Once an order is placed the operator who often takes the money up front for a product, then places an order on his supplier, which in a lot of cases is a distributor like Ingram Micro, or Synnex, or the hundreds of other distributors who are now selling products such as Smartphones, Apple accessories, TVs  or PCs to online operators.

    This mode of operation means that the cash is in the bank before the online operator has had to place an order on his distributor.

    In the case of the Kogan Technologies’ web site he is primarily selling “Made in China” products such as TVs appliances, Tablets, Set Top Boxes.

     

     

    Another issue for Kogan is that Australia is known for short runs, when it comes to manufacturing products for this market and the fact that Kogan is a very small operator further impacts his ability to get the cheapest price from a manufacturer who is more interested in large runs than short runs. On the up side Kogan can very easily identify a no brand product made in China and then have the manufacturer slap a Kogan logo on the device.

    For Kogan the issue is cash flow and I suspect that this is why he has started offering consumers an upfront deal that if they pay for a product before it is manufactured they get a discount.

    The Kogan model operates in almost reverse as to how most online operators trade for the simple reason that he has to mostly, pay for a product up front, then wait for it to be manufactured and shipped from China to Australia. There are also handling and distribution costs which have to be paid for upfront.

    In some cases Kogan is advertising a product for sale using pictures and words, taking an order and then having the consumer wait for delivery. Ben Knapinski is a disgruntled Kogan customer who had to wait two months to get delivery of a Kogan 46″ LED TV which Kogan claimed was superior to what Gerry Harvey was selling in his stores.

    When Knapinski finally got his Made in China TV he described it as “absolute rubbish”.

    “I don’t even want to put it in the kids play room as I am worried the flickering and jitter will hurt their eyes it’s that bad” he said.

    In any online operation cash flow is king and margins are thin. I also suspect that Australian independent online operators that are not aligned with a major consumer electronics or IT brand are going to come under pressure next year as more consumers move online, Kogan among them.

    Dick Smith Grows 9%

    Sales at the Woolworths group’s Australian and NZ consumer electronics stores – including the Dick Smith chain – grew 9 percent to $839 million in the six months to December 31, the group said yesterday.

    Sales at the Woolworths group’s Australian and NZ consumer electronics stores – including the Dick Smith chain – grew 9 percent to $839 million in the six months to December 31, the group said yesterday.

    Growth in the second quarter was 11.6 per cent compared with just 6.1pc in the first quarter – possibly reflecting extra Christmas sales spurred by the Rudd Government’s stimulus program.

    A Woolworths statement to the ASX noted that the consumer electronics sales spurt had been delivered at a lower margin “as we transition out of certain categories and experience both changes in sales mix and a highly competitive market.”

    Woollies opened 29 new DSE stores and three Powerhouse stores during the six months, talking the total to 433 stores.

    A joint venture with Tata in India now has 26 consumer electronics stores operating under the Croma brand, producing sales of A$90 million for the half year.

    Panasonic Look For Growth By Outsourcing Sales And Merchandising

    Panasonic, who are looking for aggressive expansion across several categories, has moved to outsource a large part of their sales, training and store merchandising operations to US Company Crossmark who have more than 2,000 employees in Australia.

    According to Panasonic CEO Steve Rust the move will give Panasonic extensive reach across Australia including rural and metro areas that they are currently struggling to service.

    “More importantly it will allow Panasonic  to expand the categories that we compete in” said Rust who indicated that he is looking to move into the hardware market with a range of electrical tools that Panasonic sell in Japan and into healthcare and grooming markets, via pharmacy and Supermarket chains.

    “We will retain our direct relationships with the big retailers where we are selling our consumer electronics goods, but we will outsource our selling merchandising and in the future training which is a costly overhead” said Rust.


    “Primarily, we are looking to expand our selling capability across Australia especially in the smaller outlets where Crossmark is already operating. We are also looking at bringing a lot of new products into Australia and it makes sense to use an operation like Crossmark as they have over 2,000 full and part time staff who are dealing everyday with pharmacies, supermarkets, hardware chains and other retail outlets where we believe, we can range and sell, Panasonic products that we are not currently bringing into Australia”.

    Last month, as a forerunner to the Crossmark announcement, Panasonic Australia announced a major restricting of their Australian operation with the laying off of over 20 staff.

    Rust said that 11 Panasonic sales staff in the New South Wales office have been offered positions with Crossmark.

    More to follow.

     


     

    Comment: Whats Wrong At LG OZ?

    COMMENT: The exit of David Brand from LG Australia was well and truly on the cards when the company was exposed by Choice Australia for fudging the truth about their products.Now the Korean company,  who is facing multimillion dollar fines, is undergoing a major shakeout and relaunching LG Australia, after the appointment of William Cho, the former President and CEO of LG Canada, as Chief Executive of LG Australia.

    Cho, who was shipped into Australia days after LG Australia was found to have lied about the power performance of their refrigerators, is a tough performance-driven operator with a track record of success in the Canadian market where he headed the company’s operations.

    High on his agenda will be improving LG Australia’s profitability. In the 2008/2009 financial year they only managed a $13K profit on nearly a billion dollars turnover.

    In Australia, Cho has already taken action to fix the company’s endemic problems with the axing of several key managers. Its been suggested that this is only the start and that several other heads, including several in sales, will be axed as part of the restructure.

    This could be a precarious exercise as the likes of Graeme Cunningham, the current sales director of LG Australia, has excellent contacts, is trusted by the retail channel and has often been the glue that has held the LG operation together in Australia.

    Among those who have gone from the company since Cho’s appointment  are David Brand, the former Marketing Director, Carli Wilson, the former Marketing Manager of the company’s struggling Communications Division, who late last month was still running what some observers described as “froth and bubble” marketing events for handsets that are going nowhere in the Australia market up against offerings from arch rivals like Samsung, HTC and Apple.  

    Cho has already started to stamp his own management style on the Australian operation with the appointment of Kim Barnes as marketing manager of consumer products. Barnes came from LG Canada. He has also hired former Coca Cola executive Mark Van Dyke. He is also on the lookout for a new Marketing Director who will be given a brief to basically relaunch the company.

    During the past three years Brand has had one disaster after another from a Scarlet TV launch that went pear shaped, to multimillion dollar phone launches that did little to stimulate sales, to the exposure of LG as a serial offender in the appliance market which resulted in LG being nobbled three times by the Australian Competition and Consumer Commission for misleading consumers. There was  also the issue of several recalls of LG air conditioners and appliances.

     

    The LG slogan “Life’s Good” was well and truly on the nose.

    Five years ago under the direction of Paul Reeves, LG’s former Marketing Director, the LG brand was hitting a sweet spot and the slogan Life’s Good really meant something with consumers because it was unique, locally developed and above all in touch with the Australian way of life.

    Then along came Brand, who in reality was a puppet of what his Korean task masters wanted.

    Killed off  was the memorable local advertising. This was replaced with big budget International advertising that failed dismally. The big budget Scarlet TV campaign came and went along with several other International campaigns.

    At one stage Brand was told by his corporate masters in Korea that he had to appoint WPP group agencies in Australia. This resulted in Mindshare taking responsibility for media planning and buying.

    George Patterson Y&R was appointed to  handle above-the-line advertising duties, while Publicis Mojo’s digital arm Publicis Digital, were appointed to manage website and digital marketing.

    Brand was then forced to call a pitch for public relations. This resulted in several WPP owned PR companies fighting among themselves as to who would get the spoils.

    The incumbent, Burson Marsteller, threw in the towel and WPP owned Pulse was appointed to set up a new operation called LG One. This operation was driven out of Korea, with the local management given little opportunity to build the local brand.

    In reality LG is a dynamic company. Their display operation is among the best in world, even Steve Jobs at Apple gives them credit for that with the company tasked with the development of new AMOLED and OLED screens for several Apple products.

     

    They also make great appliances.

    After gaining popularity with their mobile phones in Australia, LG has failed to keep pace with offerings from Samsung, HTC and Apple. Big investments in fluff PR events using the likes of Chris Noth came to nothing.

    What LG needs to do is invest in local marketing and not try and feed Australians on a diet of overseas marketing swill.

    Their advertising needs to be locally relevant and focused, and this will only be achieved if they empower a local marketing director who is given the choice of either using an International campaign or a locally developed campaign.

    The company also needs to talk more about the brand and stop dishing out boring product press release that are more product numbers and specs than brand substance.

    They also need to develop their people to be brand ambassadors, talking about LG as a company.

    Because, at the end of the day, a brand is remembered long after a product has become obsolete.

    JB Hi Fi Storms Home 40% Profit Lift

    Consumer electronics retailer JB Hi Fi has defied the market by reporting a bumper 40% increase in profits and a sales increase of 28%. The Company also believes that the CE industry at large will benefit from further Federal Government stimulus packages.

    As a Reserve Bank board member called  for a change in the way that the stimulus package is distributed in an effort to prevent consumers spending on what they described yesterday to a Senate hearing as “Plasma & Pokies”, JB HI Fi has said that sales in January and February are as forecast.

    One JB Hi Fi Manager said “We are definitely benefiting from the money that was handed out in December. This has certainly helped our sales.”

    During the past six months as Harvey Norman was closing stores JB Hi Fi has opened 14 stores during and will open a further seven stores in the second half.  They are also looking at new locations following the collapse of several retailers, said Richard Uechtritz during a recent interview with ChannelNews.

    Uechtritz said, “Whilst the retail outlook is less certain than previous reporting dates, the company is cautiously optimistic that it will have another strong year and confirms its previous guidance that sales will be circa $2.35 billion or a 28 per cent increase on the prior financial year.”

    Store growth rose 11.1 per cent across its regions and management said margins remained stable at 21.4 per cent despite discounting at its competitors.

     

     

    The tough economic environment for the retail sector will present expansion opportunities, the company said.

    “The weak retail climate should throw up expansion opportunities,” Mr Uechtritz said during a teleconference.

    “Sites from companies like Crazy Clark’s and Go-Lo are possibilities and some pass down sales that would have otherwise gone to the Strathfield business upon closure of those stores.”

    The company renewed its debt facilities in December for another three years and said it had an ability to take advantage of any attractive growth opportunities that may arise in the current weak economic climate.