Friday 9 February 2024

Uploading Files in WordPress (in PHP, not using plugins)

It took me a quite while to work out a few things about file uploads so I am hoping to help others get there faster and easier.

The following is a simple form that we will use, the file input control has the name "upload", but it can be anything. I have seen CSS methods to make the "choose file" text pretty (actually replace it), but not without losing the attached file information in the form and a tooltip.

The maximum file size is specified to the PHP server (in "MAX_FILE_SIZE") and it is also check client size with Javascript.

Any form that uploads files should have an "enctype" of "multipart/form-data". The "accept" parameter below specifies the allowed file types/extension, the user can override this so mime types of uploaded files should be checked on the server for security reasons.


<h1>PDF + IMAGE Upload Form</h1>
<form id="upload_form" action="https://bungalooknursery.com.au/reimbursements" enctype="multipart/form-data" method="post" onsubmit='CheckFileSizes();'>
    <input type="hidden" id="MAX_FILE_SIZE" name="MAX_FILE_SIZE" value="3145728" />
    <p><input id="upload" name="upload[]" id="upload" type="file" accept="application/pdf, image/gif, image/png, image/webp, image/jpeg, image/pjpeg" multiple=true /></p>

    <p><input id="btnSubmit" type="submit" value="Upload Files" />
       <input id="reset_upload_form" type="reset" value="Reset form" /></p>
</form>

<script>
function CheckFileSizes()
{
    //--- Loops though files to be uploaded (check size) --------------
    //debugger;
    let MaxSizeObj = document.getElementById('MAX_FILE_SIZE');
    let UploadObj  = document.getElementById('upload');
    let MaxSize    = parseInt( MaxSizeObj.value );
    let FileList   = UploadObj.files;
    let Oops = '';
    for (var i in FileList)
    {
        let ThisFileObj = FileList[i];
        let FileSize    = ThisFileObj.size;
        let FileName    = ThisFileObj.name;
        if  (FileSize > MaxSize)
        {
            if (Oops != '')
               Oops = Oops + "\n";
            Oops = Oops + '* "' + FileName + '" (' + bytesToSize(FileSize) + ' bytes)';
        }
    }

    //--- Report and cancel submission --------------------------------
    if  (Oops != '')
    {
       //--- Report to user -------------------------------------------
       Oops =        'One of more files (listed below) are larger than ' + bytesToSize(MaxSize) + '.\n\n' + Oops + '\n\n';
       Oops = Oops + 'The easiest way to compress an image is using a photo editor to resize the picture while ensuring it remains legible!  If you halve both dimenstions you will generally lose 75% of the size.';
       alert('\n' + Oops);

       //--- Cancel submission ----------------------------------------
       event.preventDefault()      //Stop form being submitted!
    }
}

function bytesToSize(bytes) //https://gist.github.com/lanqy/5193417 // http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/
{
    var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
    if (bytes == 0) return 'zero bytes';
    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
    if (i == 0) return bytes + ' ' + sizes[i];
    return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
};
</script>

PHP has an INI value "upload_max_filesize" configured which is the maximum size of an upload it will accept. The "MAX_FILE_SIZE" control is used to further restrict the size below the maximum possible upload size for your particular form and is used above informs PHP to abort the upload of the file if it is larger than this value, this results in a quicker error.

You can validate the file size in Javascript (that is not shown here), but if it is critical, you should also check the filesize in your PHP code. You should also check the mime types uploaded, since while I specified that I only want certain mime types uploaded, the user can override that on the client and send any type of files.

The below code is PHP on the server.... When your PHP code starts, the files have already been uploaded (or failed doing so) to a temporary directory and filenames. It is best to use "move_uploaded_file" to move the file for the extra validations it performs.


HandleAnyUploadedFiles();

//=============================================================================================
function HandleAnyUploadedFiles()
//=============================================================================================
{
    //--- Any files attached? -------------------------------------------
    $Rc = 0;
    if  (! empty( $_FILES ) )
    {
        //--- Get the destination directory -----------------------------
        //echo MyDump($_FILES, '$_FILES');
        //echo MyDump($_POST,  '$_POST');
        $UploadDir = wp_get_upload_dir();
        $ToDir     = $UploadDir['basedir'] . '/$Reimbursements/' . '{IdEventually}';

        //--- We accept any number of uploads at once -------------------
        $Uploaded  = $_FILES[ 'upload' ];        //The name of the HTML "input" control doing the uploading
        $Names     = $Uploaded['name'];          //The name of the file at the client end
        $TmpNames  = $Uploaded['tmp_name'];      //Names of already uploaded files
        $MimeTypes = $Uploaded['type'];          //What is the mime type of the file
        $UlErrors  = $Uploaded['error'];         //Upload RC
        for ($i = 0; $i < count($Names); $i++)
        {
            //--- Get filename info for the current upload --------------
            $FromFile = $TmpNames[$i];
            $ToFileSN = basename( $Names[$i] );
            $ToFile   = $ToDir . '/' . $ToFileSN;
            $UlError  = $UlErrors[$i];
            $MimeType = $MimeTypes[$i];

            //--- Check for upload error --------------------------------
            if ($UlError != UPLOAD_ERR_OK)
                $Oops = "The uploaded failed, RC: " . GetFileUploadErrorMessage($UlError);
            else
            {
                if  (! ValidateMimeType($MimeType))
                    $Oops = "The mime type of \"{$MimeType}\" is unsupported";
                else
                {

                    //--- Sanity check, Does the tmp file exist (it should) ---
                    if  (!is_file($FromFile))
                        $Oops = "The uploaded file \"{$FromFile}\" doesn't exist!";
                    else
                    {
                        //--- Make sure the destination dir exists ----------
                        if  (!is_dir($ToDir))
                            mkdir($ToDir, recursive: true);

                        //--- Move the file (it will overwrite existing if required which we probably want) ---
                        error_clear_last();
                        $MoveRc = move_uploaded_file($FromFile, $ToFile);
                        if  (! $MoveRc)
                            $Oops = "The upload file \"{$FromFile}\" couldn't be moved, REASON: " . GetLastErrorMessage();
                        else
                            $Oops = '';         //ALL GOOD
                    }
                }
            }

            //--- Report failures ---------------------------------------
            if  ($Oops != '')
            {
                echo "ERROR: Upload of \"{$ToFileSN}\" failed: {$Oops}";
                $Rc = $Rc + 1;
            }
        }
    }
    return $Rc;     //Count of failed uploads
}



//=============================================================================================
function GetFileUploadErrorMessage($Rc)
//       https://www.php.net/manual/en/function.error-get-last.php
//       SIMILAR TO: $php_errormsg
//=============================================================================================
{
    //--- List of error messages ----------------------------------------
    $UploadErrors = array(
            0 => 'There is no error, the file uploaded with success',
            1 => 'The uploaded file exceeds the "upload_max_filesize" directive in PHP (currently "' . ini_get('upload_max_filesize') . '", often set in php.ini, or CPANEL,PHP OPTIONS)',
            2 => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form (currently ' . $_REQUEST['MAX_FILE_SIZE'] . ' bytes)',
            3 => 'The uploaded file was only partially uploaded',
            4 => 'No file was uploaded',
            6 => 'Missing a temporary folder',
            7 => 'Failed to write file to disk',
            8 => 'A PHP extension stopped the file upload',
    );

    //--- We were pass the index to the above array, return text if it exists ---
    $R = $Rc;
    if  ( isset($UploadErrors[$Rc]) )
        $R = $R . ' = ' . $UploadErrors[$Rc];
    return $R;
}

//=============================================================================================
function ValidateMimeType($MimeType)
//       https://wpengine.com/support/mime-types-wordpress/
//                * .jpg    image/jpeg, image/pjpeg
//                * .jpeg   image/jpeg, image/pjpeg
//                * .png    image/png
//                * .gif    image/gif
//                * .pdf    application/pdf
//=============================================================================================
{
    //Supported mime types
    //--- List of error messages ----------------------------------------
    $DoesNotMatter = '';
    $AllowedMimes = array(
            'application/pdf' => $DoesNotMatter,
            'image/gif'       => $DoesNotMatter,
            'image/jpeg'      => $DoesNotMatter, 'image/pjpeg' => $DoesNotMatter,
            'image/png'       => $DoesNotMatter,
            'image/webp'      => $DoesNotMatter,
    );

    //Is the mime type OK?
    if  (isset($AllowedMimes[$MimeType]))
        $R = true;
    else
        $R = false;    //Not in the allowed list
    return $R;
}



//=============================================================================================
function GetLastErrorMessage()
//       https://www.php.net/manual/en/function.error-get-last.php
//       SIMILAR TO: $php_errormsg
//=============================================================================================
{
    $Err = error_get_last();
    if  ($Err == null)
        $R = '';
    else
        $R = $Err['message'];
    return $R;
}

The following dump/code shows what "$_FILES" contains on the server (for the 2 files being uploaded):


array (
  'upload' =>
  array (
    'name' =>
    array (
      0 => '[PICTURE] TCL TV model P745.webp',
      1 => '[Owners Manual] TCL TV model P745 - P745_Series_Owners_Manual.pdf',
    ),
    'full_path' =>
    array (
      0 => '[PICTURE] TCL TV model P745.webp',
      1 => '[Owners Manual] TCL TV model P745 - P745_Series_Owners_Manual.pdf',
    ),
    'type' =>
    array (
      0 => 'image/webp',
      1 => 'application/pdf',
    ),
    'tmp_name' =>
    array (
      0 => '/tmp/phpDKxB48',
      1 => '/tmp/phplqdiXg',
    ),
    'error' =>
    array (
      0 => 0,
      1 => 0,
    ),
    'size' =>
    array (
      0 => 48276,
      1 => 2611756,
    ),
  ),
)

The code fails with a a 403 response code on one particular PDF (out of many) for no obvious reasons. I have also seen this in other Wordpress situations, so I suspect something about the file's contents breaks some sort of Wordpress or browser (Chrome on Windows in my case) encoding/decoding. Googling came up with many other possible reasons none of which applied to my situation.

Thursday 4 January 2024

Sharing a Google "Plus Code" via a URL for Navigation

A lot of information exists on how to obtain a "plus code", but almost nothing about how they should be used.  

In this post I will use the local plus code for Monash Men's Shed (Australia) which is "4594+2C Glen Waverley, Victoria". You can see this place in maps here.  If you try to navigate there it will take you close to it (and often leave you to work out where it actually is, at least when compared with a street address).  In maps, below the phone number for the men's shed you will find the plus code listed.

Since a "+" is within all plus codes, you first need to escape it with "%2B" ("4594%2B2C Glen Waverley, Victoria") or it will be parsed to a space (they should have used another character...).

The URL "https://maps.google.com/maps?q=4594%2B2C Glen Waverley, Victoria" will open maps with the exact location highlighted (shaded red square) and navigation to it takes you to the exact spot.

You can also append the original or escaped plus code to "https://plus.codes/", see the location with the URL "https://plus.codes/4594+2C Glen Waverley, Victoria", this will redirect to "https://plus.codes/4RJ74594+2C" which unlike my original local one (which exists in multiple places around the globe), is a global that only exists in one spot.


Monday 4 September 2023

GMAIL Removing CSS STYLE (and referencing classes)

I'm sending emails using PHP in WordPress, there seem to be many issues with email clients in general (as a quick google will confirm).

In  GMAIL in 2023, if it doesn't like anything at all it will drop the complete "style" block (all CSS).  It will also update the email's HTML to remove any references to the dropped styles.

To check your email's HTML in general, click the Gmail messages "..." menu and choose "Show Original".  That will open up a new window, you can copy/paste the HTML you sent and run it through online HTML and/or CSS validators to check for obvious issues.

You can use Chrome's debug tools (<F12>) to find the source code and use <Ctrl><F> to search for something you expect to find in the CSS contents), if you don't find it, then you know it has been removed.

In my case, I'd fixed any issues reported but it still happened.  I went to the old standby binary tree debugging, where you keep removing code until it starts working, then keep inserting/deleting bits until you identify the style the line(s) causing the issue.

In my case, it would appear that it didn't like the incorrect important tag "!Important" (that the validators didn't pick up), fixing it solved the issue (it should have been "!important").

<style>.OrderInvoice table, .OrderInvoice th, .OrderInvoice td
{
    border: 2px solid black;
    padding: 5px;
    border-width: thin;
    border-collapse: collapse !Important;
}</style>

Friday 21 July 2023

Debugging Javascript Within JsFiddle, CodePen etc

As long as a debugger is open (such as Chrome's Debug Dev Tools <F12>), then the Javascript statement "debugger;" can be placed anywhere in your Javascript code to pause execution, when it is encountered you will be able to single step and watch variables as usual.

The debugger statement will have no effect if debugging tools are not already open.

You probably want to turn off any "Auto Run" option first (JsFiddle, Settings, Behavior, Auto-run code) to avoid confusion and frustration.

Here is an example jfiddle you can try.  The example uses "console.log()" to display some messages, which is the ultimate fallback for debugging any language (say, echo etc) and can help supplement a proper debugger by providing more context about the state of the code's execution.

Friday 30 June 2023

Prevent PHP Deprecated Messages in Wordpress PHP error logs

WordPress ignores the PHP error level so to set your own you need to use a must-use plugin, these reside in a "mu-plugins" directory next to the "plugins" one.  

A must-use plugin does not need activating and can be seen in WordPress in the "Plugins" section under a tab called "Must-Use".

To use the following code put in into a PHP file, add "<?php" to the start and use FTP or CPANEL to upload the file into the WordPress "mu-plugins" directory.


/*
 *  Plugin Name: Prevent PHP Deprecated Messages in PHP ERROR Logs
 *  Description: Wordpress stuffs up the PHP error level with it's own "error_reporting" levels so you can't rely on setting it in the "wp-config.php" configuration file for PHP!  This Must Use plugin removes almost all of the unwanted messages that can otherwise flood the error log.  It sets: error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)
 *  Version: 2023.06.30
 *  Author: Dennis Bareis
 */


    error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);  //Doesn't work in wp-config.php

Tuesday 6 June 2023

WordPress and "Confirm use of weak password"

 I don't want people to use weak passwords on my WordPress site, I looked for PHP code to disable the "Confirm use of weak password" row and checkbox but couldn't find anything that worked.  

My environment is WordPress 6.2.2 with a child OceanWP theme.

It seemed easiest to simply use CSS to disable it.  Obviously the CSS needs to be enqueued with PHP (for admin):


add_action( 'admin_head', 'BungalookAdministrationHeaderAssets');

 The CSS itself follows (include it in the enqueued CSS file):


.pw-weak { display: none !important; }

To improve the password requirements you can have a look at "Enforce Strong Passwords Without a Plugin - WordPress Tutorial", this is good but perhaps a bit confusing as you must first pass WordPress's "weak" check before the password can be submitted for further validation by the extra code.

Any error message is displayed at the top of the window (which may not be visible) and WordPress doesn't always scroll there...

I have modified the code a bit to display more informative error messages such as this one:


The complete modified PHP code follows:


/**
 * Enforce strong passwords (ESP) for all website users.
 *
 * https://wp-tutorials.tech/optimise-wordpress/enforce-strong-passwords-without-a-plugin/
 *
 * To disable enforcing strong passwords:
 *   define('ESP_IS_ENABLED', false);
 */

defined('WPINC') || die();

/**
 * Initialise constants and handlers.
 */
//===========================================================================
function esp_init()
//===========================================================================
{
   if (defined('ESP_IS_ENABLED') && (ESP_IS_ENABLED === false)) {
      // Disabled by configuration.
   } else {
      add_action('user_profile_update_errors',  'esp_user_profile_update_errors', 0, 3);
      add_action('resetpass_form',              'esp_resetpass_form', 10);
      add_action('validate_password_reset',     'esp_validate_password_reset', 10, 2);
   }
}
add_action('init', 'esp_init');



//===========================================================================
function esp_user_profile_update_errors($errors, $update, $user_data)
//===========================================================================
{
   return esp_validate_password_reset($errors, $user_data);
}

//===========================================================================
function esp_resetpass_form($user_data)
//===========================================================================
{
   return esp_validate_password_reset(false, $user_data);
}

//===========================================================================
/**
 * Sanitise the input parameters and then check the password strength.
 */
function esp_validate_password_reset($errors, $user_data)
//===========================================================================
{
   $is_password_ok = false;

   $user_name = null;
   if (isset($_POST['user_login'])) {
      $user_name = sanitize_text_field($_POST['user_login']);
   } elseif (isset($user_data->user_login)) {
      $user_name = $user_data->user_login;
   } else {
      // No user specified.
   }

   $password = null;
   if (isset($_POST['pass1']) && !empty(trim($_POST['pass1']))) {
      $password = sanitize_text_field(trim($_POST['pass1']));
   }

   $error_message = null;
   if (is_null($password)) {
      // Don't do anything if there isn't a password to check.
   } elseif (is_wp_error($errors) && $errors->get_error_data('pass')) {
      // We've already got a password-related error.
   } elseif (empty($user_name)) {
      $error_message = __('User name cannot be empty.');
   } else
   {
      $error_message = esp_is_password_ok($password, $user_name);
   }

   if (!empty($error_message))
   {
      //--- Display error message -------------------------------------------
      $error_message = 'ERROR: ' . $error_message;
      if (!is_a($errors, 'WP_Error')) {
         $errors = new WP_Error('pass', $error_message);
      } else {
         $errors->add('pass', $error_message);
      }
   }

   return $errors;
}

//===========================================================================
/**
 * Given a password, return "" if it's OK, otherwise return Reason it isn't.
 */
function esp_is_password_ok($password, $user_name)
//===========================================================================
{
    //--- Get userid & Password ---------------------------------------------
    $password = sanitize_text_field($password);
    $user_name = sanitize_text_field($user_name);

    //--- Run some checks ---------------------------------------------------
    $is_number_found    = preg_match('/[0-9]/',          $password);
    $is_lowercase_found = preg_match('/[a-z]/',          $password);
    $is_uppercase_found = preg_match('/[A-Z]/',          $password);
    $is_symbol_found    = preg_match('/[^a-zA-Z0-9]/',   $password);

    //--- Passed the above checks? ------------------------------------------
    $MinLen = 8;
    $R = "bug: oops";
    if       (strlen($password) < $MinLen) {
       $R =  __("The password is too short.");
    } elseif (strtolower($user_name) == strtolower($password)) {
       $R =  __("The User name and password can't be the same!");
    } elseif (!$is_number_found) {
       $R =  __("The password must contain a digit.");
    } elseif (!$is_lowercase_found) {
       $R =  __("The password must contain a lower case character.");
    } elseif (!$is_uppercase_found) {
       $R =  __("The password must contain an upper case character.");
    } elseif (!$is_symbol_found) {
       $R =  __("The password must contain a symbol (such as '@' or '#' etc).");
    } else {
       $R = '';  //Password good
    }
    if  ($R != '')
        $R = $R .  __("  The requirements for an acceptable password are that they must be at least ") . $MinLen .  __(" characters long and contain at least one each of [1] digits, [2] symbols, [3] lower case and [4] upper case");
    return $R;
}




Thursday 25 May 2023

Enable WordPress to Search Custom Fields (PODS etc) and Display Automatically Generated Excerpt

 By default, WordPress doesn't allow searching on dynamically generated pages, but if a search matches the title then it will not display any content in the search results!  There are many plugins that can improve this but so can a little bit of PHP code.

The following code allows standard WordPress to search within the extended fields (which is stored as meta data):

 

//=======================================================

function AddCustomFieldsToSearchText($query)

//      https://wordpress.org/support/topic/default-wordpress-search-does-not-work/

//      This allows Wordpress Search to search pods fields as well as it's normal content.

//=======================================================

{

    //--- Abort if we shouldn't perform the following code ------------------

    //if (! is_main_query() )           //Function is_main_query was called <strong>incorrectly</strong>. In <code>pre_get_posts</code>, use the <code>WP_Query->is_main_query()</code> method, not the <code>is_main_query()</code> function. See https://developer.wordpress.org/reference/functions/is_main_query/. Please see <a href="https://wordpress.org/support/article/debugging-in-wordpress/">Debugging in WordPress</a> for more information. (This message was added in version 3.7.0.) in /home/wcipporg/public_html/wp-includes/functions.php on line 5865

    //   return;

    if (! $query->is_main_query())

        return;

    if (! is_search() )

        return;



    add_filter( 'posts_join',

                function( $join )

                {

                    global $wpdb;

                    return $join .' LEFT JOIN ' . $wpdb->postmeta . ' ON '. $wpdb->posts . '.ID = ' . $wpdb->postmeta . '.post_id ';

                }

              );


    add_filter( 'posts_where',

                function ( $where )

                {

                    global $wpdb;


                    $or = array(

                                    "(".$wpdb->posts.".post_title LIKE $1)",

                                    "(".$wpdb->postmeta.".meta_value LIKE $1)",

                               );


                    if ( is_main_query() && is_search() )

                    {

                        $where = preg_replace(

                                                "/\(\s*".$wpdb->posts.".post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",

                                                implode( ' OR ', $or ),

                                                $where

                                             );

                    }

                    return $where;

                }

              );


    add_filter( 'posts_distinct',

                function ()

                {

                    global $wpdb;

                    return "DISTINCT";

                }

              );

}

add_action( 'pre_get_posts', 'AddCustomFieldsToSearchText', 9 );

Now, for the excerpt,  you need to make sure it is enabled/supported (PODS Advanced).  That adds the field allowing you to manually enter it, but we will automatically add it after the POD (page) is saved.

In the following code, I use a PODS-specific hook but with a little variation to the code, you could also use the WordPress "save_post" hook:


//==============================================

function PLANT_post_save_function($pieces, $is_new_item, $PlantId)

// https://stackoverflow.com/questions/38049208/set-wordpress-excerpt-and-post-thumbnail-based-on-custom-field

//==============================================

{

    //$post_excerpt   = get_the_excerpt( $PlantId );               //Get Excerpt


    //--- Work out the New Excerpt ------------------------------------------

    $PrevBotanicalNames = $pieces['fields']['plant_previous_botanical_names']['value'];

    $CnArray            = $pieces['fields']['plant_common_names']['value'];

    $Size               = trim( $pieces['fields']['plant_size']['value'] );

    $Flowers            = trim( $pieces['fields']['plant_flowers']['value'] );

    $GeneralComments    = trim( $pieces['fields']['plant_general_comments']['value'] );

    $AKA                = PlantAKA($PrevBotanicalNames, $CnArray);

    $NE = "Australian native plant";

    if  ($AKA != '')

        $NE = 'An ' . $NE . ', also known as: <b>' . $AKA . '</b>';

    if  ($Size != '')

        $NE = $NE . '<br><b>SIZE:</b> ' . $Size;

    if  ($Flowers != '')

        $NE = $NE . '<br><b>FLOWERS:</b> ' . $Flowers;

    if  ($Flowers != '')

        $NE = $NE . '<br><b>COMMENTS:</b> ' . $GeneralComments;


    //--- Set up the array to save the Excerpt ------------------------------

    $post_array = array(

                            'ID'            => $PlantId,

                            'post_excerpt'  => $NE,

                       );



    //--- Saving ------------------------------------------------------------

    remove_action('pods_api_post_save_pod_item_plant', 'PLANT_post_save_function');

                    wp_update_post( $post_array );

    add_action('pods_api_post_save_pod_item_plant', 'PLANT_post_save_function', 10, 3);

}

add_action('pods_api_post_save_pod_item_plant', 'PLANT_post_save_function', 10, 3);


Tuesday 30 November 2021

MyHeritage Often Fails as they appear to develop on their production systems (or have insufficient change control)

It is amazing that a global company would develop in production systems.  I have seen the the results of their code development change minute by minute over a few hours and the end result only usable after a day.

If they are not developing in production they are definitely fixing things (developing fixes) in production after failed change control.

Here are the latest examples where their search fails:


Template text



No results at all shown
(if you go to the next page ONE is shown)





Wednesday 11 April 2012

ANZ's Credit Card Overseas Transaction Fee Misleading

ANZ Bank in Australia charge a credit card processing fee. Other than the fact that I don't like being charged it (and its excessive) I don't actually have an issue with them doing so (ANZ struggled with this concept). 

What I have trouble with is how it appears on the statement, this is an example "PAYPAL *ACCESSORYJA 4028358723 INCL OVERSEAS TXN FEE 8.03 AUD".  The last underlined bit is the issue because:
  1. Its ANZ that is charging the fee. 
  2. It implies its NOT ANZ charging the fee. I don't know whether this is intentional or not. I was originally getting upset with PayPal as it looks like a PayPal fee.  PayPal should be complaining to ANZ also.
  3. I can't reconcile the amount I expect to find with the statement value as they differ. I bought an item for lets say $200.01, you can't find that on the statement, it will be something like $220.54.
  4. If ANZ is charging a fee it should be a separate line item clearly indicating who is charging the  fee.
  5. I have been charged this fee many times and figured I'd look into PayPal alternatives at some stage or just use the credit card directly. Now that I know its an ANZ fee and only payable on credit cards I can work around the system so I no longer pay the fee.
I have raised this issue with ANZ and they "welcomed my feedback" but won't do anything.  Apparently they would welcome more feedback so if you also believe the way they handle the fee is wrong please use secure mail in the Internet Banking application or ring them up and let them know you are unhappy, they will reverse the fee (one item only) for starters.  I told them to keep it :-)

Saturday 3 March 2012

Automatically Arm and Disarm IP Camera using Tasker


I could not find any good Android apps that made it simple to arm and disarm my camera (only real option was "Remote4cam"). I wanted a location based approach  as I really didn't want to do it manually as that would be a pain and I'm forgetful :-)

So I have created a Tasker profile which will disarm my camera when I'm at home and arm it when I leave.  It will display its current state in a permanent Android notification.  I can override the automation with arm and disarm widgets on a homescreen (see picture at top right). It will detect errors in the process and the notification will in that case have a red "X" icon and contain the reason code (http status).

I have a Foscam FI8904W camera but the script will probably work without too many changes for any IP camera. It is written to handle a single camera but you could modify it for more (and decide how you want to handle errors etc).

I have replaced all personal information (passwords, gps coord, port number etc) in the Tasker profile file so you should change any string that looks like "{REPLACE:something}" with the correct value for the something and then import the corrected profile.  The GPS coordinates of the profile are easiest fixed after importing the profile into Tasker.

I recommend setting up a specific camera userid for the use of Tasker and it may be wise to change it's password occasionally if you are paranoid :-)

To import the profile press and hold the "Profiles" tab and select import from the menu that popped up.  The widgets you can create yourself, select the "Task" widget and select the correct task.

Note that you will need to have set up appropriate port forwarding on any routers etc to get to your camera, please verify that you can remotely access your camera via a browser, this will also mean you will know the correct value for the port number etc.

Disarm means untick "Motion Detect Armed" in "Alarm Service Settings" and arm means tick it and set up values for the other options.  Note that the "api" of this camera has the sensitivity reversed so if you have a sensitivity of "4" via the web page you will need to set  "6" (10-4) via this Tasker script.  In any case you want to review the options and verify them after disarming and re-arming.

The following is a summary of the configuration (as generated by Tasker) (you can download a profile you can import by clicking here):

Profile: ArmDisarmCameras (13)
Priority: 5 CoolDown: 300
Location: -37.839915 / 145.191906 / 300.0m
Enter: Disarm_IpCamera (38)
Abort Existing Task
A1: Flash [ Text:Disarming Camera.. Long:Off ] 
A2: Set Widget Label [ Name:Disarm_IpCamera Label:Disarm ] 
A3: Notify Cancel [ Title:Camera Disarmed Warn Not Exist:Off ] 
A4: Notify Cancel [ Title:Camera Armed Warn Not Exist:Off ] 
A5: HTTP Get [ Server:Port:{REPLACE:YourCameraDnsNameOrIpAddress}:{REPLACE:PortNumber} Path:set_alarm.cgi Attributes:motion_armed=0
user={REPLACE:UserId}
pwd={REPLACE:UserPassword} Timeout:10 Mime Type: Output File:$tmp/DrivewayCamera-DISARM.htm Continue Task After Error:On ] 
A6: If [ %HTTPR ~ 200 ]
A7: Notify [ Title:Camera Disarmed Text:Disarmed at %TIME on %DATE Icon: Number:0 Permanent:On ] 
A8: Else 
A9: Vibrate [ Time:100 ] 
A10: Notify [ Title:Camera Disarmed Text:[%HTTPR] Disarming failed at %TIME on %DATE Icon: Number:0 Permanent:On ] 
A11: End If

Exit: Arm_IpCamera (36)
Abort Existing Task
A1: Flash [ Text:Arming Camera.. Long:Off ] 
A2: Set Widget Label [ Name:Arm_IpCamera Label:Arm ] 
A3: Notify Cancel [ Title:Camera Disarmed Warn Not Exist:Off ] 
A4: Notify Cancel [ Title:Camera Armed Warn Not Exist:Off ] 
A5: HTTP Get [ Server:Port:{REPLACE:YourCameraDnsNameOrIpAddress}:{REPLACE:PortNumber} Path:set_alarm.cgI Attributes:motion_armed=1
motion_sensitivity=6
iolinkage=1
mail=1
upload_interval=1
schedule_enable=0
user={REPLACE:UserId}
pwd={REPLACE:UserPassword} Timeout:10 Mime Type: Output File:$tmp/DrivewayCamera-ARM.htm Continue Task After Error:On ] 
A6: If [ %HTTPR ~ 200 ]
A7: Notify [ Title:Camera Armed Text:Armed at %TIME on %DATE Icon: Number:0 Permanent:On ] 
A8: Else 
A9: Vibrate Pattern [ Pattern:0,50,100,50,500,50,100,50 ] 
A10: Notify [ Title:Camera Armed Text:[%HTTPR] Arming failed at %TIME on %DATE Icon: Number:0 Permanent:On ] 
A11: End If 

Let me know of any issues as I haven't actually "round tripped" the exported profile.

Saturday 31 December 2011

Fix Metadata in Samsung Galaxy Nexus Panorama Photos

The camera in the Samsung Galaxy Nexus makes it simple to create landscape panoramas, the problem is (at least in ICS 4.0.1 ) that it has a bug which forgets to add the camera and picture details (meta data).

Apart from not being able to easily see what camera took what picture in Windows Explorer, it prevented me from fixing up images modified by Picasa as described at "https://plus.google.com/101791833503159629380/posts/ceH1fTsWRyM".

I have created a batch file which can be run over these file, it prompts for the name of a JPG file or directory name and will create a copy of the image and mark it "FIXED", it does not change the original image as a safe precaution.

You can download  "FixUpPhotoMetaData.zip" from my site, it has been tested in Windows 7 but would be surprised if it didn't run everywhere.  It does require that you also download "exiv2.exe", you can install this in the "PATH" or in the same directory as my batch file.

It easiest to create a shortcut to the file on your desktop.

This is an example of the added metadata:
Exif.Image.Make                   = [Samsung]
Exif.Image.Model                  = [Galaxy Nexus]
Exif.Photo.DateTimeOriginal  = 2011:12:21 12:03:00
Exif.Photo.DateTimeDigitized = 2011:12:21 12:03:00
Exif.Photo.UserComment       = _FixUpPhotoMetaData_.cmd (FIX_NEXUS_PANORAMA_PHOTO_INFO, v11.263)

Sunday 20 November 2011

Pig of an APP: Nero MediaHome 4 Essentials

This is Nero's DLNA server and it came for free with my LG 3D TV.

Every time I turned my media drive on my machine would grind to a halt, I found this was Nero's service. Closing down the application was not enough as the service would remain. After deciding to uninstall it, I still had to wait for about 20 minutes for this to complete.  My machine is quite highly speced...

I'll find a good free DLNA server, I used oone years ago on quite inferior hardware with the massive resource usage of Nero's piece of garbage.

Wednesday 5 October 2011

Hate McAfee...

Got this security product with the DELL otherwise I wouldn't be running it.  It has one of the worst user interfaces I've seen so any time I need to use it I end up doing a lot of swearing.

Well the picture on the right gives me one more reason to hate it.

Not only isn't there a virus but McAfee told me it knew that in a separate popup (which popped up in the background and unfortunately I didn't capture).

It couldn't actually scan the file as it was in use, the message on the right is in my opinion a very stupid way of saying that...

It frequently fails to do anything at all when selecting something like "history and Logs", I've just realized one of the reasons for this it often opens it in a new window of the same size UNDERNEATH the current one so its effectively invisible.

Of and one of my other pet hates is products that force an update then nag for reboots, hoping that you'll accidently agree to it while in the middle of doing something important.

Tuesday 13 September 2011

Recursive touch command for Windows by Steve Miller

It took me a while to find so I'm documenting it here but this is the only windows command line "touch.exe" program that I could find that not only handles file masks but has a recursion option to process a whole directory tree!

Its another application by Steve Miller and is bundled in his "Win32 Console ToolBox 1.0" download.  I knew it was a winner as soon as I realised he is also the author of "Dependency Walker" and "pure text", the latter is in my opinion is a must have application on any windows machine.

Tuesday 9 August 2011

If GoldCardTool fails use GoldCard Helper :-)

Hi,

For my HTD DESIRE HD, I tried "GoldCardTool.exe" version 0.0.7 and 0.0.5 as recommended, this failed to retrieve a CID for MMC1 but did for MMC1, unfortunately this (reversed) CID did not work resulting in the "Error [131]: CUSTOMER ID ERROR" message. Of course they wouldn't have wanted to check this earlier in the RUU's execution would they :-(

After proving with a hex editor that "GoldCardTool.exe" was correctly updating the first sector of the sdcard, after much googling I found "GoldCard Helper", this tool gave me two reversed cids, MMC2 and MMC0, unfortunately it would only copy MMC0's which was the same as I'd already tried, so I manually typed MMC2 off the screen, used the website to create the gold card image and then the "GoldCardTool.exe" to update the sdcard and it worked :-)

As I'd seen a different CID reversal scheme which reversed nybbles (characters), I can say that certainly that is not the case for my HTC DESIRE HD and it reverses bytes (character pairs) and changes the first two to "00" (as "GoldCardTool.exe" and does and most other websites specify).

Anyway hope that helps someone else...

Sunday 17 April 2011

Diagnosing Intermittent Android Events Such As Reboots via Log Capture

When your phone's problem doesn't involve reboots and it is fairly predictable etc you can use tools like "Log Collector" to manually send an email to yourself containing the log of Android's activity after you have noticed the issue occur.

When a reboot is involved (like the HTC one that occurs to me overnight) you really want to record what is happening all the time and the reboot will effectively stop the recording for you :-)

I've been using "CatLog - Logcat Reader!" but another possibility I found was "aLogcat (free) - logcat" (just found a CatLog varient called "LogViewer (LogCat)").

The "CatLog" program has a "Write Period" setting, the smaller the value the closer you will capture to the time of reboot but the more it will impact (I'm guessing) your phone's performance. As it writes to the SDCARD, if its the cra@ppy one that came with your phone the performance won't be as good as with a class 10 card :-)

I'd also recommend that you power your phone or at least watch the remaining charge closely to know how much it impacts your battery life.

If you don't mind being attached to your computer then the Android SDK's "Dalvik Debug Monitor" (DDMS) can capture this trace information also and doesn't have the "Write Period" issue either :-)

Tuesday 5 April 2011

Issues with the Android Market

There are many thing right with the market however these are the main issues wit it (in MY opinion):

  1. Regardless of what some people think having only FREE & PAID categories (which can't sell for $0.00) force authors to call DEMO and TRIAL products "free" when most people (except perhaps the author or a lawyer) would consider that unreasonable. Free does not just relate to money and no one can take something freely given to me from me. The PadGen standard defines "Shareware", "Freeware", "Adware", "Demo", and others so I'm not alone in thinking two is crazy.
  2. You can't sell or transfer purchases (even to yourself on a new email address).
  3. If you do choose a refund, you can't do it again when a new release comes out (which hopefully fixes the reason you refunded in the first place)!
  4. You only have 15 minutes from when you paid (not after download!) to decide if you want a refund.
  5. There are many products where even 24 hours is not enough to evaluate the product which is why products like "Touch Down" are forced into producing trials outside the market or deceptively categorising trials/demos as "free". The author should be allowed to specify the refund period. Hopefully users will be loyal to those authors who allow a longer return period.
  6. You can't choose the default install location (SDCARD or PHONE), mine defaults to SDCARD (when the app supports it) and on my HTC DESIRE HD this causes constant issues with A2SD corruption and the SD CARD unmounting itself.
  7. You can set up "auto update", however unless you don't pay for 3G data you won't as the there is no "Auto Download on WIFI Only" option.
  8. You can't get a clear list of purchases you have made.
  9. When you can't auto update because permissions have changed, the permission changes are not highlighted.
Anyway thats my list :-)




Monday 28 March 2011

HTC Desire HD Boots Loudly and Nightly

I've been noticing that HTC have been doing stealth updates of their "HTC Sync" (and probably sense etc), I think this had fixed my nightly reboots as I haven't had any for quite a while but they are back.

I've been woken up the last 2 nights. I've rebooted the phone as its been 17 days just in case but I'm guessing I'll probably get woken up tonight :-(

Wednesday 9 March 2011

Microsoft 'Improves' IE to Break Automation

If you are finding that IE automation ("InternetExplorer.Application") is hung then you can blame Microsoft for an extremely badly thought out "enhancement".

If IE dies and you manually invoke it it will may display the "Your last browsing session closed unexpectedly. Would you like to restore your last session, or go to your home page?" dialog shown on the top right.

Not related to automation (so bug #2), notice that there is no button for "continue doing what I told you to" option so basically command line automation/invocation is also broken.

Anyway back to automation, if IE was in a state where it wants to display the dialog, it will hang when automated. Some really bright people designed this...

IE has recently become so reliable I have converted to Chrome :-)

Saturday 26 February 2011

Auto Rotate not working on Samsung Galaxy Tab? Blame Samsung...


My wife asked me why auto rotate (swapping between portrait and landscape modes) wasn't working any longer on her Samsung phone. I found this was fairly common but no one had a fix, I was hoping it wasn't a hardware issue...

After a bit of playing I posted this solution.

Basically some idiot decided that the "Orientation lock" in Samsung's notification bar should toggle a different value than Android and all existing toggle widgets! This means that there are two autorotate locks and both have to be on for it to work!

So to avoid this issue either never use Samsung's "Orientation lock" (and leave it turned on) or always use it and never use a widget.

To fix the issue make sure Samsung's "Orientation lock" is on and then reinstall or use your widget/tool to also turn it on and confirm that autorotate now works (then uninstall or never use it again). If you have a hardware issue then bad luck...

I documented this issue to help you, so hopefully it has :-)