Friday, January 31, 2014

Install Group Office with 1-click using Installatron

We are proud to announce that Group-Office is now officially available from Installatron.

Installatron is a one-click web application installer that enables Group Office and other top web applications to be instantly installed and effortlessly managed. Get Group Office up and running on your website in seconds and discover just how easy Installatron makes it to manage web applications. Group Office installations managed by Installatron can updated (on-demand or automated), cloned, backed up and restored, edited to change installation parameters, and more.

Many web hosting providers provide Installatron through their web hosting control panel. If Installatron is not available from your provider, you can use Installatron directly from Installatron.com.

To install Group Office through your web hosting provider's control panel (if available):

* Login to your web host's control panel, navigate to "Installatron", click "Group Office", and choose the "Install this application" option.
* Change any of the install prompts to customize the install. For example, you can choose a different language for Group Office.
* Click the "Install" button to begin the installation process. You will be redirect to a progress page where you can watch as Group Office is installed within a few seconds to your website.

To install Group Office directly from Installatron.com:

* Navigate to Group Office @Installatron and choose the "Install this application" option.
* Enter your hosting account's FTP or SSH account information, and then enter MySQL database information for a created database. For increased security, create a separate FTP account and MySQL database for your Group Office installation.
* Change any of the install prompts to customize the install. For example, you can choose a different language for Group Office.
* Click the "Install" button to begin the installation process. You will be redirect to a progress page where you can watch as Group Office is installed within a few seconds to your website.

If you experience any problems or want to share your experience using Group Office and Installatron together, email Installatron at: feedback (at) installatron.com

Monday, January 27, 2014

Implementing PHP namespaces in an existing project

If you've been developing for a longer time you know the problem. You started coding a project and now PHP comes with this new cool feature. At first you want to stay backwards compatible with older versions but there comes a point where you want to use these new features. Otherwise your code will be outdated and less attractive to new developers.
Such a feature we didn't have were namespaces. Namespaces are very useful to organize code and to avoid naming conflicts when you include a vendor library. For example you can have a function called "sendmail()". Now you'll have a problem when you want to include a 3rd party mail library which uses the exact same function.

With Group-Office we solved this problem before PHP namespaces arrived by prefixing. For example a class in the address book module would be called "GO_Addressbook_Model_Contact". All of our classes where prefixed by "GO" and followed up by the module name. This way we hacked our namespaces in with underscores. This was invented before PHP 5.3 became mainstream. Now almost all servers run PHP 5.3 and higher so it's time to change to namespaces! But this can be a tricky task which such a large project as Group-Office. But because we used the underscore namespace hack it wasn't so difficult at all. I wrote up with a script that could convert our project in one GO! It turns

class GO_Addressbook_Model_Contact{} into:

<?php

namespace \GO\Addressbook\Model;

class Contact{

}


Some prerequisites

While writing this script I did face a couple of problems I had to resolve manually. We used some names that where incompatible. For example:

GO_Base_Util_Array would convert into:

<?php

namespace GO\Base\Util;


class Array{

}


Using "Array" as a class name is invalid in PHP so I had to refactor these names. I found out that refactoring these names before using the script was the best approach. This way we could use Netbeans to easily refactor these names.
The list of illegal names was:

  • Function
  • Interface
  • Abstract
  • Switch
  • List
There are probably more but these where the only ones used in Group-Office.

I also had to make sure all existing PHP classes that were used are prefixed with a backslash. For example "new PDO" had to be written as "new \PDO".

The script

This script assumes "GO" is a global static singleton class and all classes are written in the GO_Module_Type_Class form. It also requires one class per file. You could easily modify it to work for your project if you meet these requirements.


<?php
/**
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Intermesh BV 
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */


//find all PHP files except updates.php and updates.inc.php because we shouldn't touch them
$cmd = 'find . -type f \( -iname "*.php" ! -iname "updates*" \);';
exec($cmd, $scripts, $return_var);

//return var should be 0 otherwise something went wrong
if($return_var!=0)
 exit("Find command did not run successfully.\n");

foreach($scripts as $script){
 
 //skip old files. We don't use .inc.php anymore in the new framework
 if(substr($script,-14)=='.class.inc.php' || in_array(basename($script),array('namespacer.php','action.php','json.php')))
  continue;
 
 
 
 //get the contents of the PHP Script
 $content = $oldContent = file_get_contents($script);
 
 //Our main global static function GO::function() is easiest to identify like this
 $content = str_replace('GO::', '\\GO::', $content);
 
 //All GO classes are build up like GO_Module_SomeClass so we can match them with
 //a regular expression.
 $regex = '/[^A-Za-z0-9_-](GO(_[A-Za-z0-9]+)+)\b/';
 
 $classes = array();
 
 if(preg_match_all($regex, $content, $matches))
 {
  
  
  //loop through the matched class names and store the old classname as key
  //and the new classname as value
  foreach($matches[1] as $className){
   
   //skip all uppercase classnames. They are old eg. GO_USERS, GO_LINKS
   if($className!=strtoupper($className)){ 
    if(!in_array($className, $classes)){
     $classes[$className]='\\'.str_replace('_','\\', $className);
    }
   }
  }
  
  //replace all old class names with the new namespaced ones.
  foreach($classes as $oldClassName=>$newClassName){
   $content = str_replace($oldClassName, $newClassName, $content);
  }
  
  //now we have a problem with the class declarations.  
  //we only have one class per file!
  foreach($classes as $oldClassName=>$newClassName){
   $classDeclarationRegex = '/(class|interface)\s('.preg_quote($newClassName,'/').')/';

            //Attempt to find a class definition in this file.
   if(preg_match($classDeclarationRegex,$content, $classDeclarationMatches,PREG_OFFSET_CAPTURE)){
    
    echo "Found ".$newClassName."\n";
    
    //strip last part of the class name to become the namespace.
    //eg. class; \GO\Email\Model\ImapMessageAttachment will have namespace:
    //GO\Email\Model
    $namespace = trim($newClassName,'\\');
    $lastBackSlashPos = strrpos($namespace,'\\');
    $namespace = substr($namespace,0, $lastBackSlashPos);
    

    //find place in the file to enter the "namespace GO\Email\Model;" declaration.
    //we can do this above the line with declaration "class ImapMessageAttachment"    
    $offset = $classDeclarationMatches[0][1];
    $lastLineBreakPos = strrpos(substr($content,0,$offset), "\n");
    
    $declaration = "\n\nnamespace ".$namespace.";\n\n";

                //Inset the declaration in the file content
    $firstPart = substr($content,0,$lastLineBreakPos);
    $lastPart = substr($content, $lastLineBreakPos);    
    $content = $firstPart.$declaration.$lastPart;
    
    //now we must remove the namespace from class usages in this file.
                //eg. \GO\Base\Db\ActiveRecord becomes ActiveRecord.
    $content = preg_replace('/([^"\'])\\\\'.preg_quote($namespace,'/').'\\\\/', "$1", $content);

   }
  }
  

 }
 
 //some doubles could have been made
 $content = str_replace('\\\\GO', '\\GO', $content);
 
 //if the contents were modified then write them to the file.
 if($oldContent != $content){
  echo "\nReplacing $script\n";
  file_put_contents($script, $content);
 }


    echo "All done!\n";
 
}

Friday, January 17, 2014

Rsync backup script for our web and MySQL servers

One of the most important matters for an IT company is taking care of backups. We make backups in twofold. We make full snapshots of our virtual machines in the data center and we make incremental backups to another location. This is absolutely necessary in case of a major disaster like theft or a fire. We keep 14 incremental backups so when a customer wants to recover a single file he deleted accidentally we can recover it easily as long as it didn't happen earlier than 14 days ago.

We used to have a script on each server that took care of the backup. With rsync and mysql dump. This worked well at first but as our number of servers increased it could happen that the process would fail due to multiple backups running at the same time. Multiple copy commands on the server made everything very slow. When you aborted a slow backup the incremental snapshot could only be half copied resulting in duplicate data being transferred the next backup. When this happpened I decided to turn the process around. I wrote a script that runs on the backup server that connects to each server one by one. The programming language I know best is PHP so I wrote it in that language.

Preparing the server that we need to backup

The server needs to support "rsync". I also want to backup MySQL so I also needed "mysqldump". "mysqldump" comes with the package "mysql-client-5.5" and is usually already installed.
I ran:

$ apt-get install rsync

That's all I needed to do!

Installing the backup server

The backup server can run any Linux distribution supporting PHP on the command line and rsync. I used SSH keys to connect to the servers.
In my case I installed a minimal Debian server with php and rsync on it. I ran:

$ apt-get install rsync php-cli

I used SSH to connect to the web servers with a private and public key pair. I generated the key pair with the following command:

$ ssh-keygen

The resulting key files are then located in /root/.ssh/id_rsa(.pub).

Adding a target server

Now it's time to connect to the target web server. For example. "web1.example.com". To install the public key I ran:

$ ssh-copy-id root@web1.example.com

I entered the root password so that my public key (/root/.ssh/id_rsa.pub) is added to the authorized keys on the web server.

I verified that this worked by logging in with the key. There's no need to enter a password:

$ ssh root@web1.example.com
$ exit

Now we have a backup server that can access our web server with public key authentication.

Installing the script

Put the script on the server. For example in /root/backup.php
Edit the following values on top of the script:
<?php

...


/**
 * The backup report will be mailed to this address.
 * 
 * @var String 
 */
private $emailTo = 'postmaster@example.com';

/**
 * The location of the private key to use for SSH
 * 
 * @var String 
 */
private $sshKey = '/root/.ssh/id_rsa'; 

/**
 * An array of server to backup
 * 
 * @var array 
 */
private $servers = array(
 array(
  'host' => 'web1.example.com', //hostname of target server to backup
  'user' => 'root', //the user on the target server.          
  'port' => 22, //SSH port
  'backupFolders' => array('/etc', '/home', '/var/www'), //folders to backup
  'targetFolder' => '/home/backup/web1', //folder on the backup server to store the backups
  'rotations' => 1, //number of rotations.
  'backupMysql' => true, //Backup MySQL database. Note that "mysqldump" must be installed on the target server,
  'mysqlUser' => 'root',
  'mysqlPassword' => 'secret',
  'mysqlBackupDir' => '/home/mysql_backup' //MySQL backup directory on the target server
 )
);
 
...




After configuration I ran a test:

$ php /root/backup.php

Then I scheduled it in a cron job:

$ crontab -e

I added this line to run it on midnight every day:

0 0 * * php /root/backup.php


I hope this little tutorial is useful! This is the full backup script:

<?php
/**
 * Backup script to backup multiple servers including MySQL support.
 * Author: Merijn Schering <info@intermesh.nl> 
 * 
 * 

The MIT License (MIT)

Copyright (c) 2014 Intermesh BV <info@intermesh.nl>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 *
 */

class backup{

 /**
  * The backup report will be mailed to this address.
  * 
  * @var String 
  */
 private $emailTo = 'postmaster@example.com';

 /**
  * The location of the private key to use for SSH
  * 
  * @var String 
  */
 private $sshKey = '/root/.ssh/id_rsa'; 

 /**
  * An array of server to backup
  * 
  * @var array 
  */
 private $servers = array(
   array(
     'host' => 'web1.example.com', //hostname of target server to backup
     'user' => 'root', //the user on the target server.          
     'port' => 22, //SSH port
     'backupFolders' => array('/etc', '/home', '/var/www'), //folders to backup
     'targetFolder' => '/home/backup/web1', //folder on the backup server to store the backups
     'rotations' => 1, //number of rotations.
     'backupMysql' => true, //Backup MySQL database. Note that "mysqldump" must be installed on the target server,
     'mysqlUser' => 'root',
     'mysqlPassword' => 'secret',
     'mysqlBackupDir' => '/home/mysql_backup' //MySQL backup directory on the target server
   )
 );
 
 
 private $logDir;
 private $sshCmd;
 private $dateFormat;
 private $logFile;
 private $errors=array();

 public function __construct() {

  //set the timezone to avoid date() notices from PHP
  date_default_timezone_set('europe/amsterdam');

  //Make sure files are created with the right permissions
  umask(000);

  $old_error_handler = set_error_handler(array($this, "errorHandler"));

  $this->dateFormat = 'Y-m-d_H-i-s';

  if (!isset($this->logDir)) {
   $this->logDir = dirname(__FILE__) . '/log/';
  }
  $this->logFile = $this->logDir . '/' . date($this->dateFormat) . '.log';

  $this->execCommand('mkdir -p ' . $this->logDir);
 }

 /**
  * Executes command line program
  * 
  * @param stirng $cmd
  * @param boolean $halt
  * @return mixed false on failure or command output
  */
 private function execCommand($cmd, $halt = true) {
  exec($cmd, $output, $ret);
  if ($ret != 0) {

   $this->error("Failed to run command: '" . $cmd . "'");
   $this->info(implode("\n", $output));


   if ($halt) {
    $this->exitBackup();
   } else {
    return false;
   }
  } else {
   return $output;
  }
 }

 /**
  * Executes command on the remote server.
  * 
  * @param string $cmd
  * @return mixed false on failure or command output
  */
 private function execRemoteCommand($cmd, $halt=true) {

  $cmd = $this->sshCmd . ' "' . str_replace('"', '\"', $cmd) . '"';

  return $this->execCommand($cmd, $halt);
 }

 /**
  * Exits the backup  script and sends backup report
  * @param string $error
  */
 private function exitBackup($error = '') {
  if ($error != ''){
   $this->error($error);
  }

  $subject = "Backup report (success)";
  $body="";
  
  if(count($this->errors)){
   $subject = "Backup report (ERRORS!)";
   
   $body = "Error summary: \n".implode("\n---\n", $this->errors);
  }else
  {
   $subject = "Backup report (success)";
  }
  
  
  $body .= file_get_contents($this->logFile);
  
  $header = "From: Backup server \r\n";

  mail($this->emailTo, $subject, $body, $header);

  exit();
 }

 /**
  * Writes log message
  * 
  * @param string $text
  */
 private function info($text) {
  $text = '[' . date('Y-m-d H:i') . '] ' . $text . "\n";

  echo $text;

  file_put_contents($this->logFile, $text, FILE_APPEND);
 }
 
 
 /**
  * Writes error message
  * 
  * @param string $text
  */
 private function error($text) {
  $text = '[' . date('Y-m-d H:i') . '] !!ERROR!! :' . $text . "\n";

  echo $text;
  
  $this->errors[]=$text;

  file_put_contents($this->logFile, $text, FILE_APPEND);
 }

 private function setSshCmd($server) {
  $this->sshCmd = 'ssh -i ' . $this->sshKey . ' ' . $server['user'] . '@' . $server['host'] . ' -p ' . $server['port'];

  $this->info("Testing SSH connection");
  if($this->execCommand($this->sshCmd . ' -o "PasswordAuthentication=no" "exit"' , false)!==false){
   $this->info("Test OK");
  }else
  {
   $this->error("SSH test for ".$server['host']." failed!");
   return false;
  }


  $this->sshCmd .= ' -o "ServerAliveInterval 60"';

  return true;
 }

 /**
  * Runs the backup
  */
 public function run() {

  //We never want this script to run twice at the same time
  $lockFile = dirname(__FILE__) . '/backup.lock';
  if (file_exists($lockFile)) {
   $this->exitBackup("Lock file '" . $lockFile . "' exists. Please check last backup! It failed or it is still running.");
  } else {
   touch($lockFile);
  }

  foreach ($this->servers as $server) {


   $this->info("\n\n\n----------------------------------------------------\n\n\n");
   $this->info("Backing up " . $server['host']);

   if($this->updateLatest){
    $server['backupMysql']=false;
    $server['rotations']=1;
   }

   if(!$this->setSshCmd($server)){
    continue;
   }

   //Create the backup folder
   $this->execCommand('mkdir -p ' . $server['targetFolder']);


   //Delete the oldest backup
   $ls = $this->execCommand('ls -dXr ' . $server['targetFolder'] . '/*/', false);

   $newestBackup = isset($ls[0]) ? $ls[0] : false;

   if ($server['rotations'] > 1) {
    $oldBackupIndex = $server['rotations'] - 1;
    if (isset($ls[$oldBackupIndex])) {
     $this->info("Deleting oldest backup: " . $ls[$oldBackupIndex]);
     $this->execCommand('rm -Rf ' . $ls[$oldBackupIndex]);
     $this->info("Delete done");
    }
   }

   //Make a copy using hard links so we don't duplicate in diskspace
   $backupDate = date($this->dateFormat);
   $currentBackupDir = $server['targetFolder'] . '/' . $backupDate;

   if ($newestBackup) {
    if ($server['rotations'] > 1) {
     $this->info("Copying all from " . $newestBackup . " to " . $currentBackupDir);
     $this->execCommand('cp -al ' . $newestBackup . ' ' . $currentBackupDir);
    } else {
     $this->info("Moving previous backup dir from " . $newestBackup . " to " . $currentBackupDir);
     $this->execCommand("mv " . $newestBackup . " " . $currentBackupDir);
    }
   } else {
    $this->info("Creating first backup directory " . $currentBackupDir);
    $this->execCommand('mkdir ' . $currentBackupDir);
   }

   if (!is_writable($currentBackupDir)) {
    $this->exitBackup("Error: backup directory $currentBackupDir is not writable!");
   }


   $this->backupMysql($server);


   //Now run rsync for each backup source
   foreach ($server['backupFolders'] as $remoteBackupFolder) {
    $this->info("Backing up " . $remoteBackupFolder);

    $localBackupFolder = $currentBackupDir . '/' . $remoteBackupFolder;
    $this->execCommand('mkdir -p ' . $localBackupFolder);

    $cmd = 'rsync -av --delete -e "ssh -i ' . $this->sshKey . ' -p' . $server['port'] . '" ' . $server['user'] . '@' . $server['host'] . ':' . $remoteBackupFolder . '/ ' . $localBackupFolder . '/';
    $this->info("Running " . $cmd);
    //system($cmd, $ret);
    //run the rsync command and read the response line by line
    $fp = popen($cmd, "r");
    while (!feof($fp)) {
     // send the current file part to the browser 
     $this->info(fread($fp, 1024));
    }
    $ret = pclose($fp);

    if ($ret != 0) {
    
     if($ret == 24){
      $this->info("Some source files vanished during transfer. This is probably normal because the system is live.");
     }else{    
      $this->error("Failed running rsync command!\ncmd: ".$cmd."\nReturn code: ".$ret);
     }
    }
   }

   $this->info("Done with " . $server['host']);

   $this->info("\n\n\n----------------------------------------------------\n\n\n");

   //$contents = ob_get_contents(); 
  }

  //remove lock file
  unlink($lockFile);

  $this->exitBackup();
 }

 private function backupMysql($server) {
  if ($server['backupMysql']) {

   $this->info("Starting MySQL backup");

   if (empty($server['mysqlBackupDir'])) {
    $this->error("Please set " . $server['mysqlBackupDir']);
    return false;
   }
   if($this->execRemoteCommand('mkdir -p ' . $server['mysqlBackupDir'], false)===false){
    return false;
   }

   $cmd = 'mysql --user=' . $server['mysqlUser'] . ' --password=' . $server['mysqlPassword'] . ' -e "SHOW DATABASES;" | tr -d "| " | grep -v Database';
   //$this->info($cmd);
   $databases = $this->execRemoteCommand($cmd, false);

   foreach ($databases as $database) {
    if ($database != "information_schema" && $database != 'performance_schema') {
     $this->info("Dumping $database");
     $cmd = 'mysqldump --force --opt --user=' . $server['mysqlUser'] . ' --password=' . $server['mysqlPassword'] . ' --databases ' . $database . ' > "' . $server['mysqlBackupDir'] . '/' . $database . '.sql"';

     if($this->execRemoteCommand($cmd, false)===false){
      continue;
     }

     $this->info("Compressing " . $database);
     $cmd = "tar -C " . $server['mysqlBackupDir'] . " -czf " . $server['mysqlBackupDir'] . "/$database.tar.gz $database.sql";
     if($this->execRemoteCommand($cmd, false)===false){
      continue;
     }

     $cmd = 'rm ' . $server['mysqlBackupDir'] . "/$database.sql";
     if($this->execRemoteCommand($cmd, false)===false){
      continue;
     }



     //break;
    }
   }
   $this->info("MySQL backup done");
  } else {
   $this->info("MySQL backup NOT enabled");
  }
 }

 public function errorHandler($errno, $errstr, $errfile, $errline) {
  switch ($errno) {
   case E_USER_ERROR:
    // Send an e-mail to the administrator
    $msg = "!!!!! Error: $errstr \n Fatal error on line $errline in file $errfile";

    // Write the error to our log file
    $msg = "!!!!! Error: $errstr \n Fatal error on line $errline in file $errfile";
    break;

   case E_USER_WARNING:
    // Write the error to our log file
    $msg = "!!!!! Warning: $errstr \n in $errfile on line $errline";
    break;

   case E_USER_NOTICE:
    // Write the error to our log file
    $msg = "!!!!! Notice: $errstr \n in $errfile on line $errline";
    break;

   default:
    // Write the error to our log file
    $msg = "!!!!! Unknown error [#$errno]: $errstr \n in $errfile on line $errline";
    break;
  }

  $this->error($msg);

  // Don't execute PHP's internal error handler
  return TRUE;
 }

}

$backup = new backup();
$backup->run();

Monday, January 13, 2014

Introducing our weekly blog

Let me introduce Intermesh, a small company in the Netherlands that develops business software since 2003. We mainly develop Group-Office groupware and CRM (See the first post). Apart from that we also develop tailor made software. We stand for:
  • Easy and useful software
  • Great and fast support
  • Long term customer relationships
  • Fair pricing
In this blog I want to write about the challenges we face in our company every week on Monday. We hope that readers will learn from it, or that we learn from our readers comments!

I want to start with some topics about the software we use on our Desktops and why. Choosing the right software for your company is very important and can be difficult. There's so much software around it can be confusing. In this post I'll quickly run by the software we've chosen to use most and go more in depth in future posts.

LAMP

Short for Linux, Apache, MySQL and PHP. That's what we use on our server platform since the beginning and it never failed us! Of course everything has it's drawbacks. But we'll get to that in a future post.
  • Linux is the operating system that's very stable and secure.
  • Apache is the most popular web server for Linux.
  • MySQL is a very fast and free database server that supports the most features on Linux.
  • PHP is a very flexible programming language for web applications that's widely used. Because of it's popularity it's easy to find programmers and learning material. Everything you need is available on the web.

Ubuntu Desktop

We use Ubuntu on our desktops as operating system. Why? Because all of our web applications run on Debian Linux, it makes sense to develop in a similar environment. Because Ubuntu is derived from Debian Linux, they are very much alike. When our programmers use Ubuntu every day, they will feel at home on a Debian server as well. In our company all programmers have multiple roles. They all do programming, software deployment and handle customer support. That's why they need to know how a Linux system works because our customer use it on their servers too.

Netbeans IDE

Netbeans is the editor we use to code PHP, Java and Javascript. We have some requirements in our editors:
  1. Syntax highlighting
  2. Code completion
  3. Navigating through code
  4. Subversion version control support
Netbeans proved to be the most user friendly editor that has all these features and runs them fast.

Subversion

All of our code is stored in our subversion repository. This makes it easy to maintain our releases, review code history and work in branches. Lately Git has become more and more popular. But I believe that when you develop code in your company with your own developers it makes much more sense to use subversion. In another post I'll do a comparison.

Pencil

When we are making quotes or project descriptions, screen mockups can be very useful and clear. We use pencil to create screen mockups for software development projects.

Group-Office

Of course! We use our own software for all the features it supports:
  1. E-mail
  2. Calendar
  3. Projects
  4. Mobile sync
  5. Customer Relation Management
  6. Support tickets
  7. etc.

This are the main software packages we use on the desktop. We'll go in depth in future posts and we'll write about the server software too so stay tuned!