* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Mage\Task\BuiltIn\Ioncube; use Mage\Task\AbstractTask; use Mage\Console; use Mage\Task\ErrorWithMessageException; /** * This allows intergrating IonCube PHP * encoder into deployment system * It takes the source path renames * it to .raw creates a fresh source * path and runs ioncube encoder placing * encoded files into source folder. * Afterwards it removes the old .raw * folder This means that we dont have * to change the source path within the * main scripts and allows the built * in rsync and other tasks to operate * on the encrypted files. * * IonCube PHP Encoder can be downloaded from * http://www.actweb.info/ioncube.html * * Example enviroment.yaml file at end * * @todo add support for creating license files. * * (c) ActWeb 2013 * (c) Matt Lowe (marl.scot.1@googlemail.com) * * Extends Magallanes (c) Andrés Montañez * */ class EncryptTask extends AbstractTask { /** * Name of the task * * @var string */ private $name = 'IonCube Encoder'; /** * Array of default Ioncube * options * * @var array */ private $default = array (); /** * Array of YAML Ioncube * options * * @var array */ private $yaml = array (); /** * Array of file Ioncube * options (taken from additional * external config file if supplied) * * @var array */ private $file = array (); /** * Source directory as used by * main scripts * * @var string */ private $source = ''; /** * Name of tempory folder * for source code to be moved * to. * * @var string */ private $ionSource = ''; /** * How the default/yaml/project * files interact with each other * * @var string */ private $ionOverRide = ''; /** * Config options from the * enviroment config file * * @var array */ private $mageConfig = array (); /** * Final version of the IonCube * options, after merging all * sources together * * @var array */ private $ionCubeConfig = array (); /** * Default encoder version to use * for the ioncube encoder * * @var string */ private $encoder = 'ioncube_encoder54'; /** * Name of tempory IonCube Project * file, used when running encoder * * @var string */ private $projectFile = ''; /** * If true then run a check on * all files after encoding and * report which ones are not encoded * if any are found to not be encoded * then prompt if we should continue * with the process * If not then clean up the temp files * and exit with cancled code. * * @var bool */ private $checkEncoding = false; /** * List of file extensions to exclude * from encrypted/encoded test * * @var array */ private $checkIgnoreExtens = array(); /** * List of paths to exclude from * encrypted/encoded test * Paths must begin with '/' * and are all relative to the * project root * * @var array */ private $checkIgnorePaths = array(); /** * (non-PHPdoc) * * @see \Mage\Task\AbstractTask::getName() */ public function getName() { return $this->name; } /** * (non-PHPdoc) * * @see \Mage\Task\AbstractTask::init() */ public function init() { // Set the default extensions to ignore $this->checkIgnoreExtens = array ( 'jpg', 'jpeg', 'png', 'js', 'gif', 'css', 'ttf', 'svg', 'map', 'ico', ); // Get any options specfic to this task $this->mageConfig = $this->getConfig()->environmentConfig( 'ioncube' ); /* * Get all our IonCube config options */ $this->_getAllIonCubeConfigs(); /* * get the source code location */ $this->source = $this->getConfig ()->deployment ( 'from' ); /* * remove trailing slash if present */ if (substr ( $this->source, - 1 ) == DIRECTORY_SEPARATOR) { $this->source = substr ( $this->source, 0, - 1 ); } /* * Set the name of the folder that the un-encrypted * files will be moved into */ $this->ionSource = $this->source . '.raw'; /* * set the filename for the ioncube project build file */ $this->projectFile = $this->source . '.prj'; /* * Check if we have been given an encoder script * If not then we will just use the default */ if (isset ( $this->mageConfig ['encoder'] )) { $this->encoder = $this->mageConfig ['encoder']; } /* * Check if a differant merge type has been * supplied, this defines how the 3 differant * config files will be merged together. */ if (isset ( $this->mageConfig ['override'] )) { $this->ionOverRide = $this->mageConfig ['override']; } /* * Check if we have been asked to * confirm all encodings */ if (isset ( $this->mageConfig ['checkencoding'])) { $this->checkEncoding=true; } /* * Check if we have been passed any extra * file extensions to exclude from * encrypt/encode file check * */ if (isset ( $this->mageConfig ['checkignoreextens'])) { $this->checkIgnoreExtens=array_merge($this->ignoreExtens, $this->mageConfig['ignoreextens']); } /* * Check if we have been passed any extra * file paths/files to exclude from * encrypt/encode file check * */ if (isset ( $this->mageConfig ['checkignorepaths'])) { $this->checkIgnorePaths=array_merge($this->checkIgnorePaths, $this->mageConfig['checkignorepaths']); } /* * now merge all the config options together */ $this->ionCubeConfig = $this->mergeConfigFiles (); } /** * This gets all the Ioncube configs * Basicly it gets the default options contianed within this script * It reads any project options from the enviroment.yaml config file * It reads any additional options from external project file if set * * @return void */ private function _getAllIonCubeConfigs() { /* * Get a set of default IonCube options */ $this->default = $this->getOptionsDefault (); /* * Check if there is a 'project' section, * if so then get the options from there */ if (isset ( $this->mageConfig ['project'] )) { $this->yaml = $this->getOptionsFromYaml ( $this->mageConfig ['project'] ); } else { $this->yaml = array ( 's' => array (), 'p' => array () ); } /* * Check if a seperate projectfile has been specified, and if so * then read the options from there. */ if (isset ( $this->mageConfig ['projectfile'] )) { $this->file = $this->getOptionsFromFile ( $this->mageConfig ['projectfile'] ); } else { $this->file = array ( 's' => array (), 'p' => array () ); } } /** * Encrypt the project * Steps are as follows : * Switch our current source dir to the ioncube srouce dir and create new empty dir to encrypt into * Write the IonCube project file (this is the file that controls IonCube encoder) * Run IonCube encoder * Delete the tempory files that we created (so long as we hadn't set 'keeptemp') * Return the result of the IonCube encoder * * @see \Mage\Task\AbstractTask::run() * * @return bool */ public function run() { $this->switchSrcToTmp (); $this->writeProjectFile (); $result = $this->runIonCube (); Console::output("Encoding result :".($result ? 'OK' : 'Bad!')."\n", 0, 2); if (!$result) { $this->deleteTmpFiles (); throw new ErrorWithMessageException('Ioncube failed to encode your project :'.$result); } if (($this->checkEncoding) && (!$this->checkEncoding())) { $this->deleteTmpFiles(); throw new ErrorWithMessageException('Operation canceled by user.'); } $this->deleteTmpFiles (); return $result; } /** * Runs through all files in the encoded * folders and lists any that are not * encoded. If any are found then prompt * user to continue or quit. * If user quites, then clean out encoded * files, and return true to indicate error * * @return bool */ private function checkEncoding() { $src = $this->source; // $ask holds flag to indicate we need to prompt the end user $ask = false; // scan through the directory $rit = new \RecursiveDirectoryIterator ( $src ); foreach ( new \RecursiveIteratorIterator ( $rit ) as $filename => $cur ) { // get the 'base dir' for the project, eg. relative to the temp folder $srcFileName = (str_replace ( $this->source, '', $filename )); /* * Scan through the ignor directorys array * and if it matches the current path/filename * then mark the file to be skipped from testing */ $skip = false; foreach ( $this->checkIgnorePaths as $path ) { if (fnmatch ( $path, $srcFileName )) { $skip = true; } } // check if we should test this file if (! $skip) { // get the file exten for this file and compare to our fileexten exclude array $exten = pathinfo ( $filename, PATHINFO_EXTENSION ); if (! in_array ( strtolower ( $exten ), $this->checkIgnoreExtens )) { // ok, this extension needs to be checked if ($this->checkFileCoding ( $filename )) { // file was encrypted/encoded } else { // file was not encrypted/encoded Console::output("File :" . $srcFileName . " -> Was not encrypted", 0, 1); $ask = true; } } } } if ($ask) { // ok lets ask the user if they want to procede Console::output("\n\nDo you wish to procede (y/N):", 0, 0); if (! $this->promptYn ()) { return false; } } return true; } /** * This simply for user to enter * 'y' or 'Y' and press enter, if * a single 'y' is not entered then * false is returned, otherwise * true is returned. * * @return bool True if 'y' pressed */ private function promptYn() { $handle = fopen ("php://stdin","r"); $line = strtolower(fgets($handle)); if(trim($line) != 'y'){ return false; } return true; } /** * This will take the passed file and try to * work out if it is an encoded/encrypted * ioncube file. * It dosent test the file exten, as it * expects the calling method to have done * that before. * * @param string $filename Filename, with path, to check * * @return boolean True if file was encoded/encrypted */ private function checkFileCoding($filename) { // check to see if this is an encrypted file $ioncube = ioncube_read_file($filename, $ioncubeType); if (is_int ( $ioncube )) { // we got an error from ioncube, so its encrypted return true; } // read first line of file $f = fopen ( $filename, 'r' ); $line = trim ( fgets ( $f, 32 ) ); fclose ( $f ); // if first line is longer than 30, then this isnt a php file if (strlen ( $line ) > 30) { return false; } // if first line starts 'mageConfig ['keeptemp'] )) { return; } Console::log('Deleting tempory files :', 1); $ret1 = Console::executeCommand ( 'rm -Rf ' . $this->ionSource, $out1 ); $ret2 = Console::executeCommand ( 'rm ' . $this->projectFile, $out2 ); if ($ret1 && $ret2) { return; } throw new ErrorWithMessageException ( 'Error deleting temp files :' . $out1 . ' : ' . $out2, 40 ); } /** * Builds the ioncube command * and runs it, returning the result * * @return bool */ private function runIonCube() { $cli = $this->encoder . ' --project-file ' . $this->projectFile . ' ' . $this->ionSource . DIRECTORY_SEPARATOR.'*'; $ret = Console::executeCommand ( $cli, $out ); return $ret; } /** * Write the config options into * a project file ready for use * with ioncube cli * * @throws ErrorWithMessageException If it cant write the project file * * @return void */ private function writeProjectFile() { // array used to build config file into $out = array (); // set the project destination $out [] = '--into ' . $this->source . PHP_EOL; // output the switches foreach ( $this->ionCubeConfig ['s'] as $key => $value ) { if ($value) { // switch was set to true, so output it $out [] = '--' . $key . PHP_EOL; } } // output the options foreach ( $this->ionCubeConfig ['p'] as $key => $value ) { // check if we have an array of values if (is_array ( $value )) { foreach ( $value as $entry ) { $out [] = '--' . $key . ' "' . $entry . '"' . PHP_EOL; } } else { // ok just a normal single option if (strlen ( $value ) > 0) { $out [] = '--' . $key . ' "' . $value . '"' . PHP_EOL; } } } $ret = file_put_contents ( $this->projectFile, $out ); if (! $ret) { // something went wrong $this->deleteTmpFiles (); throw new ErrorWithMessageException ( 'Unable to create project file.', 20 ); } } /** * This merges the 3 config arrays * depending on the 'override' option * * @return array Final config array */ private function mergeConfigFiles() { /* * Options are the order the arrays are in * F - Project File * Y - YAML config options (enviroment file) * D - Default options as stored in script * * more options could be added to make this a bit more flexable * @todo I'm sure this could be combined into a loop to make it easier and shorter * */ $s = array (); $p = array (); switch (strtolower ( $this->ionOverRide )) { case 'fyd' : // FILE / YAML / DEFAULT $s = array_merge ( $this->file ['s'], $this->yaml ['s'], $this->default ['s'] ); $p = array_merge ( $this->file ['p'], $this->yaml ['p'], $this->default ['p'] ); break; case 'yfd' : // YAML / FILE / DEFAULT $s = array_merge ( $this->yaml ['s'], $this->file ['s'], $this->default ['s'] ); $p = array_merge ( $this->yaml ['p'], $this->file ['p'], $this->default ['p'] ); break; case 'dyf' : // DEFAULT / YAML / FILE $s = array_merge ( $this->default ['s'], $this->yaml ['s'], $this->file ['s'] ); $p = array_merge ( $this->default ['p'], $this->yaml ['p'], $this->file ['p'] ); break; case 'd' : default : // Use defaults only $s = $this->default ['s']; $p = $this->default ['p']; break; } return array ( 's' => $s, 'p' => $p ); } /** * Switches the original source * code dir to tempory name * and recreates orginal dir * allows encryption to be done * into source dir, so other functions * work without changing * * @throws ErrorWithMessageException If source dir can't be renamed * @throws ErrorWithMessageException If original source dir cant be created * * @return bool */ private function switchSrcToTmp() { //echo "\nSwitching :" . $this->source . " -> To :" . $this->ionSource."\n"; $ret = Console::executeCommand ( 'mv ' . $this->source . ' ' . $this->ionSource, $out ); if (! $ret) { throw new ErrorWithMessageException ( 'Cant create tmp dir :' . $out, $ret ); } $ret = Console::executeCommand ( 'mkdir -p ' . $this->source, $out ); if (! $ret) { throw new ErrorWithMessageException ( 'Cant re-create dir :' . $out, $ret ); } return true; } /** * Reads a set of options taken from the YAML config * Compares keys against the default ioncube settings * if a key doesnt appear in the default options, it * is ignored * * return array */ private function getOptionsFromYaml($options) { $s = array (); $p = array (); foreach ( $options as $key => $value ) { if (array_key_exists ( $key, $this->default ['s'] )) { $s [$key] = true; } if (array_key_exists ( $key, $this->default ['p'] )) { $p [$key] = $value; } } return array ( 's' => $s, 'p' => $p ); } /** * reads an existing ioncube project * file. * * @return array */ private function getOptionsFromFile($fileName) { $s = array (); $p = array (); $fileContents = file_get_contents ( $fileName ); /* * split the config file on every occurance of '--' at start of a line * Adds a PHP_EOL at the start, so we can catch the first '--' */ $entrys = explode ( PHP_EOL . '--', PHP_EOL . $fileContents ); foreach ( $entrys as $line ) { $line = trim ( $line ); if ($line != '') { /* * get position of first space * so we can split the options out */ $str = strpos ( $line, ' ' ); if ($str === false) { /* * Ok, no spaces found, so take this as a single line */ $str = strlen ( $line ); } $key = substr ( $line, $str ); $value = substr ( $line, $str + 1 ); if ((array_key_exists ( $key, $this->default ['s'] ))) { /* * ok this key appears in the switch config * so store it as a switch */ $s [$key] = true; } if ((array_key_exists ( $key, $this->default ['p'] ))) { /* * Ok this key exists in the parameter section, * So store it allong with its value */ $p [$key] = $this->splitParam ( $value ); } } } return array ( 's' => $s, 'p' => $p ); } /** * Takes supplied line and splits it if required * into an array * returns ether the array, or a plain * string. * Allows options to be spread accross several lines * * @param $string String to split * * @return mixed */ private function splitParam($string) { $split = explode ( PHP_EOL, $string ); if ($split === false) { // nothing found, so return a blank string return ''; } if (count ( $split ) == 1) { return $split [0]; } else { return $split; } } /** * returns an array of default ioncube options * This is also used as a 'filter' for the YAML * and Config files, if an option hasnt got an * entry in this list, then it can not be set * via the VAML or Config files * * @return array */ private function getOptionsDefault() { $s = array (); $p = array (); // Set the switches $s ['allow-encoding-into-source'] = false; $s ['ascii'] = false; $s ['binary'] = true; $s ['replace-target'] = true; $s ['merge-target'] = false; $s ['rename-target'] = false; $s ['update-target'] = false; $s ['only-include-encoded-files'] = false; $s ['use-hard-links'] = false; $s ['without-keeping-file-perms'] = false; $s ['without-keeping-file-times'] = false; $s ['without-keeping-file-owner'] = false; $s ['no-short-open-tags'] = false; $s ['ignore-strict-warnings'] = false; $s ['ignore-deprecated-warnings'] = false; $s ['without-runtime-loader-support'] = false; $s ['without-loader-check'] = false; $s ['disable-auto-prepend-append'] = true; $s ['no-doc-comments'] = true; // Now set the params $p ['encrypt'] [] = '*.tpl.html'; $p ['encode'] = array (); $p ['copy'] = array (); $p ['ignore'] = array ( '.git', '.svn', '.mage', '.gitignore', '.gitkeep', 'nohup.out' ); $p ['keep'] = array (); $p ['obfuscate'] = ''; $p ['obfuscation-key'] = ''; $p ['obfuscation-exclusion-file'] = ''; $p ['expire-in'] = '7d'; $p ['expire-on'] = ''; $p ['allowed-server'] = ''; $p ['with-license'] = 'license.txt'; $p ['passphrase'] = ''; $p ['license-check'] = ''; $p ['apply-file-user'] = ''; $p ['apply-file-group'] = ''; $p ['register-autoglobal'] = array (); $p ['message-if-no-loader'] = ''; $p ['action-if-no-loader'] = ''; $p ['loader-path'] = ''; $p ['preamble-file'] = ''; $p ['add-comment'] = array (); $p ['add-comments'] = ''; $p ['loader-event'] = array (); $p ['callback-file'] = ''; $p ['property'] = ''; $p ['propertys'] = ''; $p ['include-if-property'] = array (); $p ['optimise'] = 'max'; $p ['shell-script-line'] = ''; $p ['min-loader-version'] = ''; return array ( 's' => $s, 'p' => $p ); } }