PageRenderTime 74ms CodeModel.GetById 31ms RepoModel.GetById 0ms 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
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, GPL-3.0

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