PageRenderTime 62ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/engine/classes/Module.php

https://github.com/etienne/qsprog
PHP | 1359 lines | 1024 code | 164 blank | 171 comment | 218 complexity | 89f984ce38f17a72b8341d637cdb39a3 MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause, GPL-2.0
  1. <?php
  2. require_once('classes/Database.php');
  3. require_once('classes/Date.php');
  4. require_once('classes/Filesystem.php');
  5. require_once('classes/Form.php');
  6. require_once('classes/HTTP.php');
  7. require_once('classes/Path.php');
  8. require_once('classes/Layout.php');
  9. require_once('classes/Template.php');
  10. require_once('classes/Query.php');
  11. require_once('classes/Query.php');
  12. class Module {
  13. var $name;
  14. var $moduleID;
  15. var $itemID;
  16. var $config;
  17. var $schema;
  18. var $isRoot;
  19. var $modulePath;
  20. var $keyFieldName;
  21. var $hasFiles;
  22. var $hasMulti;
  23. var $hasLocalizedFields;
  24. var $isLocalizable;
  25. var $adminMenuStrings;
  26. var $strings;
  27. var $parentModule;
  28. var $items;
  29. var $item;
  30. var $rawData;
  31. var $processedData;
  32. var $postData = array();
  33. var $missingData;
  34. var $invalidData;
  35. var $fileUploadError;
  36. var $files;
  37. var $layout;
  38. var $view = array();
  39. /*
  40. * Constructor
  41. */
  42. function Module($name, $item = '') {
  43. global $_JAM;
  44. $this->name = $name;
  45. $this->itemID = $item;
  46. // Determine whether we are the root module
  47. if (!isset($_JAM->rootModuleName)) {
  48. $this->isRoot = true;
  49. $_JAM->rootModuleName = $this->name;
  50. // Create layout object
  51. $this->layout = new Layout();
  52. }
  53. }
  54. /*
  55. * Static
  56. */
  57. function DisplayNewModule($name, $item = '') {
  58. $module = Module::GetNewModule($name, $item);
  59. return $module->Display();
  60. }
  61. function GetNewModule($name, $item = '', $hasParent = false) {
  62. global $_JAM;
  63. if (!$_JAM->availableModules[$name]) {
  64. trigger_error("Couldn't create new module because '". $name ."' module does not exist", E_USER_ERROR);
  65. }
  66. $className = ucfirst($name) .'Module';
  67. $classPath = 'modules/'. $name .'/'. $className .'.php';
  68. if (Filesystem::FileExistsInIncludePath($classPath)) {
  69. // There is a custom module class; load it and create new instance
  70. require_once($classPath);
  71. $module = new $className($name, $item);
  72. } else {
  73. // There is no custom module class; use plain Module class
  74. $module = new Module($name, $item);
  75. }
  76. // Don't run FinishSetup() if module has parent; will run later in NestModule
  77. // FIXME: Kludgy.
  78. if (!$hasParent) {
  79. $module->FinishSetup();
  80. }
  81. return $module;
  82. }
  83. function ParseConfigFile($moduleName, $iniFile, $processSections = false) {
  84. global $_JAM;
  85. // Determine whether requested module is a custom (app-specific) or engine module
  86. $iniFileRoot = in_array($moduleName, $_JAM->appModules) ? 'app' : 'engine';
  87. // Build path to config file
  88. $iniFilePath = $iniFileRoot .'/modules/'. $moduleName .'/'. $iniFile;
  89. return IniFile::Parse($iniFilePath, $processSections);
  90. }
  91. /*
  92. * Static private
  93. */
  94. function GetAdminMenuString($module) {
  95. global $_JAM;
  96. $config = Module::ParseConfigFile($module, 'config/config.ini');
  97. $strings = Module::ParseConfigFile($module, 'strings/'. $_JAM->language .'.ini');
  98. if ($config['hideFromAdmin']) {
  99. // Module asks not to show up in menu
  100. return false;
  101. } elseif ($module == 'users' && $_JAM->projectConfig['singleUser']) {
  102. // Project is single-user, and we don't want the "users" module to show up in the admin interface
  103. return false;
  104. } elseif ($config['canView'] && !$_JAM->user->HasPrivilege($config['canView'])) {
  105. // User doesn't have sufficient privileges to view the module
  106. return false;
  107. } elseif ($string = $strings['adminTitle']) {
  108. return $string;
  109. } else {
  110. return $module;
  111. }
  112. }
  113. function InsertTableNames ($array, $oldName, $newName) {
  114. // Recursive function to process custom parameters
  115. if ($array) {
  116. foreach ($array as $key => $value) {
  117. if (is_string($value)) {
  118. $returnArray[$key] = preg_replace('/^'. $oldName .'$/', $newName, $value);
  119. } elseif (is_array($value)) {
  120. $returnArray[$key] = Module::InsertTableNames($value, $oldName, $newName);
  121. } else {
  122. $returnArray[$key] = $value;
  123. }
  124. }
  125. return $returnArray;
  126. } else {
  127. return false;
  128. }
  129. }
  130. /*
  131. * Private
  132. */
  133. function FinishSetup() {
  134. global $_JAM;
  135. // Check whether this is an app-level or engine-level module
  136. $modulePathRoot = in_array($this->name, $_JAM->appModules) ? 'app' : 'engine';
  137. $this->modulePath = $modulePathRoot .'/modules/'. $this->name .'/';
  138. // Make sure this module exists
  139. if (!$_JAM->availableModules[$this->name]) {
  140. return false;
  141. }
  142. // Load configuration files
  143. $this->config = IniFile::Parse($this->modulePath .'config/config.ini', true);
  144. $this->strings = IniFile::Parse($this->modulePath .'strings/'. $_JAM->language .'.ini', true);
  145. // Check whether we should disable cache
  146. if ($this->config['disableCache']) {
  147. $_JAM->cache->Forbid();
  148. }
  149. // Get info for this module's table, if there is one
  150. if ($schema = IniFile::Parse($this->modulePath .'config/schema.ini', true)) {
  151. // Determine key field
  152. if (!$this->keyFieldName = $this->config['keyField']) {
  153. // If config file didn't specify a key field, use the first field
  154. reset($schema);
  155. $this->keyFieldName = key($schema);
  156. }
  157. // Merge with standard basic fields if applicable
  158. if ($this->config['useCustomTable']) {
  159. $this->schema = $schema;
  160. } else {
  161. $this->schema = $_JAM->moduleFields;
  162. if ($this->config['keepVersions']) {
  163. // Additional fields are needed for versions support
  164. $this->schema += $_JAM->versionsSupportFields;
  165. }
  166. $this->schema += $schema;
  167. }
  168. foreach ($this->schema as $name => $info) {
  169. // Determine whether we have localized fields
  170. if ($info['localizable']) {
  171. $this->isLocalizable = true;
  172. }
  173. // Look for specific field types
  174. switch ($info['type']) {
  175. case 'file':
  176. $this->hasFiles = true;
  177. break;
  178. case 'multi':
  179. $this->hasMulti = true;
  180. $relatedModuleName = $info['relatedModule'];
  181. $relatedModuleID = array_search($relatedModuleName, $_JAM->installedModules);
  182. $this->multiRelatedModules[$relatedModuleID] = $name;
  183. break;
  184. }
  185. }
  186. }
  187. // Make sure module is installed and get ID for this module
  188. if ($this->moduleID = @array_search($this->name, $_JAM->installedModules)) {
  189. // Update data if this module has a table and we have the right POST data
  190. if ($this->schema && $_POST['module'] == $this->name) {
  191. $this->ProcessData();
  192. }
  193. // Fetch data for this item, if one was specified
  194. if ($this->itemID && $this->schema) {
  195. $this->FetchItem($this->itemID);
  196. }
  197. // Run initialization method if one was defined
  198. if (method_exists($this, 'Initialize')) {
  199. if ($this->Initialize() == false) return false;
  200. }
  201. }
  202. }
  203. function Install() {
  204. global $_JAM;
  205. // Make sure table has not already been installed
  206. if ($installedModules = Query::SimpleResults('_modules')) {
  207. $_JAM->installedModules = $installedModules;
  208. if (in_array($this->name, $_JAM->installedModules)) {
  209. return true;
  210. }
  211. }
  212. // Determine whether we need a table at all
  213. if ($this->schema) {
  214. foreach ($this->schema as $name => $info) {
  215. // Split fields between main table and localized table
  216. if ($info['localizable']) {
  217. $localizedTableSchema[$name] = $info;
  218. } else {
  219. $mainTableSchema[$name] = $info;
  220. }
  221. // Check whether we need to install other modules first
  222. if (
  223. ($relatedModule = $info['relatedModule']) &&
  224. !in_array($this->name, $_JAM->installedModules) &&
  225. $relatedModule != $this->name
  226. ) {
  227. $module = Module::GetNewModule($relatedModule);
  228. $module->Install();
  229. }
  230. }
  231. // Create main table
  232. if ($mainTableSchema) {
  233. if (!Database::CreateTable($this->name, $mainTableSchema)) {
  234. trigger_error("Couldn't create table for module ". $this->name, E_USER_ERROR);
  235. return false;
  236. }
  237. // If localized fields were found, we need a localized table
  238. if ($localizedTableSchema) {
  239. $baseFields = IniFile::Parse('engine/database/localizedTableFields.ini', true);
  240. $localizedTableSchema = $baseFields + $localizedTableSchema;
  241. if (!Database::CreateTable($this->name .'_localized', $localizedTableSchema)) {
  242. trigger_error("Couldn't create localized table for module ". $this->name, E_USER_ERROR);
  243. return false;
  244. }
  245. }
  246. }
  247. }
  248. // Add entry to _modules table
  249. $params = array('name' => $this->name);
  250. if (Database::Insert('_modules', $params)) {
  251. // Get ID of the row we just inserted
  252. $this->moduleID = Database::GetLastInsertID();
  253. // Add admin path to _paths table FIXME: Untested
  254. $adminModuleID = array_search('admin', $_JAM->installedModules);
  255. if (!Path::Insert('admin/'. $this->name, $adminModuleID, $this->moduleID)) {
  256. trigger_error("Couldn't add admin path for module ". $this->name, E_USER_ERROR);
  257. return false;
  258. }
  259. // Add paths to _paths table if needed
  260. if ($this->config['path']) {
  261. // Add paths for each language
  262. foreach ($this->config['path'] as $language => $path) {
  263. if (!Path::Insert($path, $this->moduleID, 0, true, $language)) {
  264. trigger_error("Could't add path for module ". $this->name, E_USER_ERROR);
  265. return false;
  266. }
  267. }
  268. }
  269. return true;
  270. } else {
  271. trigger_error("Couldn't install module ". $this->name, E_USER_ERROR);
  272. return false;
  273. }
  274. }
  275. function CanView() {
  276. global $_JAM;
  277. if ($this->config['canView']) {
  278. return $_JAM->user->HasPrivilege($this->config['canView']);
  279. } else {
  280. return true;
  281. }
  282. }
  283. function CanInsert() {
  284. global $_JAM;
  285. if ($this->config['canInsert']) {
  286. return $_JAM->user->HasPrivilege($this->config['canInsert']);
  287. } else {
  288. return true;
  289. }
  290. }
  291. function CanDelete() {
  292. global $_JAM;
  293. if ($this->config['canDelete']) {
  294. return $_JAM->user->HasPrivilege($this->config['canDelete']);
  295. } else {
  296. return true;
  297. }
  298. }
  299. function NestModule($name, $item = '') {
  300. $module = Module::GetNewModule($name, $item, true);
  301. $module->AttachParent($this);
  302. $module->FinishSetup();
  303. return $module;
  304. }
  305. function AttachParent(&$parentModule) {
  306. $this->parentModule =& $parentModule;
  307. }
  308. function DisplayNestedModule($name, $item = '') {
  309. $module = $this->NestModule($name, $item);
  310. $module->Display();
  311. }
  312. function FetchItem($id) {
  313. global $_JAM;
  314. if ($this->config['keepVersions']) {
  315. $where = '('. $this->name .'.master = '. $id .' OR '.
  316. $this->name .'.id = '. $id .' AND '. $this->name .'.master IS NULL)';
  317. } else {
  318. $where = $this->name .'.id = '. $id;
  319. }
  320. $params = array(
  321. 'where' => $where,
  322. 'limit' => 1
  323. );
  324. if ($items = $this->FetchItems($params)) {
  325. $this->itemID = $id;
  326. return $this->item = current($items);
  327. } else {
  328. return false;
  329. }
  330. }
  331. function FetchItems($queryParams = '') {
  332. global $_JAM;
  333. $query = new Query();
  334. $query->AddFrom($this->name);
  335. if ($this->config['keepVersions']) {
  336. // This is a multiversions table; fetch 'master' field
  337. $query->AddFields(array(
  338. 'master' => 'IF('. $this->name .'.master IS NULL, '. $this->name .'.id, '. $this->name .'.master)'
  339. ));
  340. $query->AddWhere($this->name .'.current = TRUE');
  341. } else {
  342. // This is a standard table; fetch 'id' field
  343. $query->AddFields(array('id' => $this->name .'.id'));
  344. }
  345. /*
  346. // Order by master if we're keeping versions
  347. if ($this->config['keepVersions']) {
  348. $query->AddOrderBy('master DESC');
  349. }*/
  350. // Add localized data
  351. if ($this->isLocalizable) {
  352. $localizedTable = $this->name .'_localized';
  353. $query->AddFields(array('language' => $localizedTable .'.language'));
  354. $query->AddFrom($localizedTable);
  355. $where = array(
  356. $localizedTable .'.item = '. $this->name .'.id',
  357. $localizedTable .".language = '". $_JAM->language ."'"
  358. );
  359. $query->AddWhere($where);
  360. }
  361. // Load all fields if none were specified
  362. if (!$queryParams['fields']) {
  363. foreach($this->schema as $name => $info) {
  364. $queryParams['fields'][] = $name;
  365. }
  366. }
  367. foreach($this->schema as $name => $info) {
  368. // Manually remove multi fields from query; they will be processed anyway (possibly kludgy)
  369. if ($info['type'] == 'multi') {
  370. if ($multiFieldKey = array_search($name, $queryParams['fields'])) {
  371. unset($queryParams['fields'][$multiFieldKey]);
  372. }
  373. }
  374. // Process custom parameters
  375. if ($info['localizable']) {
  376. $replaceString = $this->name .'_localized.'. $name;
  377. } else {
  378. $replaceString = $this->name .'.'. $name;
  379. }
  380. // Fetch data for related modules
  381. if (@in_array($name, $queryParams['fields'])) {
  382. if (
  383. $info['type'] == 'int' &&
  384. ($relatedModule = $info['relatedModule']) &&
  385. $relatedModule != 'users' &&
  386. $relatedModule != $this->name
  387. ) {
  388. // Add fields from foreign module
  389. $relatedModuleSchema = Module::ParseConfigFile($relatedModule, 'config/schema.ini', true);
  390. foreach($relatedModuleSchema as $foreignName => $foreignInfo) {
  391. $fields[$name .'_'. $foreignName] = $relatedModule .'.'. $foreignName;
  392. }
  393. $query->AddFields($fields);
  394. // Determine whether we should look for 'master' or 'id' field
  395. $relatedModuleConfig = Module::ParseConfigFile($relatedModule, 'config/config.ini', true);
  396. $joinCondition = $this->name .'.'. $name .' = ';
  397. if ($relatedModuleConfig['keepVersions']) {
  398. $joinCondition .= $relatedModule .'.master AND '. $relatedModule .'.current = TRUE';
  399. } else {
  400. $joinCondition .= $relatedModule .'.id';
  401. }
  402. // Build query
  403. $joinTable = $relatedModule;
  404. $query->AddJoin($this->name, $joinTable, $joinCondition);
  405. }
  406. }
  407. $queryParams = Module::InsertTableNames($queryParams, $name, $replaceString);
  408. }
  409. // Load custom parameters
  410. $query->LoadParameters($queryParams);
  411. // Load paths if appropriate
  412. if ($this->config['autoPaths'] || (get_parent_class($this) && method_exists($this, 'GetPath'))) {
  413. $query->AddFields(array('path' => '_paths.path'));
  414. $joinTable = '_paths';
  415. $joinConditions[] = '_paths.module = '. $this->moduleID;
  416. $joinConditions[] = '_paths.current = 1';
  417. if ($this->config['keepVersions']) {
  418. $joinConditions[] = '((_paths.item = '. $this->name .'.id AND '. $this->name .'.master IS NULL) OR '.
  419. '_paths.item = '. $this->name .'.master)';
  420. } else {
  421. $joinConditions[] = '_paths.item = '. $this->name .'.id';
  422. }
  423. $query->AddJoin($this->name, $joinTable, $joinConditions);
  424. if ($this->isLocalizable) {
  425. $query->AddWhere($this->name . '_localized.language = _paths.language');
  426. }
  427. }
  428. // Debug query:
  429. //dp($query->GetQueryString());
  430. // Fetch actual module data
  431. if ($this->rawData = $query->GetArray()) {
  432. // Load data for 'multi' fields
  433. if ($this->hasMulti) {
  434. $where = 'frommodule = '. $this->moduleID;
  435. if ($multiArray = Query::FullResults('_relationships', $where)) {
  436. foreach($this->rawData as $id => $item) {
  437. foreach($multiArray as $multiData) {
  438. if($multiData['fromid'] == $id) {
  439. $this->rawData[$id][$this->multiRelatedModules[$multiData['tomodule']]][] = $multiData['toid'];
  440. }
  441. }
  442. }
  443. }
  444. }
  445. // Make a copy of the data for processing so we can keep the raw data available
  446. $this->processedData = $this->rawData;
  447. // Post-process data
  448. foreach($this->schema as $name => $info) {
  449. if ($info['relatedArray']) {
  450. // Fetch related array if one was specified for this field
  451. $relatedArray = $this->GetRelatedArray($name);
  452. }
  453. foreach ($this->processedData as $id => $data) {
  454. if ($this->processedData[$id][$name]) {
  455. switch ($info['type']) {
  456. case 'string':
  457. $this->processedData[$id][$name] = TextRenderer::SmartizeText($data[$name]);
  458. break;
  459. case 'text':
  460. case 'shorttext':
  461. if (!$info['wysiwyg']) {
  462. // Render text using TextRenderer if it's not a WYSIWYG field
  463. if (strstr($data[$name], "\n") !== false) {
  464. // String contains newline characters; format as multiline text
  465. $this->processedData[$id][$name] = TextRenderer::TextToHTML($data[$name]);
  466. } else {
  467. // String is a single line; format as single line
  468. $this->processedData[$id][$name] = TextRenderer::SmartizeText($data[$name]);
  469. }
  470. }
  471. break;
  472. case 'int':
  473. if ($relatedArray) {
  474. // If there's a related array, add the string representation to data
  475. $this->processedData[$id][$name .'_string'] = $relatedArray[$data[$name]];
  476. }
  477. break;
  478. case 'datetime':
  479. case 'timestamp':
  480. case 'date':
  481. case 'time':
  482. $this->processedData[$id][$name] = new Date($data[$name]);
  483. break;
  484. case 'file':
  485. $this->processedData[$id][$name] = $this->NestModule('files', $data[$name]);
  486. break;
  487. }
  488. }
  489. }
  490. }
  491. // Subclasses can provide a method to further format data
  492. if (method_exists($this, 'FormatData')) {
  493. $this->FormatData();
  494. }
  495. if ($this->items) {
  496. // If $this->items is already set, don't overwrite it
  497. return $this->processedData;
  498. } else {
  499. return $this->items = $this->processedData;
  500. }
  501. } else {
  502. return false;
  503. }
  504. }
  505. function LoadData($data) {
  506. return $this->item = $data;
  507. }
  508. function SetLayout($name) {
  509. global $_JAM;
  510. $this->layout->SetLayout($name .'.'. $_JAM->mode);
  511. }
  512. function Display() {
  513. global $_JAM;
  514. // Start output buffering
  515. ob_start('mb_output_handler');
  516. // Determine layout
  517. if (isset($this->layout)) {
  518. $layoutName = $this->config['layout'];
  519. if (!$layoutName) $layoutName = 'default';
  520. $layoutFile = $layoutName .'.'. $_JAM->mode;
  521. $this->layout->SetLayout($layoutFile);
  522. }
  523. // Determine whether we're a nested module
  524. if ($this->parentModule->name) {
  525. // Try to load view bearing parent module's name
  526. $viewDidLoad = $this->LoadView($this->parentModule->name);
  527. }
  528. // Determine whether we're looking at a single item in the module
  529. if (!$viewDidLoad && $this->itemID) {
  530. $viewDidLoad = $this->LoadView('item');
  531. }
  532. if (!$viewDidLoad) {
  533. $this->LoadView('default');
  534. }
  535. // Wrap into layout if we're the root module
  536. $buffer = ob_get_clean();
  537. if ($this->isRoot) {
  538. $this->layout->Display($buffer);
  539. } else {
  540. print $buffer;
  541. }
  542. }
  543. function LoadView($view) {
  544. global $_JAM;
  545. // Make sure we have sufficient privileges
  546. if (!$this->CanView()) {
  547. return false;
  548. }
  549. // Make sure a view was specified
  550. if (!$view) {
  551. return false;
  552. }
  553. // Try to load module data if an item ID is specified
  554. if ($this->schema && $this->itemID && !$this->items) {
  555. $this->FetchItem($this->itemID);
  556. }
  557. // Set a few variables that we'll need later
  558. $controllerMethodSuffix = 'ViewController';
  559. $moduleViewsDir = $this->modulePath .'views/';
  560. $viewFileSuffix = '.'. $_JAM->mode . '.php';
  561. // Determine controller method name
  562. $controllerMethod = ucfirst($view) . $controllerMethodSuffix;
  563. // Check whether we're in admin mode; that's a special case
  564. if ($_JAM->rootModuleName == 'admin') {
  565. // Run controller method if available
  566. $adminControllerMethod = 'Admin'. $controllerMethod;
  567. if (method_exists($this, $adminControllerMethod)) {
  568. $this->$adminControllerMethod();
  569. $methodDidRun = true;
  570. } elseif (method_exists($_JAM->rootModule, $adminControllerMethod)) {
  571. $_JAM->rootModule->$adminControllerMethod();
  572. $methodDidRun = true;
  573. }
  574. // Determine path to view file
  575. $viewFilename = 'admin_'. $view . $viewFileSuffix;
  576. $moduleViewPath = $moduleViewsDir . $viewFilename;
  577. $adminViewPath = $_JAM->rootModule->modulePath .'views/'. $viewFilename;
  578. if (file_exists($moduleViewPath)) {
  579. $viewPath = $moduleViewPath;
  580. } elseif (file_exists($adminViewPath)) {
  581. $viewPath = $adminViewPath;
  582. }
  583. } else {
  584. // Run controller method if available
  585. if (method_exists($this, $controllerMethod)) {
  586. $this->$controllerMethod();
  587. $methodDidRun = true;
  588. }
  589. // Determine path to view file
  590. $requestedViewPath = $moduleViewsDir . $view . $viewFileSuffix;
  591. if (file_exists($requestedViewPath)) {
  592. $viewPath = $requestedViewPath;
  593. }
  594. }
  595. // If no controller method has run and no view file has been found, abort process
  596. if (!$methodDidRun && !$viewPath) {
  597. return false;
  598. }
  599. // Load module data into view variables
  600. if ($this->item) {
  601. // If we have a single item, load directly into local symbol table
  602. extract($this->item);
  603. }
  604. if ($this->items) {
  605. // If we have multiple items, load as an array into $items
  606. $this->view['items'] = $this->items;
  607. }
  608. // Load view variables into local symbol table
  609. extract($this->view);
  610. // Include view file
  611. if ($viewPath) {
  612. include $viewPath;
  613. }
  614. return true;
  615. }
  616. function LoadViewInLayoutVariable($view) {
  617. global $_JAM;
  618. ob_start('mb_output_handler');
  619. $this->LoadView($view);
  620. $viewContent = ob_get_clean();
  621. // Add to layout
  622. $this->layout->AddVariable($view, $viewContent);
  623. // Add to template
  624. $_JAM->AddTemplateVariable($view, $viewContent);
  625. }
  626. function GetRelatedArray($field) {
  627. if ($relatedModule = $this->schema[$field]['relatedModule']) {
  628. $relatedModuleConfig = Module::ParseConfigFile($relatedModule, 'config/config.ini', true);
  629. // Look for keyQuery.ini
  630. if ($relatedQueryParams = Module::ParseConfigFile($relatedModule, 'config/keyQuery.ini', true)) {
  631. // Fetch array using specified query
  632. $relatedQuery = new Query($relatedQueryParams);
  633. } else {
  634. if (!$keyField = $relatedModuleConfig['keyField']) {
  635. // If no key field was specified in config file, use first field
  636. $relatedModuleSchema = Module::ParseConfigFile($relatedModule, 'config/schema.ini', true);
  637. reset($relatedModuleSchema);
  638. $keyField = key($relatedModuleSchema);
  639. }
  640. // If we do find a key field, build query according to that
  641. if ($keyField) {
  642. $params = array();
  643. if ($relatedModuleConfig['keepVersions']) {
  644. $params['fields']['id'] = 'IF(master IS NULL, id, master)';
  645. } else {
  646. $params['fields']['id'] = 'id';
  647. }
  648. $params['fields'][] = $keyField;
  649. $relatedQuery = new Query($params);
  650. }
  651. }
  652. // If we successfuly built a query, fetch data
  653. if ($relatedQuery) {
  654. $relatedQuery->AddFrom($relatedModule);
  655. // Manage versions
  656. if ($relatedModuleConfig['keepVersions']) {
  657. $relatedQuery->AddWhere($relatedModule .'.current = TRUE');
  658. }
  659. return $relatedQuery->GetSimpleArray();
  660. }
  661. } elseif ($relatedArray = $this->schema[$field]['relatedArray']) {
  662. // Array is specified in a config file
  663. $relatedArrays = IniFile::Parse($this->modulePath .'config/relatedArrays.ini', true);
  664. $relatedData = $relatedArrays[$relatedArray];
  665. // Look for localized strings
  666. foreach ($relatedData as $key => $label) {
  667. if ($string = $this->strings[$relatedArray][$label]) {
  668. $relatedData[$key] = $string;
  669. }
  670. }
  671. return $relatedData;
  672. } else {
  673. return false;
  674. }
  675. }
  676. function GetForm() {
  677. if (!$this->schema) {
  678. // This module doesn't have a corresponding table
  679. return false;
  680. }
  681. // Create Form object
  682. return new ModuleForm($this);
  683. }
  684. function AutoForm($fieldsArray = false, $hiddenFields = false) {
  685. global $_JAM;
  686. // Create Form object
  687. if (!$form = $this->GetForm()) return false;
  688. $form->Open();
  689. // Include language selection menu if applicable
  690. if (
  691. !$this->config['languageAgnostic'] &&
  692. (count($_JAM->projectConfig['languages']) > 1) &&
  693. !($fieldsArray && !@in_array('language', $fieldsArray))
  694. ) {
  695. if ($this->item) {
  696. print $form->Hidden('language');
  697. } else {
  698. foreach ($_JAM->projectConfig['languages'] as $language) {
  699. $languagesArray[$language] = $_JAM->strings['languages'][$language];
  700. }
  701. print $form->Popup('language', $languagesArray, $_JAM->strings['fields']['language']);
  702. }
  703. }
  704. // Include sortIndex field if applicable
  705. if ($this->config['allowSort']) {
  706. print $form->Field('sortIndex', '3', $_JAM->strings['fields']['sortIndex']);
  707. }
  708. foreach ($this->schema as $name => $info) {
  709. // Don't include basic module fields
  710. if (!$this->config['useCustomTable'] && $_JAM->moduleFields[$name]) {
  711. continue;
  712. }
  713. // Don't include versions support fields
  714. if ($this->config['keepVersions'] && $_JAM->versionsSupportFields[$name]) {
  715. continue;
  716. }
  717. // Skip this item if $fieldsArray is present and it doesn't contain this item
  718. if ($fieldsArray && !in_array($name, $fieldsArray)) {
  719. continue;
  720. }
  721. // Get proper title from string
  722. if (!$title = $this->strings['fields'][$name]) {
  723. // Use field name if no localized string is found
  724. $title = $name;
  725. }
  726. print $form->AutoItem($name, $title);
  727. }
  728. // Display related modules if we have item data
  729. if ($this->itemID) {
  730. foreach ($_JAM->installedModules as $module) {
  731. // First check whether we have a view configured for related module display
  732. if (
  733. ($relatedModuleSchema = Module::ParseConfigFile($module, 'config/schema.ini', true)) &&
  734. ($relatedModuleKeyQuery = Module::ParseConfigFile($module, 'config/keyQuery.ini', true))
  735. ) {
  736. foreach($relatedModuleSchema as $field => $info) {
  737. if ($info['relatedModule'] == $this->name) {
  738. $relatedModule = Module::GetNewModule($module);
  739. // Load all fields
  740. $queryParams = array(
  741. 'fields' => $relatedModuleKeyQuery['fields'],
  742. 'where' => $module .'.'. $field .' = '. $this->itemID
  743. );
  744. $relatedModule->FetchItems($queryParams);
  745. $relatedModule->LoadView('subform');
  746. }
  747. }
  748. }
  749. }
  750. }
  751. if ($hiddenFields) {
  752. foreach ($hiddenFields as $field => $value) {
  753. print $form->Hidden($field, $value);
  754. }
  755. }
  756. print $form->Submit();
  757. $form->Close();
  758. return true;
  759. }
  760. function ValidateData() {
  761. // First check whether we have sufficient privileges to insert
  762. if (!$_POST['master']) {
  763. // We're inserting, not updating
  764. if (!$this->CanInsert()) {
  765. trigger_error("Insufficient privileges to insert into module ". $this->name, E_USER_ERROR);
  766. return false;
  767. }
  768. }
  769. // Get data from $_POST and make sure required data is present
  770. foreach ($this->schema as $field => $info) {
  771. // Collect data from $_POST
  772. if (isset($_POST[$field])) {
  773. $this->postData[$field] = $_POST[$field];
  774. }
  775. // Look for missing data
  776. if (array_key_exists($field, $_POST) && !$_POST[$field] && $info['required']) {
  777. $this->missingData[] = $field;
  778. }
  779. switch ($info['type']) {
  780. case 'bool':
  781. // Bool types need to be manually inserted
  782. if ($info['type'] == 'bool') {
  783. $this->postData[$field] = $_POST[$field] ? 1 : 0;
  784. }
  785. break;
  786. case 'datetime':
  787. case 'timestamp':
  788. case 'date':
  789. // Reassemble datetime elements into a single string
  790. if (isset($_POST[$field .'_year'])) {
  791. $date['year'] = $_POST[$field .'_year'];
  792. $dateElements = array('month', 'day', 'hour', 'minutes', 'seconds');
  793. foreach ($dateElements as $element) {
  794. $date[$element] = Date::PadWithZeros($_POST[$field .'_'. $element]);
  795. }
  796. $dateString =
  797. $date['year'] .'-'. $date['month'] .'-'. $date['day'] .' '.
  798. $date['hour'] .':'. $date['minutes'] .':'. $date['seconds'];
  799. // Store values for each individual fields in case we don't have anything better
  800. foreach ($date as $element => $value) {
  801. $this->postData[$field .'_'. $element] = $value;
  802. }
  803. // Prepare and validate date
  804. $localDate = new Date($dateString, true);
  805. if ($localDate->isValid) {
  806. $databaseDate = $localDate->DatabaseTimestamp();
  807. $this->postData[$field] = $databaseDate;
  808. } else {
  809. $this->invalidData[] = $field;
  810. }
  811. }
  812. break;
  813. case 'time':
  814. $hour = $_POST[$field .'_hour'];
  815. $minutes = $_POST[$field .'_minutes'];
  816. $timeString = $hour .':'. $minutes;
  817. // Store values for both individual fields in case we don't have anything better
  818. $this->postData[$field .'_hour'] = $hour;
  819. $this->postData[$field .'_minutes'] = $minutes;
  820. // Prepare and validate date
  821. if (Date::ValidateTime($timeString)) {
  822. // Warning: Time is not localized according to database time
  823. $this->postData[$field] = $timeString;
  824. } else {
  825. $this->invalidData[] = $field;
  826. }
  827. break;
  828. case 'file':
  829. // Add data from $_POST manually for files
  830. $this->postData[$field] = $_POST[$field .'_id'];
  831. // Look for file upload errors
  832. $errorCode = $_FILES[$field]['error'];
  833. // The 'no file' error should not trigger an error
  834. if ($errorCode && $errorCode != UPLOAD_ERR_NO_FILE) {
  835. $this->fileUploadError = $errorCode;
  836. }
  837. // Add 'files' module if it doesn't exist
  838. if (!$this->files) {
  839. $this->files = $this->NestModule('files');
  840. }
  841. // Check whether a file needs to be deleted
  842. if ($_POST['deleteFile_'. $field]) {
  843. $this->files->DeleteItem($_POST[$field .'_id']);
  844. $this->postData[$field] = 0;
  845. }
  846. // Make sure file was uploaded correctly
  847. if ($_FILES[$field]['error'] === 0) {
  848. // Update 'files' table
  849. $this->postData[$field] = $this->files->AddUploadedFile($field);
  850. }
  851. break;
  852. }
  853. }
  854. // Language field should be included as well
  855. if (isset($_POST['language'])) {
  856. $this->postData['language'] = $_POST['language'];
  857. }
  858. }
  859. function ProcessData() {
  860. global $_JAM;
  861. // Validate data; this fills $this->postData
  862. $this->ValidateData();
  863. // Display error and abort if there is invalid or missing data or a file upload error
  864. if ($this->invalidData || $this->missingData || $this->fileUploadError) {
  865. return false;
  866. }
  867. // Clear cache entirely; very brutal but will do for now
  868. $_JAM->cache->Clear();
  869. // Run custom action method if available
  870. if ($action = $_POST['action']) {
  871. $actionMethod = $action . 'Action';
  872. if (method_exists($this, $actionMethod)) {
  873. $this->$actionMethod();
  874. return true;
  875. } elseif ($this->parentModule->name == 'admin') {
  876. // We're in admin mode; look for action in admin module
  877. if (method_exists($this->parentModule, $actionMethod)) {
  878. $this->parentModule->$actionMethod($this);
  879. return true;
  880. }
  881. }
  882. }
  883. // Determine what we need to insert from what was submitted
  884. foreach ($this->schema as $name => $info) {
  885. // Omit fields which we can't edit
  886. if ($info['canEdit'] && !$_JAM->user->HasPrivilege($info['canEdit'])) {
  887. continue;
  888. }
  889. // Make sure data exists, and exclude 'multi' fields; we handle them later
  890. if (isset($this->postData[$name]) && $info['type'] != 'multi') {
  891. if ($info['localizable']) {
  892. $localizedData[$name] = $this->postData[$name];
  893. } else {
  894. $insertData[$name] = $this->postData[$name];
  895. }
  896. }
  897. }
  898. if (!$_GET['item']) { // FIXME: More kludge! Translations again.
  899. if (!$this->config['useCustomTable']) {
  900. // This is a standard table with special fields
  901. // If user is logged in, insert user ID
  902. if ($_JAM->user->id) {
  903. $insertData['user'] = $_JAM->user->id;
  904. }
  905. }
  906. if (!$this->config['keepVersions']) {
  907. // Standard table; simple update
  908. if ($_POST['master']) {
  909. // Update mode
  910. $where = 'id = '. $_POST['master'];
  911. if (!$this->UpdateItems($insertData, $where)) {
  912. // Update failed
  913. trigger_error("Couldn't update module", E_USER_ERROR);
  914. return false;
  915. }
  916. $insertID = $_POST['master'];
  917. } else {
  918. // Post mode
  919. if (!$this->config['useCustomTable']) {
  920. $insertData['created'] = $_JAM->databaseTime;
  921. }
  922. if (!Database::Insert($this->name, $insertData)) {
  923. trigger_error("Couldn't insert into module ". $this->name, E_USER_ERROR);
  924. return false;
  925. }
  926. // Keep ID of inserted item for path
  927. $insertID = Database::GetLastInsertID();
  928. }
  929. } else {
  930. // Special update for tables with multiple versions support
  931. // Set item as current
  932. $insertData['current'] = true;
  933. // If we already have a creation date and one wasn't specified, use that
  934. if (!$insertData['created'] && $this->item['created']) {
  935. $insertData['created'] = $this->item['created'];
  936. }
  937. if (!Database::Insert($this->name, $insertData)) {
  938. trigger_error("Couldn't insert into module ". $this->name, E_USER_ERROR);
  939. } else {
  940. // Keep ID of inserted item for path
  941. $insertID = Database::GetLastInsertID();
  942. // $this->postData now represents actual data
  943. $this->LoadData($this->postData);
  944. // Disable all other items with the same master
  945. if ($insertData['master']) {
  946. $updateParams['current'] = false;
  947. $whereArray = array(
  948. array(
  949. 'master = '. $insertData['master'],
  950. 'id = '. $insertData['master']
  951. ),
  952. 'id != '. $insertID
  953. );
  954. $where = Database::GetWhereString($whereArray);
  955. if (!Database::Update($this->name, $updateParams, $where)) {
  956. trigger_error("Couldn't update module ". $this->name, E_USER_ERROR);
  957. return false;
  958. }
  959. }
  960. }
  961. }
  962. } else {
  963. // FIXME: Kuldgy. Added to make translations work.
  964. $insertID = $_GET['item'];
  965. }
  966. // Insert localized data
  967. if ($localizedData) {
  968. $tableName = $this->name .'_localized';
  969. $localizedData['item'] = $insertID;
  970. $localizedData['language'] = $this->postData['language'];
  971. $where = array('item = '. $insertID, "language = '". $localizedData['language'] ."'");
  972. if (Database::Update($tableName, $localizedData, $where)) {
  973. // Insert if no rows were affected
  974. if (Database::GetModifiedRows() == 0) {
  975. if (Database::Insert($tableName, $localizedData)) {
  976. $success = true;
  977. } else {
  978. trigger_error("Couldn't insert localized data for module ". $this->name, E_USER_ERROR);
  979. }
  980. } else {
  981. $success = true;
  982. }
  983. // Put data into module object to reflect changes in the database
  984. if ($success) {
  985. $this->LoadData($localizedData);
  986. }
  987. } else {
  988. trigger_error("Couldn't update localized data for module ". $this->name, E_USER_ERROR);
  989. return false;
  990. }
  991. }
  992. if ($insertID) {
  993. // Update path
  994. $this->UpdatePath($insertID);
  995. // Get ID for this item
  996. $id = $_POST['master'] ? $_POST['master'] : $insertID;
  997. // Delete previous many-to-many relationships
  998. $where = array(
  999. 'frommodule = '. $this->moduleID,
  1000. 'fromid = '. $insertID
  1001. );
  1002. if (!Database::DeleteFrom('_relationships', $where)) {
  1003. trigger_error("Couldn't delete previous many-to-many relationships for module ". $this->name, E_USER_ERROR);
  1004. }
  1005. foreach ($this->schema as $name => $info) {
  1006. switch ($info['type']) {
  1007. case 'multi':
  1008. // Insert many-to-many relationships
  1009. foreach ($this->postData[$name] as $targetID) {
  1010. // Insert each item into _relationships table
  1011. $targetModuleName = $info['relatedModule'];
  1012. $targetModuleID = array_search($targetModuleName, $_JAM->installedModules);
  1013. $params = array(
  1014. 'frommodule' => $this->moduleID,
  1015. 'fromid' => $insertID,
  1016. 'tomodule' => $targetModuleID,
  1017. 'toid' => $targetID
  1018. );
  1019. if (!Database::Insert('_relationships', $params)) {
  1020. trigger_error("Couldn't insert many-to-many relationship for module ". $this->name, E_USER_ERROR);
  1021. }
  1022. }
  1023. break;
  1024. }
  1025. }
  1026. }
  1027. if (method_exists($this, 'PostProcessData')) {
  1028. $this->PostProcessData($insertID);
  1029. }
  1030. // Check whether we need to redirect to a specific anchor
  1031. $anchor = $this->config['redirectToAnchor'][$this->parentModule->name];
  1032. // Reload page
  1033. if ($_JAM->rootModuleName == 'admin' || !$this->config['postSubmitRedirect']) {
  1034. HTTP::ReloadCurrentURL('?m=updated'. ($anchor ? '#' . $anchor : ''));
  1035. } else {
  1036. HTTP::RedirectLocal($this->config['postSubmitRedirect']);
  1037. }
  1038. }
  1039. function UpdateItems($params, $where) {
  1040. // Validate parameters
  1041. foreach ($params as $field => $value) {
  1042. if ($this->schema[$field]) {
  1043. $validatedParams[$field] = $value;
  1044. }
  1045. }
  1046. if (Database::Update($this->name, $validatedParams, $where)) {
  1047. return true;
  1048. } else {
  1049. trigger_error("Couldn't update database", E_USER_WARNING);
  1050. return false;
  1051. }
  1052. }
  1053. function GetPath() {
  1054. global $_JAM;
  1055. // Only run this method if 'autoPaths' switch is set
  1056. if (!$this->config['autoPaths']) return false;
  1057. if ($keyString = $this->item[$this->keyFieldName]) {
  1058. $parentPath = $this->config['path'][$_JAM->language];
  1059. return ($parentPath ? $parentPath : $this->name) .'/'. String::PrepareForURL($keyString);
  1060. } else {
  1061. trigger_error("Couldn't get path; probably lacking item data in module object", E_USER_ERROR);
  1062. }
  1063. }
  1064. function UpdatePath($id = null) {
  1065. global $_JAM;
  1066. // Check whether we have data
  1067. if (!$this->item) {
  1068. // We don't; we need to fetch data
  1069. $itemID = $_POST['master'] ? $_POST['master'] : $id;
  1070. // FIXME: Again, fucking kludge for translations.
  1071. if ($_POST['language']) {
  1072. $originalLanguage = $_JAM->language;
  1073. $_JAM->language = $_POST['language'];
  1074. }
  1075. if (!$this->FetchItem($itemID)) {
  1076. return false;
  1077. }
  1078. if ($originalLanguage) $_JAM->language = $originalLanguage;
  1079. }
  1080. $safeInsert = $this->config['forbidObsoletePaths'] ? false : true;
  1081. // Update path for module item
  1082. if ($path = $this->GetPath()) {
  1083. $pathItemID = $_POST['master'] ? $_POST['master'] : $id;
  1084. $language = $_POST['language'];
  1085. if ($insertedPath = Path::Insert($path, $this->moduleID, $pathItemID, $safeInsert, $language)) {
  1086. $this->item['path'] = $insertedPath;
  1087. } else {
  1088. trigger_error("Couldn't insert path in database", E_USER_ERROR);
  1089. return false;
  1090. }
  1091. }
  1092. // Update path for files
  1093. if ($this->files) {
  1094. foreach ($this->schema as $name => $info) {
  1095. if ($info['type'] == 'file') {
  1096. if (!is_object($this->item[$name])) {
  1097. $this->item[$name] = $this->NestModule('files', $this->item[$name]);
  1098. }
  1099. if ($filePath = $this->item[$name]->GetPath($name)) {
  1100. if (!Path::Insert($filePath, $this->files->moduleID, $this->item[$name]->itemID, $safeInsert)) {
  1101. trigger_error("Couldn't insert path for file associated with field ". $name ." in module ". $this->name, E_USER_ERROR);
  1102. }
  1103. }
  1104. }
  1105. }
  1106. }
  1107. }
  1108. function Revert($id) {
  1109. /*
  1110. // Determine master for this item
  1111. $master = Query::SingleValue($this->name, 'master', 'id = '. $id);
  1112. $master = $master ? $master : $id;
  1113. */
  1114. $master = $_POST['master'];
  1115. // Mark all versions of this item as non-current
  1116. $params = array('current' => false);
  1117. $where = array('id = '. $master .' OR master = '. $master);
  1118. if (!$this->UpdateItems($params, $where)) {
  1119. trigger_error("Failed to mark all versions of a module item as non-current", E_USER_ERROR);
  1120. return false;
  1121. }
  1122. // Mark specified version of this item as current
  1123. $params = array('current' => true);
  1124. $where = array('id = '. $id);
  1125. if ($this->UpdateItems($params, $where)) {
  1126. // Update path
  1127. $this->UpdatePath();
  1128. return true;
  1129. } else {
  1130. trigger_error("Couldn't mark specified version of item as current", E_USER_ERROR);
  1131. return false;
  1132. }
  1133. }
  1134. function DeleteItem($master) {
  1135. // First make sure we have sufficient privileges
  1136. if (!$this->CanDelete()) {
  1137. trigger_error("Insufficient privileges to delete from module ". $this->name, E_USER_ERROR);
  1138. return false;
  1139. }
  1140. // Delete item
  1141. if ($this->config['keepVersions']) {
  1142. $where = 'id = '. $master .' OR master = '. $master;
  1143. } else {
  1144. $where = 'id = '. $master;
  1145. }
  1146. if (Database::DeleteFrom($this->name, $where)) {
  1147. // Delete was successful; get rid of this item in _paths table
  1148. if (Path::DeleteAll($this->moduleID, $master)) {
  1149. return true;
  1150. } else {
  1151. trigger_error("Couldn't delete paths associated with deleted item", E_USER_ERROR);
  1152. return false;
  1153. }
  1154. // Eventually, delete from _relationships where frommodule = this module
  1155. } else {
  1156. if (Database::GetErrorNumber() == 1451) {
  1157. return ERROR_FOREIGN_KEY_CONSTRAINT;
  1158. } else {
  1159. trigger_error("Couldn't delete module item from database", E_USER_ERROR);
  1160. return false;
  1161. }
  1162. }
  1163. }
  1164. }
  1165. ?>