Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • AutomaticTranslationCommand
  • ConsoleFormatterUtil
  • CronCommand
  • CryptSetupCommand
  • DummyCommand
  • ExportFixtureCommand
  • MigrateCustomCommand
  • SampleDataCommand
  • UpdateCommand
  • UpdaterPackageCommand
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: 
  3: /*****************************************************************************************
  4:  * X2Engine Open Source Edition is a customer relationship management program developed by
  5:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
  6:  * 
  7:  * This program is free software; you can redistribute it and/or modify it under
  8:  * the terms of the GNU Affero General Public License version 3 as published by the
  9:  * Free Software Foundation with the addition of the following permission added
 10:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
 11:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
 12:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 13:  * 
 14:  * This program is distributed in the hope that it will be useful, but WITHOUT
 15:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 16:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 17:  * details.
 18:  * 
 19:  * You should have received a copy of the GNU Affero General Public License along with
 20:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
 21:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 22:  * 02110-1301 USA.
 23:  * 
 24:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
 25:  * California 95067, USA. or at email address contact@x2engine.com.
 26:  * 
 27:  * The interactive user interfaces in modified source and object code versions
 28:  * of this program must display Appropriate Legal Notices, as required under
 29:  * Section 5 of the GNU Affero General Public License version 3.
 30:  * 
 31:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 32:  * these Appropriate Legal Notices must retain the display of the "Powered by
 33:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
 34:  * technical reasons, the Appropriate Legal Notices must display the words
 35:  * "Powered by X2Engine".
 36:  *****************************************************************************************/
 37: 
 38: Yii::import('application.commands.X2ConsoleCommand');
 39: 
 40: /**
 41:  * Update/migrate custom code from the "custom" folder using Git.
 42:  *
 43:  * Notes:
 44:  * (1) This command requires a Unix-like shell environment with rsync and git
 45:  *     installed in it in order to run properly.
 46:  * (2) The Git repository must be up-to-date and have tags corresponding to
 47:  *     the versions updating to and from.
 48:  * (3) This script will not work properly if the git repository is a clone of
 49:  *     the public repository found on Github, and if using Professional Edition.
 50:  *     Otherwise, if using Open Source Edition, this script should work with a
 51:  *     clone of that repository (assuming the clone has all version tags).
 52:  * (4) Since controller classes only extend their base-code analogues, and do
 53:  *     not fully copy/replace them, they are ignored by this whole process.
 54:  *     Updating them should just be a matter of updating only the methods that
 55:  *     were overridden, if any, instead of the entire file.
 56:  *
 57:  * @property string $branch The name of the temporary branch that will be used
 58:  *  for merging and updating custom code.
 59:  * @property array $fileList List of custom files to be copied.
 60:  * @property string $gitdir The directory of the git repository. If unspecified,
 61:  *  it is assumed to be one level above the web root.
 62:  * @property string $rsync Default rsync command to use for synchronizing files.
 63:  * @property string $source The path to the custom folder. If unspecified, it is
 64:  *  assumed that it is the custom folder inside the current installation.
 65:  * @package application.commands
 66:  * @author Demitri Morgan <demitri@x2engine.com>
 67:  */
 68: class MigrateCustomCommand extends X2ConsoleCommand {
 69: 
 70:     const DEBUG = 0;
 71: 
 72:     const PERSIST_FILE = '.x2_git_migrate.json';
 73: 
 74:     /**
 75:      * The version from which the custom code is being updated.
 76:      * @var string
 77:      */
 78:     public $origin;
 79: 
 80:     /**
 81:      * The version to which the custom code should be updated.
 82:      * @var string
 83:      */
 84:     public $target;
 85: 
 86:     private $_branch;
 87:     private $_fileList;
 88:     private $_gitdir;
 89:     private $_source;
 90:     /**
 91:      * Stores parameter names.
 92:      * @var array
 93:      */
 94:     private $params = array();
 95: 
 96:     /**
 97:      * Updates the custom code.
 98:      *
 99:      * If it can automatically merge, and if there are no merge conflicts, it
100:      * will copy the files back into the source folder.
101:      *
102:      * @param string $source Path to the custom code folder.
103:      * @param string $origin The version of the X2Engine installation at which it
104:      *  was customized. In other words, the version from which X2Engine is being
105:      *  updated.
106:      * @param string $target The target version to which the X2Engine
107:      *  customizations will be updated.
108:      * @param string $gitdir The directory
109:      * @param string $branch Name of branch to use for merging upstream changes
110:      *  into the custom code
111:      */
112:     public function actionUpdate($origin,$target,$source=null,$gitdir=null,$branch=null,$nocopy=0) {
113:         // Initialize
114:         $this->initParams(get_defined_vars());
115: 
116:         // Prompt the user if there's a branch name collision
117:         $delBranch = 'y';
118:         if($this->branchExists()) {
119:             $msg = "A branch named {$this->branch} will be created, but such a "
120:                    ."branch already exists in the Git repository. It will be "
121:                    ."deleted. Continue?";
122:             $msg = $this->formatter($msg)->bold()->color('red')->format();
123:             $delBranch = $this->prompt($msg,'y');
124:         }
125:         if(strtolower($delBranch)=='n') {
126:             return;
127:         }
128:         
129:         // Assume we're starting a new update, so clean everything up:
130:         $this->cleanUp();
131:         
132:         // Create the new branch at the start tag:
133:         $this->headerMsg("-- Creating a Git branch for the update at {$this->origin} --");
134:         $mkBranch = $this->git("branch {$this->branch} {$this->origin}");
135:         if($mkBranch != 0) {
136:             $this->end();
137:         }
138: 
139:         // Checkout:
140:         $this->headerMsg('-- Switching to the new branch --');
141:         $checkout = $this->git("checkout {$this->branch}");
142:         if($checkout != 0) {
143:             $this->end();
144:         }
145: 
146:         // Copy files into the repo:
147:         $this->copyForth();
148:         
149:         // Commit:
150:         $this->headerMsg('-- Committing changes --');
151:         $this->git("add ./");
152:         $commit = $this->git("commit -a -m 'Local custom changes as of {$this->origin}'");
153:         if($commit != 0) {
154:             $this->end();
155:         }
156: 
157:         // Update:
158:         $this->headerMsg("-- Merging upstream changes to version {$this->target} --");
159:         $update = $this->git("merge {$this->target}",false);
160:         if($update != 0) {
161:             $this->headerMsg("Automatic merge failed. Resolve conflicts, commit changes, and run \"migratecustom copy --source={$this->source}\"",'red');
162:             $this->end();
163:         }
164: 
165:         // Copy the merged files:
166:         $this->copyBack();
167: 
168:         // Done.
169:         $this->end(true);
170:     }
171: 
172:     /**
173:      * Copies the current source files onto analogues found in the git
174:      * directory.
175:      * @param string $source Path to the source (custom folder)
176:      *
177:      */
178:     public function actionCopy($origin=null,$target=null,$source=null,$gitdir=null,$branch=null) {
179:         if(!isset($source) || !isset($origin,$target) || !isset($branch)) {
180:             // Use the persist file to restore data, in case of having to
181:             // manually fix conflicts and merge, so that the process can be
182:             // resumed
183:             $this->restoreParams(get_defined_vars());
184:         }
185:         $this->copyBack();
186: 
187:         // Done.
188:         $this->end(true);
189:     }
190: 
191:     public function branchExists() {
192:         return (int) $this->git("show-branch {$this->branch}",false) == 0;
193:     }
194: 
195:     /**
196:      * Removes the persist file and deletes the temporary branch.
197:      */
198:     public function cleanUp() {
199:         // Delete persist file
200:         if(file_exists($persistFile = $this->source.DIRECTORY_SEPARATOR.self::PERSIST_FILE)){
201:             $this->headerMsg("-- Deleting the persist file --");
202:             unlink($persistFile);
203:         }
204:         // Delete branch if it exists
205:         if($this->branchExists()) {
206:             $this->headerMsg("-- Deleting temporary branch {$this->branch} --");
207:             $this->git('reset --hard HEAD');
208:             $this->git('checkout -q master');
209:             $this->git("branch -D {$this->branch}");
210:         }
211:     }
212: 
213:     /**
214:      * Copies the custom code from the git repository back into the original
215:      * folder, overwriting originals.
216:      */
217:     public function copyBack() {
218:         $this->headerMsg('-- Copying merged files from the Git repository back into the source --');
219:         $this->sys("{$this->rsync} --existing {$this->gitdir}/x2engine/ {$this->source}/");
220:     }
221: 
222:     public function copyForth(){
223:         $this->headerMsg('-- Copying customized files into the Git repository --');
224:         $this->sys("{$this->rsync} {$this->source}/ {$this->gitdir}/x2engine/");
225:     }
226: 
227:     /**
228:      * Displays debugging messages
229:      * @param type $msg
230:      */
231:     public function debug($msg) {
232:         if(self::DEBUG) { 
233:             echo $this->formatter('[debug] ')->color('blue')->bold()->format()."$msg\n";
234:         }
235:     }
236: 
237:     /**
238:      * 
239:      */
240:     public function end($cleanUp = false) {
241:         if($cleanUp){
242:             $this->cleanUp();
243:         }else{
244:             $this->saveParams();
245:         }
246:         Yii::app()->end();
247:     }
248: 
249:     /**
250:      * Opens a git subprocess in the git directory.
251:      * 
252:      * @param string $command Git command to run
253:      * @param bool $echo Whether to echo (true) or suppress (false) any output
254:      *  from the command.
255:      * @param bool $embolden Whether to embolden error output and turn it red.
256:      */
257:     public function git($command,$echo=true,$embolden = true){ // &$pipes,$descriptorSpec=array()) {
258:         return $this->sys("git $command",$this->gitdir,$echo,$embolden);
259:     }
260: 
261:     /**
262:      * Run a system command, echo its output.
263:      *
264:      * @param type $command
265:      * @param bool $echo Whether to echo (true) or suppress (false) any standard
266:      *  output from the command.
267:      * @param bool $embolden Whether to embolden error output and turn it red.
268:      * @return type
269:      */
270:     public function sys($command,$cwd=null,$echo=true,$embolden=true) {
271:         if($cwd == null) {
272:             $cwd = __DIR__;
273:         }
274:         $descriptorSpec = array(
275:             0 => array('pipe', 'r'), // stdin
276:             1 => array('pipe', 'w'), // stdout
277:             2 => array('pipe', 'w'), // stderr
278:         );
279:         $this->debug("Running: $command");
280:         $cmd = proc_open("$command", $descriptorSpec, $pipes, $cwd);
281:         $stdOut = stream_get_contents($pipes[1]);
282:         $stdErr = stream_get_contents($pipes[2]);
283:         $code = proc_close($cmd);
284:         $this->debug("Exit code for $command: $code\n");
285:         if($code != 0 && $echo) {
286:             if($embolden) {
287:                 $this->headerMsg($stdErr,'red',false);
288:             } else {
289:                 echo $stdErr;
290:             }
291:         } elseif($echo) {
292:             echo $stdOut;
293:         }
294:         return $code;
295:     }
296: 
297:     /**
298:      * Gets the default source path, which is guaranteed to exist more or less
299:      * @return string
300:      */
301:     public function getDefaultSource() {
302:         return realpath(implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'..','custom')));
303:     }
304: 
305:     /**
306:      * Getter for {@branch}
307:      * @return type
308:      */
309:     public function getBranch() {
310:         if(empty($this->_branch)) {
311:             $this->_branch = "custom_code_update_{$this->origin}-{$this->target}";
312:         }
313:         return $this->_branch;
314:     }
315: 
316:     /**
317:      * Getter for {@link fileList}
318:      * @return array
319:      */
320:     public function getFileList() {
321:         if(!isset($this->_fileList)) {
322:             $cmd =  new CommandUtil();
323:             $findCmd = "find {$this->source}/ -type f";
324:             $this->debug("Running find command: $findCmd");
325:             $output = $cmd->run($findCmd)->output();
326:             $this->debug("Output: $output");
327:             $output = explode("\n",$output);
328:             $this->_fileList = array();
329:             foreach($output as $line) {
330:                 if(preg_match(':(?<path>protected.+\.php)$:',$line,$match)) {
331:                     $this->_fileList[] = $match['path'];
332:                     $this->debug("file in file list: ".$match['path']);
333:                 } else {
334:                     $this->debug("line not part of file list: $line");
335:                 }
336:             }
337:         }
338:         return $this->_fileList;
339:     }
340: 
341:     /**
342:      * Getter for {@link gitdir}
343:      * @return string
344:      */
345:     public function getGitdir() {
346: 
347:         if(empty($this->_gitdir)) {
348:             $this->_gitdir = realpath(implode(DIRECTORY_SEPARATOR,array(Yii::app()->basePath,'..','..')));
349:         }
350:         return $this->_gitdir;
351:     }
352: 
353:     /**
354:      * Getter for {@link rsync}
355:      * @return string
356:      */
357:     public function getRsync() {
358:         return 'rsync -ac --exclude="*~"';
359:     }
360: 
361:     /**
362:      * Getter for {@link source}
363:      * @return string
364:      */
365:     public function getSource() {
366:         if(empty($this->_source)) {
367:             $this->_source = $this->getDefaultSource();
368:         }
369:         return $this->_source;
370:     }
371: 
372:     /**
373:      * Sets properties initially
374:      * @param array $params
375:      */
376:     public function initParams($params) {
377:         foreach($params as $name=>$value) {
378:             if($this->canSetProperty($name) || property_exists($this, $name)) {
379:                 if($name=='gitdir' || $name == 'source')
380:                     $value = rtrim($value,DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
381:                 $this->params[$name] = $name;
382:                 $this->$name = $value;
383:             }
384:         }
385:     }
386: 
387:     /**
388:      * Uses parameters saved to the persistence file during the current operation
389:      *
390:      * @param type $params Optional parameters to override old saved parameters.
391:      */
392:     public function restoreParams($params=array()) {
393:         $persistFile = $this->source.DIRECTORY_SEPARATOR.self::PERSIST_FILE;
394:         $savedParams = file_exists($persistFile) ? json_decode(file_get_contents($persistFile),1) : array();
395:         $savedParams = empty($savedParams) ? array() : $savedParams;
396:         foreach(array_keys($params) as $name) {
397:             if(!empty($savedParams[$name]) && empty($params[$name])) {
398:                 $params[$name] = $savedParams[$name];
399:             }
400:         }
401:         $this->initParams($params);
402:         $this->headerMsg("-- Continuing with previous parameters --");
403:         foreach($this->params as $property) {
404:             echo $this->formatter($property)->color('green')->format().": {$this->$property}\n";
405:         }
406:     }
407: 
408:     /**
409:      * Saves parameters to the persistence file.
410:      */
411:     public function saveParams() {
412:         foreach($this->params as $property) {
413:             $params[$property] = $this->$property;
414:         }
415:         file_put_contents($this->source.DIRECTORY_SEPARATOR.self::PERSIST_FILE,json_encode($params));
416:     }
417: 
418:     /**
419:      * Setter for {@link branch}
420:      * @param string $value
421:      */
422:     public function setBranch($value){
423:         $this->_branch = $value;
424:     }
425: 
426:     /**
427:      * Setter for {@link gitdir}
428:      * @param string $value
429:      */
430:     public function setGitdir($value){
431:         $this->_gitdir = $this->validPath($value,'gitdir');
432:     }
433: 
434:     /**
435:      * Setter for {@link source}
436:      * @param type $value
437:      */
438:     public function setSource($value){
439:         $this->_source = $this->validPath($value,'source');
440:     }
441: 
442:     public function validPath($value,$name) {
443:         $path = realpath(str_replace('~','/home/'.get_current_user(),$value));
444:         if(!$path) {
445:             $this->headerMsg("Invalid path specified for $name: $value",'red');
446:             Yii::app()->end();
447:         }
448:         return $path;
449:     }
450: }
451: 
452: ?>
453: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0