euromark 14 years ago
parent
commit
67a7e86b17

+ 308 - 0
Console/Command/CcShell.php

@@ -0,0 +1,308 @@
+<?php
+
+App::uses('Folder', 'Utility');
+App::uses('File', 'Utility');
+
+if (!defined('LF')) {
+	define('LF', PHP_EOL); # use PHP to detect default linebreak
+}
+
+/**
+ * Code Completion Shell
+ * Workes perfectly with PHPDesigner - but should also work with most other IDEs out of the box
+ * 
+ * @version 1.1
+ * @cakephp 2.0
+ * @author Mark Scherer
+ * @license MIT
+ * 2011-11-24 ms
+ */
+class CcShell extends AppShell {
+	public $uses = array();
+
+	protected $plugins = null;
+	protected $content = '';
+
+	public function main() {
+		$this->out('Code Completion Dump - customized for PHPDesigner');
+
+		$this->filename = APP.'CodeCompletion.php';
+
+		# get classes
+		$this->models();
+		$this->behaviors();
+
+		$this->controller();
+		$this->helpers();
+
+		# write to file
+		$this->_dump();
+
+		$this->out('...done');
+	}
+
+
+
+	public function models() {
+		$files = $this->_getFiles('Model');
+
+		$content = LF;
+		$content .= '/*** model start ***/'.LF;
+		$content .= 'class AppModel extends Model {'.LF;
+		if (!empty($files)) {
+			$content .= $this->_prepModels($files);
+		}
+
+		$content .= '}'.LF;
+		$content .= '/*** model end ***/'.LF;
+
+		$this->content .= $content;
+	}
+
+	public function behaviors() {
+		$files = $this->_getFiles('Model/Behavior');
+
+		$content = LF;
+		$content .= '/*** behavior start ***/'.LF;
+		$content .= 'class AppModel extends Model {'.LF;
+		if (!empty($files)) {
+			$content .= $this->_prepBehaviors($files);
+		}
+		$content .= '}'.LF;
+		$content .= '/*** behavior end ***/'.LF;
+
+
+		$content .= '/*** model start ***/'.LF;
+
+		$this->content .= $content;
+	}
+
+	/**
+	 * components + models
+	 */
+	public function controller() {
+		$content = LF;
+		$content .= '/*** component start ***/'.LF;
+		$content .= 'class AppController extends Controller {'.LF;
+		
+		$files = $this->_getFiles('Controller/Component');
+		if (!empty($files)) {
+			$content .= $this->_prepComponents($files);
+		}
+		
+		$content .= LF.LF;
+		
+		$files = $this->_getFiles('Model');
+		if (!empty($files)) {
+			$content .= $this->_prepModels($files);
+		}
+		
+		$content .= '}'.LF;
+		$content .= '/*** component end ***/'.LF;
+
+		$this->content .= $content;
+	}
+
+	public function helpers() {
+		$files = $this->_getFiles('View/Helper');
+		$content = LF;
+		$content .= '/*** helper start ***/'.LF;
+		$content .= 'class AppHelper extends Helper {'.LF;
+		if (!empty($files)) {
+			$content .= $this->_prepHelpers($files);
+		}
+		$content .= '}'.LF;
+		$content .= '/*** helper end ***/'.LF;
+
+
+		$this->content .= $content;
+	}
+
+	protected function _prepModels($files) {
+		$res = '';
+		foreach ($files as $name) {
+
+
+			$res .= '
+	/**
+	* '.$name.'
+	*
+	* @var '.$name.'
+	*/
+	public $'.$name.';
+'.LF;
+		}
+
+		$res .= '	public function __construct() {';
+
+		foreach ($files as $name) {
+			$res .= '
+		$this->'.$name.' = new '.$name.'();';
+		}
+
+		$res .= LF.'	}'.LF;
+		return $res;
+	}
+
+	protected function _prepBehaviors($files) {
+		$res = '';
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Behavior'))) {
+				continue;
+			}
+			$res .= '
+	/**
+	* '.$name.'Behavior
+	*
+	* @var '.$varName.'
+	*/
+	public $'.$varName.';
+'.LF;
+		}
+
+		$res .= '	public function __construct() {';
+
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Behavior'))) {
+				continue;
+			}
+			$res .= '
+		$this->'.$varName.' = new '.$name.'();';
+		}
+
+		$res .= LF.'	}'.LF;
+		return $res;
+	}
+
+	/**
+	 * check on correctness to avoid duplicates
+	 */
+	protected function _varName($name, $type) {
+		if (($pos = strrpos($name, $type)) === false) {
+			return '';
+			//return $name;
+		}
+		return substr($name, 0, $pos);
+	}
+
+
+	protected function _prepComponents($files) {
+		$res = '';
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Component'))) {
+				continue;
+			}
+			$res .= '
+	/**
+	* '.$name.'
+	*
+	* @var '.$varName.'
+	*/
+	public $'.$varName.';
+'.LF;
+		}
+
+		$res .= '	public function __construct() {';
+
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Component'))) {
+				continue;
+			}
+			$res .= '
+		$this->'.$varName.' = new '.$name.'();';
+		}
+
+		$res .= LF.'	}'.LF;
+		return $res;
+	}
+
+	protected function _prepHelpers($files) {
+		# new ones
+		$res = '';
+
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Helper'))) {
+				continue;
+			}
+			$res .= '
+	/**
+	* '.$name.'
+	*
+	* @var '.$varName.'
+	*/
+	public $'.$varName.';
+'.LF;
+		}
+
+		$res .= '	public function __construct() {';
+
+		foreach ($files as $name) {
+			if (!($varName = $this->_varName($name, 'Helper'))) {
+				continue;
+			}
+			$res .= '
+		$this->'.$varName.' = new '.$name.'();';
+		}
+
+		$res .= LF.'	}'.LF;
+
+		return $res;
+	}
+
+
+	protected function _dump() {
+		//$File = new File($this->filename, true);
+
+		$content = '<?php exit();'.PHP_EOL.PHP_EOL;
+		$content .= 'class CodeCompletion {'.PHP_EOL;
+		$content .= '}'.PHP_EOL.PHP_EOL;
+		$content .= '//Printed: '.date('d.m.Y, H:i:s').PHP_EOL;
+		$content .= $this->content;
+		
+		//return $File->write($content);
+		file_put_contents($this->filename, $content);
+	}
+	
+
+	protected function _getFiles($type) {
+		$files = App::objects($type, null, false);
+		$corePath = App::core($type);
+		$coreFiles = App::objects($type, $corePath, false);
+		$files = am($coreFiles, $files);
+		//$paths = (array)App::path($type.'s');
+		//$libFiles = App::objects($type, $paths[0] . 'lib' . DS, false);
+	
+		if (!isset($this->plugins)) {
+			$this->plugins = App::objects('plugin');
+		}
+	
+		if (!empty($this->plugins)) {
+			foreach ($this->plugins as $plugin) {
+				$pluginType = $plugin.'.'.$type;
+					$pluginFiles = App::objects($pluginType, null, false);
+					if (!empty($pluginFiles)) {
+						foreach ($pluginFiles as $t) {
+							$files[] = $t;
+						}
+					}
+			}
+		}
+		$files = array_unique($files);
+		sort($files);
+			$appIndex = array_search('App', $files);
+			if ($appIndex !== false) {
+				unset($files[$appIndex]);
+			}
+	
+			# no test/tmp files etc (helper.test.php or helper.OLD.php)
+		foreach ($files as $key => $file) {
+				if (strpos($file, '.') !== false || !preg_match('/^[\da-zA-Z_]+$/', $file)) {
+					unset($files[$key]);
+				}
+			}
+		return $files;
+	}
+
+}
+
+

+ 165 - 0
Console/Command/ConvertShell.php

@@ -0,0 +1,165 @@
+<?php
+
+if (!defined('CHMOD_PUBLIC')) {
+	define('CHMOD_PUBLIC', 0770);
+}
+App::uses('AppShell', 'Console/Command');
+
+/**
+ * uses dos2unix >= 5.0
+ * console call: dos2unix [-fhkLlqV] [-c convmode] [-o file ...] [-n inputfile outputfile ...]
+ *
+ * @cakephp 2.0
+ * @author Mark Scherer
+ * @license MIT
+ * 2011-11-04 ms
+ */
+class ConvertShell extends AppShell {
+	public $uses = array();
+
+
+	/**
+	 * predefined options
+	 */
+	public $modes = array(
+		'd2u', 'u2d', 'git', # dos/unix 
+		'd2m', 'm2d', # dos/mac
+		'u2m', 'm2u' # unix/mac
+	);
+
+	/**
+	 * Shell startup, prints info message about dry run.
+	 *
+	 * @return void
+	 */
+	public function startup() {
+		parent::startup();
+
+		if ($this->params['dry-run']) {
+			$this->out(__d('cake_console', '<warning>Dry-run mode enabled!</warning>'), 1, Shell::QUIET);
+		}
+		if (!$this->_test()) {
+			$this->out(__d('cake_console', '<warning>dos2unix not available</warning>'), 1, Shell::QUIET);
+		}
+	}
+
+
+
+
+	public function folder() {
+		$this->out('Converting folder...');
+
+		$folder = APP;
+		$mode = $this->params['mode'];
+		if (empty($mode) || !in_array($mode, $this->modes)) {
+			$this->error('Invalid mode', 'Please specify d2u, u2d, git (d2u+u2d) ...');
+		}
+		if (!empty($this->args)) {
+			$folder = array_shift($this->args);
+			$folder = realpath($folder);
+		}
+		if (empty($folder)) {
+			$this->error('Invalid dir', 'No valid dir given (either absolute or relative to APP)');
+		}
+
+		$this->_convert($folder, $mode);
+		$this->out('Done!');
+	}
+
+
+	public function _test() {
+		# bug - always outputs the system call right away, no way to catch and surpress it
+		return true;
+
+		ob_start();
+		system('dos2unix -h', $x);
+		$output = ob_get_contents();
+		ob_end_clean();
+
+		return !empty($output) && $x === 0;
+	}
+
+
+	public function _convert($dir, $mode, $excludes = array()) {
+		$Iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),
+			RecursiveIteratorIterator::CHILD_FIRST);
+		foreach ($Iterator as $path) {
+			$fullPath = $path->__toString();
+			$continue = false;
+
+			foreach ($excludes as $exclude) {
+				if (strpos($fullPath, $exclude) === 0) {
+					$continue = true;
+					break;
+				}
+			}
+			if ($continue) {
+				continue;
+			}
+
+			if ($path->isDir()) {
+				continue;
+			}
+			if (strpos($fullPath, DS.'.') !== false) {
+				continue;
+			}
+			if (!empty($this->params['verbose'])) {
+				$this->out('Converting file: '.$fullPath);
+			}
+			if (empty($this->params['dry-run'])) {
+				ob_start();
+				if ($mode == 'git') {
+					system('dos2unix --'.'d2u'.' --skipbin '.$fullPath, $x);
+					system('dos2unix --'.'u2d'.' --skipbin '.$fullPath, $x);
+				} else {
+					system('dos2unix --'.$mode.' --skipbin '.$fullPath, $x);
+				}
+				$output = ob_get_contents();
+				ob_end_clean();
+			}
+		}
+	}
+
+
+
+	/**
+	 * get the option parser
+	 *
+	 * @return ConsoleOptionParser
+	 */
+	public function getOptionParser() {
+		$subcommandParser = array(
+			'options' => array(
+				'mode' => array(
+					'short' => 'm',
+					'help' => __d('cake_console', 'Mode'),
+					'default' => '' # auto detect
+				),
+				'ext' => array(
+					'short' => 'e',
+					'help' => __d('cake_console', 'Specify extensions [php|txt|...]'),
+					'default' => '',
+				),
+				'dry-run'=> array(
+					'short' => 'd',
+					'help' => __d('cake_console', 'Dry run the clear command, no files will actually be deleted. Should be combined with verbose!'),
+					'boolean' => true
+				),
+				'exclude'=> array(
+					'short' => 'x',
+					'help' => __d('cake_console', 'exclude the following files or folders'),
+					'boolean' => true,
+					'default' => ''
+				)
+			)
+		);
+
+		return parent::getOptionParser()
+			->description(__d('cake_console', "The Convert Shell converts files from dos/unix/mac to another system"))
+			->addSubcommand('folder', array(
+				'help' => __d('cake_console', 'Convert folder recursivly (Tools.Convert folder [options] [path])'),
+				'parser' => $subcommandParser
+			));
+	}
+
+}

+ 150 - 0
Console/Command/HashShell.php

@@ -0,0 +1,150 @@
+<?php
+
+//include_once('files/sha256.inc');
+
+class HashShell extends AppShell {
+	
+	const DEFAULT_HASH_ALG = 4; # sha1
+	
+	public $active = array('md5', 'sha1', 'sha256', 'sha512');
+	
+	public $tasks = array();
+
+
+	/**
+	 * Override main() for help message hook
+	 *
+	 * @return void
+	 */
+	public function main() {
+		$this->out($this->OptionParser->help());
+	}
+
+	public function string() {
+		$this->out('Hash Strings...');
+		$hashAlgos = hash_algos();
+		
+		$types = am(array_keys($hashAlgos), array('q'));
+		foreach ($hashAlgos as $key => $t) {
+			$this->out(($key+1).': '.$t.(in_array($t, $this->active)?' (!)':''));
+		}
+		while (!isset($type) || !in_array($type-1, $types)) {
+			$type = $this->in(__('Select hashType - or [q] to quit'), null, self::DEFAULT_HASH_ALG);
+			if ($type == 'q') {
+				die('Aborted!');
+			}
+		}
+		$type--;
+		
+		while (empty($pwToHash) || mb_strlen($pwToHash) < 2) {
+			$pwToHash = $this->in(__('String to Hash (2 characters at least)'));
+		}
+		
+		$pw = $this->_hash($hashAlgos[$type], $pwToHash);
+		$this->out('type: '.strtoupper($hashAlgos[$type]).' (length: '.mb_strlen($pw).')');
+		$this->hr();
+		echo $pw;	
+	}
+	
+	/**
+	 * list all available
+	 */
+	public function available() {
+		$hashAlgos = hash_algos();
+		foreach ($hashAlgos as $hashAlgo) {
+			$this->out('- '.$hashAlgo);
+		}
+	}
+
+	public function compare() {
+		$algos = hash_algos();
+		$data = "hello";
+		foreach ($algos as $v) {
+			$res = hash($v, $data, false);
+			$r = str_split($res, 50);
+			printf("%-12s %3d  %s\n", $v, strlen($res), array_shift($r));
+			while (!empty($r)) {
+				printf("                  %s\n", array_shift($r));
+			}
+		}
+	}
+
+	public function time() {
+		$data = '';
+		for ($i = 0; $i < 64000; $i++) {
+			$data .= hash('md5', rand(), true);
+		}
+		echo strlen($data) . ' bytes of random data built !' . PHP_EOL . PHP_EOL . 'Testing hash algorithms ...' . PHP_EOL;
+
+		$results = array();
+		foreach (hash_algos() as $v) {
+			echo $v . PHP_EOL;
+
+			$time = microtime(true);
+			hash($v, $data, false);
+			$time = microtime(true) - $time;
+			$results[$time * 1000000000][] = "$v (hex)";
+			$time = microtime(true);
+			hash($v, $data, true);
+			$time = microtime(true) - $time;
+			$results[$time * 1000000000][] = "$v (raw)";
+		}
+
+		ksort($results);
+
+		echo PHP_EOL . PHP_EOL . 'Results: ' . PHP_EOL;
+
+		$i = 1;
+		foreach ($results as $k => $v) {
+		foreach ($v as $k1 => $v1) {
+		echo ' ' . str_pad($i++ . '.', 4, ' ', STR_PAD_LEFT) . '  ' . str_pad($v1, 30, ' ') . ($k / 1000000) . ' ms' . PHP_EOL;
+			}
+		}
+	}
+
+
+	public function help() {
+		$this->out('-- Hash Strings --');
+		$this->out('-- cake Tools.Hash [method]');
+		$this->out('---- for custom hashing of pwd strings (method name optional)');
+		$this->out('-- cake Tools.Hash compare');
+		$this->out('---- to list all available methods and their lenghts');
+	}
+	
+	public function getOptionParser() {
+		$parser = parent::getOptionParser();
+
+		$parser->description(__('A console tool for hashing strings'))
+			->addSubcommand('string', array(
+				'help' => __('Hash a string'),
+				'parser' => array(
+					'description' => __('hash'),
+				)
+			))->addSubcommand('compare', array(
+				'help' => __('Compare algs'),
+				'parser' => array(
+					'description' => __('Compare algs'),
+				)
+			))->addSubcommand('time', array(
+				'help' => __('Measure alg times'),
+				'parser' => array(
+					'description' => __('Measure alg times'),
+				)
+			))->epilog(
+				array(
+					__('sha1 is the default algorithm')
+				)
+			);
+		return $parser;
+	}
+	
+	
+	protected function _hash($type, $string) {
+		if (in_array(strtolower($type), hash_algos())) {
+			return hash($type, $string);
+		}
+		return $string;
+	}
+	
+}
+

+ 316 - 0
Console/Command/IndentShell.php

@@ -0,0 +1,316 @@
+<?php
+//Configure::write('debug', 1);
+
+if (!defined('TB')) {
+	define('TB', "\t");
+}
+if (!defined('NL')) {
+	define('NL', "\n");
+}
+if (!defined('CR')) {
+	define('CR', "\r");
+}
+App::uses('Folder', 'Utility');
+
+/**
+ * Indend Shell
+ * 
+ * @cakephp 2.0
+ * @author Mark Scherer
+ * @license MIT
+ * 2011-11-04 ms
+ */
+class IndentShell extends AppShell {
+
+	protected $changes = null;
+
+	public $settings = array(
+		'files' => array('php', 'ctp', 'inc', 'tpl'),
+		'spacesPerTab' => 4,
+		'againWithHalf' => true, # if 4, go again with 2 afterwards
+		'test' => false, # just count - without doing anything
+		'outputToTmp' => false, # write to filename_.ext
+		'debug' => false # add debug info after each line
+	);
+
+	protected $_paths = array();
+	protected $_files = array();
+
+	/**
+	 * Main execution function to indend a folder recursivly
+	 *
+	 * @return void
+	 */
+	public function folder() {
+		if (!empty($this->args)) {
+			if (in_array('test', $this->args)) {
+				$this->settings['test'] = true;
+			}
+
+			if (!empty($this->args[0]) && $this->args[0] != 'app') {
+				$folder = realpath($this->args[0]);
+				if (!file_exists($folder)) {
+					die('folder not exists: ' . $folder . '');
+				}
+				$this->_paths[] = $folder;
+			} elseif ($this->args[0] == 'app') {
+				$this->_paths[] = APP;
+			}
+
+			if (!empty($this->params['files'])) {
+				$this->settings['files'] = explode(',', $this->params['files']);
+			}
+
+			$this->_searchFiles();
+			$this->out('found: ' . count($this->_files));
+			if ($this->settings['test']) {
+				$this->out('TEST DONE');
+			} else {
+				$this->_correctFiles3();
+				$this->out('DONE');
+			}
+
+		} else {
+			$this->out('Usage: cake intend folder');
+			$this->out('"folder" is then intended recursivly');
+			$this->out('default file types are');
+			$this->out('['.implode(', ', $this->settings['files']).']');
+
+			$this->out('');
+			$this->out('Specify file types manually:');
+			$this->out('-files php,js,css');
+		}
+	}
+	
+	
+	
+	public function getOptionParser() {
+		$subcommandParser = array(
+			'options' => array(
+				'dry-run'=> array(
+					'short' => 'd',
+					'help' => __d('cake_console', 'Dry run the update, no files will actually be modified.'),
+					'boolean' => true
+				),
+				'log'=> array(
+					'short' => 'l',
+					'help' => __d('cake_console', 'Log all ouput to file log.txt in TMP dir'),
+					'boolean' => true
+				),
+				'interactive'=> array(
+					'short' => 'i',
+					'help' => __d('cake_console', 'Interactive'),
+					'boolean' => true
+				),
+			)
+		);
+		
+		return parent::getOptionParser()
+			->description(__d('cake_console', "Correct indentation of files"))
+			->addSubcommand('folder', array(
+				'help' => __d('cake_console', 'Indent all files in a folder'),
+				'parser' => $subcommandParser
+			));
+	}	
+
+
+	public function _write($file, $text) {
+		$text = implode(PHP_EOL, $text);
+		if ($this->settings['outputToTmp']) {
+			$filename = extractPathInfo('file', $file);
+			if (mb_substr($filename, -1, 1) == '_') {
+				return;
+			}
+			$file = extractPathInfo('dir', $file).DS.$filename.'_.'.extractPathInfo('ext', $file);
+		}
+		return file_put_contents($file, $text);
+	}
+
+	public function _read($file) {
+		$text = file_get_contents($file);
+		if (empty($text)) {
+			return array();
+		}
+		$pieces = explode(NL, $text);
+		return $pieces;
+	}
+
+
+	/**
+	 * NEW TRY!
+	 * idea: just count spaces and replace those
+	 *
+	 * 2010-09-12 ms
+	 */
+	public function _correctFiles3() {
+		foreach ($this->_files as $file) {
+			$this->changes = false;
+			$textCorrect = array();
+
+			$pieces = $this->_read($file);
+			foreach ($pieces as $piece) {
+				$tmp = $this->_process($piece, $this->settings['spacesPerTab']);
+				if ($this->settings['againWithHalf'] && ($spacesPerTab = $this->settings['spacesPerTab']) % 2 === 0 && $spacesPerTab > 3) {
+					$tmp = $this->_process($tmp, $spacesPerTab/2);
+				}
+
+				$textCorrect[] = $tmp;
+			}
+
+			if ($this->changes) {
+				$this->_write($file, $textCorrect);
+			}
+		}
+	}
+
+	public function _process($piece, $spacesPerTab) {
+		$pos = -1;
+		$spaces = $mod = $tabs = 0;
+		$debug = '';
+
+		$newPiece = $piece;
+		//TODO
+		while (mb_substr($piece, $pos+1, 1) === ' ' || mb_substr($piece, $pos+1, 1) === TB) {
+			$pos++;
+		}
+		$piece1 = mb_substr($piece, 0, $pos+1);
+		$piece1 = str_replace(str_repeat(' ', $spacesPerTab), TB, $piece1, $count);
+		if ($count > 0) {
+			$this->changes = true;
+		}
+
+		$piece2 = mb_substr($piece, $pos+1);
+
+		$newPiece = $piece1 . $piece2;
+		return rtrim($newPiece) . $debug;
+	}
+
+
+
+	/**
+	 * NEW TRY!
+	 * idea: hardcore replaceing
+	 *
+	 * 2010-09-12 ms
+	 */
+	public function _correctFiles2() {
+		foreach ($this->_files as $file) {
+			$changes = false;
+			$textCorrect = array();
+
+			$pieces = $this->_read($file);
+			foreach ($pieces as $piece) {
+				$spaces = $mod = $tabs = 0;
+				$debug = '';
+
+				$newPiece = $piece;
+				//TODO: make sure no other text is in front of it!!!
+				$newPiece = str_replace(str_repeat(' ', $this->settings['spacesPerTab']), TB, $newPiece, $count);
+				if ($count > 0) {
+					$changes = true;
+				}
+
+				$textCorrect[] = rtrim($newPiece) . $debug;
+			}
+
+			if ($changes) {
+				$this->_write($file, $textCorrect);
+			}
+		}
+	}
+
+	/**
+	 * Old try - sometimes TABS at the beginning are not recogized...
+	 * idea: strip tabs and spaces, remember their amount and add tabs again!
+	 * 2010-09-12 ms
+	 */
+	public function _correctFiles() {
+		foreach ($this->_files as $file) {
+			$changes = false;
+			$textCorrect = array();
+
+			$pieces = $this->_read($file);
+			foreach ($pieces as $piece) {
+				$pos = -1;
+				$spaces = $mod = $tabs = 0;
+				$debug = '';
+
+				$newPiece = trim($piece, CR);
+				$newPiece = trim($newPiece, NL);
+				//$debug .= ''.stripos($newPiece, TB);
+
+				# detect tabs and whitespaces at the beginning
+				//while (($pieceOfString = mb_substr($newPiece, 0, 1)) == ' ' || ($pieceOfString = mb_substr($newPiece, 0, 1)) == TB) {
+				while ((stripos($newPiece, ' ')) === 0 || (stripos($newPiece, TB)) === 0) {
+					$pieceOfString = mb_substr($newPiece, 0, 1);
+					if ($pieceOfString === ' ') {
+						$pos++;
+						$spaces++;
+					} elseif ($pieceOfString === TB) {
+						$pos++;
+						$spaces += $this->settings['spacesPerTab'];
+					} else {
+						die('???');
+					}
+
+					$newPiece = mb_substr($newPiece, 1);
+				}
+
+				if ($pos >= 1) {
+					$changes = true;
+
+					# if only spaces and tabs, we might as well trim the line
+					//should be done
+
+					# now correct
+					//$newPiece = mb_substr($piece, $pos + 1);
+
+					# clear single spaces
+					/*
+					if (mb_substr($newPiece, 0, 1) === ' ' && mb_substr($newPiece, 1, 1) !== '*') {
+						$newPiece = mb_substr($newPiece, 1);
+					}
+					*/
+
+
+					$mod = $spaces % $this->settings['spacesPerTab'];
+					$tabs = ($spaces - $mod) / $this->settings['spacesPerTab'];
+
+
+
+					//$beginning = str_replace('  ', TB, $piece);
+					$beginning = str_repeat(TB, $tabs);
+					$beginning .= str_repeat(' ', $mod);
+					$newPiece = $beginning . trim($newPiece);
+				} else {
+					$newPiece = rtrim($newPiece);
+				}
+
+				if ($this->settings['debug']) {
+					$debug .= ' '. ($changes ? '[MOD]': '[]') .' (SPACES '.$tabs.', POS '.$pos.', TABS '.$tabs.', MOD '.$mod.')';
+				}
+				$textCorrect[] = $newPiece . $debug;
+			}
+			if ($changes) {
+				$this->_write($file, $textCorrect);
+			}
+			//die();
+		}
+	}
+
+	/**
+	 * Search files that may contain translateable strings
+	 *
+	 * @return void
+	 * @access private
+	 */
+	public function _searchFiles() {
+		foreach ($this->_paths as $path) {
+			$Folder = new Folder($path);
+			$files = $Folder->findRecursive('.*\.('.implode('|', $this->settings['files']).')', true);
+			$this->_files += $files;
+		}
+	}
+}
+
+

+ 304 - 0
Console/Command/IndexShell.php

@@ -0,0 +1,304 @@
+<?php
+
+if (!defined('CONFIGS')) {
+	define('CONFIGS', APP.'Config'.DS);
+}
+App::uses('ConnectionManager', 'Model');
+App::uses('AppShell', 'Console/Command');
+
+/**
+ * Add missing indexes to your schema in a snip
+ * based on ad7six' UuidifyShell
+ * 
+ * Currently supports automatically:
+ * - BTREE Indexes for UUIDs
+ * 
+ * TODO:
+ * - BTREE Indexes for AIIDs if desired
+ * - PRIMARY_KEY Indexes for primary keys ("id")
+ * 
+ * @author     Mark Scherer
+ * @link
+ * @license    http://www.opensource.org/licenses/mit-license.php The MIT License
+ * @cakephp 2.0
+ * 2011-12-14 ms
+ */
+class IndexShell extends AppShell {
+
+	/**
+	 * Runtime settings
+	 *
+	 * @var array
+	 * @access public
+	 */
+	public $settings = array(
+		'ds' => 'default',
+	);
+
+
+	/**
+	 * The Stack of sql queries to run as an array
+	 *
+	 * @var array
+	 * @access protected
+	 */
+	protected $_script = array();
+
+
+	/**
+	 * startup method
+	 *
+	 * @return void
+	 * @access public
+	 */
+	function startup() {
+		parent::startup();
+		
+		$this->_welcome();
+	}
+
+	/**
+	 * initialize method
+	 *
+	 * If the flags -h, -help or --help are present bail here and show help
+	 *
+	 * @return void
+	 * @access public
+	 */
+	function initialize() {
+		parent::initialize();
+		/*
+		if (file_exists(APP . 'Config' . DS . 'index.php')) {
+			include(APP . 'Config' . DS . 'index.php');
+			if (!empty($config)) {
+				$this->settings = am($this->settings, $config);
+			}
+		}
+		*/
+		$this->_loadModels();
+	}
+
+	/**
+	 * main method
+	 *
+	 * Generate the required sql, and then run it
+	 * To run for more than one datasource - comma seperate them:
+	 * 	cake Tools.Index default,permissions,other
+	 *
+	 * @return void
+	 * @access public
+	 */
+	public function run() {
+		$this->_buildScript(explode(',', $this->settings['ds']));
+		$this->_runScript();
+	}
+
+	/**
+	 * Process each named datasource in turn
+	 *
+	 * E.g. ->_buildScript(array('default', 'users'));
+	 *
+	 * @param array $sources array()
+	 * @return void
+	 * @access protected
+	 */
+	protected function _buildScript($sources = array()) {
+		foreach ($sources as $ds) {
+			$this->_buildScriptForDataSource($ds);
+		}
+	}
+
+	/**
+	 * Generate the conversion sql for the requested datasource.
+	 *
+	 * For each table in the db - find all primary or foreign keys (that follow conventions)
+	 * currently skips primary keys (should already be PRIMARY)
+	 *
+	 * @param mixed $ds
+	 * @return void
+	 * @access protected
+	 */
+	protected function _buildScriptForDataSource($ds = 'default') {
+		$tables = $this->_tables($ds);
+		$db = ConnectionManager::getDataSource($ds);
+		$usePrefix = empty($db->config['prefix']) ? '': $db->config['prefix'];
+		
+		$doneSomething = false;
+		foreach ($tables as $table) {
+			if (in_array($table, array('i18n'))) {
+				continue;
+			}
+			
+			$model = Inflector::classify($table);
+			$Inst = ClassRegistry::init(array(
+				'class' => $model,
+				'table' => $table,
+				'ds' => $ds
+			));
+			if (!is_callable(array($Inst, 'schema'))) {
+				continue;
+			}
+			$fields = $Inst->schema();
+			
+			$indexInfo = $Inst->query('SHOW INDEX FROM `'.$usePrefix . $table.'`');
+			
+			foreach ($fields as $field => $details) {
+				if (!preg_match('@(^|_)(id|key)$@', $field)) {
+					continue;
+				}
+				if ($details['type'] !== 'integer' && ($details['type'] !== 'string' || $details['length'] !== 36)) {
+					continue;
+				}
+				# right now ONLY for uuids
+				if ($details['type'] !== 'string') {
+					continue;
+				}
+				
+				foreach ($indexInfo as $info) {
+					$column = $info['STATISTICS']['Column_name'];
+					$key = $info['STATISTICS']['Key_name'];
+					# dont override primary keys
+					if ($column == $field && $key == 'PRIMARY') {
+						continue 2;
+					}
+					# already exists
+					if ($column == $field && $key == $field) {
+						continue 2;
+					}
+				}
+
+				$this->out('Create index for '.$table.'.'.$field);
+				$this->_script[$ds]['index'][] = "ALTER TABLE `$usePrefix$table` ADD INDEX (`$field`)";
+			}
+		}	
+	}
+
+	protected function _tables($useDbConfig = 'default') {
+		if (!$useDbConfig) {
+			return array();
+		}
+		require_once(CONFIGS. 'database.php');
+		$connections = get_class_vars('DATABASE_CONFIG');
+		if (!isset($connections[$useDbConfig])) {
+			return array();
+		}
+		$db = ConnectionManager::getDataSource($useDbConfig);
+		if (!$db) {
+			return array();
+		}
+		$usePrefix = empty($db->config['prefix']) ? '': $db->config['prefix'];
+		$tables = array();
+		if ($usePrefix) {
+			foreach ($db->listSources() as $table) {
+				if (!strncmp($table, $usePrefix, strlen($usePrefix))) {
+					$tables[$useDbConfig . '::' . $table] = substr($table, strlen($usePrefix));
+				}
+			}
+		} else {
+			$_tables = $db->listSources();
+			foreach ($_tables as $table) {
+				$tables[$useDbConfig . '::' . $table] = $table;
+			}
+		}
+		return $tables;
+	}
+
+	/**
+	 * query method
+	 *
+	 * If the force (or the shortcut f) parameter is set, don't ask for confirmation
+	 * If the user chooses to quit - stop processing at that moment
+	 * If the parameter `dry-run` is specified - don't do anything except dump the script to stdout
+	 *
+	 * @param mixed $statement
+	 * @return void
+	 * @access public
+	 */
+	protected function _query($statement) {
+		if (!$statement) {
+			$this->out();
+			return;
+		}
+		$statement .= ';';
+
+		$this->out($statement);
+		if (!empty($this->params['dry-run'])) {
+			return;
+		}
+		if (empty($this->params['interactive'])) {
+			$continue = 'Y';
+		} else {
+			$continue = strtoupper($this->in(__('Run this statement?'), array('Y', 'N', 'A', 'Q')));
+			switch ($continue) {
+				case 'Q':
+					$this->_stop();
+					return;
+				case 'N':
+					return;
+				case 'A':
+					$continue = 'Y';
+					$this->params['interactive'] = false;
+				case 'Y':
+					break;
+			}
+		}
+		if ($continue === 'Y') {
+			$this->Db->query($statement);
+		}
+	}
+
+	/**
+	 * Loop over the script running each statement in turn
+	 *
+	 * @return void
+	 * @access protected
+	 */
+	protected function _runScript() {
+		foreach ($this->_script as $ds => $steps) {
+			ksort($steps);
+			$this->Db = ConnectionManager::getDataSource($ds);
+			foreach ($steps as $step => $statements) {
+				foreach ($statements as $statement) {
+					$this->_query($statement);
+				}
+			}
+		}
+	}
+
+
+
+	public function getOptionParser() {
+		$subcommandParser = array(
+			'options' => array(
+				'dry-run'=> array(
+					'short' => 'd',
+					'help' => __d('cake_console', 'Dry run the update, no files will actually be modified.'),
+					'boolean' => true
+				),
+				'log'=> array(
+					'short' => 'l',
+					'help' => __d('cake_console', 'Log all ouput to file log.txt in TMP dir'),
+					'boolean' => true
+				),
+				'interactive'=> array(
+					'short' => 'i',
+					'help' => __d('cake_console', 'Interactive'),
+					'boolean' => true
+				),
+				'ds'=> array(
+					'short' => 'c',
+					'help' => __d('cake_console', 'custom ds'),
+					'boolean' => true
+				)
+			)
+		);
+		
+		return parent::getOptionParser()
+			->description(__d('cake_console', "..."))
+			->addSubcommand('run', array(
+				'help' => __d('cake_console', 'Run'),
+				'parser' => $subcommandParser
+			));
+	}
+
+}

+ 112 - 0
Console/Command/PhpTagShell.php

@@ -0,0 +1,112 @@
+<?php
+App::uses('Folder', 'Utility');
+
+/**
+ * removes closing php tag (?>) from php files
+ * it also makes sure there is no whitespace at the beginning of the file
+ * 
+ * @author Mark Scherer, Maximilian Ruta 
+ * @cakephp 2.0
+ * @license MIT
+ * 2011-02-21 de
+ */
+class PhpTagShell extends AppShell {
+
+	public $autoCorrectAll = false;
+	# each report: [0] => found, [1] => corrected
+	public $report = array(
+		'leading'=>array(0, 0), 
+		'trailing'=>array(0, 0)
+	);
+
+	/**
+	 * note: uses provided folder (first param)
+	 * otherwise complete APP
+	 * 2011-08-01 ms
+	 */
+	public function main() {
+		if (isset($this->args[0]) && !empty($this->args[0])) {
+			$folder = realpath($this->args[0]);
+		} else {
+			$folder = APP;
+		}
+		if (is_file($folder)) {
+			$r = array($folder);
+		} else {
+			$App = new Folder($folder);
+			$this->out("Find recursive *.php in [".$folder."] ....");
+			$r = $App->findRecursive('.*\.php');
+		}
+
+		$folders = array();
+
+		foreach ($r as $file) {
+			$error = array();
+			$action = '';
+
+			$c = file_get_contents($file);
+			if (preg_match('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', $c)) {
+				$error[] = 'leading';
+			}
+			if (preg_match('/\?\>[\n\r|\n\r|\n|\r|\s]*$/', $c)) {
+				$error[] = 'trailing';
+			}
+			if (!empty($error)) {
+				foreach ($error as $e) {
+					$this->report[$e][0]++;
+				}
+				$this->out('');
+				$this->out('contains '.rtrim(implode($error, ', '), ', ').' whitespaces / php tags: '.$this->shortPath($file));
+
+				if (!$this->autoCorrectAll) {
+					$dirname = dirname($file);
+
+					if (in_array($dirname, $folders)) {
+						$action = 'y';
+					}
+
+					while (empty($action)) {
+						//TODO: [r]!
+						$action = $this->in(__('Remove? [y]/[n], [a] for all in this folder, [r] for all below, [*] for all files(!), [q] to quit'), array('y','n','r','a','q','*'), 'q');
+					}
+				} else {
+					$action = 'y';
+				}
+
+				if ($action == '*') {
+					$action = 'y';
+					$this->autoCorrectAll = true;
+
+				} elseif ($action == 'a') {
+					$action = 'y';
+					$folders[] = $dirname;
+					$this->out('All: '.$dirname);
+				}
+
+				if ($action == 'q') {
+					die('Abort... Done');
+				} elseif ($action == 'y') {
+					$res = $c;
+					if (in_array('leading', $error)) {
+						$res = preg_replace('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', '<?php', $res);
+					}
+					if (in_array('trailing', $error)) {
+						$res = preg_replace('/\?\>[\n\r|\n\r|\n|\r|\s]*$/', "\n", $res);
+					}
+					file_put_contents($file, $res);
+					foreach ($error as $e) {
+						$this->report[$e][1]++;
+						$this->out('fixed '.$e.' php tag: '.$this->shortPath($file));
+					}
+				}
+			}
+		}
+
+		# report
+		$this->out('--------');
+		$this->out('found '.$this->report['leading'][0].' leading, '.$this->report['trailing'][0].' trailing ws / php tag');
+		$this->out('fixed '.$this->report['leading'][1].' leading, '.$this->report['trailing'][1].' trailing ws / php tag');
+	}
+
+}
+

+ 45 - 0
Console/Command/PwdShell.php

@@ -0,0 +1,45 @@
+<?php
+App::uses('ComponentCollection', 'Controller');
+
+class PwdShell extends AppShell {
+	public $tasks = array();
+	//public $uses = array('User');
+
+	public $Auth = null;
+
+	public function hash() {
+		$components = array('Tools.AuthExt', 'Auth');
+
+		$class = null;
+		foreach ($components as $component) {
+			if (App::import('Component', $component)) {
+				$component .='Component';
+				list($plugin, $class) = pluginSplit($component); 
+				break;
+			}
+		}
+		if (!$class || !method_exists($class, 'password')) {
+			$this->out(__('No Auth Component found'));
+			die();
+		}
+
+		$this->out('Using: '.$class);
+
+		while (empty($pwToHash) || mb_strlen($pwToHash) < 2) {
+			$pwToHash = $this->in(__('Password to Hash (2 characters at least)'));
+		}
+
+		$pw = $class::password($pwToHash);
+		$this->hr();
+		echo $pw;
+	}
+
+	
+
+	public function help() {
+		$this->out('-- Hash Passwort with Auth(Ext) Component --');
+		$this->out('-- cake Tools.Pwd hash');
+		$this->out('---- using the salt of the core.php (!)');
+	}
+}
+

+ 108 - 0
Console/Command/ResetShell.php

@@ -0,0 +1,108 @@
+<?php
+
+# enhancement for plugin user model
+if (!defined('CLASS_USER')) {
+	define('CLASS_USER', 'User');
+}
+
+/**
+ * reset user data
+ * 2011-08-01 ms
+ */
+class ResetShell extends AppShell {
+	public $tasks = array();
+	//public $uses = array('User');
+
+	public $Auth = null;
+
+
+	public function main() {
+		$this->help();
+	}
+
+	/**
+	 * reset all emails - e.g. your admin email (for local development)
+	 * 2011-08-16 ms
+	 */
+	public function email() {
+		$this->out('email:');
+		App::uses('Validation', 'Utility');
+		while (empty($email) || !Validation::email($email)) {
+			$email = $this->in(__('New email address (must have a valid form at least)'));
+		}
+		
+		$this->User = ClassRegistry::init(CLASS_USER);
+		if (!$this->User->hasField('email')) {
+			$this->error(CLASS_USER.' model doesnt have an email field!');
+		}
+		
+		$this->hr();
+		$this->out('resetting...');
+		Configure::write('debug', 2);
+		$this->User->recursive = -1;
+		$this->User->updateAll(array('User.email'=>'\''.$email.'\''), array('User.email !='=>$email));
+		$count = $this->User->getAffectedRows();
+		$this->out($count.' emails resetted - DONE');	
+	}	
+
+
+	/**
+	 * reset all pwds to a simply pwd (for local development)
+	 * 2011-08-01 ms
+	 */
+	public function pwd() {
+		$components = array('AuthExt', 'Auth');
+		foreach ($components as $component) {
+			if (App::import('Component', $component)) {
+				$component .='Component';
+				$this->Auth = new $component();
+				break;
+			}
+		}
+		if (!is_object($this->Auth)) {
+			$this->out('No Auth Component found');
+			die();
+		}
+
+		$this->out('Using: '.get_class($this->Auth).' (Abort with STRG+C)');
+
+
+		if (!empty($this->args[0]) && mb_strlen($this->args[0]) >= 2) {
+			$pwToHash = $this->args[0];
+		}
+		while (empty($pwToHash) || mb_strlen($pwToHash) < 2) {
+			$pwToHash = $this->in(__('Password to Hash (2 characters at least)'));
+		}
+		$this->hr();
+		$this->out('pwd:');
+		$this->out($pwToHash);
+		$pw = $this->Auth->password($pwToHash);
+		$this->hr();
+		$this->out('hash:');
+		$this->out($pw);
+
+		$this->hr();
+		$this->out('resetting...');
+
+		$this->User = ClassRegistry::init(CLASS_USER);
+		if (!$this->User->hasField('password')) {
+			$this->error(CLASS_USER.' model doesnt have a password field!');
+		}
+		
+		if (method_exists($this->User, 'escapeValue')) {
+			$newPwd = $this->User->escapeValue($pw);
+		} else {
+			$newPwd = '\''.$pw.'\'';
+		}
+		$this->User->recursive = -1;
+		$this->User->updateAll(array('password'=>$newPwd), array('password !='=>$pw));
+		$count = $this->User->getAffectedRows();
+		$this->out($count.' pwds resetted - DONE');
+	}
+
+
+	public function help() {
+		$this->out('-- pwd: Hash and Reset all user passwords with Auth(Ext) Component --');
+		$this->out('-- email: Reset all user emails --');
+	}
+}

+ 126 - 0
Console/Command/UserShell.php

@@ -0,0 +1,126 @@
+<?php
+
+if (!defined('CLASS_USER')) {
+	define('CLASS_USER', 'User');
+}
+
+App::uses('AppShell', 'Console/Command');
+App::uses('ComponentCollection', 'Controller');
+//App::uses('AuthExtComponent', 'Tools.Controller/Component');
+
+/**
+ * create a new user from CLI
+ * 
+ * @cakephp 2.x
+ * @author Mark Scherer
+ * @license MIT
+ * 2011-11-05 ms
+ */
+class UserShell extends AppShell {
+	
+	public $tasks = array();
+	public $uses = array(CLASS_USER);
+
+
+	//TODO: refactor (smaller sub-parts)
+	public function main() {
+		if (App::import('Component', 'AuthExt') && class_exists('AuthExtComponent')) {
+			$this->Auth = new AuthExtComponent(new ComponentCollection());
+		} else {
+			App::import('Component', 'Auth');
+			$this->Auth = new AuthComponent(new ComponentCollection());
+		}
+
+		while (empty($username)) {
+			$username = $this->in(__('Username (2 characters at least)'));
+		}
+		while (empty($password)) {
+			$password = $this->in(__('Password (2 characters at least)'));
+		}
+
+		$schema = $this->User->schema();
+
+		if (isset($this->User->Role) && is_object($this->User->Role)) {
+			$roles = $this->User->Role->find('list');
+
+			if (!empty($roles)) {
+				$this->out('');
+				pr($roles);
+			}
+
+			$roleIds = array_keys($roles);
+			while (!empty($roles) && empty($role)) {
+				$role = $this->in(__('Role'), $roleIds);
+			}
+		} elseif (method_exists($this->User, 'roles')) {
+			$roles = User::roles();
+
+			if (!empty($roles)) {
+				$this->out('');
+				pr ($roles);
+			}
+
+			$roleIds = array_keys($roles);
+			while (!empty($roles) && empty($role)) {
+				$role = $this->in(__('Role'), $roleIds);
+			}
+		}
+		if (empty($roles)) {
+			$this->out('No Role found (either no table, or no data)');
+			$role = $this->in(__('Please insert a role manually'));
+		}
+
+		$this->out('');
+		$pwd = $this->Auth->password($password);
+
+		$data = array('User'=>array(
+			'password' => $pwd,
+			'active' => 1
+		));
+		if (!empty($username)) {
+			$data['User']['username'] = $username;
+		}
+		if (!empty($email)) {
+			$data['User']['email'] = $email;
+		}
+		if (!empty($role)) {
+			$data['User']['role_id'] = $role;
+		}
+
+		if (!empty($schema['status']) && method_exists('User', 'statuses')) {
+			$statuses = User::statuses();
+			pr($statuses);
+			while (empty($status)) {
+				$status = $this->in(__('Please insert a status'), array_keys($statuses));
+			}
+			$data['User']['status'] = $status;
+		}
+
+		if (!empty($schema['email'])) {
+			$provideEmail = $this->in(__('Provide Email? '),array('y', 'n'), 'n');
+			if ($provideEmail === 'y') {
+				$email = $this->in(__('Please insert an email'));
+				$data['User']['email'] = $email;
+			}
+			if (!empty($schema['email_confirmed'])) {
+				$data['User']['email_confirmed'] = 1;
+			}
+		}
+
+		$this->out('');
+		$continue = $this->in(__('Continue? '), array('y', 'n'), 'n');
+		if ($continue != 'y') {
+			$this->error('Not Executed!');
+		}
+
+		$this->out('');
+		$this->hr();
+		if ($this->User->save($data)) {
+			$this->out('User inserted! ID: '.$this->User->id);
+		} else {
+			$this->error('User could not be inserted ('.print_r($this->User->validationErrors, true).')');
+		}
+	}
+	
+}
+

+ 86 - 0
Console/Command/WhitespaceShell.php

@@ -0,0 +1,86 @@
+<?php
+App::import('Core',array('Folder'));
+
+class WhitespaceShell extends AppShell {
+	public $tasks = array();
+	public $uses = array();
+
+	public $autoCorrectAll = false;
+	# each report: [0] => found, [1] => corrected
+	public $report = array('leading'=>array(0, 0),'trailing'=>array(0, 0));
+
+	public function main() {
+		$App = new Folder(APP);
+
+		$r = $App->findRecursive('.*\.php');
+		$this->out("Checking *.php in ".APP);
+
+		$folders = array();
+
+		foreach ($r as $file) {
+			$error = '';
+			$action = '';
+
+			$c = file_get_contents($file);
+			if (preg_match('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', $c)) {
+				$error = 'leading';
+			}
+			if (preg_match('/\?\>[\n\r|\n\r|\n|\r|\s]+$/', $c)) {
+				$error = 'trailing';
+			}
+
+			if (!empty($error)) {
+				$this->report[$error][0]++;
+				$this->out('');
+				$this->out('contains '.$error.' whitespaces: '.$this->shortPath($file));
+
+				if (!$this->autoCorrectAll) {
+					$dirname = dirname($file);
+
+					if (in_array($dirname, $folders)) {
+						$action = 'y';
+					}
+
+					while (empty($action)) {
+						//TODO: [r]!
+						$action = $this->in(__('Remove? [y]/[n], [a] for all in this folder, [r] for all below, [*] for all files(!), [q] to quit'), array('y','n','r','a','q','*'), 'q');
+					}
+				} else {
+					$action = 'y';
+				}
+
+				if ($action == '*') {
+					$action = 'y';
+					$this->autoCorrectAll = true;
+
+				} elseif ($action == 'a') {
+					$action = 'y';
+					$folders[] = $dirname;
+					$this->out('All: '.$dirname);
+				}
+
+				if ($action == 'q') {
+					die('Abort... Done');
+
+				} elseif ($action == 'y') {
+					if ($error == 'leading') {
+						$res = preg_replace('/^[\n\r|\n\r|\n|\r|\s]+\<\?php/', '<?php', $c);
+					} else { //trailing
+						$res = preg_replace('/\?\>[\n\r|\n\r|\n|\r|\s]+$/', '?>', $c);
+					}
+
+					file_put_contents($file, $res);
+					$this->report[$error][1]++;
+					$this->out('fixed '.$error.' whitespaces: '.$this->shortPath($file));
+				}
+			}
+		}
+
+		# report
+		$this->out('--------');
+		$this->out('found '.$this->report['leading'][0].' leading, '.$this->report['trailing'][0].' trailing ws');
+		$this->out('fixed '.$this->report['leading'][1].' leading, '.$this->report['trailing'][1].' trailing ws');
+	}
+
+}
+