Merge pull request #3580 from monishdeb/CRM-14701
[civicrm-core.git] / xml / GenCode.php
index 2e897839a83ba4086d68821937babffe821365ed..0bad0ff1511d1733c5dd339078016db55fe291b4 100644 (file)
@@ -1,6 +1,18 @@
 <?php
 ini_set('include_path', '.' . PATH_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'packages' . PATH_SEPARATOR . '..');
-ini_set('memory_limit', '512M');
+// make sure the memory_limit is at least 512 MB
+$memLimitString = trim(ini_get('memory_limit'));
+$memLimitUnit   = strtolower(substr($memLimitString, -1));
+$memLimit       = (int) $memLimitString;
+switch ($memLimitUnit) {
+    case 'g': $memLimit *= 1024;
+    case 'm': $memLimit *= 1024;
+    case 'k': $memLimit *= 1024;
+}
+
+if ($memLimit >= 0 and $memLimit < 536870912) {
+    ini_set('memory_limit', '512M');
+}
 date_default_timezone_set('UTC'); // avoid php warnings if timezone is not set - CRM-10844
 
 define('CIVICRM_UF', 'Drupal');
@@ -8,12 +20,18 @@ define('CIVICRM_UF', 'Drupal');
 require_once 'CRM/Core/ClassLoader.php';
 CRM_Core_ClassLoader::singleton()->register();
 
-$genCode = new CRM_GenCode_Main('../CRM/Core/DAO/', '../sql/', '../', '../templates/');
-$genCode->main(
-  @$argv[2],
-  @$argv[3],
-  empty($argv[1]) ? 'schema/Schema.xml' : $argv[1]
+$genCode = new CRM_GenCode_Main(
+  '../CRM/Core/DAO/',                                                         // $CoreDAOCodePath
+  '../sql/',                                                                  // $sqlCodePath
+  '../',                                                                      // $phpCodePath
+  '../templates/',                                                            // $tplCodePath
+  array('../packages/Smarty/plugins', '../CRM/Core/Smarty/plugins'),          // smarty plugin dirs
+  @$argv[3],                                                                  // cms
+  empty($argv[2]) ? NULL : $argv[2],                                          // db version
+  empty($argv[1]) ? 'schema/Schema.xml' : $argv[1],                           // schema file
+  getenv('CIVICRM_GENCODE_DIGEST') ? getenv('CIVICRM_GENCODE_DIGEST') : NULL  // path to digest file
 );
+$genCode->main();
 
 class CRM_GenCode_Util_File {
   static function createDir($dir, $perm = 0755) {
@@ -38,6 +56,9 @@ class CRM_GenCode_Util_File {
     }
 
     $newTempDir = $tempDir . '/' . $prefix . rand(1, 10000);
+    if (function_exists('posix_geteuid')) {
+      $newTempDir .= '_' . posix_geteuid();
+    }
 
     if (file_exists($newTempDir)) {
       self::removeDir($newTempDir);
@@ -46,30 +67,92 @@ class CRM_GenCode_Util_File {
 
     return $newTempDir;
   }
+
+  /**
+   * Calculate a cumulative digest based on a collection of files
+   *
+   * @param array $files list of file names (strings)
+   * @param callable $digest a one-way hash function (string => string)
+   * @return string
+   */
+  static function digestAll($files, $digest = 'md5') {
+    $buffer = '';
+    foreach ($files as $file) {
+      $buffer .= $digest(file_get_contents($file));
+    }
+    return $digest($buffer);
+  }
+
+  /**
+   * Find the path to the main Civi source tree
+   *
+   * @return string
+   * @throws RuntimeException
+   */
+  static function findCoreSourceDir() {
+    $path = str_replace(DIRECTORY_SEPARATOR, '/', __DIR__);
+    if (!preg_match(':(.*)/xml:', $path, $matches)) {
+      throw new RuntimeException("Failed to determine path of code-gen");
+    }
+
+    return $matches[1];
+  }
+
+  /**
+   * Find files in several directories using several filename patterns
+   *
+   * @param array $pairs each item is an array(0 => $searchBaseDir, 1 => $filePattern)
+   * @return array of file paths
+   */
+  static function findManyFiles($pairs) {
+    $files = array();
+    foreach ($pairs as $pair) {
+      list ($dir, $pattern) = $pair;
+      $files = array_merge($files, CRM_Utils_File::findFiles($dir, $pattern));
+    }
+    return $files;
+  }
 }
 
 class CRM_GenCode_Main {
   var $buildVersion;
+  var $db_version;
   var $compileDir;
   var $classNames;
+  var $cms; // drupal, joomla, wordpress
 
   var $CoreDAOCodePath;
   var $sqlCodePath;
   var $phpCodePath;
   var $tplCodePath;
+  var $schemaPath; // ex: schema/Schema.xml
+
+  /**
+   * @var string|NULL path in which to store a marker that indicates the last execution of
+   * GenCode. If a matching marker already exists, GenCode doesn't run.
+   */
+  var $digestPath;
+
+  /**
+   * @var string|NULL a digest of the inputs to the code-generator (eg the properties and source files)
+   */
+  var $digest;
 
   var $smarty;
 
-  function __construct($CoreDAOCodePath, $sqlCodePath, $phpCodePath, $tplCodePath) {
+  function __construct($CoreDAOCodePath, $sqlCodePath, $phpCodePath, $tplCodePath, $smartyPluginDirs, $argCms, $argVersion, $schemaPath, $digestPath) {
     $this->CoreDAOCodePath = $CoreDAOCodePath;
     $this->sqlCodePath = $sqlCodePath;
     $this->phpCodePath = $phpCodePath;
     $this->tplCodePath = $tplCodePath;
+    $this->cms = $argCms;
+    $this->digestPath = $digestPath;
+    $this->digest = NULL;
 
     require_once 'Smarty/Smarty.class.php';
     $this->smarty = new Smarty();
     $this->smarty->template_dir = './templates';
-    $this->smarty->plugins_dir = array('../packages/Smarty/plugins', '../CRM/Core/Smarty/plugins');
+    $this->smarty->plugins_dir = $smartyPluginDirs;
     $this->compileDir = CRM_GenCode_Util_File::createTempDir('templates_c_');
     $this->smarty->compile_dir = $this->compileDir;
     $this->smarty->clear_all_cache();
@@ -91,6 +174,17 @@ class CRM_GenCode_Main {
     $this->beautifier->setNewLine("\n");
 
     CRM_GenCode_Util_File::createDir($this->sqlCodePath);
+
+    $versionFile        = "version.xml";
+    $versionXML         = &$this->parseInput($versionFile);
+    $this->db_version         = $versionXML->version_no;
+    $this->buildVersion = preg_replace('/^(\d{1,2}\.\d{1,2})\.(\d{1,2}|\w{4,7})$/i', '$1', $this->db_version);
+    if (isset($argVersion)) {
+      // change the version to that explicitly passed, if any
+      $this->db_version = $argVersion;
+    }
+
+    $this->schemaPath = $schemaPath;
   }
 
   function __destruct() {
@@ -100,20 +194,20 @@ class CRM_GenCode_Main {
   /**
    * Automatically generate a variety of files
    *
-   * @param $argVersion string, optional
-   * @param $argCms string, optional; "drupal" or "joomla"
-   * @param $file, the path to the XML schema file
    */
-  function main($argVersion, $argCms, $file) {
-    $versionFile        = "version.xml";
-    $versionXML         = &$this->parseInput($versionFile);
-    $db_version         = $versionXML->version_no;
-    $this->buildVersion = preg_replace('/^(\d{1,2}\.\d{1,2})\.(\d{1,2}|\w{4,7})$/i', '$1', $db_version);
-    if (isset($argVersion)) {
-      // change the version to that explicitly passed, if any
-      $db_version = $argVersion;
+  function main() {
+    if (!empty($this->digestPath) && file_exists($this->digestPath) && $this->hasExpectedFiles()) {
+      if ($this->getDigest() === file_get_contents($this->digestPath)) {
+        echo "GenCode has previously executed. To force execution, please (a) omit CIVICRM_GENCODE_DIGEST\n";
+        echo "or (b) remove {$this->digestPath} or (c) call GenCode with new parameters.\n";
+        exit();
+      }
+      // Once we start GenCode, the old build is invalid
+      unlink($this->digestPath);
     }
-    echo "\ncivicrm_domain.version := $db_version\n\n";
+
+
+    echo "\ncivicrm_domain.version := ". $this->db_version . "\n\n";
     if ($this->buildVersion < 1.1) {
       echo "The Database is not compatible for this version";
       exit();
@@ -129,12 +223,12 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
       exit();
     }
 
-    $this->generateTemplateVersion($argVersion);
+    $this->generateTemplateVersion();
 
-    $this->setupCms($argCms, $db_version);
+    $this->setupCms($this->db_version);
 
-    echo "Parsing input file $file\n";
-    $dbXML = $this->parseInput($file);
+    echo "Parsing input file ".$this->schemaPath."\n";
+    $dbXML = $this->parseInput($this->schemaPath);
     // print_r( $dbXML );
 
     echo "Extracting database information\n";
@@ -179,30 +273,21 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     // $this->generateDropSql($archiveTables, 'civicrm_archive_drop.mysql');
 
     $this->generateNavigation();
-    $this->generateLocalDataSql($db_version, $this->findLocales());
+    $this->generateLocalDataSql($this->findLocales());
     $this->generateSample();
     $this->generateInstallLangs();
     $this->generateDAOs($tables);
     $this->generateSchemaStructure($tables);
-  }
-
-  function generateListAll($tables) {
-    $allDAO = "<?php\n\$dao = array ();";
-    $dao = array();
 
-    foreach ($tables as $table) {
-      $base = $table['base'] . $table['objectName'];
-      if (!array_key_exists($table['objectName'], $dao)) {
-        $dao[$table['objectName']] = str_replace('/', '_', $base);
-        $allDAO .= "\n\$dao['" . $table['objectName'] . "'] = '" . str_replace('/', '_', $base) . "';";
-      }
-      else {
-        $allDAO .= "\n//NAMESPACE ERROR: " . $table['objectName'] . " already used . " . str_replace('/', '_', $base) . " ignored.";
-      }
+    if (!empty($this->digestPath)) {
+      file_put_contents($this->digestPath, $this->getDigest());
     }
+  }
 
-    // TODO deal with the BAO's too ?
-    file_put_contents($this->CoreDAOCodePath . "listAll.php", $allDAO);
+  function generateListAll($tables) {
+    $this->smarty->clear_all_assign();
+    $this->smarty->assign('tables', $tables);
+    file_put_contents($this->CoreDAOCodePath . "AllCoreTables.php", $this->smarty->fetch('listAll.tpl'));
   }
 
   function generateCiviTestTruncate($tables) {
@@ -245,7 +330,7 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     file_put_contents($this->sqlCodePath . "civicrm_navigation.mysql", $this->smarty->fetch('civicrm_navigation.tpl'));
   }
 
-  function generateLocalDataSql($db_version, $locales) {
+  function generateLocalDataSql($locales) {
     $this->reset_smarty_assignments();
 
     global $tsLocale;
@@ -262,7 +347,7 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
       $data[] = $this->smarty->fetch('civicrm_data.tpl');
       $data[] = $this->smarty->fetch('civicrm_navigation.tpl');
 
-      $data[] = " UPDATE civicrm_domain SET version = '$db_version';";
+      $data[] = " UPDATE civicrm_domain SET version = '" . $this->db_version . "';";
 
       $data = implode("\n", $data);
 
@@ -356,16 +441,8 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     $this->beautifier->save();
   }
 
-  function generateTemplateVersion($argVersion) {
-    // add the Subversion revision to templates
-    // use svnversion if the version was not specified explicitely on the commandline
-    if (isset($argVersion) and $argVersion != '') {
-      $svnversion = $argVersion;
-    }
-    else {
-      $svnversion = `svnversion .`;
-    }
-    file_put_contents($this->tplCodePath . "/CRM/common/version.tpl", $svnversion);
+  function generateTemplateVersion() {
+    file_put_contents($this->tplCodePath . "/CRM/common/version.tpl", $this->db_version);
   }
 
   function findLocales() {
@@ -396,25 +473,56 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     return $locales;
   }
 
-  function setupCms($argCms, $db_version) {
+  function setupCms() {
     // default cms is 'drupal', if not specified
-    $cms = isset($argCms) ? strtolower($argCms) : 'drupal';
-    if (!in_array($cms, array(
-      'drupal', 'joomla'))) {
-      echo "Config file for '{$cms}' not known.";
+    $this->cms = isset($this->cms) ? strtolower($this->cms) : 'drupal';
+    if (!in_array($this->cms, array(
+      'drupal', 'joomla', 'wordpress'))) {
+      echo "Config file for '{$this->cms}' not known.";
       exit();
     }
-    elseif ($cms !== 'joomla') {
-      echo "Generating civicrm.config.php\n";
-      copy("../{$cms}/civicrm.config.php.{$cms}", '../civicrm.config.php');
+    elseif ($this->cms !== 'joomla') {
+      $configTemplate = $this->findConfigTemplate($this->cms);
+      if ($configTemplate) {
+        echo "Generating civicrm.config.php\n";
+        copy($configTemplate, '../civicrm.config.php');
+      } else {
+        throw new Exception("Failed to locate template for civicrm.config.php");
+      }
     }
 
     echo "Generating civicrm-version file\n";
-    $this->smarty->assign('db_version', $db_version);
-    $this->smarty->assign('cms', ucwords($cms));
+    $this->smarty->assign('db_version', $this->db_version);
+    $this->smarty->assign('cms', ucwords($this->cms));
     file_put_contents($this->phpCodePath . "civicrm-version.php", $this->smarty->fetch('civicrm_version.tpl'));
   }
 
+  /**
+   * @param string $cms "drupal"|"wordpress"
+   * @return null|string path to config template
+   */
+  public function findConfigTemplate($cms) {
+    $candidates = array();
+    switch ($cms) {
+      case 'drupal':
+        $candidates[] = "../drupal/civicrm.config.php.drupal";
+        $candidates[] =  "../../drupal/civicrm.config.php.drupal";
+        break;
+      case 'wordpress':
+        $candidates[] = "../../civicrm.config.php.wordpress";
+        $candidates[] = "../WordPress/civicrm.config.php.wordpress";
+        $candidates[] = "../drupal/civicrm.config.php.drupal";
+        break;
+    }
+    foreach ($candidates as $candidate) {
+      if (file_exists($candidate)) {
+        return $candidate;
+        break;
+      }
+    }
+    return NULL;
+  }
+
   // -----------------------------
   // ---- Schema manipulation ----
   // -----------------------------
@@ -599,6 +707,19 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
       $table['foreignKey'] = &$foreign;
     }
 
+    if ($this->value('dynamicForeignKey', $tableXML)) {
+      $dynamicForeign = array();
+      foreach ($tableXML->dynamicForeignKey as $foreignXML) {
+        if ($this->value('drop', $foreignXML, 0) > 0 and $this->value('drop', $foreignXML, 0) <= $this->buildVersion) {
+          continue;
+        }
+        if ($this->value('add', $foreignXML, 0) <= $this->buildVersion) {
+          $this->getDynamicForeignKey($foreignXML, $dynamicForeign, $name);
+        }
+      }
+      $table['dynamicForeignKey'] = $dynamicForeign;
+    }
+
     $tables[$name] = &$table;
     return;
   }
@@ -684,6 +805,7 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     }
 
     $field['required'] = $this->value('required', $fieldXML);
+    $field['collate']  = $this->value('collate', $fieldXML);
     $field['comment']  = $this->value('comment', $fieldXML);
     $field['default']  = $this->value('default', $fieldXML);
     $field['import']   = $this->value('import', $fieldXML);
@@ -702,15 +824,33 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     $field['dataPattern'] = $this->value('dataPattern', $fieldXML);
     $field['uniqueName'] = $this->value('uniqueName', $fieldXML);
     $field['pseudoconstant'] = $this->value('pseudoconstant', $fieldXML);
-    if(!empty($fieldXML->pseudoconstant)){
+    if(!empty($field['pseudoconstant'])){
       //ok this is a bit long-winded but it gets there & is consistent with above approach
       $field['pseudoconstant'] = array();
-      $validOptions = array('name', 'optionGroupName', 'table', 'keyColumn', 'labelColumn','class');
-      foreach ($validOptions as $pseudoOption){
+      $validOptions = array(
+        // Fields can specify EITHER optionGroupName OR table, not both
+        // (since declaring optionGroupName means we are using the civicrm_option_value table)
+        'optionGroupName',
+        'table',
+        // If table is specified, keyColumn and labelColumn are also required
+        'keyColumn',
+        'labelColumn',
+        // Non-translated machine name for programmatic lookup. Defaults to 'name' if that column exists
+        'nameColumn',
+        // Where clause snippet (will be joined to the rest of the query with AND operator)
+        'condition',
+      );
+      foreach ($validOptions as $pseudoOption) {
         if(!empty($fieldXML->pseudoconstant->$pseudoOption)){
           $field['pseudoconstant'][$pseudoOption] = $this->value($pseudoOption, $fieldXML->pseudoconstant);
         }
       }
+      // For now, fields that have option lists that are not in the db can simply
+      // declare an empty pseudoconstant tag and we'll add this placeholder.
+      // That field's BAO::buildOptions fn will need to be responsible for generating the option list
+      if (empty($field['pseudoconstant'])) {
+        $field['pseudoconstant'] = 'not in database';
+      }
     }
     $fields[$name] = &$field;
   }
@@ -842,6 +982,15 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     $foreignKeys[$name] = &$foreignKey;
   }
 
+  function getDynamicForeignKey(&$foreignXML, &$dynamicForeignKeys) {
+    $foreignKey = array(
+      'idColumn' => trim($foreignXML->idColumn),
+      'typeColumn' => trim($foreignXML->typeColumn),
+      'key' => trim($this->value('key', $foreignXML)),
+    );
+    $dynamicForeignKeys[] = $foreignKey;
+  }
+
   protected function value($key, &$object, $default = NULL) {
     if (isset($object->$key)) {
       return (string ) $object->$key;
@@ -927,4 +1076,55 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     $this->smarty->clear_all_cache();
     $this->smarty->assign('generated', "DO NOT EDIT.  Generated by " . basename(__FILE__));
   }
+
+
+  /**
+   * Compute a digest based on the inputs to the code-generator (ie the properties
+   * of the codegen and the source files loaded by the codegen).
+   *
+   * @return string
+   */
+  function getDigest() {
+    if ($this->digest === NULL) {
+      $srcDir = CRM_GenCode_Util_File::findCoreSourceDir();
+      $files = CRM_GenCode_Util_File::findManyFiles(array(
+        // array("$srcDir/CRM/Core/CodeGen", '*.php'),
+        array("$srcDir/xml", "*.php"),
+        array("$srcDir/xml", "*.tpl"),
+        array("$srcDir/xml", "*.xml"),
+      ));
+
+      $properties = var_export(array(
+        CRM_GenCode_Util_File::digestAll($files),
+        $this->buildVersion,
+        $this->db_version,
+        $this->cms,
+        $this->CoreDAOCodePath,
+        $this->sqlCodePath,
+        $this->phpCodePath,
+        $this->tplCodePath,
+        $this->schemaPath,
+        // $this->getTasks(),
+      ), TRUE);
+
+      $this->digest = md5($properties);
+    }
+    return $this->digest;
+  }
+
+  function getExpectedFiles() {
+    return array(
+      $this->sqlCodePath . '/civicrm.mysql',
+      $this->phpCodePath . '/CRM/Contact/DAO/Contact.php',
+    );
+  }
+
+  function hasExpectedFiles() {
+    foreach ($this->getExpectedFiles() as $file) {
+      if (!file_exists($file)) {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
 }