Sunday, 4 December 2011

Load any typoscript in an extension

When you write an extension you get passed it's typoscript config (plugin.tx_myext_pi1). However I'm writing a service class that doesn't have access to this passed data so how do you get this information? The TS Parser:
//Change this key to whatever typoscript object you need
$myTSKey = 'plugin.tx_myext_pi1';
//Create new TSParser object
$TSparser = t3lib_div::makeInstance('t3lib_TSparser');
//Load conf with myTSkey's config data
list(,$conf) = $TSparser->getVal($myTSKey, $GLOBALS['TSFE']->tmpl->setup);

Friday, 11 November 2011

Get TYPO3 domain for FE plugin

To get the domain for your current TYPO3 install use:

t3lib_div::getIndpEnv('TYPO3_SITE_URL')

Maybe this is documented somewhere obvious but I couldn't find it easily...

Wednesday, 19 October 2011

Fix broken links in tt_news text


This relates to tt_news version 3.01, the problem may be present in other versions however.

For some reason the required typoscript processing for the RTE text field is commented out in the static typoscript, you can see it by viewing the template in the template analyser.
This leaves any links and other RTE related functions left unrendered, though the links are the most obvious problem. You'll probably see <link youadderss=""></link> tags in the source of the page which means you defiantly have this problem.

To fix this simply add:
plugin.tt_news.general_stdWrap parseFunc < tt_content.text.20.parseFunc
to a top level template. You may need to clear the cache to see the changes.

Friday, 7 October 2011

Magento External Login and Buy

I needed a way for a user to login to a Magento shop from an external website and have a selected product automatically added to their cart.

First off you'll need to create an API user, there are lots of tutorials out there on how to do this. Make sure this user has access to at least "Customers->Retrieve customer info" and "Category Inventory->Retrieve stock data".

You'll obviously need a form on your external site for the customer to enter their details and select which product they want to buy. The following is just an (untested) example form to give you a rough idea.
Please note: You'll notice I've included an iframe pointing to the shops login page, this is important as it will create the required cookies for the customer to login. If you don't include this iframe any customer who is missing the required cookies won't be logged in and will have nothing added to their cart!

<iframe src="https://shop.address/customer/account/login/" style="position:absolute;height:1px;width:1px;top:-100px;left:-100px;"></iframe>
<form action="your_external_script.php" method="post">
	<div class="login_field">
		<label>Choose your product</label>
		<div class="product_option">
			<label for="product_1">Product #1</label>
			<!-- Value below should be a real product
			in Magento that the user has access to! -->
			<input type="radio" name="login[product_id]" value="1" />
		</div>
		<div class="product_option">
			<label for="product_2">Product #2</label>
			<!-- Value below should be a real product
			in Magento that the user has access to! -->
			<input type="radio" name="login[product_id]" value="2" />
		</div>
		<div class="product_option">
			<label for="product_3">Product #3</label>
			<!-- Value below should be a real product
			in Magento that the user has access to! -->
			<input type="radio" name="login[product_id]" value="3" />
		</div>
	</div>
	<div class="login_field">
		<label for="login_username">Username</label>
		<input id="login_username" type="text" value="" name="login[username]" />
	</div>
	<div class="login_field">
		<label for="login_password">Password</label>
		<input id="login_password" type="password" value="" name="login[password]" />
	</div>
	<div class="login_field">
		<input type="submit" value="Buy selected product" />
	</div>
</form>

Now when the user submits that data you'll have to check three things, that the user exists, the password is correct and that the product is in stock and available.
<?php
/**
 * Validates a Magento password
 *
 * @param	string		$password: The user entered password
 * @param	string		$hash: The password hash from Magento customer info
 * @return	boolean		Returns true if the password is valid
 */
function validatePassword($password, $hash) {
	$hashArr = explode(':', $hash);
	switch(count($hashArr)) {
		case 1:
			return md5($password) === $hash;
		case 2:
			return md5($hashArr[1].$password) === $hashArr[0];
	}
	return false;
}
$apiUrl = 'https://shop.address/api/?wsdl';
$apiUser = 'my_api_user';
$apiKey = 'my_api_key';

if(isset($_POST['login'])) {
	try {
		$client = new SoapClient($apiUrl);
		$session = $client->login($apiUser, $apiKey);
	} catch (Exception $e) {
		echo '<p class="error">Network error, please try again later.</p>';
		exit;
	}
	//Check selected product is in stock...
	try {
		$product = $client->call($session, 'product_stock.list', array(intval($_POST['login']['product_id'])));
	} catch (Exception $e) {
		echo '<p class="error">'.$e->getMessage().'</p>';
		exit;
	}
	if(empty($product)) {
		echo '<p class="error">Product doesn\'t exist.</p>';
		exit;
	} else if($product[0]['is_in_stock'] == 0 || $product[0]['qty'] == 0) {
		//You could possible add the title of
		//the product here, I am lazy though and didn't look up where to get it from
		echo '<p class="error">The product you chose has sold out.</p>';
		exit;
	}
	//Lookup customer record
	list($customer) = $client->call(
		$session,
		'customer.list',
		array(
			array(
				'email' => addslashes($_POST['login']['username'])
			)
		)
	);
	if(is_array($customer)) {
		if(validatePassword($_POST['login']['password'], $customer['password_hash'])) {
			echo '<form action="https://shop.address/login_buy.php" method="post">';
			echo '<input type="hidden" name="login[username]" value="'.$customer['email'].'" />';
			echo '<input type="hidden" name="login[password]" value="'.$customer['password'].'" />';
			echo '<input type="hidden" name="login[product_id]" value="'.intval($_POST['login']['product_id']).'" />';
			echo '<input type="submit" value="Proceed to checkout" />';
			echo '</form>';
			exit;
		} else {
			echo '<p class="error">Invalid login or password</p>';
			exit;
		}
	} else {
		echo '<p class="error">Invalid login or password</p>';
		exit;
	}
}

?>
This then posts data to a custom script you'll have to add to the index of your Magento shop:
<?php
// Load the Magento core
require_once 'app/Mage.php';
umask(0);
Mage::app()->setCurrentStore('default');

Mage::getSingleton("core/session", array("name" => "frontend"));
$session = Mage::getSingleton("customer/session");

//Log out any existing sessions
if(!$session->isLoggedIn()) {
	$session->logout();
}
//Log user in
$login = Mage::getSingleton('core/app')->getRequest()->getPost('login');
$session->login($login['username'], $login['password']);

$productId = $login['product_id'];

header('Location: https://shop.address/checkout/onepage/add?product='.$productId.'&qty=1');
exit;
?>

Friday, 30 September 2011

SOAP-ERROR on localhost, Windows 7

I was having issues connecting to an external magento site's API via my local test server (apache, windows 7), kept getting the error:
SOAP-ERROR: Parsing WSDL: Couldn't load from 'https://shop.address/api/?wsdl' : failed to load external entity "https://shop.address/api/?wsdl"
Turns out the problem was the https bit. Accessing the API via the non-secure http works fine. Obviously some bug there which I couldn't be bothered looking into at the moment.

Tuesday, 6 September 2011

Fix TYPO3's powermail custom css option

Powermail is a great little plugin, one thing I absolutely hate about it though is the automatic html it generates.
The biggest problem is it generates the form elements using unique id's which change every time you create a new form. This is fine till you want to style the form with css. Look at the problem you get:
<div class="tx_powermail_pi1_fieldwrap_html tx_powermail_pi1_fieldwrap_html_text tx_powermail_pi1_fieldwrap_html_151 even" id="powermaildiv_uid151">
<label for="uid151">First Name<span class="powermail_mandatory">*</span></label>
<input type="text" required="required" tabindex="1" id="uid151" class="powermail_name powermail_text powermail_uid151 invalid" value="" name="tx_powermail_pi1[uid151]">
</div>
Essentially the only way to change the style of a form item is by using the unique id, so your style sheets break every time you want to copy the form. Now powermail has an option "Add CSS class" which I thought would solve the problem, however it only added the class to the child element (the input element in the above HTML), which to me was useless because I needed to style the parent div and label. The only way I could see of getting this working was using a powermail hook and overriding the code as follows:
Create a file called class.tx_powermailmods.php and place it in either a custom extension or just in typo3conf.
<?php
class tx_powermail_mods {
function PM_FieldWrapMarkerHook($uid, $xml, $type, $title, $markerArray, $piVarsFromSession, $pObj) {
$class = 'class="'; // start class tag
$class .= ($pObj->pi_getFFvalue(t3lib_div::xml2array($pObj->xml), 'mandatory') == 1 ? 'validate-one-required ' : ''); // add required class if needed
$class .= 'powermail_' . $pObj->formtitle; // add form title
$class .= ' powermail_' . $pObj->type; // add input type
$class .= ' powermail_uid' . $pObj->uid; // add input uid
$class .= ' powermail_subuid' . $pObj->uid . '_' . $i; // add input subuid
$class .= '" '; // close tag
$pObj->markerArray['###CLASS###'] = $class;
$pObj->markerArray['###CUSTOM_CLASS###'] = ($pObj->class_f != '' ? ' ' . $pObj->class_f : ''); // add manual class
}
}
?>
This removes the custom class from the class attributes of the form inputs and creates a new marker CUSTOM_CLASS which you can use in the template, but first you'll have to enable this hook. In localconf.php or the custom extensions ext_localconf.php put the following:
<?php
//If your using a custom extension called powermail_mods:
require_once(t3lib_extMgm::extPath('powermail_mods') . 'class.tx_powermailmods.php');
//Or if the file is sitting in typo3conf
//require_once(PATH_typo3conf . 'class.tx_powermailmods.php');
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['powermail']['PM_FieldWrapMarkerArrayHook'][] = 'tx_powermail_mods'; ?>
Now you just have to add in the new custom marker, you'll need to modify the template tmpl_fieldwrap.html which you'll find in the powermail/templates directory (typo3conf/ext/powermail/templates). Create a copy of this file and place is somewhere in fileadmin (fileadmin/templates for example). Now modify the powermail config in a template:
plugin.tx_powermail_pi1.template.fieldWrap = fileadmin/templates/tmpl_fieldwrap.html
Now you can modify the template using the new marker, which I've used like so:
<!-- ###POWERMAIL_FIELDWRAP_HTML_TEXT### begin -->
<div id="powermaildiv_uid###POWERMAIL_FIELD_UID###" class="tx_powermail_pi1_fieldwrap_html tx_powermail_pi1_fieldwrap_html_text tx_powermail_pi1_fieldwrap_html_###POWERMAIL_FIELD_UID### ###ALTERNATE### ###CUSTOM_CLASS###"###DIVJS###>
<label for="###LABEL_NAME###">###LABEL######DESCRIPTION######MANDATORY_SYMBOL###</label>
<input type="text" ###ONFOCUS######NAME######VALUE######CLASS######ID######SIZE######MAXLENGTH######READONLY######TABINDEX######ACCESSKEY######JS###/>
</div>
<!-- ###POWERMAIL_FIELDWRAP_HTML_TEXT### end -->
This allows you to define a custom class and reuse it across multiple forms. Much better.

Thursday, 25 August 2011

Magento - Export Products with Full URLs

I needed to export a list of products with full URL links included. Normally you would just use the built in Import/Export functions, but these do not create the full URLs that I required.

This is what I came up with:
<?php

// Load the Magento core

require_once 'app/Mage.php';
umask(0);
Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);
$userModel = Mage::getModel('admin/user');
$userModel->setUserId(0);

// Load the product collection

$collection = Mage::getModel('catalog/product')
 ->getCollection()
 ->addAttributeToSelect('*') //Select everything from the table
 ->addUrlRewrite(); //Generate nice URLs
/*
 For this example I am generating a CSV file,
 but you can change this to suit your needs.
*/

echo "title,sku,id,url\n";

foreach($collection as $product) {
 //Load the product categories
 $categories = $product->getCategoryIds();
 //Select the last category in the list
 $categoryId = end($categories);
 //Load that category
 $category = Mage::getModel('catalog/category')->load($categoryId);
 
 echo '"'.$product->getTitle().'","'.
  $product->getSku().'",'.
  $product->getId().',"'.
  //This will the proper URL, the base url is optional, though make sure you remove the trailing export.php (or whatever you name this file)
  str_replace('export.php/','',Mage::getBaseUrl()).$product->getUrlPath($category).'"'.
  "\n";
}


?>

Save it as export.php in your root Magento folder and browse to it in your browser.

Friday, 24 June 2011

jQuery plugin template

(function($) {
 //Set default settings
 //These will be overridden by the values passed to the plugin
 var settings = {
  foo: false,
  bar: true
 };
 //Define public methods
 var methods = {
  init: function(options) {
   return this.each(function() {
    var $this = $(this),
     data = $this.data('pluginName');
    if(options) {
     //Override plugin settings with options passed
     $.extend(settings, options);
    };
    if(!data) {
     //If data isn't set, plugin hasn't been run before
     //so do your initial setup
     //Set any persistent data
     $this.data('pluginName', {
      runCount: 0
     });
     $this.pluginName('foobar');
    } else {
     $this.pluginName('foobar');
    }
   });
  },
  foobar: function(options) {
   return this.each(function() {
    var $this = $(this),
     data = $this.data('pluginName');
    data.runCount++;
    if(options) {
     //Override plugin settings with options passed
     $.extend(settings, options);
    };
    if(settings.foo) {
     $this.append('<div>Foo ' + data.runCount + '</div>');
    }
    if(settings.bar) {
     $this.append('<div>Bar ' + data.runCount + '</div>');
    }
   });
  }
 };
 $.fn.pluginName = function(method) {
  var $this = $(this);
  if(methods[method]) {
   return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
  } else if(typeof method === 'object' || ! method) {
   return methods.init.apply(this, arguments);
  } else {
   $.error('Method ' +  method + ' does not exist on jQuery.pluginName');
  };
 };
})(jQuery);

You call this plugin like so:

jQuery('.mydiv').pluginName();

or custom settings:

jQuery('.mydiv').pluginName({foo:true,bar:false});

Once initialised you can use the public methods:

jQuery('.mydiv').pluginName('foobar', {bar:true});

Thursday, 23 June 2011

Simple front end editing with feeditadvanced

If you need to allow people to edit things other than pages and content on the front end of your TYPO3 site you can use some simple typoscript and feeditadvanced to achieve it. The following example lets you edit front end users on the storage page 13.

temp.recordEditor = CONTENT
temp.recordEditor {
    table = fe_users
    select.pidInList = 13
    renderObj = COA_INT
    renderObj {
        10 = TEXT
        10 {
            field = name
            wrap =<div>|</div>
            stdWrap.editPanel = 1
            stdWrap.editPanel {
                allow = edit,new,delete,hide
                line = 10
                label = %s
                onlyCurrentPid = 0
                previewBorder = 0
                edit.displayRecord = 1
            }
        }
    }
}



Then you just have to attach this to the page somewhere (eg page.10 < temp.recordEditor). You can edit any table this way (tt_address, tt_news, your own custom table) by changing the table and field selection.

Of course anyone who wants to edit the records will have to be logged into the back end and have proper access to the page/records. You can allow users from the front end to edit things by using the simulate back end user extension: http://typo3.org/extensions/repository/view/simulatebe/current/

You may need to setup feeditadvanced specially for this, I use the following TSConfig:

FeEdit.disable = 0
FeEdit.useAjax = 0
FeEdit.reloadPageOnContentUpdate = 1
FeEdit.clickContentToEdit = 1
FeEdit.menuBar.disable = 1


You seem to need reloadPageOnContentUpdate because I find the records don't get updated properly unless the page is reloaded.