Merge pull request #3554 from eileenmcnaughton/4.4
[civicrm-core.git] / xml / GenCode.php
index 582a0fffe7c85f5df06871457be76d32a826090c..0bad0ff1511d1733c5dd339078016db55fe291b4 100644 (file)
@@ -20,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) {
@@ -50,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);
@@ -58,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();
@@ -103,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() {
@@ -112,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();
@@ -141,12 +223,12 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
       exit();
     }
 
-    $this->generateTemplateVersion($db_version);
+    $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";
@@ -191,11 +273,15 @@ 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);
+
+    if (!empty($this->digestPath)) {
+      file_put_contents($this->digestPath, $this->getDigest());
+    }
   }
 
   function generateListAll($tables) {
@@ -244,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;
@@ -261,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);
 
@@ -355,8 +441,8 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
     $this->beautifier->save();
   }
 
-  function generateTemplateVersion($dbVersion) {
-    file_put_contents($this->tplCodePath . "/CRM/common/version.tpl", $dbVersion);
+  function generateTemplateVersion() {
+    file_put_contents($this->tplCodePath . "/CRM/common/version.tpl", $this->db_version);
   }
 
   function findLocales() {
@@ -387,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 ----
   // -----------------------------
@@ -688,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);
@@ -706,7 +824,7 @@ 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(
@@ -714,16 +832,25 @@ Alternatively you can get a version of CiviCRM that matches your PHP version
         // (since declaring optionGroupName means we are using the civicrm_option_value table)
         'optionGroupName',
         'table',
-        // Optional additional params will be passed into CRM_Core_PseudoConstant::get()
+        // 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){
+      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;
   }
@@ -949,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;
+  }
 }