BugFeatures blog open for comments! 
I have enabled comments on the blog after a few requests for it... play nice!
  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 162 )
Symfony and PhpBB3 integration 
I recently had to integrate a forum into a Symfony 1.0 app and, having selected PhpBB3 for the job, went about writing an authentication module. Here's how...

1. Create the auth module

Lets say I want to call the module 'symfony'. I create a file called auth_symfony.php and drop it in the phpbb /includes/auth folder.

2. Write the autologin method

Because I want my forums to be on the same domain, I have saved the phpbb files in web/forum. This way I have access to the session variables stored in my symfony app. So when a user goes into the forums, the autologin method can be used to interrogate my smyfony session.

In a nutshell, the autologin method needs to verify the user's session, grab their details and log them into phpbb. So as to avoid having to manage forum users from my symfony app, I also create new phpbb users here if one doesn't already exist for the current user.

Note that phpbb needs its own session, so we need to switch between sessions here as well.
/**
* Autologin function
*/
function autologin_symfony()
{
include_once('includes/functions_user.php');
global $db, $config, $user;

$sess = session_name();
session_name('symfony');
session_start();

$sfSession = $_SESSION['symfony/user/sfUser/attributes']['userData'];

@session_name($sess);
@session_start();


if (isset($_REQUEST['admin'])){
$_SESSION['admin'] = $_REQUEST['admin'];
}


if (isset($_SESSION['data'])){
return $_SESSION['data'];
}elseif (!isset($sfSession['username']) &&
!isset($_SESSION['admin'])){
header("Location: /");
exit;
}elseif (isset($sfSession['username'])){
$user_row = array(
'username' => $sfSession['username'],
'user_password' => phpbb_hash($sfSession['username']),
'user_email' => $sfSession['email'],
'user_type' => USER_NORMAL,
'group_id' => 2
);

$sql ='SELECT *
FROM ' . USERS_TABLE . "
WHERE user_email = '"
. $db->sql_escape(utf8_clean_string($sfSession['email']))
."'";
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);

if ($row){
// Successful login...
$data = array_merge($row,array(
'status' => LOGIN_SUCCESS,
'error_msg' => false,
'autologin' => 1,
'user_row' => $row
));
$_SESSION['data'] = $data;
return $data;
}else{
//check for existing name
$sql ='SELECT *
FROM ' . USERS_TABLE . "
WHERE username_clean = '"
. $db->sql_escape(utf8_clean_string($sfSession['username']))
."'";
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);
if ($row){
//randomise username if duplicate found
$user_row['username'] = $user_row['username']
. " - " . rand(1000,9999);
}
//create new user
$user_id = user_add($user_row);
$user_row['user_id'] = $user_id;
$data = array_merge($user_row,array(
'status' => LOGIN_SUCCESS_CREATE_PROFILE,
'error_msg' => false,
'autologin' => 1,
'user_row' => $user_row,
'user_type' => USER_NORMAL,
'group_id' => 2
));
$_SESSION['data'] = $data;
return $data;
}
}
}

Note also that the $_SESSION['data'] array is used to avoid hitting the database everytime the user loads a new page in phpbb.

3. Create admin login

You might have noticed that the above code looks for a session var called 'admin'. I use this to track whether a user has come from the backend. This is used to display the phpbb login form. Otherwise the user is simply sent back to the symfony app login.
/**
* Login function
*/
function login_symfony($username, $password)
{
global $db, $config, $user;

$sql ='SELECT * FROM ' . USERS_TABLE
. " WHERE username_clean = '"
.$db->sql_escape(utf8_clean_string($username))
."' and user_password = '".md5($password)."'";
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);

if ($row){

$data = array_merge($row,array(
'status' => LOGIN_SUCCESS,
'error_msg' => false,
'user_row' => $row
));

$_SESSION['data'] = $data;
return $data;

}else{

return array(
'status' => LOGIN_ERROR_PASSWORD,
'error_msg' => 'LOGIN_ERROR_PASSWORD',
'user_row' => array('user_id' => ANONYMOUS),
);
}
}

4. Create logout method

This simply wipes both sessions and redirects the user to the symfony app homepage.
/**
* Logout function
*/
function logout_symfony($user_row)
{
$sess = session_name();
session_name('symfony');
session_start();
session_destroy();

@session_name($sess);
@session_start();
@session_destroy();
header("Location: /");
exit;
}

5. Create validate session method

Last but not least this method is used to check that the current user session is valid. Not 100% required, but a good precaution.
/**
* Validate session function
*/
function validate_session_symfony()
{
$sess = session_name();
session_name('symfony');
session_start();
$auth = $_SESSION['symfony/user/sfUser/authenticated'];

@session_start($sess);
@session_start();
//always redirect to home
$admin = isset($_SESSION['admin']);
if ($admin || $auth){
return true;
}

return false;

}

6. Configure

You'll need to log into the phpbb ACP and set the authentication module to your custom module. Then you need to bypass symfony in the .htaccess for the /forum URL
RewriteRule ^forum/.*$ - [PT]
RewriteCond %{REQUEST_URI} !^/forum

And that's basically it :)

  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 197 )
Something nice to say about EFI-X 
A lot of people moan about EFI-X so I thought I'd tell a good story. I got my EFI-X chip about 6 months ago. After a botched OS X 10.5.7 update the chip fried. I sent it back. In the interim I toyed with every OSx86 kernel out there without any success. Then today my chip arrived back from Taiwan (desplite the "made in Holland" insignia). I popped it back into the PC and installed OSX again from a retail DVD first time no trouble at all. To be honest, I doubted I'd ever see that chip again, but now I'm feeling better...
  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 169 )
Send JWPlayer flashvars from shadowbox 
Shadowbox is a great way to play FLV videos in a lightbox, but it is a little shortsighted in terms of the parameters it expects you'll want to pass onto JWPlayer. So here's a little workaround I came up with.

If you want to send a playlist position and repeat variable you need to modify the shadowbox-flv.js file as follows:

var D=["file="+this.obj.content,"height="+F,"width="+I,
"autostart="+C,"displayheight="+K,"showicons="+H,
"backcolor=0x000000","frontcolor=0xCCCCCC","lightcolor=0x557722",
'repeat='+(this.obj.repeat?this.obj.repeat:'false'),
'playlist='+(this.obj.playlist?this.obj.playlist:'none')]

Then you can send the params from your JS code:
 Shadowbox.open({
player: 'flv',
title: 'FLV',
content: 'playlist.xml',
height: 400,
width: 550,
autostart: true,
repeat: 'list',
playlist: 'right'

});

  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 165 )
Die IE6 DIE6! 
I look forward to the day IE6 is wiped from the face of the internet like doomsday cults await their rapturous glory. I have read rumours - and I tend to agree - that IE6 is costing the world economy millions of dollars - I would like to see the figure properly researched, it could be billions!

Slowly we see big sites like YouTube phasing out IE6 support, slowly. Alas, I have client sites with 20% of visitors using IE6 so the end is not yet in sight...

The Tech Crunch story

What Ajaxian had to say about it

Join the list of sites not supporting IE6


  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 137 )
OnChange Radio Buttons - Symfony, Prototype and me 
Using Symfony's observe_field() on radio buttons will work only the first time the value is changed. If a user changes the values more than once the onchange event stops firing. To get around this you need to use the onclick event instead (as observer_field() uses onchange - and to support keyboard events you need to add an onkeyup handler as well).

A solution was posted at Symfony nerds in the comments suggesting a hardcoded onclick Ajax call in the radiobutton() method. I suggest using the remote_function() instead to simplify the code, avoid duplication and allow the helper to do the work. After all, that's what helpers are for...

<?php 
echo radiobutton_tag(
"tt_product",
1,
true,
'onclick=toolPref(1)'
);
?>

<?php
echo javascript_tag("
function toolPref(val){
".remote_function(array(
'url' => 'user/tallypref',
'with' => "'tally_product='+val"))."
}"
);
?>

  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 128 )
MySQL Replication - what to do when a slave fails 
Thought this was worth posting as it could save someone a lot of grief. I had a slave MySQL server stop updating without warning. A SELECT SLAVE STATUS showed there was an error due to an existing record. This would have occurred when a SELECT DATA INFILE populated 50,000 records and things went a little haywaire. The solution was quite simple:

1. STOP SLAVE;
2. TRUNCATE TABLE table_name;
3. START SLAVE;

This wipes out all data in the table with errors and allows the master to rebuild it from scratch. If you have foreign keys you might need to turn them off first...
  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 113 )
Using gmail SMTP with Swift Mailer 
Want your web app to send email from your gmail or google apps hosted email account? This way your messages are stored in your sent box. Here's how to do it with a failover to localhost (in case there is a problem with the gmail server or authentication)...

try{
$connection = new Swift_Connection_SMTP(
sfConfig::get("app_gmail_host"),
Swift_Connection_SMTP::PORT_SECURE,
Swift_Connection_SMTP::ENC_TLS
);
$connection->setUsername(sfConfig::get("app_gmail_username"));
$connection->setPassword(sfConfig::get("app_gmail_password"));
$connection->attachAuthenticator(
new Swift_Authenticator_PLAIN()
);
$mailer = new Swift($connection);
$mailer->disconnect();
echo "Using gmail..." . PHP_EOL;
unset($mailer);
} catch (Exception $e) {
echo "Using localhost..." . PHP_EOL;
$connection = new Swift_Connection_NativeMail();
}

//use our working mailer
$mailer = new Swift($connection);

Note you need to disconnect, destroy and recreate your SMTP mailer or else gmail will accuse you of trying to change identity!
  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 102 )
Assign an environment to a subdomain in Symfony 
I have a couple of symfony sites that my clients like to test without messing up their data (go figure). By modifying the controller a little I can detect the subdomain from the URL and then switch the syfmony environment accordingly. Then, using a filter, I can put a demo stylesheet in place to make the site look different as well...

By using this technique you can have exact copies of an application without duplicating a single file!

Step 1 - Set up the subdomain

Create a new subdomain in the DNS and configure apache to accept it as an alias for the live site.

<VirtualHost *:80>
ServerName site.com
ServerAlias www.site.com demo.site.com
DocumentRoot "/var/www/"
</VirtualHost>

Step 2 - Create a new database for the subdomain and configure the databases.yml file


test:
propel:
class: sfPropelDatabase
param:
dsn: mysql://root:pass@localhost/live

demo:
propel:
class: sfPropelDatabase
param:
dsn: mysql://root:pass@localhost/demo

Step 3 - Modify the controller

Symfony 1.0:

//check for DEMO site
$env = "live";
$debug = false;
$domain = $_SERVER['HTTP_HOST'];
if (strpos($domain,"demo") !== false){
$env = "demo";
$debug = true;
}

define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/../..'));
define('SF_APP', 'my_app');
define('SF_ENVIRONMENT', $env);
define('SF_DEBUG', $debug);

Or in Symfony 1.2:

$configuration = ProjectConfiguration::getApplicationConfiguration('my_app', $env, $debug);
sfContext::createInstance($configuration)->dispatch();

Step 4 - Create the filter

My favourite bit - save it in your lib folder:

<?php

class demoFilter extends sfFilter
{
public function execute ($filterChain)
{

if (SF_ENVIRONMENT == "demo" && $this->isFirstCall())
{
$this->getContext()->getResponse()->addStylesheet('demo','last');
}

// execute next filter
$filterChain->execute();
}
}
?>

Step 5 - Add your stylesheet & images etc

The above filter adds demo.css to each page, so create the file and any images it requires and upload to your existing css and images folders and you're done.
  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 106 )
Working with MySQL replication in PHP 
I recently set up mySQL replication for a client and thought I'd share this quick and easy way of distributing database load over a few machines.

First, set up replication - it's very simple. Here's a simplified list of tasks based on the info at mysql.com:

http://forums.gentoo.org/viewtopic.php?t=241123

Then you need to adapt your site to use the master server for all write operations and distribute the read load across your slaves (and master if you like).

My setup was moving from a single server to 2 servers. The primary server runs apache and the master database. The secondary server takes all the database read traffic (high CPU low bandwidth) and also serves up media files (low CPU high bandwidth).

The way I set up the config for the databases was to allow for failover from the slave back onto the master. Now this site was built in 2003 and uses some very old PEAR classes (remember PEAR? ;-), but you get the idea:

$options = array(
'debug' => 2,
'portability' => DB_PORTABILITY_ALL,
);

$dsn = array(
'phptype' => 'mysql',
'username' => def_user,
'password' => def_pass,
'hostspec' => def_host,
'database' => def_dbname,
);

$db_master =& DB::connect($dsn, $options);
if (PEAR::isError($db_master)) {
showError($db_master->getMessage());
}
$db_master->setFetchMode(DB_FETCHMODE_OBJECT);


//use 2nd database server if available
//otherwise duplicate original as failover
if (defined('def_host2')){
$dsn2 = array(
'phptype' => 'mysql',
'username' => def_user2,
'password' => def_pass2,
'hostspec' => def_host2,
'database' => def_dbname2,
);
$db =& DB::connect($dsn2, $options);
if (PEAR::isError($db)) {
$db = $db_master;
}else{
$db->setFetchMode(DB_FETCHMODE_OBJECT);
}
}else{
$db = $db_master;
}


Then the next step was to modify the pages that included DELETE, UPDATE or INSERT queries to use $db_master instead of $db. The whole process took a little over an hour and the site was only offline for 20 minutes :)



  |  [ 0 trackbacks ]   |  permalink  |  related link  |   ( 3 / 140 )

Back Next