Magento failed upload image on Mac OSX

It is occurred on Magento version 1.6.1.0
Product image upload error on Mac OSX.
It’s working fine on Windows but on Mac OSX it shows “Upload HTTP Error” when I clicked “Upload Files”.
After some research I found out that it is caused by HTTP_USER_AGENT validation:

General > Web > Session Validation Settings > Validate HTTP_USER_AGENT

In my site the value is Yes. Everything works fine if I change it into No.
Curiosity is coming.
I trace the code.

I found out that when we click “Upload Files”, it will trigger Mage_Adminhtml_Catalog_Product_GalleryController -> uploadAction

It never reach that place when the error appeared (Upload HTTP Error) because HTTP_USER_AGENT is checked on preDispatch.
In some part of the code, it will call session’s model.
Under this session model, it will validate several things including HTTP_USER_AGENT.

Now I’ll broke down the HTTP_USER_AGENT validation:

Mage_Core_Model_Session_Abstract_Varien

protected function _validate()
{
    ...
    if ($this->useValidateHttpUserAgent()
        && $sessionData[self::VALIDATOR_HTTP_USER_AGENT_KEY] != $validatorData[self::VALIDATOR_HTTP_USER_AGENT_KEY]
        && !in_array($validatorData[self::VALIDATOR_HTTP_USER_AGENT_KEY], $this->getValidateHttpUserAgentSkip())) {
        return false;
    }

    return true;
}

This is the value I got on Mac OSX and Windows:

= Mac OSX =
$sessionData[self::VALIDATOR_HTTP_USER_AGENT_KEY] = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25
$validatorData[self::VALIDATOR_HTTP_USER_AGENT_KEY] = Adobe Flash Player 11

= Windows =
$sessionData[self::VALIDATOR_HTTP_USER_AGENT_KEY] = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1
$validatorData[self::VALIDATOR_HTTP_USER_AGENT_KEY] = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1

= user agent validation skip=
$this->getValidateHttpUserAgentSkip() = Array
(
[0] => Shockwave Flash
[1] => Adobe Flash Player 9
[2] => Adobe Flash Player 10
)

Mage_Core_Model_Session_Abstract

public function getValidateHttpUserAgentSkip()
{
    $userAgents = array();
    $skip = Mage::getConfig()->getNode(self::XML_NODE_USET_AGENT_SKIP);
    foreach ($skip->children() as $userAgent) {
        $userAgents[] = (string)$userAgent;
    }
    return $userAgents;
}

Mage/Core/etc/config.xml

<global>
    <session>
        <validation>
            <http_user_agent_skip>
                <flash>Shockwave Flash</flash>
                <flash_9_mac>Adobe Flash Player 9</flash_9_mac>
                <flash_10_mac>Adobe Flash Player 10</flash_10_mac>
            </http_user_agent_skip>
        </validation>
    </session>
</global>

Mage_Core_Model_Session_Abstract_Varien

public function getValidatorData()
{
    ...
    // collect user agent data
    if (isset($_SERVER['HTTP_USER_AGENT'])) {
        //(string)$_SERVER['HTTP_USER_AGENT'] on Mac OSX this value is 'Adobe Flash Player 11'
        $parts[self::VALIDATOR_HTTP_USER_AGENT_KEY] = (string)$_SERVER['HTTP_USER_AGENT'];
    }

    return $parts;
}

Testing it on different OS & browser:

= Ubuntu =
Shockwave Flash -> Firefox
Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1 -> Chrome

= Mac OSX =
Adobe Flash Player 11 -> Firefox
Adobe Flash Player 11 -> Safari
Adobe Flash Player 11 -> Chrome

= Windows =
Shockwave Flash -> Firefox
Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1 -> Chrome

Temp fix:
add this line of codes in your config.xml

<global>
    <session>
        <validation>
            <http_user_agent_skip>
                <flash_11_mac>Adobe Flash Player 11</flash_11_mac>
            </http_user_agent_skip>
        </validation>
    </session>
</global>

This issue has been fixed in Magento version 1.7.0.0

I’ll update it into Indonesian version later.

auto increment, insert / update

Pernah mencoba mindahin isi suatu tabel ke tabel baru via model nya Magento?
Misal kita mau clone sales_flat_order ke sales_flat_order_tmp
Kalau via query langsung mungkin udah biasa
Kebetulan saya ada nyobain mindahin via modelnya Magento

Saya pinginnya ngopy beserta primary key nya:

$source = Mage::getModel('sales/order')->load(123);
$tmp = Mage::getModel('sales/order_tmp')->setData($source->getData())->save();

Waduh, gagal ke save, kenapa ya?

Setelah ditelusuri ternyata karena:

Mage_Core_Model_Resource_Db_Abstract

public function save(Mage_Core_Model_Abstract $object)
{
    if ($object->isDeleted()) {
        return $this->delete($object);
    }

    $this->_serializeFields($object);
    $this->_beforeSave($object);
    $this->_checkUnique($object);
    if (!is_null($object->getId()) && (!$this->_useIsObjectNew || !$object->isObjectNew())) {
        $condition = $this->_getWriteAdapter()->quoteInto($this->getIdFieldName().'=?', $object->getId());
        /**
         * Not auto increment primary key support
         */
        if ($this->_isPkAutoIncrement) {
            $data = $this->_prepareDataForSave($object);
            unset($data[$this->getIdFieldName()]);
            $this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);
        } else {
            $select = $this->_getWriteAdapter()->select()
                ->from($this->getMainTable(), array($this->getIdFieldName()))
                ->where($condition);
            if ($this->_getWriteAdapter()->fetchOne($select) !== false) {
                $data = $this->_prepareDataForSave($object);
                unset($data[$this->getIdFieldName()]);
                if (!empty($data)) {
                    $this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);
                }
            } else {
                $this->_getWriteAdapter()->insert($this->getMainTable(), $this->_prepareDataForSave($object));
            }
        }
    } else {
        $bind = $this->_prepareDataForSave($object);
        if ($this->_isPkAutoIncrement) {
            unset($bind[$this->getIdFieldName()]);
        }
        $this->_getWriteAdapter()->insert($this->getMainTable(), $bind);

        $object->setId($this->_getWriteAdapter()->lastInsertId($this->getMainTable()));

        if ($this->_useIsObjectNew) {
            $object->isObjectNew(false);
        }
    }

    $this->unserializeFields($object);
    $this->_afterSave($object);

    return $this;
}

liat conditional:

if (!is_null($object->getId()) && (!$this->_useIsObjectNew || !$object->isObjectNew())) {

} else {

}

di sini dilakukan pengecekan, apakah object nya baru dan memiliki id? (primary key)

karena object yg akan kita save memiliki id (dari sales_flat_order sebelumnya) maka akan masuk ke conditional yg pertama (atas)

slanjutnya dia milih:

if ($this->_isPkAutoIncrement) {

} else {

}

disini dia akan melakukan pengecekan apakah primary key nya auto increment atau tidak
- KALAU dia auto increment, dia akan melakukan update:

$this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);

- KALAU bukan auto increment, dia bakal ngecek lg

if ($this->_getWriteAdapter()->fetchOne($select) !== false) {
	
} else {
	
}

dia akan mencoba mengambil data dengan id yang akan kita save (dalam kasus kita, id = 123)
KALAU ada data dari query tersebut:
- dia akan melakukan update:

$this->_getWriteAdapter()->update($this->getMainTable(), $data, $condition);

KALAU tidak ada:
- dia akan melakukan insert:

$this->_getWriteAdapter()->insert($this->getMainTable(), $this->_prepareDataForSave($object));

Dalam kasus kita, karena object yg akan kita save memiliki id dan primary key nya merupakan auto increment, dia akan melakukan update:
update sales_flat_order_tmp set … where entity_id = 123;

Lalu bagaimana cara mengakali agar kita dapat melakukan save?
Caranya adalah dengan mengubah resource model nya agar menjadi `not-auto-increment`

Apabila dia bukan auto-increment, dia akan melakukan insert (jika datanya belum ada)

not auto increment

Bagaimana cara membuat tabel dengan primary id yg bukan auto increment?

Di dalam resource model nya kita cukup definisiin:

$this->_isPkAutoIncrement = false;

kurang lebih jadi seperti ini:

protected function _construct() {
	$this->_init('module/path_entity', 'primary_id');
	$this->_isPkAutoIncrement = false;
}

Rewrite Module Bertingkat

Misal kita pernah rewrite block catalog/product_view
dengan konfigurasi sebagai berikut:

- app/etc/modules:

<config>
	<modules>
		<Modul_Satu>
			<active>true</active>
			<codePool>local</codePool>
		</Modul_Satu>
  	</modules>
</config>

- config.xml:

<global>
	<blocks>
		<catalog>
			<rewrite>
				<product_view>Modul_Satu_Block_Product_View</product_view>
			</rewrite>
		</catalog>
	</blocks>
</global>

- View.php:

class Modul_Satu_Block_Product_View extends Mage_Catalog_Block_Product_View

Lalu suatu saat kita pingin rewrite lagi block itu tapi dengan module yang terpisah (kita ngga mao utak-atik module Modul_Satu)
Gimana caranya?
* Kita harus pastiin module yang baru ini (Modul_Dua) di load setelah module yang sebelumnya (Modul_Satu).
Caranya yaitu dengan menggunakan tag <depends> di dalam app/etc/modules
* Module kita yang baru (Modul_Dua) meng-extend module yang lama (Modul_Satu)

- app/etc/modules:

<config>
	<modules>
		<Modul_Dua>
			<active>true</active>
			<codePool>local</codePool>
			<depends>
				<Modul_Satu />
			</depends>
		</Modul_Dua>
  	</modules>
</config>

- config.xml:

<global>
	<blocks>
		<catalog>
			<rewrite>
				<product_view>Modul_Dua_Block_Product_View</product_view>
			</rewrite>
		</catalog>
	</blocks>
</global>

- View.php:

class Modul_Dua_Block_Product_View extends Modul_Satu_Block_Product_View

Magento Product Url

tes bkin action sendiri:

public function urlAction(){
	$id = 290;
	Mage::app()->setCurrentStore('default');
	echo "store_id: ".Mage::app()->getStore()->getId()."<br>";
	$url = Mage::helper('catalog/product')->getProductUrl($id);
	echo $url."<br>";
	
	//change store id
	Mage::app()->setCurrentStore('admin');
	echo "store_id: ".Mage::app()->getStore()->getId()."<br>";
	$url = Mage::helper('catalog/product')->getProductUrl($id);
	echo $url."<br>";
}

RESULT:

store_id: 1
http://www.example.com/surestep-pro-diabetic-test-strips-50-strips-professional-care.html
store_id: 0
https://www.example.com/index.php/catalog/product/view/id/290/s/surestep-pro-diabetic-test-strips-50-strips-professional-care/

kalau ditelusuri, cara dia generate url buat product ada di Mage_Catalog_Model_Product_Url -> getUrl

public function getUrl(Mage_Catalog_Model_Product $product, $params = array())
{
    $routePath      = '';
    $routeParams    = $params;

    $storeId    = $product->getStoreId();
    if (isset($params['_ignore_category'])) {
        unset($params['_ignore_category']);
        $categoryId = null;
    } else {
        $categoryId = $product->getCategoryId() && !$product->getDoNotUseCategoryId()
            ? $product->getCategoryId() : null;
    }

    if ($product->hasUrlDataObject()) {
        $requestPath = $product->getUrlDataObject()->getUrlRewrite();
        $routeParams['_store'] = $product->getUrlDataObject()->getStoreId();
    } else {
        $requestPath = $product->getRequestPath();
        if (empty($requestPath) && $requestPath !== false) {
            $idPath = sprintf('product/%d', $product->getEntityId());
            if ($categoryId) {
                $idPath = sprintf('%s/%d', $idPath, $categoryId);
            }
            $rewrite = $this->getUrlRewrite();
            $rewrite->setStoreId($storeId)
                ->loadByIdPath($idPath);
            if ($rewrite->getId()) {
                $requestPath = $rewrite->getRequestPath();
                $product->setRequestPath($requestPath);
            } else {
                $product->setRequestPath(false);
            }
        }
    }

    if (isset($routeParams['_store'])) {
        $storeId = Mage::app()->getStore($routeParams['_store'])->getId();
    }

    if ($storeId != Mage::app()->getStore()->getId()) {
        $routeParams['_store_to_url'] = true;
    }

    if (!empty($requestPath)) {
        $routeParams['_direct'] = $requestPath;
    } else {
        $routePath = 'catalog/product/view';
        $routeParams['id']  = $product->getId();
        $routeParams['s']   = $product->getUrlKey();
        if ($categoryId) {
            $routeParams['category'] = $categoryId;
        }
    }

    // reset cached URL instance GET query params
    if (!isset($routeParams['_query'])) {
        $routeParams['_query'] = array();
    }

    return $this->getUrlInstance()->setStore($storeId)
        ->getUrl($routePath, $routeParams);
}

di sini dia bakal cek $requestPath

if (!empty($requestPath)) {
    $routeParams['_direct'] = $requestPath;
} else {
    $routePath = 'catalog/product/view';
    $routeParams['id']  = $product->getId();
    $routeParams['s']   = $product->getUrlKey();
    if ($categoryId) {
        $routeParams['category'] = $categoryId;
    }
}

yg membedakan untuk store_id 0 (admin) dan store_id 1 (default) ada di sini:

$rewrite->setStoreId($storeId)
    ->loadByIdPath($idPath);
if ($rewrite->getId()) {
    $requestPath = $rewrite->getRequestPath();
    $product->setRequestPath($requestPath);
} else {
    $product->setRequestPath(false);
}

untuk store_id 1, masuk if yg atas, jd dia punya $requestPath
untuk store_id 0, dia masuk else, dia ga punya $requestPath

kalau dia ga punya $requestPath (store_id 0), dia bakal generate product url dengan cara concat parameter-parameter sebagai berikut:

$routePath = 'catalog/product/view';
$routeParams['id']  = $product->getId();
$routeParams['s']   = $product->getUrlKey();
if ($categoryId) {
    $routeParams['category'] = $categoryId;
}

yg hasilnya menjadi:
catalog/product/view/id/290/s/surestep-pro-diabetic-test-strips-50-strips-professional-care/

Magento Javascript Validation

contoh:
[root]/app/design/frontend/base/default/template/customer/form/login

<form action="<?php echo $this->getPostActionUrl() ?>" method="post" id="login-form">
.
.
.
</form>

<script type="text/javascript">
//<![CDATA[
    var dataForm = new VarienForm('login-form', true);
//]]>
</script>

VarienForm: [root]/js/varien/form.js

VarienForm.prototype = {
    initialize: function(formId, firstFieldFocus){
        .
        .
        .
        this.validator  = new Validation(this.form);
        .
        .
        .
    }
}

Validation: [root]/js/prototype/validation.js

Validation.prototype = {
    initialize : function(form, options){
        .
        .
        .
        if(this.options.onSubmit) Event.observe(this.form,'submit',this.onSubmit.bind(this),false);
        .
        .
        .
    },
    .
    .
    .
    onSubmit :  function(ev){
        if(!this.validate()) Event.stop(ev);
    },
    .
    .
    .
}

Intinya kalau dia gagal validate, form-nya ga jadi di submit => Event.stop(ev);

Debugging Magento

kalo kita nemuin suatu error, kita akan melakukan debugging buat mencari error tersebut.
cara paling tradisional dalam debugging adalah dengan meng-echo (print debugging) di dalem kodingnya, buat meng-echo suatu object atau array kita dapat menggunakan var_dump() atau print_r()

pada magento, supaya debugging ngga muncul di layar, kita dapat menggunakan Mage::log($message);
isi dari $message secara default akan muncul pada {{base_dir}}/var/log/system.log (error log harus di enable dulu melalui backend)
kenapa ga langsung echo aja? yah kadang-kadang ga bisa sembarang echo di mana aja klo ada ajax suka jadi ga jalan

Mage::log() juga dapat menyimpan log ke dalam file dengan nama yang kita inginkan, yaitu dengan Mage::log($message,null,'namafile.log');