Inserting leads to Zoho CRM using PHP

Here is a simple PHP class (one  file) which you can use to insert leads to Zoho CRM. It is designed to be hooked up with your site contact form. Instead of emailing the contact form results, they go directly to the CRM and are available for the sales person to call. The code based on the orignal Python based mfabrik.zoho package. PHP curl needed as a requirement.

<?php
/**
 * Simple Zoho CRM inserter.
 *
 * MIT licensed. Copyright 2011 Pete Sevander and Mikko Ohtamaa.
 *
 */

class ZohoException extends Exception { }

class Zoho {

    public function __construct($username, $password, $apikey, $extra_auth_params = array(), $auth_url="https://accounts.zoho.com/login") {
        $this->username = $username;
        $this->password = $password;
        $this->apikey = $apikey;

        $this->ticket = null;
    }

    public function open() {
        $this->ticket = $this->_createTicket();
    }

    public function _createTicket() {

        $params = array(
            "servicename" => "ZohoCRM",
            "FROM_AGENT" => "true",
            "LOGIN_ID" => $this->username,
            "PASSWORD" => $this->password
        );

        //$params = array_map('urlencode', $params);

        $url = "https://accounts.zoho.com/login";

        $body = openUrl($url, $params);

        $data = $this->_parse_ticket_response($body);
        $this->data = $data;

        if (isset($data["WARNING"]) || isset($data['CAUSE'])) {
            $warning = (isset($data["WARNING"])) ? $data["WARNING"] : $data["CAUSE"];
            if ($warning != "null") {
                throw new ZohoException("Could not auth: " . $warning);
            }
        }
        if ($data["RESULT"] != "TRUE") {
            throw new ZohoException("Ticket result was not valid");
        }

        return $data["TICKET"];
    }

    public function _parse_ticket_response($data) {

        $output = array();

        $lines = explode("\n", $data);

        foreach($lines as $line) {
            if (substr($line, 0,1) == "#") {
                continue;
            }
            if ($line == "") {
                continue;
            }
            if (!strstr($line, "=")) {
                continue;
            }
            $line = explode("=", $line);
            $output[$line[0]] = $line[1];
        }

        return $output;
    }

    public function ensure_opened() {
        if ($this->ticket == null) {
            throw new ZohoException("Login first");
        }
    }

    /**
    * https://crm.zoho.com/crm/private/xml/Leads/insertRecords?newFormat=1&apikey=APIkey&ticket=Ticket
    **/
    public function insertRecords($leads, $extra_post_parameters=array()) {
        $this->ensure_opened();

        $xmldata = $this->XMLfy($leads);

        $post = array(
            'newFormat' => 1,
            'ticket' => $this->ticket,
            'apikey' => $this->apikey,
            'version' => 2,
            'xmlData' => $xmldata,
            'duplicateCheck' => 2,
            'wfTrigger' => 'true'
        );

        array_merge($post, $extra_post_parameters);

        // We'll bump created time to make sure that duplicate data entry
        // gets bumped up on the salesdroids list

        // $created = strftime('%Y-%m-%d %H:%M');
        // $post['Created Time'] = $created;

        // XXX: Good idea but Zoho silently ignores changes to the creation time

        $q = http_build_query($post);

        //print_r($post);

        $response = openUrl("https://crm.zoho.com/crm/private/xml/Leads/insertRecords", $q);

        //print_r($response);
        //print_r($xmldata);
        $this->check_successful_xml($response);

        return true;

    }

    public function getRecords($columns ='leads(Name)') {
        $this->ensure_opened();

        $post = array(
            'newFormat' => 1,
            'ticket' => $this->ticket,
            'apikey' => $this->apikey,
            'version' => 2,
            'selectColumns' => $columns,
        );

        $q = http_build_query($post);
        $response = openUrl("https://crm.zoho.com/crm/private/json/Leads/getRecords", $q );

        echo $response;

    }

    public function check_successful_xml($response) {
        $html = new DOMDocument();
        $html->loadXML($response);

        if ($err = $html->getElementsByTagName('error')->item(0)) {
            throw new ZohoException($err->getElementsByTagName('message')->item(0)->nodeValue);
        }

        return true;
    }

    public function XMLfy ($arr) {
        $xml = "<Leads>";
        $no = 1;
        foreach ($arr as $a) {
            $xml .= "<row no=\"$no\">";
            foreach ($a as $key => $val) {
                $xml .= "<FL val=\"$key\">$val</FL>";
            }
            $xml .= "</row>";
            $no += 1;
        }
        $xml .= "</Leads>";
        return $xml;
    }
}

function openUrl($url, $data=null) {
    $ch = curl_init();
    $timeout = 5;

    if($data) {
        curl_setopt($ch,CURLOPT_POST,1);
        curl_setopt($ch,CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch,CURLOPT_VERBOSE, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);

       // log output
       //$f = fopen("/tmp/zoho-curl.txt", "wt");
       //curl_setopt($ch,CURLOPT_STDERR, $f);

   }

    curl_setopt($ch,CURLOPT_URL,$url);
    curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
    curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,$timeout);
    $data = curl_exec($ch);
    curl_close($ch);
    return $data;
}

?>

And this is how you use it:

<?php

    $ZOHO_USER="yyyyy@xxxxxx.com";
    $ZOHO_PASSWORD="v3ryS3cr3t";
    $ZOHO_API_KEY='GET THIS FROM ZOHO CRM SETTINGS';

    require_once 'Zoho.php';

    $z = new Zoho($ZOHO_USER, $ZOHO_PASSWORD, $ZOHO_API_KEY);

    try {
        $z->open();

        $leads = array(
            'First Name' => 'Mikko',
            'Last Name' => 'Ohtamaa',
            'Company' => 'opensourcehacker.com',
            'Phone' => '+358 12 123 1234',
            'Email' => 'mikko @ foobar dot com',
            'Lead Owner' => 'yyyyy@xxxxxx.com',
        );

        try {
            $z->insertRecords(array($leads));
            $renderForm = false;
            echo '<h3>Contact information sent successfully. We will contact you soon.</h3><br /><strong>Data sent:</strong><dl>';
            foreach ($values as $key => $value) {
                echo '<dt><strong>' . $form->getElement($key)->getLabel() . '</strong></dt>';
                echo '<dd>' . $value . '</dd>';
            }
            echo '</dl>';

        } catch (ZohoException $e) {
            echo '<span>Error inserting data: ' . $e->getMessage() . '</span>';
        } 

    } catch (ZohoException $e) {
        echo '<span>Can\'t connect to Zoho: ' . $e->getMessage() . '</span>';
    }

?>

Some things to note

  • Owner must be given and be valid Zoho user
  • Company and Last Name fields are the only required fields by default
  • Zoho CRM checks duplicates using email field and thus using the same email address for (test) inserts won’t yield to visible results in My Leads view when duplicateCheck=2
  • Creation Time field cannot be changed
  • You can customize fields in Zoho CRM settings

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Trouble-filled installation tutorial of WordPress Network (a.k.a multi-site)

WordPress Network is a feature allowing you to manage multiple WordPress installations within the same code base

  • all sites share PHP codebase and plug-ins
  • all sites reside within the same database
  • sites have different settings
  • sites can have centralized admin users through the primary site
  • sites can have different plug-ins enabled and different plug-in settings

This tutorial was written for Ubuntu/Debian server (not shared hosting). It was based on migrating very old WordPress installation (5+ years) to the current version and then turning on the multi-site feature.

Note: All plug-ins might not be multisite compatible. You will find it out hard way.

Note: WordPress is recommended to be installed at a virtual host root for Network feature to work well. Also it makes sense to have the WordPress running in its final domain name, as domain name will be written in all over the settings (in the case you prepare it on the test server first). Use /etc/hosts trick to spoof the domain name of the test server if needed.

These instructions are for WordPress 3.2. They are baed on the reference manul, with my own insight and troubles mixed in. This tutorial is targeted for professionals with advanced UNIX experience and lacks hand-holding.

1. Disable plug-ins

Disable all plug-ins (can be re-enabled later). Otherwise Network feature cannot be turned on.

2.  Enabling Network feature installation

Add line:

define('WP_ALLOW_MULTISITE', true);

to your wp-config.php.

3. Back-up the existing site

Copy PHP files to a back-up folder

cp -r your-site-folder your-site-folder-backup

Dump a copy of database

mysqldump -uDBUSER -p DBNAME > wordpress.sql

See that dump was succesful

ls -lh wordpress.sql
-rw-r--r-- 1 root root 7.0M Aug 22 05:34 wordpress.sql

4. Prepare file-system

You need to create blogs.dir folder which will contain content for each WordPressinstance.

# Ubuntu / Debian uses www-data user for Apache 2
cd wp-content/
mkdir blogs.dir
chown -R www-data:www-data blogs.dir/

5. Turn on Network feature

You should now see Network Tools in Tools menu in Dashboard.

It will display you a bunch of changes you need to do .htaccess and wp-config.php files which are domain name specific (ugly: I am not very keen to have something domain name specific in config files, as it makes moving the site much more difficult, but as WordPress/PHP does not do proper virtual hosting using Virtual Host Base domain name rewrite pattern)

Make sure you put wp-config.php changes in the middle of the file before  wp-settings.php line or you will be seriously screwed up.

Sanitized wp-login.php changes look somewhat like:

define('DB_COLLATE', ...)
define( 'AUTH_KEY', 'x' );
define( 'SECURE_AUTH_KEY', 'x' );
define( 'LOGGED_IN_KEY', 'x' );
define( 'NONCE_KEY', 'x' );
define( 'AUTH_SALT', 'x' );
define( 'SECURE_AUTH_SALT', 'x' );
define( 'LOGGED_IN_SALT', 'x' );
define( 'NONCE_SALT', 'x' );
...
// snip
...

define('WP_ALLOW_MULTISITE', true);

// These lines MUST come after wp-settings.php include
define( 'SUBDOMAIN_INSTALL', false );
$base = '/';
define( 'DOMAIN_CURRENT_SITE', 'site.com' );
define( 'PATH_CURRENT_SITE', '/' );
define( 'SITE_ID_CURRENT_SITE', 1 );
define( 'BLOG_ID_CURRENT_SITE', 1 );

WordPress failed to detect DOMAIN_CURRENT_SITE for me currently, so I had to fix it manually.

6. Relogin

Log-out and log in again

7. Fail #1: You do not have sufficient permissions to access this page

Well… it didn’t go well. The first login for me produced an error page:

You do not have sufficient permissions to access this page.

To honor PHP programming practices there is no log information or any hint what actually went wrong. So you can only guess.

It turns out wp-config.php edit can fail if you add change after wp-settings.php include (PHP include mechanism and define() is quite hacky and WordPress cannot warn you if you fail).

If you happened to do this then I suggest you roll back the database and files in this point and start from zero.

8. Fail #2: Error establishing database connection

After fixing wp-config.php settings order I ge the white screen of death

Error establishing database connection

This comes up when I set

define( 'MULTISITE', true );

Looks like the internet is full of posts and less and less uselful answers about what is going on. Also, WordPress Codex etc. material does not provide very helpful material what should happen when MULTISITE is turned on, when tables are migrated and so on. The only way to solve this is seems to dive head first to WordPress PHP. Argh. Well… if I cannot fix it, then who can`?

So I had to modify wp_functions.php to give me more meaningful trace back for the white screen of death:

function dead_db() {   
        global $wpdb;

        throw new Exception("PHP is pile of shit");

This way you can actually know where the error happens in the code.

Which leads to me functions.php is_blog_installed() where after adding in new die() statement I get:

Cannot find wp_users

Hmmm. I wonder why… Especially this comment is useful:

        // Loop over the WP tables.  If none exist, then scratch install is allowed.
        // If one or more exist, suggest table repair since we got here because the options
        // table could not be accessed.

So it thinks the blog is not installed, but still finds some tables and dies with useless “Error establish database connection” error.

More closely examination with print_r() shows that $alloptions variable used in is_blog_installed() is empty.

It tries to load options from a table called wp_2_options which hints that it is a multisite enabled table which has not yet been created. This is strange as we clearly say the site id is 1 in wp-config.php. So looks like WordPress think the site id is wrong.

Since there is no way to trace back where $wpdb array containing this bad is populated, the next grep goes with SITE_ID_CURRENT_SITE.

wpmu_current_site() seems to return correct id 1. False lead.

More careful examination of  print_r() $wpdb tells blog id is 2 (note: blog id differs from site id…).

Grepping again tells there exist $wpdb function called set_blog_id(). It seems to be called in a function called switch_to_blog(). However, putting exception there tells that it is never called.

Finally finally MySQL dump of wp_blogs table shows that it is damaged due to earlier run of multisite migration with bad config:

mysql> select * from wp_blogs;
+---------+---------+----------------------+--------------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
| blog_id | site_id | domain               | path         | registered          | last_updated        | public | archived | mature | spam | deleted | lang_id |
+---------+---------+----------------------+--------------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
|       1 |       1 | site1.fi             | /xxxx-wp/    | 2011-08-22 08:36:46 | 0000-00-00 00:00:00 |      1 | 0        |      0 |    0 |       0 |       0 |
|       2 |       1 | site2.com            | /            | 2011-08-22 10:47:51 | 0000-00-00 00:00:00 |      1 | 0        |      0 |    0 |       0 |       0 |
+---------+---------+----------------------+--------------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
2 rows in set (0.00 sec)

Purge this table from bad data

delete from wp_blogs;

Also drop other multi-site config;

drop table wp_site;
drop table wp_sitemeta;

Now when you go to http://site.com/wp-admin/network.php it should be in the pristine state and you can create multi-site configuration from the scratch, in a working manner.

Make sure that Server name field show the correct value on the WordPress Network creation screen.

Press Install.

Re-enable multi-site in wp-config.php:

define( 'MULTISITE', true );

Well.. still failure. But this finally seemed to fix the site when we force the blog id (looks like it is incremental column which did not reset properly):

 update wp_blogs set blog_id=1 where blog_id=3;

Still it is looking for bad URL fix we can fix by forcing WordPress RELOCATE. RELOCATE setting in wp-config.php will update site URL in the database for the current wp-login.php page URL.

define('RELOCATE',true);

Now go to login page. I was finally able to login to migrated multisite instance.

Post-mortem: Error establishing database connection error message was misleading. The real error was along the lines “Multi-site configuration for site id X is broken.” However this is not very usual error and WordPress does not correctly check this error situation.

9. Relogin (now with success)

Now you should see Network Adminin your username name in the top right corner. Start adding those sites!

10. Fail #3: Not Found

So far so good. However, after creating a new site and trying to access the site through path (e.g. yoursite.com/subsite) it gives 404 Not Found. Lovely.

The error instantly hints that maybe something is from with Apache and .htaccess configuration.

So it seems – changes from WordPress Network installation didn’t end up correctly to .htaccess. Just to refresh the memory the correct .htaccess settings are:

# BEGIN WordPress
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]

# uploaded files
RewriteRule ^([_0-9a-zA-Z-]+/)?files/(.+) wp-includes/ms-files.php?file=$2 [L]

# add a trailing slash to /wp-admin
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule  ^[_0-9a-zA-Z-]+/(wp-(content|admin|includes).*) $1 [L]
RewriteRule  ^[_0-9a-zA-Z-]+/(.*\.php)$ $1 [L]
RewriteRule . index.php [L]
# END WordPress

Even after fixing .htaccess it stills gives 404. Looks like .htaccess files were not properly enabled in the virtual host config /etc/apache2/sites-enabled directory. This fixed the problem:

<VirtualHost *>
    ServerName site.com

    <Directory /var/www/site>
    AllowOverride All
    </Directory>

</VirtualHost>

11. Fail #4: Error establishing database connection for new multi-site

Ok. This time we can already guess the problem. Blog ids and site ids don’t match somewhere.

MySQL command prompt reveals what we suspect:

mysql> select * from wp_blogs;
+---------+---------+-------------+------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
| blog_id | site_id | domain      | path | registered          | last_updated        | public | archived | mature | spam | deleted | lang_id |
+---------+---------+-------------+------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
|       1 |       1 | site.com    | /    | 2011-08-22 14:57:12 | 0000-00-00 00:00:00 |      1 | 0        |      0 |    0 |       0 |       0 |
|       4 |       1 | site.com    | /de/ | 2011-08-22 15:10:34 | 0000-00-00 00:00:00 |      1 | 0        |      0 |    0 |       0 |       0 |
+---------+---------+-------------+------+---------------------+---------------------+--------+----------+--------+------+---------+---------+
2 rows in set (0.00 sec)

blog_id is currently having its own life.

Delete the new site (/de/) through WordPress dashboard.

When checking wp_blogs table more closely we notice that blog_id is auto-increment column:

describe wp_blogs;
+--------------+---------------+------+-----+---------------------+----------------+
| Field        | Type          | Null | Key | Default             | Extra          |
+--------------+---------------+------+-----+---------------------+----------------+
| blog_id      | bigint(20)    | NO   | PRI | NULL                | auto_increment |
| site_id      | bigint(20)    | NO   |     | 0                   |                |
| domain       | varchar(200)  | NO   | MUL |                     |                |
| path         | varchar(100)  | NO   |     |                     |                |
| registered   | datetime      | NO   |     | 0000-00-00 00:00:00 |                |
| last_updated | datetime      | NO   |     | 0000-00-00 00:00:00 |                |
| public       | tinyint(2)    | NO   |     | 1                   |                |
| archived     | enum('0','1') | NO   |     | 0                   |                |
| mature       | tinyint(2)    | NO   |     | 0                   |                |
| spam         | tinyint(2)    | NO   |     | 0                   |                |
| deleted      | tinyint(2)    | NO   |     | 0                   |                |
| lang_id      | int(11)       | NO   | MUL | 0                   |                |
+--------------+---------------+------+-----+---------------------+----------------+
12 rows in set (0.00 sec)

We can reset the counter via MySQL command:

ALTER TABLE wp_blogs AUTO_INCREMENT=2

12. Migrating multi-sites to new domain name

As now we are familiar with wp_site and wp_blogs tables we know how to manipulate these tables if we want to migrate our test site to the production domain name:

update wp_site set domain="new.site.com";
update wp_blogs set domain="new.site.com";

Change value of DOMAIN_CURRENT_SITE in wp-config.php

define( 'DOMAIN_CURRENT_SITE', 'new.site.com' );

After this you can access the primary site at new.site.com. Then you can access each subsite settings and toggle on Update siteurl and home. Save site settings to force new site URL for the subsite.

Note: It was still hiding the old URL value for the primary site somewhere and I didn’t track down it yet.

13. More info

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Reducing MySQL memory usage on Ubuntu / Debian Linux

If you are running your services on a low end virtual hosting every byte of memory you can save is important. The memory is often the limiting factor of how many applications you can run on VPS: CPUs are shared, memory not, on the same physical host.

  • Low-end VPS come with 512 MB memory or less
  • Front front-end server Apache / Nginx / Varnish takes > 100 MB +  min. 20 MB for each child process
  • Memecached takes its toll
  • MySQL takes 200 – 400 MB
  • Each Python / PHP process takes at least 15 MB and you need parallel processes for paraller HTTP requests (FCGI, pre-fork, others… )
  • Operating system processes need some memory (SSH, cron, sendmail)

As you can see it gets very crowded in 512 MB.

It’s especially troublesome since the memory is allocated lazily and the memory usage builds up slowly. In some point caches are no longer caches, but swapped to a disk – virtual memory usage grows beyond available RAM. To keep the server response, everything time critical should fit to RAM once and if the processes themselves don’t know how to release memory in this situation you need to tune a memory cap for them.

1. MySQL memory consumption

MySQL can be a greedy bastard what comes to memory consumption. Here on this server MySQL seems to take 417M virtual memory which seems to be little excessive for just running two WordPress instances and one Django / Python application:

1310 mysql     20   0  417M 21100  2776 S  0.0  1.2  0:00.00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --user=mysql --pid-file=/v

After some tuning I was able to bring it down a bit

3354 mysql     20   0  276m  19m 2848 S    0  1.2   3:41.19 mysqld

A reduction of 130 MB, or 1/4 of the server total memory. Not bad.

Use mtop to monitor running MySQL, its querieries, etc. so you know what’s going on. As you can see this MySQL has very good cache rate meaning that basically it is keeping everything in memory. If the content of the sites is less than 10 MBytes total, 400 MB contains plenty of space to cache the content:

load average: 0.05, 0.08, 0.16 mysqld 5.0.51a-3ubuntu5.8-log up 1 day(s), 19:47 hrs                                                             
2 threads: 1 running, 6 cached. Queries/slow: 187.1K/0 Cache Hit: 99.39%

2. What eats memory

I am not an expert on MySQL, so I hope someone with more insight could post comments regarding how to tune MySQL for low memory situations and how it is expected to behave.

Some ideas I run through my head

  • MySQL default cache settings are not too tight on Ubuntu/Debian, making it suitable for moderate loads, not low loads. If you don’t have much content, everything is just kept in memory (even if not needed)
  • MySQL uses round robin for connections and if there is 100 max connections it will allocate a thread stack for each connection (someone please confirm this – I found contracting infos).

3. Configuring MySQL

Here are listed some methods how to reduce the memory usage. This is what I done on this little box

MySQL is mostly configured in /etc/mysql/my.cnf on Ubuntu / Debian.

The final adjustments

key_buffer              = 8M
max_connections         = 30
query_cache_size        = 8M
query_cache_limit       = 512K
thread_stack            = 128K

4. More info

Send in more tips please! Is 32-bit better than 64-bit for low end VPS, how much this affects MySQL?

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Copy/move phpBB3 forum from a server to another computer (Ubuntu/Linux)

Here are short instructions what you need to do in order to move / copy phpBB3 forum.

1. Prerequisites

What you need in order to benefit from these instructions

  • Basic UNIX command-line knowledge
  • SSH access to the server
  • MySQL access to the database
  • LAMP stack ready on the new server

These instructions have been tested on Ubuntu/Debian/Linux but they should work in other environments too.

2. Write down database access information

Get password from config.php file on the old server:

cd /var/www/phpBB3
cat config.php

Write down database name, username and password.

3. Copy files

Use rsync to remotely copy forum files to a new computer. On new computer, in /var/www folder

rsync -av --compress-level=9 user@oldserver.com:/var/www/phpBB3 .

4. Dump and copy database

Execute the following command on the new server. It takes SSH connection to the old server and dumps phpBB3 database to the new server over the SSH connection.

ssh user@oldserver.com -C -o CompressionLevel=9 mysqldump -u databaseuser --password=databasepassword --skip-lock-tables --add-drop-table databasename > phpbb3.sql

5. Create a new database

Use the old access information from config.php to create a database with identical access information on the new server. You need a MySQL root access to create new databases.

mysql -uroot -p

Create database and grant access to phpBB3 user for it.

mysql> create database databasename;
mysql> GRANT ALL ON databasename.* TO 'databaseuser'@'localhost' identified by 'databasepassword';

Load the database on the new server from the dump file:

mysql> connect databasename;
mysql> source phpbb3.sql

6. Configure Apache virtualhost for the new server

The last step is to set-up Apache virtual host on the new server, so you can access the phpBB3 using a domain name. Note that this doesn’t need to be a real domain name, but you can spoof the domain name using /etc/hosts file on your local workstation.

Add file /etc/apache2/sites-enabled/phpbb3.conf (or pick a filename based on forum name if you host multiple forums)

<VirtualHost *>
 ServerName yourdomainname.com

 DocumentRoot /var/www/phpBB3
 <Directory />
   Options FollowSymLinks
   AllowOverride None
 </Directory>

</VirtualHost>

Note that <virtualhost *> may change depending on how Apache has been set up to listen IP addresses and ports. Also if you are using a shared hosting package or VPS you might need to use the server control panel (cPanel) to do this step.

Then check if your new config file is ok and restart Apache:

apache2ctl configtest
apache2ctl graceful

7. Hosts spoofing trick

If you are not having a DNS server of your own which you can use for the copy you can always use /etc/hosts file trick to spoof domain names. This way you can make Apache to serve the forum from the server even if the forum is not connected to any real domain name yet.

 

 

 

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+