PageRenderTime 10ms CodeModel.GetById 81ms app.highlight 98ms RepoModel.GetById 22ms app.codeStats 1ms

/QuickApps/Controller/Component/InstallerComponent.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 1972 lines | 1239 code | 270 blank | 463 comment | 224 complexity | f50cd260aad35d8d2c652911f6d20a05 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/**
   3 * Installer Component
   4 *
   5 * PHP version 5
   6 *
   7 * @package	 QuickApps.Controller.Component
   8 * @version	 1.0
   9 * @author	 Christopher Castro <chris@quickapps.es>
  10 * @link	 http://www.quickappscms.org
  11 */
  12class InstallerComponent extends Component {
  13/**
  14 * Holds a list of all errors.
  15 *
  16 * @var array
  17 */
  18	public $errors = array();
  19
  20/**
  21 * Controller reference.
  22 *
  23 * @var Controller
  24 */
  25	public $Controller;
  26
  27/**
  28 * Array of defaults options used by install process.
  29 *
  30 * `type`: type of package to install, 'module' or 'theme'. (default 'module')
  31 * `status`: set to 1 for "install and activate", set to zero (0) otherwise. (default 1)
  32 *
  33 * @var array
  34 */
  35	public $options = array(
  36		'name' => null,
  37		'type' => 'module',
  38		'status' => 1
  39	);
  40
  41/**
  42 * Initializes InstallerComponent for use in the controller.
  43 *
  44 * @param Controller $controller A reference to the instantiating controller object
  45 * @return void
  46 */
  47	public function initialize(Controller $Controller) {
  48		$this->Controller = $Controller;
  49
  50		return true;
  51	}
  52
  53/**
  54 * Begins install process for the specified package.
  55 * An update will be performed if the three conditions below are met:
  56 *	- Module/theme is already installed.
  57 *	- The module/theme being installed is newer than the installed (higher version number).
  58 *	- The module/theme has the `beforeUpdate` callback on its `InstallComponent` class.
  59 *
  60 * #### Expected module package estructure
  61 *
  62 * - ZIP/
  63 *	  - ModuleFolderName/
  64 *		  - Config/
  65 *			  - bootstrap.php
  66 *			  - routes.php
  67 *		  - Controller/
  68 *			  - Component/
  69 *				  - InstallComponent.php
  70 *		  - ModuleFolderName.yaml
  71 *
  72 * #### Expected theme package estructure
  73 *
  74 * - ZIP/
  75 *    - CamelCaseThemeName/
  76 *        - Layouts/
  77 *        - app/
  78 *            - ThemeCamelCaseThemeName/  (Note `Theme` prefix)
  79 *                ... (same as modules)
  80 *        - webroot/
  81 *        - CamelCaseThemeName.yaml
  82 *        - thumbnail.png  (206x150px recommended)
  83 *
  84 * @param array $data Data form POST submit of the .app package ($this->data)
  85 * @param array $options Optional settings, see InstallerComponent::$options
  86 * @return bool TRUE on success or FALSE otherwise
  87 */
  88	public function install($data = false, $options = array()) {
  89		if (!$data) {
  90			return false;
  91		}
  92
  93		$oldMask = umask(0);
  94		$this->options = array_merge($this->options, $options);
  95		$Folder = new Folder;
  96
  97		if (is_string($data['Package']['data'])) {
  98			$workingDir = CACHE . 'installer' . DS . md5($data['Package']['data']) . DS;
  99
 100			$Folder->delete($workingDir);
 101
 102			// download from remote url
 103			if (!$this->__downloadPackage($data['Package']['data'], $workingDir . 'package' . DS)) {
 104				$this->errors[] = __t('Could not download package file.');
 105
 106				return false;
 107			} else {
 108				$file_dst_pathname = $workingDir . 'package' . DS . md5($data['Package']['data']) . '.zip';
 109			}
 110		} else {
 111			// Upload
 112			App::import('Vendor', 'Upload');
 113
 114			$workingDir = CACHE . 'installer' . DS . $data['Package']['data']['name'] . DS;
 115			$Upload = new Upload($data['Package']['data']);
 116			$Upload->allowed = array('application/*');
 117			$Upload->file_overwrite = true;
 118			$Upload->file_src_name_ext = 'zip';
 119
 120			$Folder->delete($workingDir);
 121			$Upload->Process($workingDir . 'package' . DS);
 122
 123			if (!$Upload->processed) {
 124				$this->errors[] = __t('Package upload error.') . ": {$Upload->error}";
 125				return false;
 126			}
 127
 128			$file_dst_pathname = $Upload->file_dst_pathname;
 129		}
 130
 131		// Unzip & Install
 132		App::import('Vendor', 'PclZip');
 133
 134		$PclZip = new PclZip($file_dst_pathname);
 135
 136		if (!$v_result_list = $PclZip->extract(PCLZIP_OPT_PATH, $workingDir . 'unzip')) {
 137			$this->errors[] = __t('Unzip error.') . ": " . $PclZip->errorInfo(true);
 138
 139			return false;
 140		} else {
 141			// Package Validation
 142			$Folder->path = $workingDir . 'unzip' . DS;
 143			$folders = $Folder->read();$folders = $folders[0];
 144			$packagePath = isset($folders[0]) && count($folders) === 1 ? $workingDir . 'unzip' . DS . str_replace(DS, '', $folders[0]) . DS : false;
 145			$appName = (string)basename($packagePath);
 146
 147			// Look for GitHub Package:
 148			// username-QACMS-ModuleNameInCamelCase-last_commit_id
 149			if (preg_match('/(.*)\-QACMS\-(.*)\-([a-z0-9]*)/', $appName, $matches)) {
 150				$appName = $matches[2];
 151			}
 152
 153			$this->options['__packagePath'] = $packagePath;
 154			$this->options['__appName'] = $appName;
 155
 156			if (!$packagePath) {
 157				$this->errors[] = __t('Invalid package structure after unzip.');
 158
 159				return false;
 160			}
 161
 162			switch ($this->options['type']) {
 163				case 'module':
 164					default:
 165						$tests = array(
 166							'ForbiddenName' => array(
 167								'test' => (
 168									strpos('Theme', Inflector::camelize($appName)) !== 0 &&
 169									!in_array(Inflector::camelize($appName), array('Default')) &&
 170									strlen(Inflector::camelize($appName)) != 3 &&
 171									preg_match('/^[a-zA-Z0-9]+$/', Inflector::camelize($appName))
 172								),
 173								'header' => __t('Forbidden Names'),
 174								'msg' => __t('Forbidden module name "%s"', $appName, Inflector::camelize($appName))
 175							),
 176							'CamelCaseName' => array(
 177								'test' => (Inflector::camelize($appName) == $appName),
 178								'header' => __t('Theme name'),
 179								'msg' => __t('Invalid module name (got "%s", expected: "%s")', $appName, Inflector::camelize($appName))
 180							),
 181							'notAlreadyInstalled' => array(
 182								'test' => (
 183									$this->Controller->Module->find('count', array('conditions' => array('Module.name' => $appName, 'Module.type' => 'module'))) === 0 &&
 184									!file_exists(ROOT . DS . 'Modules' . DS . $appName)
 185								),
 186								'header' => __t('Already installed'),
 187								'msg' => __t('This module is already installed')
 188							),
 189							'Config' => array(
 190								'test' => file_exists($packagePath . 'Config'),
 191								'header' => __t('Config Folder'),
 192								'msg' => __t('"Config" folder not found')
 193							),
 194							'bootstrap' => array(
 195								'test' => file_exists($packagePath . 'Config' . DS . 'bootstrap.php'),
 196								'header' => __t('Bootstrap File'),
 197								'msg' => __t('"Config/bootstrap.php" file not found')
 198							),
 199							'routes' => array(
 200								'test' => file_exists($packagePath . 'Config' . DS . 'routes.php'),
 201								'header' => __t('Routes File'),
 202								'msg' => __t('"Config/routes.php" file not found')
 203							),
 204							'Controller' => array(
 205								'test' => file_exists($packagePath . 'Controller'),
 206								'header' => __t('Controller Folder'),
 207								'msg' => __t('"Controller" folder not found')
 208							),
 209							'Component' => array(
 210								'test' => file_exists($packagePath . 'Controller' . DS . 'Component'),
 211								'header' => __t('Component Folder'),
 212								'msg' => __t('"Component" folder not found')
 213							),
 214							'InstallComponent.php' => array(
 215								'test' => file_exists($packagePath . 'Controller' . DS . 'Component' . DS . 'InstallComponent.php'),
 216								'header' => __t('Installer File'),
 217								'msg' => __t('Installer file (InstallComponent.php) not found')
 218							),
 219							'yaml' => array(
 220								'test' => file_exists($packagePath . "{$appName}.yaml"),
 221								'header' => __t('YAML File'),
 222								'msg' => __t('YAML File (%s) not found', "{$appName}.yaml")
 223							)
 224						);
 225				break;
 226
 227				case 'theme':
 228					$tests = array(
 229						'CamelCaseName' => array(
 230							'test' => (Inflector::camelize($appName) == $appName),
 231							'header' => __t('Theme name'),
 232							'msg' => __t('Invalid theme name (got "%s", expected: "%s")', $appName, Inflector::camelize($appName))
 233						),
 234						'notAlreadyInstalled' => array(
 235							'test' => (
 236								$this->Controller->Module->find('count', array('conditions' => array('Module.name' => 'Theme' . $appName, 'Module.type' => 'theme'))) === 0 &&
 237								!file_exists(THEMES . $appName)
 238							),
 239							'header' => __t('Already Installed'),
 240							'msg' => __t('This theme is already installed')
 241						),
 242						'Layouts' => array(
 243							'test' => file_exists($packagePath . 'Layouts'),
 244							'header' => __t('Layouts Folder'),
 245							'msg' => __t('"Layouts" folder not found')
 246						),
 247						'app' => array(
 248							'test' => file_exists($packagePath . 'app'),
 249							'header' => __t('app Folder'),
 250							'msg' => __t('"app" folder not found')
 251						),
 252						'plugin_app' => array(
 253							'test' => file_exists($packagePath . 'app' . DS . 'Theme' . $appName),
 254							'header' => __t('Plugin app'),
 255							'msg' => __t('Module app ("%s") folder not found', 'Theme' . Inflector::camelize($appName))
 256						),
 257						'Config' => array(
 258							'test' => file_exists($packagePath . 'app' . DS . 'Theme' . $appName . DS . 'Config'),
 259							'header' => __t('Config Folder'),
 260							'msg' => __t('"Config" folder not found')
 261						),
 262						'bootstrap' => array(
 263							'test' => file_exists($packagePath . 'app' . DS . 'Theme' . $appName . DS . 'Config' . DS . 'bootstrap.php'),
 264							'header' => __t('Bootstrap File'),
 265							'msg' => __t('"Config/bootstrap.php" file not found')
 266						),
 267						'routes' => array(
 268							'test' => file_exists($packagePath . 'app' . DS . 'Theme' . $appName . DS . 'Config' . DS . 'routes.php'),
 269							'header' => __t('Routes File'),
 270							'msg' => __t('"Config/routes.php" file not found')
 271						),
 272						'InstallComponent.php' => array(
 273							'test' => file_exists($packagePath . 'app' . DS . 'Theme' . $appName .  DS . 'Controller' . DS . 'Component' . DS . 'InstallComponent.php'),
 274							'header' => __t('Installer File'),
 275							'msg' => __t('Installer file (InstallComponent.php) not found')
 276						),
 277						'webroot' => array(
 278							'test' => file_exists($packagePath . 'webroot'),
 279							'header' => __t('webroot Folder'),
 280							'msg' => __t('"webroot" folder not found')
 281						),
 282						'yaml' => array(
 283							'test' => file_exists($packagePath . "{$appName}.yaml"),
 284							'header' => __t('YAML File'),
 285							'msg' => __t('YAML File (%s) not found', "{$appName}.yaml")
 286						),
 287						'thumbnail' => array(
 288							'test' => file_exists($packagePath . 'thumbnail.png'),
 289							'header' => __t('Theme thumbnail'),
 290							'msg' => __t('Thumbnail image ("%s") not found', 'thumbnail.png')
 291						)
 292					);
 293				break;
 294			}
 295
 296			// Load YAML
 297			$yaml = Spyc::YAMLLoad($packagePath . "{$appName}.yaml");
 298
 299			// Install component
 300			$installComponentPath = $this->options['type'] == 'theme' ? $packagePath . 'app' . DS . 'Theme' . $appName . DS . 'Controller' . DS . 'Component' . DS : $packagePath . 'Controller' . DS . 'Component' . DS;
 301			$Install = $this->loadInstallComponent($installComponentPath);
 302			$doUpdate = !$tests['notAlreadyInstalled']['test'] && method_exists($Install, 'beforeUpdate');
 303
 304			if ($doUpdate && $this->options['type'] == 'module') {
 305				$doUpdate = isset($yaml['version']) ? version_compare(Configure::read("Modules.{$appName}.yaml.version"), $yaml['version'], '<') : false;
 306			}
 307
 308			if ($doUpdate) {
 309				unset($tests['notAlreadyInstalled']);
 310			}
 311
 312			if (!$this->checkTests($tests)) {
 313				return false;
 314			}
 315
 316			// YAML validation
 317			switch ($this->options['type']) {
 318				case 'module':
 319					default:
 320						$tests = array(
 321							'yaml' => array(
 322								'test' => (
 323									(isset($yaml['name']) && !empty($yaml['name'])) &&
 324									(isset($yaml['description']) && !empty($yaml['description'])) &&
 325									(isset($yaml['category']) && !empty($yaml['category'])) &&
 326									(isset($yaml['version']) &&  !empty($yaml['version'])) &&
 327									(isset($yaml['core']) && !empty($yaml['core']))
 328								),
 329								'header' => __t('YAML Validation'),
 330								'msg' => __t('Module configuration file (%s) appears to be invalid.', "{$appName}.yaml")
 331							)
 332						);
 333				break;
 334
 335				case 'theme':
 336					$tests = array(
 337						'yaml' => array(
 338							'test' => (
 339									(isset($yaml['info']) && !empty($yaml['info'])) &&
 340									(isset($yaml['info']['name']) && !empty($yaml['info']['name'])) &&
 341									(isset($yaml['info']['core']) && !empty($yaml['info']['core'])) &&
 342									(isset($yaml['regions']) && !empty($yaml['regions'])) &&
 343									(isset($yaml['layout']) && !empty($yaml['layout']))
 344							),
 345							'header' => __t('YAML Validation'),
 346							'msg' => __t('Theme configuration file (%s) appears to be invalid.', "{$appName}.yaml")
 347						),
 348						'requiredRegions' => array(
 349							'test' => (
 350								!isset($yaml['info']['admin']) ||
 351								!$yaml['info']['admin'] ||
 352								(
 353									isset($yaml['info']['admin']) &&
 354									$yaml['info']['admin'] &&
 355									in_array('toolbar', array_keys($yaml['regions'])) &&
 356									in_array('help', array_keys($yaml['regions']))
 357								)
 358							),
 359							'header' => __t('Required regions'),
 360							'msg' => __t('Missing theme region(s) "toolbar" and/or "help". Are required for backend themes.')
 361						)
 362					);
 363				break;
 364			}
 365
 366			if (!$this->checkTests($tests)) {
 367				$this->errors[] = __t('Invalid information file (.yaml)');
 368
 369				return false;
 370			}
 371
 372			// Validate dependencies and required core version
 373			$core = $this->options['type'] == 'module' ? "core ({$yaml['core']})" : "core ({$yaml['info']['core']})";
 374			$r = $this->checkIncompatibility($this->parseDependency($core), Configure::read('Variable.qa_version'));
 375
 376			if ($r !== null) {
 377				if ($this->options['type'] == 'module') {
 378					$this->errors[] = __t('This module is incompatible with your QuickApps version.');
 379				} else {
 380				   $this->errors[] = __t('This theme is incompatible with your QuickApps version.');
 381				}
 382
 383				return false;
 384			}
 385
 386			if (
 387				($this->options['type'] == 'theme' && isset($yaml['info']['dependencies']) && !$this->checkDependency($yaml['info']['dependencies'])) ||
 388				($this->options['type'] == 'module' && isset($yaml['dependencies']) && !$this->checkDependency($yaml['dependencies']))
 389			) {
 390				if ($this->options['type'] == 'module') {
 391					$this->errors[] = __t("This module depends on other modules that you do not have or doesn't meet the version required: %s", implode(', ', $yaml['dependencies']));
 392				} else {
 393					$this->errors[] = __t("This theme depends on other modules that you do not have or doesn't meet the version required: %s", implode(', ', $yaml['info']['dependencies']));
 394				}
 395
 396				return false;
 397			}
 398			// end of dependencies check
 399
 400			// Validate custom fields.
 401			// Only modules are allowed to define fields.
 402			if ($this->options['type'] == 'module' && file_exists($packagePath . 'Fields')) {
 403				$Folder = new Folder($packagePath . 'Fields');
 404				$fields = $Folder->read();
 405				$fieldErrors = false;
 406
 407				if (isset($fields[0])) {
 408					$fields = $fields[0];
 409
 410					foreach ($fields as $field) {
 411						if (strpos($field, $appName) === 0) {
 412							if (file_exists($packagePath . 'Fields' . DS . $field . DS . "{$field}.yaml")) {
 413								$yaml = Spyc::YAMLLoad($packagePath . 'Fields' . DS . $field . DS . "{$field}.yaml");
 414
 415								if (!isset($yaml['name']) || !isset($yaml['description'])) {
 416									$fieldErrors = true;
 417									$this->errors[] = __t('invalid information file (.yaml). Field "%s"', $field);
 418								}
 419							} else {
 420								$fieldErrors = true;
 421								$this->errors[] = __t('Invalid field "%s". Information file (.yaml) not found.', $field);
 422							}
 423						} else {
 424							$fieldErrors = true;
 425							$this->errors[] = __t('Invalid field name "%s".', $field);
 426						}
 427					}
 428				}
 429
 430				if ($fieldErrors) {
 431					return false;
 432				}
 433			}
 434			// End of field validations
 435
 436			$r = true;
 437
 438			if ($doUpdate) {
 439				$r = $Install->beforeUpdate();
 440			} elseif (method_exists($Install, 'beforeInstall')) {
 441				$r = $Install->beforeInstall();
 442			}
 443
 444			if ($r !== true) {
 445				return false;
 446			}
 447
 448			// Copy files
 449			$copyTo = ($this->options['type'] == 'module') ? ROOT . DS . 'Modules' . DS . $appName . DS : THEMES . $appName . DS;
 450
 451			if (!$this->rcopy($packagePath, $copyTo)) {
 452				return false;
 453			}
 454
 455			if (!$doUpdate) {
 456				// DB Logic
 457				$moduleData = array(
 458					'name' => ($this->options['type'] == 'module' ? $appName : 'Theme' . $appName),
 459					'type' => ($this->options['type'] == 'module' ? 'module' : 'theme' ),
 460					'status' => intval($this->options['status']),
 461					'ordering' => 0
 462				);
 463
 464				if ($this->options['type'] == 'module') {
 465					$max_order = $this->Controller->Module->find('first',
 466						array(
 467							'conditions' => array('Module.type' => 'module'),
 468							'order' => array('Module.ordering' => 'DESC'),
 469							'recursive' => -1
 470						)
 471					);
 472					$max_order = $max_order ? $max_order['Module']['ordering'] + 1 : 0;
 473					$moduleData['ordering'] = $max_order;
 474				}
 475
 476				$this->Controller->Module->save($moduleData); // register module
 477
 478				// Build ACOS && Register module in core
 479				switch ($this->options['type']) {
 480					case 'module':
 481						$this->buildAcos($appName);
 482					break;
 483
 484					case 'theme':
 485						$this->buildAcos(
 486							'Theme' . $appName,
 487							THEMES . $appName . DS . 'app' . DS
 488						);
 489
 490						App::build(array('plugins' => array(THEMES . $appName . DS . 'app' . DS)));
 491					break;
 492				}
 493
 494				// copy block positions
 495				if ($this->options['type'] == 'theme') {
 496					$BlockRegion = ClassRegistry::init('Block.BlockRegion');
 497
 498					$BlockRegion->bindModel(
 499						array(
 500							'belongsTo' => array(
 501								'Block' => array(
 502									'className' => 'Block.Block'
 503								)
 504							)
 505						)
 506					);
 507
 508					$regions = $BlockRegion->find('all',
 509						array(
 510							'conditions' => array(
 511								'BlockRegion.theme' => Inflector::camelize(Configure::read('Variable.site_theme')),
 512								'BlockRegion.region' => array_keys($yaml['regions'])
 513							)
 514						)
 515					);
 516
 517					foreach ($regions as $region) {
 518						if (strpos($region['Block']['module'], 'Theme') === 0) {
 519							continue;
 520						}
 521
 522						$region['BlockRegion']['theme'] = $appName;
 523						$region['BlockRegion']['ordering']++;
 524
 525						unset($region['BlockRegion']['id']);
 526						$BlockRegion->create();
 527
 528						if ($BlockRegion->save($region['BlockRegion']) &&
 529							$region['Block']['id'] &&
 530							strpos($region['Block']['themes_cache'], ":{$appName}:") === false
 531						) {
 532							$region['Block']['themes_cache'] .= ":{$appName}:";
 533							$region['Block']['themes_cache'] = str_replace('::', ':', $region['Block']['themes_cache']);
 534
 535							$BlockRegion->Block->save(
 536								array(
 537									'id' => $region['Block']['id'],
 538									'themes_cache' => $region['Block']['themes_cache']
 539								)
 540							);
 541						}
 542					}
 543				}
 544			}
 545
 546			// Delete unziped package
 547			$Folder->delete($workingDir);
 548
 549			// Finish
 550			if ($doUpdate) {
 551				if (method_exists($Install, 'afterUpdate')) {
 552					$Install->afterUpdate();
 553				}
 554			} elseif (!$doUpdate && method_exists($Install, 'afterInstall')) {
 555				$Install->afterInstall();
 556			}
 557
 558			$this->__clearCache();
 559		}
 560
 561		umask($oldMask);
 562
 563		return true;
 564	}
 565
 566/**
 567 * Uninstall module by name.
 568 *
 569 * @param string $pluginName
 570 *	Name of the plugin to uninstall, it may be either a theme-associated-module or module.
 571 *	Both formats are allowed CamelCase and under_scored. e.g.: `ModuleName` or `module_name`
 572 * @return boolean TRUE on success or FALSE otherwise
 573 */
 574	public function uninstall($pluginName = false) {
 575		if (!$pluginName ||
 576			!is_string($pluginName) ||
 577			!in_array($this->options['type'], array('module', 'theme'))
 578		) {
 579			return false;
 580		}
 581
 582		$this->options['name'] = $pluginName;
 583		$Name = Inflector::camelize($this->options['name']);
 584		$pData = $this->Controller->Module->findByName($Name);
 585
 586		if (!$pData) {
 587			$this->errors[] = __t('Module does not exist.');
 588
 589			return false;
 590		} elseif (in_array($Name, Configure::read('coreModules'))) {
 591			$this->errors[] = __t('Core modules can not be uninstalled.');
 592
 593			return false;
 594		}
 595
 596		$dep = $this->checkReverseDependency($Name);
 597
 598		if (count($dep)) {
 599			$this->errors[] = __t('This module can not be uninstalled, because it is required by: %s', implode('<br />', Hash::extract($dep, '{n}.name')));
 600
 601			return false;
 602		}
 603
 604		// useful for before/afterUninstall
 605		$this->options['type'] = $pData['Module']['type'];
 606		$this->options['__data'] = $pData;
 607		$this->options['__path'] = $pData['Module']['type'] == 'theme' ? THEMES . preg_replace('/^Theme/', '', $Name) . DS . 'app' . DS . $Name . DS : CakePlugin::path($Name);
 608		$this->options['__Name'] = $Name;
 609
 610		// check if can be deleted
 611		$folderpath = ($this->options['type'] == 'module') ? $this->options['__path'] : dirname(dirname($this->options['__path']));
 612
 613		if (!$this->isRemoveable($folderpath)) {
 614			$this->errors[] = __t('This module can not be uninstalled because some files/folder can not be deleted, please check the permissions.');
 615
 616			return false;
 617		}
 618
 619		// core plugins can not be deleted
 620		if (in_array($this->options['__Name'], array_merge(array('ThemeDefault', 'ThemeAdmin'), Configure::read('coreModules')))) {
 621			return false;
 622		}
 623
 624		$pluginPath = $this->options['__path'];
 625
 626		if (!file_exists($pluginPath)) {
 627			return false;
 628		}
 629
 630		$Install =& $this->loadInstallComponent($pluginPath . 'Controller' . DS . 'Component' . DS);
 631
 632		if (!is_object($Install)) {
 633			return false;
 634		}
 635
 636		$r = true;
 637
 638		if (method_exists($Install, 'beforeUninstall')) {
 639			$r = $Install->beforeUninstall();
 640		}
 641
 642		if ($r !== true) {
 643			return false;
 644		}
 645
 646		if (!$this->Controller->Module->deleteAll(array('Module.name' => $Name))) {
 647			return false;
 648		}
 649
 650		/**
 651		 * System.Controller/ThemeController does not allow to delete in-use-theme,
 652		 * but for precaution we assign to Core Default ones if for some reason
 653		 * the in-use-theme is being deleted.
 654		 */
 655		if ($this->options['type'] == 'theme') {
 656			if (Configure::read('Variable.site_theme') == preg_replace('/^Theme/', '', $Name)) {
 657				ClassRegistry::init('System.Variable')->save(
 658					array(
 659						'name' => 'site_theme',
 660						'value' => 'Default'
 661					)
 662				);
 663			} elseif (Configure::read('Variable.admin_theme') == preg_replace('/^Theme/', '', $Name)) {
 664				ClassRegistry::init('System.Variable')->save(
 665					array(
 666						'name' => 'admin_theme',
 667						'value' => 'Admin'
 668					)
 669				);
 670			}
 671		}
 672
 673		if (method_exists($Install, 'afterUninstall')) {
 674			$Install->afterUninstall();
 675		}
 676
 677		$this->afterUninstall();
 678
 679		return true;
 680	}
 681
 682	public function enableModule($module) {
 683		return $this->__toggleModule($module, 1);
 684	}
 685
 686	public function disableModule($module) {
 687		return $this->__toggleModule($module, 0);
 688	}
 689
 690	public function afterUninstall() {
 691		$this->__clearCache();
 692
 693		// delete all menus created by module/theme
 694		ClassRegistry::init('Menu.Menu')->deleteAll(
 695			array(
 696				'Menu.module' => $this->options['__Name']
 697			)
 698		);
 699
 700		// delete foreign links
 701		$MenuLink = ClassRegistry::init('Menu.MenuLink');
 702		$links = $MenuLink->find('all',
 703			array(
 704				'conditions' => array(
 705					'MenuLink.module' => $this->options['__Name']
 706				)
 707			)
 708		);
 709
 710		foreach ($links as $link) {
 711			$MenuLink->Behaviors->detach('Tree');
 712			$MenuLink->Behaviors->attach('Tree',
 713				array(
 714					'parent' => 'parent_id',
 715					'left' => 'lft',
 716					'right' => 'rght',
 717					'scope' => "MenuLink.menu_id = '{$link['MenuLink']['menu_id']}'"
 718				)
 719			);
 720			$MenuLink->removeFromTree($link['MenuLink']['id'], true);
 721		}
 722
 723		// delete blocks created by module/theme
 724		ClassRegistry::init('Block.Block')->deleteAll(
 725			array(
 726				'Block.module' => $this->options['__Name']
 727			)
 728		);
 729
 730		// delete module (and related fields) acos
 731		$this->__removeModuleAcos($this->options['__Name']);
 732
 733		// delete node types created by module/theme
 734		ClassRegistry::init('Node.NodeType')->deleteAll(
 735			array(
 736				'NodeType.module' => $this->options['__Name']
 737			)
 738		);
 739
 740		// delete blocks position & cache
 741		if ($this->options['type'] == 'theme') {
 742			$themeName = preg_replace('/^Theme/', '', $this->options['__Name']);
 743			$BlockRegion = ClassRegistry::init('Block.BlockRegion');
 744
 745			$BlockRegion->bindModel(
 746				array(
 747					'belongsTo' => array(
 748						'Block' => array(
 749							'className' => 'Block.Block'
 750						)
 751					)
 752				)
 753			);
 754
 755			$regions = $BlockRegion->find('all',
 756				array(
 757					'conditions' => array(
 758						'BlockRegion.theme' => $themeName
 759					)
 760				)
 761			);
 762
 763			foreach ($regions as $region) {
 764				if ($BlockRegion->delete($region['BlockRegion']['id']) &&
 765					$region['Block']['id']
 766				) {
 767					$region['Block']['themes_cache'] = str_replace(":{$themeName}:", ':', $region['Block']['themes_cache']);
 768					$region['Block']['themes_cache'] = str_replace('::', ':', $region['Block']['themes_cache']);
 769
 770					$BlockRegion->Block->save(
 771						array(
 772							'id' => $region['Block']['id'],
 773							'themes_cache' => $region['Block']['themes_cache']
 774						)
 775					);
 776				}
 777			}
 778		}
 779
 780		// delete app folder
 781		$folderpath = ($this->options['type'] == 'module') ? $this->options['__path'] : dirname(dirname($this->options['__path']));
 782		$Folder = new Folder($folderpath);
 783
 784		$Folder->delete();
 785	}
 786
 787/**
 788 * Parse a dependency for comparison by InstallerComponent::checkIncompatibility().
 789 *
 790 * ### Usage
 791 *
 792 *    parseDependency('foo (>=7.x-4.5-beta5, 3.x)');
 793 *
 794 * @param string $dependency A dependency string as example above
 795 * @return mixed
 796 *	An associative array with three keys as below, callers should pass this
 797 *	structure to `checkIncompatibility()`:
 798 *		-	`name`: includes the name of the thing to depend on (e.g. 'foo')
 799 *		-	`original_version`: contains the original version string
 800 *		-	`versions`: is a list of associative arrays, each containing the keys
 801 *			'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<',
 802 *			'<=', '>', or '>='. 'version' is one piece like '4.5-beta3'.
 803 * @see InstallerComponent::checkIncompatibility()
 804 */
 805	public function parseDependency($dependency) {
 806		$p_op = '(?P<operation>!=|==|=|<|<=|>|>=|<>)?';
 807		$p_core = '(?:' . preg_quote(Configure::read('Variable.qa_version')) . '-)?';
 808		$p_major = '(?P<major>\d+)';
 809		$p_minor = '(?P<minor>(?:\d+|x)(?:-[A-Za-z]+\d*)?)';
 810		$value = array();
 811		$parts = explode('(', $dependency, 2);
 812		$value['name'] = trim($parts[0]);
 813
 814		if (isset($parts[1])) {
 815			$value['original_version'] = ' (' . $parts[1];
 816
 817			foreach (explode(',', $parts[1]) as $version) {
 818				if (preg_match("/^\s*{$p_op}\s*{$p_core}{$p_major}\.{$p_minor}/", $version, $matches)) {
 819					$op = !empty($matches['operation']) ? $matches['operation'] : '=';
 820
 821					if ($matches['minor'] == 'x') {
 822						if ($op == '>' || $op == '<=') {
 823							$matches['major']++;
 824						}
 825
 826						if ($op == '=' || $op == '==') {
 827							$value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x');
 828							$op = '>=';
 829						}
 830					}
 831
 832					$value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']);
 833				}
 834			}
 835		}
 836
 837		return $value;
 838	}
 839
 840/**
 841 * Check whether a version is compatible with a given dependency.
 842 *
 843 * @param array $v The parsed dependency structure from `parseDependency()`
 844 * @param string $current_version The version to check against (e.g.: 4.2)
 845 * @return mixed NULL if compatible, otherwise the original dependency version string that caused the incompatibility
 846 * @see InstallerComponent::parseDependency()
 847 */
 848	public function checkIncompatibility($v, $current_version) {
 849		if (!empty($v['versions'])) {
 850			foreach ($v['versions'] as $required_version) {
 851				if ((isset($required_version['op']) && !version_compare($current_version, $required_version['version'], $required_version['op']))) {
 852					return $v['original_version'];
 853				}
 854			}
 855		}
 856
 857		return null;
 858	}
 859
 860/**
 861 * Verify if the given list of modules & version are installed and actives.
 862 *
 863 * ### Usage
 864 *
 865 *    $dependencies = array(
 866 *        'ModuleOne (1.0)',
 867 *        'ModuleTwo (>= 1.0)',
 868 *        'ModuleThree (1.x)',
 869 *        'ModuleFour'
 870 *    );
 871 *
 872 *    checkDependency($dependencies);
 873 *
 874 * @param array $dependencies List of dependencies to check.
 875 * @return boolean
 876 *	TRUE if all modules are available.
 877 *	FALSE if any of the required modules is not installed/version-compatible
 878 */
 879	public function checkDependency($dependencies = array()) {
 880		if (empty($dependencies)) {
 881			return true;
 882		}
 883
 884		if (is_array($dependencies)) {
 885			foreach ($dependencies as $p) {
 886				$d = $this->parseDependency($p);
 887
 888				if (!$m = Configure::read('Modules.' . Inflector::camelize($d['name']))) {
 889					return false;
 890				}
 891
 892				$check = $this->checkIncompatibility($d, $m['yaml']['version']);
 893
 894				if ($check !== null) {
 895					return false;
 896				}
 897			}
 898		}
 899
 900		return true;
 901	}
 902
 903/**
 904 * Verify if there is any module that depends of `$module`.
 905 *
 906 * @param string $module Module alias
 907 * @param boolean $returnList
 908 *	Set to true to return an array list of all modules that uses $module.
 909 *	This list contains all the information of each module: Configure::read('Modules.{module}')
 910 * @return mixed
 911 *	Boolean If $returnList is set to false, a FALSE return means that there are no module that uses $module.
 912 *	Or an array list of all modules that uses $module when $returnList is set to true, an empty array is
 913 *	returned if there are no module that uses $module.
 914 */
 915	function checkReverseDependency($module, $returnList = true) {
 916		$list = array();
 917		$module = Inflector::camelize($module);
 918
 919		foreach (Configure::read('Modules') as $p) {
 920			if (isset($p['yaml']['dependencies']) &&
 921				is_array($p['yaml']['dependencies'])
 922			) {
 923				$dependencies = array();
 924
 925				foreach ($p['yaml']['dependencies'] as $d) {
 926					$dependencies[] = $this->parseDependency($d);
 927				}
 928
 929				$dependencies = Hash::extract($dependencies, '{n}.name');
 930				$dependencies = array_map(array('Inflector', 'camelize'), $dependencies);
 931
 932				if (in_array($module, $dependencies, true) && $returnList) {
 933					$list[] = $p;
 934				} elseif (in_array($module, $dependencies, true)) {
 935					return true;
 936				}
 937			}
 938		}
 939
 940		if ($returnList) {
 941			return $list;
 942		}
 943
 944		return false;
 945	}
 946
 947/**
 948 * Loads plugin's installer component.
 949 *
 950 * @param string $search Path where to look for component
 951 * @return mix OBJECT Instance of component, Or FALSE if Component could not be loaded
 952 */
 953	public function loadInstallComponent($search = false) {
 954		if (!file_exists($search . 'InstallComponent.php')) {
 955			return false;
 956		}
 957
 958		include_once($search . 'InstallComponent.php');
 959
 960		$class = "InstallComponent";
 961		$component = new $class($this->Controller->Components);
 962
 963		if (method_exists($component, 'initialize')) {
 964			$component->initialize($this->Controller);
 965		}
 966
 967		if (method_exists($component, 'startup')) {
 968			$component->startup($this->Controller);
 969		}
 970
 971		$component->Installer = $this;
 972
 973		return $component;
 974	}
 975
 976/**
 977 * Creates acos for specified module by parsing its Controller folder. (Module's fields are also analyzed).
 978 * If module is already installed then an Aco update will be performed.
 979 *
 980 * ### Usage:
 981 *
 982 *    $this->Installer->buildAcos('User', APP . 'Plugin' . DS);
 983 *
 984 * The above would generate all the permissions tree for the Core module User.
 985 *
 986 * @param string $plugin Plugin name to analyze (CamelCase or underscored)
 987 * @param mixed $pluginPath Optional plugin full base path. Set to FALSE to use site modules path `ROOT/Modules`.
 988 * @return void
 989 */
 990	public function buildAcos($plugin, $pluginPath = false) {
 991		$plugin = Inflector::camelize($plugin);
 992		$pluginPath = !$pluginPath ? ROOT . DS . 'Modules' . DS : str_replace(DS . DS, DS, $pluginPath . DS);
 993
 994		if (!file_exists($pluginPath . $plugin)) {
 995			return false;
 996		}
 997
 998		$__folder = new Folder;
 999
1000		// Fields
1001		if (file_exists($pluginPath . $plugin . DS . 'Fields')) {
1002			$__folder->path = $pluginPath . $plugin . DS . 'Fields' . DS;
1003			$fieldsFolders = $__folder->read();
1004			$fieldsFolders = $fieldsFolders[0];
1005
1006			foreach ($fieldsFolders as $field) {
1007				$this->buildAcos(basename($field), $pluginPath . $plugin . DS . 'Fields' . DS);
1008			}
1009		}
1010
1011		$cPath = $pluginPath . $plugin . DS . 'Controller' . DS;
1012		$__folder->path = $cPath;
1013		$controllers = $__folder->read();
1014		$controllers = $controllers[1];
1015
1016		if (count($controllers) === 0) {
1017			return false;
1018		}
1019
1020		$appControllerPath = $cPath . $plugin . 'AppController.php';
1021		$acoExists = $this->Controller->Acl->Aco->find('first',
1022			array(
1023				'conditions' => array(
1024					'Aco.alias' => Inflector::camelize($plugin),
1025					'Aco.parent_id' => null
1026				)
1027			)
1028		);
1029
1030		if ($acoExists) {
1031			$_controllers = $this->Controller->Acl->Aco->children($acoExists['Aco']['id'], true);
1032
1033			// delete removed controllers (and all its methods)
1034			foreach ($_controllers as $c) {
1035				if (!in_array("{$c['Aco']['alias']}Controller.php", $controllers)) {
1036					$this->Controller->Acl->Aco->removeFromTree($c['Aco']['id'], true);
1037				}
1038			}
1039
1040			$_controllersNames = Hash::extract($_controllers, '{n}.Aco.alias');
1041		}
1042
1043		if (!$acoExists) {
1044			$this->Controller->Acl->Aco->create();
1045			$this->Controller->Acl->Aco->save(array('alias' => Inflector::camelize($plugin)));
1046
1047			$_parent_id =  $this->Controller->Acl->Aco->getInsertID();
1048		} else {
1049			$_parent_id = $acoExists['Aco']['id'];
1050		}
1051
1052		foreach ($controllers as $c) {
1053			if (strpos($c, 'AppController.php') !== false) {
1054				continue;
1055			}
1056
1057			$alias = str_replace(array('Controller', '.php'), '', $c);
1058			$methods = $this->__getControllerMethods($cPath . $c, $appControllerPath);
1059
1060			foreach ($methods as $i => $m) {
1061				if (strpos($m, '__') === 0 ||
1062					strpos($m, '_') === 0 ||
1063					in_array($m, array('beforeFilter', 'beforeRender', 'beforeRedirect', 'afterFilter'))
1064				) {
1065					unset($methods[$i]);
1066				}
1067			}
1068
1069			if ($acoExists && in_array($alias, $_controllersNames)) {
1070				$controller = Hash::extract($_controllers, "{n}.Aco[alias={$alias}]");
1071				$_methods = $this->Controller->Acl->Aco->children($controller[0]['id'], true);
1072
1073				// delete removed methods
1074				foreach ($_methods as $m) {
1075					if (!in_array($m['Aco']['alias'], $methods)) {
1076						$this->Controller->Acl->Aco->removeFromTree($m['Aco']['id'], true);
1077					}
1078				}
1079
1080				$_methods = Hash::extract((array)$_methods, '{n}.Aco.alias');
1081
1082				// add new methods
1083				foreach ($methods as $m) {
1084					if (!in_array($m, $_methods)) {
1085						$this->Controller->Acl->Aco->create();
1086						$this->Controller->Acl->Aco->save(
1087							array(
1088								'parent_id' => $controller[0]['id'],
1089								'alias' => $m
1090							)
1091						);
1092					}
1093				}
1094			} else {
1095				$this->Controller->Acl->Aco->create();
1096				$this->Controller->Acl->Aco->save(
1097					array(
1098						'parent_id' => $_parent_id,
1099						'alias' => $alias
1100					)
1101				);
1102
1103				$parent_id =  $this->Controller->Acl->Aco->getInsertID();
1104
1105				foreach ($methods as $m) {
1106					$this->Controller->Acl->Aco->create();
1107					$this->Controller->Acl->Aco->save(
1108						array(
1109							'parent_id' => $parent_id,
1110							'alias' => $m
1111						)
1112					);
1113				}
1114			}
1115		}
1116	}
1117
1118/**
1119 * Check if all files & folders contained in `dir` can be removed.
1120 *
1121 * @param string $dir Path content to check
1122 * @return bool TRUE if all files & folder can be removed. FALSE otherwise
1123 */
1124	public function isRemoveable($dir) {
1125		if (!is_writable($dir)) {
1126			return false;
1127		}
1128
1129		$Folder = new Folder($dir);
1130		$read = $Folder->read(false, false, true);
1131
1132		foreach ($read[1] as $file) {
1133			if (!is_writable($dir)) {
1134				return false;
1135			}
1136		}
1137
1138		foreach ($read[0] as $folder) {
1139			if (!$this->isRemoveable($folder)) {
1140				return false;
1141			}
1142		}
1143
1144		return true;
1145	}
1146
1147/**
1148 * Check if all files & folders contained in `source` can be copied to `destination`
1149 *
1150 * @param string $src Path content to check
1151 * @param string $dst Destination path that $source should be copied to
1152 * @return bool TRUE if all files & folder can be copied to `destination`. FALSE otherwise
1153 */
1154	public function packageIsWritable($src, $dst) {
1155		if (!file_exists($dst)) {
1156			return $this->packageIsWritable($src, dirname($dst));
1157		}
1158
1159		$e = 0;
1160		$Folder = new Folder($src);
1161		$files = $Folder->findRecursive();
1162
1163		if (!is_writable($dst)) {
1164			$e++;
1165			$this->errors[] = __t('path: %s, not writable', $dst);
1166		}
1167
1168		foreach ($files as $file) {
1169			$file = str_replace($this->options['__packagePath'], '', $file);
1170			$file_dst = str_replace(DS . DS, DS, $dst . DS . $file);
1171
1172			if (file_exists($file_dst) && !is_writable($file_dst)) {
1173				$e++;
1174				$this->errors[] = __t('path: %s, not writable', $file_dst);
1175			}
1176		}
1177
1178		return ($e == 0);
1179	}
1180
1181/**
1182 * Creates a new block and optianilly assign it to
1183 * the specified theme and region.
1184 *
1185 * ### Block options
1186 *
1187 *	-	module (string) [required]: CamelCased module name which is creating the block.
1188 *		default: name of the module being installed/uninstalled, "Block" otherwise.
1189 *	-	delta (string) [optional]: under_scored unique ID for block within a module.
1190 *		If not specfied, an auto-increment ID is automatically calculated for the given module. default: null
1191 *	-	title (string) [optional]: custom title for the block. default: null
1192 *	-	body (string) [optional]: block's content body, VALID ONLY WHEN `module` = "Block". default: null
1193 *	-	description (string) [required]: brief description of your block, VALID ONLY WHEN `module` = "Block".
1194 *		default: same as module.
1195 *	-	status (int) [optional]: block enabled status. 1 for enabled or 0 disabled. default: 1
1196 *	-	visibility (int) [optional]: flag to indicate how to show blocks on pages. default: 0
1197 *			- 0: Show on all pages except listed pages
1198 *			- 1: Show only on listed pages
1199 *			- 2: Use custom PHP code to determine visibility
1200 *	-	pages (string) [optional]: list of paths (one path per line) on which to include/exclude the block or PHP code
1201 *		depending on "visibility" setting. default: null
1202 *	-	locale (array) [optional]: list of language codes. default: none
1203 *	-	settings (array) [optional]: extra information used by the block. default: none
1204 *
1205 * ### Usage
1206 *
1207 *    $block = array(
1208 *        'title' => 'My block title',
1209 *        'body' => 'My block content',
1210 *        'module' => 'Block'
1211 *    );
1212 *
1213 *    createBlock($block, 'ThemeName.sidebar_left');
1214 *
1215 * The above will create a new Custom Block, and then assigned to the `sidebar_left` region of the `ThemeName` theme.
1216 *
1217 *    $block = array(
1218 *        'title' => 'Widget Title',
1219 *        'module' => 'CustomModule',
1220 *        'delta' => 'my_widget' // required!
1221 *    );
1222 *
1223 *    createBlock($block, 'ThemeName.sidebar_left');
1224 *
1225 * Similar as before, but this time it will create a Widget Block. In this case the 'delta' option is require and must
1226 * be unique within the `CustomModule` module.
1227 *
1228 * @param array $block Block information
1229 * @param string $theme Optional "Theme.region" where to assign the block
1230 * @return bool TRUE on success, FALSE otherwise
1231 */
1232	public function createBlock($block, $_theme = '') {
1233		$defaultModule = isset($this->options['__appName']) ? $this->options['__appName'] : 'Block';
1234		$Block = ClassRegistry::init('Block.Block');
1235		$block = array_merge(
1236			array(
1237				'title' => null,
1238				'body' => null,
1239				'description' => $defaultModule,
1240				'delta' => null,
1241				'module' => $defaultModule,
1242				'status' => 1,
1243				'visibility' => 0,
1244				'pages' => '',
1245				'locale' => array(),
1246				'settings' => array()
1247			), $block
1248		);
1249
1250		$block['module'] = Inflector::camelize($block['module']);
1251
1252		if (isset($block['id'])) {
1253			$block['delta'] = $block['id'];
1254			unset($block['id']);
1255		}
1256
1257		if (isset($block['themes_cache'])) {
1258			unset($block['themes_cache']);
1259		}
1260
1261		if (!$block['module']) {
1262			return false;
1263		}
1264
1265		if ($block['module'] == 'Block') {
1266			unset($block['delta']);
1267		} else {
1268			$block['delta'] = Inflector::underscore($block['delta']);
1269
1270			if (!$block['delta']) {
1271				$max_delta = $Block->find('first',
1272					array(
1273						'conditions' => array('Block.module' => $block['module']),
1274						'fields' => array('delta'),
1275						'order' => array('delta' => 'DESC')
1276					)
1277				);
1278				$max_delta = !empty($max_delta) ? (int)$max_delta['Block']['delta'] + 1 : 1;
1279				$block['delta'] = $max_delta;
1280			}
1281		}
1282
1283		list($theme, $region) = pluginSplit($_theme);
1284
1285		if (!empty($_theme)) {
1286			$block['themes_cache'] = $theme;
1287		}
1288
1289		$Block->create($block);
1290
1291		if ($save = $Block->save()) {
1292			if (!empty($_theme)) {
1293				$BlockRegion = ClassRegistry::init('Block.BlockRegion');
1294				$data = array(
1295					'region' => $region,
1296					'block_id' => $save['Block']['id'],
1297					'theme' => $theme
1298				);
1299
1300				$BlockRegion->create($data);
1301				$BlockRegion->save();
1302			}
1303
1304			if ($block['body'] && $block['module'] == 'Block') {
1305				$BlockCustom = ClassRegistry::init('Block.BlockCustom');
1306
1307				$BlockCustom->create(
1308					array(
1309						'block_id' => $save['Block']['id'],
1310						'body' => $block['body'],
1311						'description' => $block['description']
1312					)
1313				);
1314				$BlockCustom->save();
1315			}
1316
1317			return true;
1318		}
1319
1320		return false;
1321	}
1322
1323/**
1324 * Recursively copy `source` to `destination`
1325 *
1326 * @param string $src Path content to copy
1327 * @param string $dst Destination path that $source should be copied to
1328 * @return bool TRUE on success. FALSE otherwise
1329 */
1330	public function rcopy($src, $dst) {
1331		if (!$this->packageIsWritable($src, $dst)) {
1332			return false;
1333		}
1334
1335		$dir = opendir($src);
1336
1337		@mkdir($dst);
1338
1339		while(false !== ($file = readdir($dir))) {
1340			if (($file != '.') && ($file != '..')) {
1341				if (is_dir($src . DS . $file)) {
1342					$this->rcopy($src . DS . $file, $dst . DS . $file);
1343				} else {
1344					if (!copy($src . DS . $file, $dst . DS . $file)) {
1345						return false;
1346					}
1347				}
1348			}
1349		}
1350
1351		closedir($dir);
1352
1353		return true;
1354	}
1355
1356/**
1357 * Insert a new link to specified menu.
1358 *
1359 * ### Usage
1360 *
1361 * Example of use on module install, the code below will insert a new link
1362 * to the backend menu (`management`):
1363 *
1364 *    $this->Installer->menuLink(
1365 *        array(
1366 *            'link' => '/my_new_module/dashboard',
1367 *            'description' => 'This is a link to my new awesome module',
1368 *            'label' => 'My Awesome Module'
1369 *        ), 1
1370 *    );
1371 *
1372 * Notice that this example uses `1` as menu ID instead of `management`.
1373 *
1374 * @param array $link
1375 *	Associative array information of the link to add:
1376 *		-	[parent|parent_id]: Parent link ID.
1377 *		-	[url|link|path|router_path]: Link url (href).
1378 *		-	[description]: Link description used as `title` attribute.
1379 *		-	[title|label|link_title]: Link text to show between tags: <a href="">TEXT</a>
1380 *		-	[module]: Name of the module that link belongs to,
1381 *			by default it is set to the name of module being installed or
1382 *			to `System` if method is called on non-install process.
1383 * @param mixed $menu_id
1384 *	Set to string value to indicate the menu id slug, e.g.: `management`.
1385 *	Or set to one of the following integer values:
1386 *		- 0: Main menu of the site.
1387 *		- 1: Backend menu (by default).
1388 *		- 2: Navigation menu.
1389 *		- 3: User menu.
1390 * @param integer $move
1391 *	Number of positions to move the link after add.
1392 *	Negative values will move down, positive values will move up, zero value (0) wont move.
1393 * @return mixed Array information of the new inserted link. FALSE on failure
1394 */
1395	public function menuLink($link, $menu_id = 1, $move = 0) {
1396		$menu_id = is_string($menu_id) ? trim($menu_id) : $menu_id;
1397		$Menu = ClassRegistry::init('Menu.Menu');
1398		$MenuLink = ClassRegistry::init('Menu.MenuLink');
1399
1400		if (is_integer($menu_id)) {
1401			switch ($menu_id) {
1402				case 0:
1403					default:
1404						$menu_id = 'main-menu';
1405				break;
1406
1407				case 1:
1408					$menu_id = 'management';
1409				break;
1410
1411				case 2:
1412					$menu_id = 'navigation';
1413				break;
1414
1415				case 3:
1416					$menu_id = 'user-menu';
1417				break;
1418			}
1419		}
1420
1421		if (!($menu = $Menu->findById($menu_id))) {
1422			return false;
1423		}
1424
1425		// Column alias
1426		if (isset($link['path'])) {
1427			$link['router_path'] = $link['path'];
1428			unset($link['path']);
1429		}
1430
1431		if (isset($link['url'])) {
1432			$link['router_path'] = $link['url'];
1433			unset($link['url']);
1434		}
1435
1436		if (isset($link['link'])) {
1437			$link['router_path'] = $link['link'];
1438			unset($link['link']);
1439		}
1440
1441		if (isset($link['label'])) {
1442			$link['link_title'] = $link['label'];
1443			unset($link['label']);
1444		}
1445
1446		if (isset($link['title'])) {
1447			$link['link_title'] = $link['title'];
1448			unset($link['title']);
1449		}
1450
1451		if (isset($link['parent'])) {
1452			$link['parent_id'] = $link['parent'];
1453			unset($link['parent']);
1454		}
1455
1456		if (isset($this->options['__appName']) &&
1457			!empty($this->options['__appName']) &&
1458			!isset($link['module'])
1459		) {
1460			$link['module'] = $this->options['__appName'];
1461		}
1462
1463		$__link = array(
1464			'parent_id' => 0,
1465			'router_path' => '',
1466			'description' => '',
1467			'link_title' => '',
1468			'module' => 'System',
1469			'target' => '_self',
1470			'expanded' => false,
1471			'status' => 1
1472		);
1473
1474		$link = Hash::merge($__link, $link);
1475		$link['menu_id'] = $menu_id;
1476
1477		$MenuLink->Behaviors->detach('Tree');
1478		$MenuLink->Behaviors->attach('Tree',
1479			array(
1480				'parent' => 'parent_id',
1481				'left' => 'lft',
1482				'right' => 'rght',
1483				'scope' => "MenuLink.menu_id = '{$menu_id}'"
1484			)
1485		);
1486
1487		$MenuLink->create();
1488
1489		$save = $MenuLink->save($link);
1490
1491		if (is_integer($move) && $move !== 0) {
1492			if ($move > 0) {
1493				$MenuLink->moveUp($save['MenuLink']['id'], $move);
1494			} else {
1495				$MenuLink->moveDown($save['MenuLink']['id'], abs($move));
1496			}
1497		}
1498
1499		return $save;
1500	}
1501
1502/**
1503 * Defines a new content type and optionally attaches a list of fields to it.
1504 *
1505 * ### Usage
1506 *
1507 *    createContentType(
1508 *        array(
1509 *            'module' => 'Blog',  (OPTIONAL)
1510 *            'name' => 'Blog Entry',
1511 *            'label' => 'Entry Title'
1512 *        ),
1513 *        array(
1514 *            'FieldText' => array(
1515 *                'name' => 'blog_body',
1516 *                'label' => 'Entry Body'
1517 *            )
1518 *        )
1519 *    );
1520 *
1521 * Note that `module` key is OPTIONAL when the method is invoked
1522 * from an installation session. If `module` key is not set and this method is invoked
1523 * during an install process (e.g.: called from `afterInstall`) it wil use the name of the module
1524 * being installed.
1525 *
1526 * Although this method should be used on `afterInstall` callback only:
1527 * If you are adding fields that belongs to the module being installed
1528 * MAKE SURE to use this method on `afterInstall` callback, this is after
1529 * module has been installed and its fields has been REGISTERED on the system.
1530 *
1531 * @param array $type Content Type information. see $__type
1532 * @param array $fields
1533 *	Optional Associative array of fields to attach to the new content type:
1534 *
1535 *    $fields = array(
1536 *        'FieldText' => array(
1537 *            'label' => 'Title',  [required]
1538 *            'name' => 'underscored_unique_name', [required]
1539 *            'required' => true, [optional]
1540 *            'description' => 'Help text, instructions.', [optional]
1541 *            'settings' => array(  [optional array of specific settings for this field.]
1542 *			      'extensions' => 'jpg,gif,png',
1543 *			      ...
1544 *            )
1545 *        ),
1546 *	      ...
1547 *    );
1548 *
1549 *	Keys `label` and `name` are REQUIRED!
1550 *
1551 * @return mixed boolean FALSE on failure. NodeType array on success
1552 * @link https://github.com/QuickAppsCMS/QuickApps-CMS/wiki/Field-API
1553 */
1554	public function createContentType($type, $fields = array()) {
1555		$__type = array(
1556			// The module defining this node type
1557			'module' => (isset($this->options['__appName']) ? $this->options['__appName'] : false),
1558			// information
1559			'name' => false,
1560			'description' => '',
1561			'label' => false,
1562			// display format
1563			'author_name' => 0, // show publisher info.: NO
1564			'publish_date' => 0, // show publish date: NO
1565			// comments
1566			'comments' => 0, // comments: CLOSED (1: read only, 2: open)
1567			'comments_approve' => 0, // auto approve: NO
1568			'comments_per_page' => 10,
1569			'comments_anonymous' => 0,
1570			'comments_title' => 0, // allow comment title: NO
1571			// language
1572			'language' => '', // language: any
1573			// publishing
1574			'published' => 1, // active: YES
1575			'promote' => 0, // publish to front page: NO
1576			'sticky' => 0 // sticky at top of lists: NO
1577		);
1578		$type = array_merge($__type, $type);
1579
1580		if (!$type['name'] ||
1581			!$type['label'] ||
1582			empty($type['module']) ||
1583			!CakePlugin::loaded($type['module'])
1584		) {
1585			return false;
1586		}
1587
1588		$NodeType = ClassRegistry::init('Node.NodeType');
1589		$newType = $NodeType->save(
1590			array(
1591				'NodeType' => array(
1592					'module' => $type['module'],
1593					'name' => $type['name'],
1594					'description' => $type['description'],
1595					'title_label' => $type['label'],
1596					'node_show_author' => $type['author_name'],
1597					'node_show_date' => $type['publish_date'],
1598					'default_comment' => $type['comments'],
1599					'comments_approve' => $type['comments_approve'],
1600					'comments_per_page' => $type['comments_per_page'],
1601					'comments_anonymous' => $type['comments_anonymous'],
1602					'comments_subject_field' => $type['comments_title'],
1603					'default_language' => $type['language'],
1604					'default_status' => $type['published'],
1605					'default_promote' => $type['promote'],
1606					'default_sticky' => $type['sticky']
1607				)
1608			)
1609		);
1610
1611		if ($newType) {
1612			if (!empty($fields)) {
1613				$NodeType->Behaviors->attach('Field.Fieldable', array('belongsTo' => "NodeType-{$newType['NodeType']['id']}"));
1614
1615				foreach ($fields as $module => $data) {
1616					$data['field_module'] = $module;
1617
1618					if (!$NodeType->attachFieldInstance($data)) {
1619						$NodeType->delete($newType['NodeType']['id']);
1620
1621						return false;
1622					}
1623				}
1624			}
1625
1626			return $newType;
1627		} else {
1628			return false;
1629		}
1630	}
1631
1632/**
1633 * Execute an SQL statement.
1634 *
1635 * ### Usage
1636 *
1637 *    sql('DROP TABLE `#__table_to_remove`');
1638 *
1639 * NOTE: Remember to include the table prefix pattern on each query string. (`#__` by default)
1640 *
1641 * @param string $query SQL to execute
1642 * @param string $prefix_pattern Pattern to replace for database prefix. default to `#__`
1643 * @return boolean
1644 */
1645	public function sql($query, $prefix_pattern = '#__') {
1646		$dSource = $this->Controller->Module->getDataSource();
1647		$query = str_replace($prefix_pattern, $dSource->config['prefix'], $query);
1648
1649		return $dSource->execute($query);
1650	}
1651
1652/**
1653 * Insert a single or multiple messages passed as arguments.
1654 *
1655 * ### Usage
1656 *
1657 *    error('Error 1', 'Error 2');
1658 *
1659 * @return void
1660 */
1661	public function error() {
1662		$messages = func_get_args();
1663
1664		foreach ($messages as $m) {
1665			$this->errors[] = $m;
1666		}
1667	}
1668
1669/**
1670 * Checks the given test list.
1671 *
1672 * ### Usage
1673 *
1674 *    $tests = array(
1675 *        array('test' => true, 'header' => 'Test 1', 'msg' => 'Test 1 has failed'),
1676 *        array('test' => false, 'header' => 'Test 2', 'msg' => 'Test 2 has failed'),
1677 *        ...
1678 *    );
1679 *
1680 *    checkTests($tests);
1681 *
1682 * In the example above 'Test 1' is passed, but 'Te…

Large files files are truncated, but you can click here to view the full file