modelfactory.class.inc.php 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460
  1. <?php
  2. // Copyright (C) 2010-2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * ModelFactory: in-memory manipulation of the XML MetaModel
  20. *
  21. * @copyright Copyright (C) 2010-2016 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. require_once(APPROOT.'setup/moduleinstaller.class.inc.php');
  25. require_once(APPROOT.'setup/itopdesignformat.class.inc.php');
  26. require_once(APPROOT.'core/designdocument.class.inc.php');
  27. /**
  28. * Special exception type thrown when the XML stacking fails
  29. *
  30. */
  31. class MFException extends Exception
  32. {
  33. /**
  34. * @var integer
  35. */
  36. protected $iSourceLineNumber;
  37. /**
  38. * @var string
  39. */
  40. protected $sXPath;
  41. /**
  42. * @var string
  43. */
  44. protected $sExtraInfo;
  45. const COULD_NOT_BE_ADDED = 1;
  46. const COULD_NOT_BE_DELETED = 2;
  47. const COULD_NOT_BE_MODIFIED_NOT_FOUND = 3;
  48. const COULD_NOT_BE_MODIFIED_ALREADY_DELETED = 4;
  49. const INVALID_DELTA = 5;
  50. const ALREADY_DELETED = 6;
  51. const NOT_FOUND = 7;
  52. public function __construct ($message = null, $code = null, $iSourceLineNumber = 0, $sXPath = '', $sExtraInfo = '', $previous = null)
  53. {
  54. parent::__construct($message, $code, $previous);
  55. $this->iSourceLineNumber = $iSourceLineNumber;
  56. $this->sXPath = $sXPath;
  57. $this->sExtraInfo = $sExtraInfo;
  58. }
  59. /**
  60. * Get the source line number where the problem happened
  61. * @return number
  62. */
  63. public function GetSourceLineNumber()
  64. {
  65. return $this->iSourceLineNumber;
  66. }
  67. /**
  68. * Get the XPath in the whole document where the problem happened
  69. * @return string
  70. */
  71. public function GetXPath()
  72. {
  73. return $this->sXPath;
  74. }
  75. /**
  76. * Get some extra info (depending on the exception's code), like the invalid value for the _delta attribute
  77. * @return string
  78. */
  79. public function GetExtraInfo()
  80. {
  81. return $this->sExtraInfo;
  82. }
  83. }
  84. /**
  85. * ModelFactoryModule: the representation of a Module (i.e. element that can be selected during the setup)
  86. * @package ModelFactory
  87. */
  88. class MFModule
  89. {
  90. protected $sId;
  91. protected $sName;
  92. protected $sVersion;
  93. protected $sRootDir;
  94. protected $sLabel;
  95. protected $aDataModels;
  96. protected $bAutoSelect;
  97. protected $sAutoSelect;
  98. protected $aFilesToInclude;
  99. public function __construct($sId, $sRootDir, $sLabel, $bAutoSelect = false)
  100. {
  101. $this->sId = $sId;
  102. list($this->sName, $this->sVersion) = ModuleDiscovery::GetModuleName($sId);
  103. if (strlen($this->sVersion) == 0)
  104. {
  105. $this->sVersion = '1.0.0';
  106. }
  107. $this->sRootDir = $sRootDir;
  108. $this->sLabel = $sLabel;
  109. $this->aDataModels = array();
  110. $this->bAutoSelect = $bAutoSelect;
  111. $this->sAutoSelect = 'false';
  112. $this->aFilesToInclude = array('addons' => array(), 'business' => array(), 'webservices' => array(),);
  113. // Scan the module's root directory to find the datamodel(*).xml files
  114. if ($hDir = opendir($sRootDir))
  115. {
  116. // This is the correct way to loop over the directory. (according to the documentation)
  117. while (($sFile = readdir($hDir)) !== false)
  118. {
  119. if (preg_match('/^datamodel(.*)\.xml$/i', $sFile, $aMatches))
  120. {
  121. $this->aDataModels[] = $this->sRootDir.'/'.$aMatches[0];
  122. }
  123. }
  124. closedir($hDir);
  125. }
  126. }
  127. public function GetId()
  128. {
  129. return $this->sId;
  130. }
  131. public function GetName()
  132. {
  133. return $this->sName;
  134. }
  135. public function GetVersion()
  136. {
  137. return $this->sVersion;
  138. }
  139. public function GetLabel()
  140. {
  141. return $this->sLabel;
  142. }
  143. public function GetRootDir()
  144. {
  145. return $this->sRootDir;
  146. }
  147. public function GetModuleDir()
  148. {
  149. return basename($this->sRootDir);
  150. }
  151. public function GetDataModelFiles()
  152. {
  153. return $this->aDataModels;
  154. }
  155. /**
  156. * List all classes in this module
  157. */
  158. public function ListClasses()
  159. {
  160. return array();
  161. }
  162. public function GetDictionaryFiles()
  163. {
  164. $aDictionaries = array();
  165. if ($hDir = opendir($this->sRootDir))
  166. {
  167. while (($sFile = readdir($hDir)) !== false)
  168. {
  169. $aMatches = array();
  170. if (preg_match("/^[^\\.]+.dict.".$this->sName.".php$/i", $sFile, $aMatches)) // Dictionary files are named like <Lang>.dict.<ModuleName>.php
  171. {
  172. $aDictionaries[] = $this->sRootDir.'/'.$sFile;
  173. }
  174. }
  175. closedir($hDir);
  176. }
  177. return $aDictionaries;
  178. }
  179. public function IsAutoSelect()
  180. {
  181. return $this->bAutoSelect;
  182. }
  183. public function SetAutoSelect($sAutoSelect)
  184. {
  185. $this->sAutoSelect = $sAutoSelect;
  186. }
  187. public function GetAutoSelect()
  188. {
  189. return $this->sAutoSelect;
  190. }
  191. public function SetFilesToInclude($aFiles, $sCategory)
  192. {
  193. $sDir = basename($this->sRootDir);
  194. $iLen = strlen($sDir.'/');
  195. foreach($aFiles as $sFile)
  196. {
  197. $iPos = strpos($sFile, $sDir.'/');
  198. $this->aFilesToInclude[$sCategory][] = substr($sFile, $iPos+$iLen);
  199. }
  200. }
  201. public function GetFilesToInclude($sCategory)
  202. {
  203. return $this->aFilesToInclude[$sCategory];
  204. }
  205. }
  206. /**
  207. * MFDeltaModule: an optional module, made of a single file
  208. * @package ModelFactory
  209. */
  210. class MFDeltaModule extends MFModule
  211. {
  212. public function __construct($sDeltaFile)
  213. {
  214. $this->sId = 'datamodel-delta';
  215. $this->sName = 'delta';
  216. $this->sVersion = '1.0';
  217. $this->sRootDir = '';
  218. $this->sLabel = 'Additional Delta';
  219. $this->aDataModels = array($sDeltaFile);
  220. $this->aFilesToInclude = array('addons' => array(), 'business' => array(), 'webservices' => array(),);
  221. }
  222. public function GetName()
  223. {
  224. return ''; // Objects created inside this pseudo module retain their original module's name
  225. }
  226. public function GetRootDir()
  227. {
  228. return '';
  229. }
  230. public function GetModuleDir()
  231. {
  232. return '';
  233. }
  234. public function GetDictionaryFiles()
  235. {
  236. return array();
  237. }
  238. }
  239. /**
  240. * MFDeltaModule: an optional module, made of a single file
  241. * @package ModelFactory
  242. */
  243. class MFCoreModule extends MFModule
  244. {
  245. public function __construct($sName, $sLabel, $sDeltaFile)
  246. {
  247. $this->sId = $sName;
  248. $this->sName = $sName;
  249. $this->sVersion = '1.0';
  250. $this->sRootDir = '';
  251. $this->sLabel = $sLabel;
  252. $this->aDataModels = array($sDeltaFile);
  253. $this->aFilesToInclude = array('addons' => array(), 'business' => array(), 'webservices' => array(),);
  254. }
  255. public function GetRootDir()
  256. {
  257. return '';
  258. }
  259. public function GetModuleDir()
  260. {
  261. return '';
  262. }
  263. public function GetDictionaryFiles()
  264. {
  265. return array();
  266. }
  267. }
  268. /**
  269. * MFDictModule: an optional module, consisting only of dictionaries
  270. * @package ModelFactory
  271. */
  272. class MFDictModule extends MFModule
  273. {
  274. public function __construct($sName, $sLabel, $sRootDir)
  275. {
  276. $this->sId = $sName;
  277. $this->sName = $sName;
  278. $this->sVersion = '1.0';
  279. $this->sRootDir = $sRootDir;
  280. $this->sLabel = $sLabel;
  281. $this->aDataModels = array();
  282. $this->aFilesToInclude = array('addons' => array(), 'business' => array(), 'webservices' => array(),);
  283. }
  284. public function GetRootDir()
  285. {
  286. return '';
  287. }
  288. public function GetModuleDir()
  289. {
  290. return '';
  291. }
  292. public function GetDictionaryFiles()
  293. {
  294. $aDictionaries = array();
  295. if ($hDir = opendir($this->sRootDir))
  296. {
  297. while (($sFile = readdir($hDir)) !== false)
  298. {
  299. $aMatches = array();
  300. if (preg_match("/^.*dictionary\\.itop.*.php$/i", $sFile, $aMatches)) // Dictionary files are named like <Lang>.dict.<ModuleName>.php
  301. {
  302. $aDictionaries[] = $this->sRootDir.'/'.$sFile;
  303. }
  304. }
  305. closedir($hDir);
  306. }
  307. return $aDictionaries;
  308. }
  309. }
  310. /**
  311. * ModelFactory: the class that manages the in-memory representation of the XML MetaModel
  312. * @package ModelFactory
  313. */
  314. class ModelFactory
  315. {
  316. protected $aRootDirs;
  317. protected $oDOMDocument;
  318. protected $oRoot;
  319. protected $oModules;
  320. protected $oClasses;
  321. protected $oMenus;
  322. protected $oDictionaries;
  323. static protected $aLoadedClasses;
  324. static protected $aWellKnownParents = array('DBObject', 'CMDBObject','cmdbAbstractObject');
  325. // static protected $aWellKnownMenus = array('DataAdministration', 'Catalogs', 'ConfigManagement', 'Contact', 'ConfigManagementCI', 'ConfigManagement:Shortcuts', 'ServiceManagement');
  326. static protected $aLoadedModules;
  327. static protected $aLoadErrors;
  328. protected $aDict;
  329. protected $aDictKeys;
  330. public function __construct($aRootDirs, $aRootNodeExtensions = array())
  331. {
  332. $this->aDict = array();
  333. $this->aDictKeys = array();
  334. $this->aRootDirs = $aRootDirs;
  335. $this->oDOMDocument = new MFDocument();
  336. $this->oRoot = $this->oDOMDocument->CreateElement('itop_design');
  337. $this->oRoot->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
  338. $this->oRoot->setAttribute('version', ITOP_DESIGN_LATEST_VERSION);
  339. $this->oDOMDocument->AppendChild($this->oRoot);
  340. $this->oModules = $this->oDOMDocument->CreateElement('loaded_modules');
  341. $this->oRoot->AppendChild($this->oModules);
  342. $this->oClasses = $this->oDOMDocument->CreateElement('classes');
  343. $this->oRoot->AppendChild($this->oClasses);
  344. $this->oDictionaries = $this->oDOMDocument->CreateElement('dictionaries');
  345. $this->oRoot->AppendChild($this->oDictionaries);
  346. foreach (self::$aWellKnownParents as $sWellKnownParent)
  347. {
  348. $this->AddWellKnownParent($sWellKnownParent);
  349. }
  350. $this->oMenus = $this->oDOMDocument->CreateElement('menus');
  351. $this->oRoot->AppendChild($this->oMenus);
  352. $this->oMeta = $this->oDOMDocument->CreateElement('meta');
  353. $this->oRoot->AppendChild($this->oMeta);
  354. foreach($aRootNodeExtensions as $sElementName)
  355. {
  356. $oElement = $this->oDOMDocument->CreateElement($sElementName);
  357. $this->oRoot->AppendChild($oElement);
  358. }
  359. self::$aLoadedModules = array();
  360. self::$aLoadErrors = array();
  361. libxml_use_internal_errors(true);
  362. }
  363. public function Dump($oNode = null, $bReturnRes = false)
  364. {
  365. if (is_null($oNode))
  366. {
  367. $oNode = $this->oRoot;
  368. }
  369. return $oNode->Dump($bReturnRes);
  370. }
  371. public function LoadFromFile($sCacheFile)
  372. {
  373. $this->oDOMDocument->load($sCacheFile);
  374. $this->oRoot = $this->oDOMDocument->firstChild;
  375. $this->oModules = $this->oRoot->getElementsByTagName('loaded_modules')->item(0);
  376. self::$aLoadedModules = array();
  377. foreach($this->oModules->getElementsByTagName('module') as $oModuleNode)
  378. {
  379. $sId = $oModuleNode->getAttribute('id');
  380. $sRootDir = $oModuleNode->GetChildText('root_dir');
  381. $sLabel = $oModuleNode->GetChildText('label');
  382. self::$aLoadedModules[] = new MFModule($sId, $sRootDir, $sLabel);
  383. }
  384. }
  385. public function SaveToFile($sCacheFile)
  386. {
  387. $this->oDOMDocument->save($sCacheFile);
  388. }
  389. /**
  390. * To progressively replace LoadModule
  391. * @param MFElement $oSourceNode
  392. * @param MFElement $oTargetParentNode
  393. */
  394. public function LoadDelta($oSourceNode, $oTargetParentNode)
  395. {
  396. if (!$oSourceNode instanceof DOMElement) return;
  397. //echo "Load $oSourceNode->tagName::".$oSourceNode->getAttribute('id')." (".$oSourceNode->getAttribute('_delta').")<br/>\n";
  398. $oTarget = $this->oDOMDocument;
  399. $sDeltaSpec = $oSourceNode->getAttribute('_delta');
  400. if (($oSourceNode->tagName == 'class') && ($oSourceNode->parentNode->tagName == 'classes') && ($oSourceNode->parentNode->parentNode->tagName == 'itop_design'))
  401. {
  402. $sParentId = $oSourceNode->GetChildText('parent');
  403. if (($sDeltaSpec == 'define') || ($sDeltaSpec == 'force'))
  404. {
  405. // This tag is organized in hierarchy: determine the real parent node (as a subnode of the current node)
  406. $oTargetParentNode = $oTarget->GetNodeById('/itop_design/classes//class', $sParentId)->item(0);
  407. if (!$oTargetParentNode)
  408. {
  409. echo "Dumping target doc - looking for '$sParentId'<br/>\n";
  410. $this->oDOMDocument->firstChild->Dump();
  411. $sPath = MFDocument::GetItopNodePath($oSourceNode);
  412. $iLine = $oSourceNode->getLineNo();
  413. throw new MFException($sPath.' at line '.$iLine.": could not be found", MFException::PARENT_NOT_FOUND, $iLine, $sPath, $sParentId);
  414. }
  415. }
  416. else
  417. {
  418. $oTargetNode = $oTarget->GetNodeById('/itop_design/classes//class', $oSourceNode->getAttribute('id'))->item(0);
  419. if (!$oTargetNode)
  420. {
  421. if ($sDeltaSpec === 'if_exists')
  422. {
  423. // Just ignore it
  424. }
  425. else
  426. {
  427. echo "Dumping target doc - looking for '".$oSourceNode->getAttribute('id')."'<br/>\n";
  428. $this->oDOMDocument->firstChild->Dump();
  429. $sPath = MFDocument::GetItopNodePath($oSourceNode);
  430. $iLine = $oSourceNode->getLineNo();
  431. throw new MFException($sPath.' at line '.$iLine.": could not be found", MFException::NOT_FOUND, $iLine, $sPath);
  432. }
  433. }
  434. else
  435. {
  436. $oTargetParentNode = $oTargetNode->parentNode;
  437. if (($sDeltaSpec == 'redefine') && ($oTargetParentNode->getAttribute('id') != $sParentId))
  438. {
  439. // A class that has moved <=> deletion and creation elsewhere
  440. $oTargetParentNode = $oTarget->GetNodeById('/itop_design/classes//class', $sParentId)->item(0);
  441. $oTargetNode->Delete();
  442. $oSourceNode->setAttribute('_delta', 'define');
  443. $sDeltaSpec = 'define';
  444. }
  445. }
  446. }
  447. }
  448. switch ($sDeltaSpec)
  449. {
  450. case 'if_exists':
  451. case 'must_exist':
  452. case 'merge':
  453. case '':
  454. $bMustExist = ($sDeltaSpec == 'must_exist');
  455. $bIfExists =($sDeltaSpec == 'if_exists');
  456. $sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id');
  457. $oTargetNode = $oSourceNode->MergeInto($oTargetParentNode, $sSearchId, $bMustExist, $bIfExists);
  458. if ($oTargetNode)
  459. {
  460. foreach ($oSourceNode->childNodes as $oSourceChild)
  461. {
  462. // Continue deeper
  463. $this->LoadDelta($oSourceChild, $oTargetNode);
  464. }
  465. }
  466. break;
  467. case 'define_if_not_exists':
  468. $oExistingNode = $oTargetParentNode->_FindChildNode($oSourceNode);
  469. if ( ($oExistingNode == null) || ($oExistingNode->getAttribute('_alteration') == 'removed') )
  470. {
  471. // Same as 'define' below
  472. $oTargetNode = $oTarget->ImportNode($oSourceNode, true);
  473. $oTargetParentNode->AddChildNode($oTargetNode);
  474. }
  475. else
  476. {
  477. $oTargetNode = $oExistingNode;
  478. }
  479. $oTargetNode->setAttribute('_alteration', 'needed');
  480. break;
  481. case 'define':
  482. // New node - copy child nodes as well
  483. $oTargetNode = $oTarget->ImportNode($oSourceNode, true);
  484. $oTargetParentNode->AddChildNode($oTargetNode);
  485. break;
  486. case 'force':
  487. // Force node - copy child nodes as well
  488. $oTargetNode = $oTarget->ImportNode($oSourceNode, true);
  489. $oTargetParentNode->SetChildNode($oTargetNode, null, true);
  490. break;
  491. case 'redefine':
  492. // Replace the existing node by the given node - copy child nodes as well
  493. $oTargetNode = $oTarget->ImportNode($oSourceNode, true);
  494. $sSearchId = $oSourceNode->hasAttribute('_rename_from') ? $oSourceNode->getAttribute('_rename_from') : $oSourceNode->getAttribute('id');
  495. $oTargetParentNode->RedefineChildNode($oTargetNode, $sSearchId);
  496. break;
  497. case 'delete_if_exists':
  498. $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode);
  499. if (($oTargetNode !== null) && ($oTargetNode->getAttribute('_alteration') !== 'removed'))
  500. {
  501. // Delete the node if it actually exists and is not already marked as deleted
  502. $oTargetNode->Delete();
  503. }
  504. // otherwise fail silently
  505. break;
  506. case 'delete':
  507. $oTargetNode = $oTargetParentNode->_FindChildNode($oSourceNode);
  508. $sPath = MFDocument::GetItopNodePath($oSourceNode);
  509. $iLine = $oSourceNode->getLineNo();
  510. if ($oTargetNode == null)
  511. {
  512. throw new MFException($sPath.' at line '.$iLine.": could not be deleted (not found)", MFException::COULD_NOT_BE_DELETED, $iLine, $sPath);
  513. }
  514. if ($oTargetNode->getAttribute('_alteration') == 'removed')
  515. {
  516. throw new MFException($sPath.' at line '.$iLine.": could not be deleted (already marked as deleted)", MFException::ALREADY_DELETED, $iLine, $sPath);
  517. }
  518. $oTargetNode->Delete();
  519. break;
  520. default:
  521. $sPath = MFDocument::GetItopNodePath($oSourceNode);
  522. $iLine = $oSourceNode->getLineNo();
  523. throw new MFException($sPath.' at line '.$iLine.": unexpected value for attribute _delta: '".$sDeltaSpec."'", MFException::INVALID_DELTA, $iLine, $sPath, $sDeltaSpec);
  524. }
  525. if ($oTargetNode)
  526. {
  527. if ($oSourceNode->hasAttribute('_rename_from'))
  528. {
  529. $oTargetNode->Rename($oSourceNode->getAttribute('id'));
  530. }
  531. if ($oTargetNode->hasAttribute('_delta'))
  532. {
  533. $oTargetNode->removeAttribute('_delta');
  534. }
  535. }
  536. }
  537. /**
  538. * Loads the definitions corresponding to the given Module
  539. * @param MFModule $oModule
  540. * @param Array $aLanguages The list of languages to process (for the dictionaries). If empty all languages are kept
  541. */
  542. public function LoadModule(MFModule $oModule, $aLanguages = array())
  543. {
  544. try
  545. {
  546. $aDataModels = $oModule->GetDataModelFiles();
  547. $sModuleName = $oModule->GetName();
  548. $aClasses = array();
  549. self::$aLoadedModules[] = $oModule;
  550. // For persistence in the cache
  551. $oModuleNode = $this->oDOMDocument->CreateElement('module');
  552. $oModuleNode->setAttribute('id', $oModule->GetId());
  553. $oModuleNode->AppendChild($this->oDOMDocument->CreateElement('root_dir', $oModule->GetRootDir()));
  554. $oModuleNode->AppendChild($this->oDOMDocument->CreateElement('label', $oModule->GetLabel()));
  555. $this->oModules->AppendChild($oModuleNode);
  556. foreach($aDataModels as $sXmlFile)
  557. {
  558. $oDocument = new MFDocument();
  559. libxml_clear_errors();
  560. $oDocument->load($sXmlFile);
  561. //$bValidated = $oDocument->schemaValidate(APPROOT.'setup/itop_design.xsd');
  562. $aErrors = libxml_get_errors();
  563. if (count($aErrors) > 0)
  564. {
  565. self::$aLoadErrors[$sModuleName] = $aErrors;
  566. return;
  567. }
  568. $oXPath = new DOMXPath($oDocument);
  569. $oNodeList = $oXPath->query('/itop_design/classes//class');
  570. foreach($oNodeList as $oNode)
  571. {
  572. if ($oNode->getAttribute('_created_in') == '')
  573. {
  574. $oNode->SetAttribute('_created_in', $sModuleName);
  575. }
  576. }
  577. $oNodeList = $oXPath->query('/itop_design/constants/constant');
  578. foreach($oNodeList as $oNode)
  579. {
  580. if ($oNode->getAttribute('_created_in') == '')
  581. {
  582. $oNode->SetAttribute('_created_in', $sModuleName);
  583. }
  584. }
  585. $oNodeList = $oXPath->query('/itop_design/menus/menu');
  586. foreach($oNodeList as $oNode)
  587. {
  588. if ($oNode->getAttribute('_created_in') == '')
  589. {
  590. $oNode->SetAttribute('_created_in', $sModuleName);
  591. }
  592. }
  593. $oUserRightsNode = $oXPath->query('/itop_design/user_rights')->item(0);
  594. if ($oUserRightsNode)
  595. {
  596. if ($oUserRightsNode->getAttribute('_created_in') == '')
  597. {
  598. $oUserRightsNode->SetAttribute('_created_in', $sModuleName);
  599. }
  600. }
  601. $oAlteredNodes = $oXPath->query('/itop_design//*[@_delta]');
  602. if ($oAlteredNodes->length > 0)
  603. {
  604. foreach($oAlteredNodes as $oAlteredNode)
  605. {
  606. $oAlteredNode->SetAttribute('_altered_in', $sModuleName);
  607. }
  608. }
  609. $oFormat = new iTopDesignFormat($oDocument);
  610. if (!$oFormat->Convert())
  611. {
  612. $sError = implode(', ', $oFormat->GetErrors());
  613. throw new Exception("Cannot load module $sModuleName, failed to upgrade to datamodel format of: $sXmlFile. Reason(s): $sError");
  614. }
  615. $oDeltaRoot = $oDocument->childNodes->item(0);
  616. $this->LoadDelta($oDeltaRoot, $this->oDOMDocument);
  617. }
  618. $aDictionaries = $oModule->GetDictionaryFiles();
  619. try
  620. {
  621. $this->ResetTempDictionary();
  622. foreach($aDictionaries as $sPHPFile)
  623. {
  624. $sDictFileContents = file_get_contents($sPHPFile);
  625. $sDictFileContents = str_replace(array('<'.'?'.'php', '?'.'>'), '', $sDictFileContents);
  626. $sDictFileContents = str_replace('Dict::Add', '$this->AddToTempDictionary', $sDictFileContents);
  627. eval($sDictFileContents);
  628. }
  629. foreach ($this->aDict as $sLanguageCode => $aDictDefinition)
  630. {
  631. if ((count($aLanguages) > 0 ) && !in_array($sLanguageCode, $aLanguages))
  632. {
  633. // skip some languages if the parameter says so
  634. continue;
  635. }
  636. $oNodes = $this->GetNodeById('dictionary', $sLanguageCode, $this->oDictionaries);
  637. if ($oNodes->length == 0)
  638. {
  639. $oXmlDict = $this->oDOMDocument->CreateElement('dictionary');
  640. $oXmlDict->setAttribute('id', $sLanguageCode);
  641. $this->oDictionaries->AddChildNode($oXmlDict);
  642. $oXmlEntries = $this->oDOMDocument->CreateElement('english_description', $aDictDefinition['english_description']);
  643. $oXmlDict->AppendChild($oXmlEntries);
  644. $oXmlEntries = $this->oDOMDocument->CreateElement('localized_description', $aDictDefinition['localized_description']);
  645. $oXmlDict->AppendChild($oXmlEntries);
  646. $oXmlEntries = $this->oDOMDocument->CreateElement('entries');
  647. $oXmlDict->AppendChild($oXmlEntries);
  648. }
  649. else
  650. {
  651. $oXmlDict = $oNodes->item(0);
  652. $oXmlEntries = $oXmlDict->GetUniqueElement('entries');
  653. }
  654. foreach ($aDictDefinition['entries'] as $sCode => $sLabel)
  655. {
  656. $oXmlEntry = $this->oDOMDocument->CreateElement('entry');
  657. $oXmlEntry->setAttribute('id', $sCode);
  658. $oXmlValue = $this->oDOMDocument->CreateCDATASection($sLabel);
  659. $oXmlEntry->appendChild($oXmlValue);
  660. if (array_key_exists($sLanguageCode, $this->aDictKeys) && array_key_exists($sCode, $this->aDictKeys[$sLanguageCode]))
  661. {
  662. $oMe = $this->aDictKeys[$sLanguageCode][$sCode];
  663. $sFlag = $oMe->getAttribute('_alteration');
  664. $oMe->parentNode->replaceChild($oXmlEntry, $oMe);
  665. $sNewFlag = $sFlag;
  666. if ($sFlag == '')
  667. {
  668. $sNewFlag = 'replaced';
  669. }
  670. $oXmlEntry->setAttribute('_alteration', $sNewFlag);
  671. }
  672. else
  673. {
  674. $oXmlEntry->setAttribute('_alteration', 'added');
  675. $oXmlEntries->appendChild($oXmlEntry);
  676. }
  677. $this->aDictKeys[$sLanguageCode][$sCode] = $oXmlEntry;
  678. }
  679. }
  680. }
  681. catch(Exception $e)
  682. {
  683. throw new Exception('Failed to load dictionary file "'.$sPHPFile.'", reason: '.$e->getMessage());
  684. }
  685. }
  686. catch(Exception $e)
  687. {
  688. $aLoadedModuleNames = array();
  689. foreach (self::$aLoadedModules as $oModule)
  690. {
  691. $aLoadedModuleNames[] = $oModule->GetName();
  692. }
  693. throw new Exception('Error loading module "'.$oModule->GetName().'": '.$e->getMessage().' - Loaded modules: '.implode(',', $aLoadedModuleNames));
  694. }
  695. }
  696. /**
  697. * Collects the PHP Dict entries into the ModelFactory for transforming the dictionary into an XML structure
  698. * @param string $sLanguageCode The language code
  699. * @param string $sEnglishLanguageDesc English description of the language (unused but kept for API compatibility)
  700. * @param string $sLocalizedLanguageDesc Localized description of the language (unused but kept for API compatibility)
  701. * @param hash $aEntries The entries to load: string_code => translation
  702. */
  703. protected function AddToTempDictionary($sLanguageCode, $sEnglishLanguageDesc, $sLocalizedLanguageDesc, $aEntries)
  704. {
  705. $this->aDict[$sLanguageCode]['english_description'] = $sEnglishLanguageDesc;
  706. $this->aDict[$sLanguageCode]['localized_description'] = $sLocalizedLanguageDesc;
  707. if (!array_key_exists('entries', $this->aDict[$sLanguageCode]))
  708. {
  709. $this->aDict[$sLanguageCode]['entries'] = array();
  710. }
  711. foreach($aEntries as $sKey => $sValue)
  712. {
  713. $this->aDict[$sLanguageCode]['entries'][$sKey] = $sValue;
  714. }
  715. }
  716. protected function ResetTempDictionary()
  717. {
  718. $this->aDict = array();
  719. }
  720. /**
  721. * XML load errors (XML format and validation)
  722. */
  723. function HasLoadErrors()
  724. {
  725. return (count(self::$aLoadErrors) > 0);
  726. }
  727. function GetLoadErrors()
  728. {
  729. return self::$aLoadErrors;
  730. }
  731. function GetLoadedModules($bExcludeWorkspace = true)
  732. {
  733. if ($bExcludeWorkspace)
  734. {
  735. $aModules = array();
  736. foreach(self::$aLoadedModules as $oModule)
  737. {
  738. if (!$oModule instanceof MFWorkspace)
  739. {
  740. $aModules[] = $oModule;
  741. }
  742. }
  743. }
  744. else
  745. {
  746. $aModules = self::$aLoadedModules;
  747. }
  748. return $aModules;
  749. }
  750. function GetModule($sModuleName)
  751. {
  752. foreach(self::$aLoadedModules as $oModule)
  753. {
  754. if ($oModule->GetName() == $sModuleName) return $oModule;
  755. }
  756. return null;
  757. }
  758. public function CreateElement($sTagName, $sValue = '')
  759. {
  760. return $this->oDOMDocument->createElement($sTagName, $sValue);
  761. }
  762. public function GetNodeById($sXPath, $sId, $oContextNode = null)
  763. {
  764. return $this->oDOMDocument->GetNodeById($sXPath, $sId, $oContextNode);
  765. }
  766. /**
  767. * Apply extensibility rules into the DOM
  768. * @param array aRestrictionRules Array of array ('selectors' => array of XPaths, 'rules' => array of rules)
  769. * @return void
  770. */
  771. public function RestrictExtensibility($aRestrictionRules)
  772. {
  773. foreach ($aRestrictionRules as $aRestriction)
  774. {
  775. foreach ($aRestriction['selectors'] as $sSelector)
  776. {
  777. foreach($this->GetNodes($sSelector) as $oNode)
  778. {
  779. $oNode->RestrictExtensibility($aRestriction['rules']);
  780. }
  781. }
  782. }
  783. }
  784. /**
  785. * Check if the class specified by the given node already exists in the loaded DOM
  786. * @param DOMNode $oClassNode The node corresponding to the class to load
  787. * @throws Exception
  788. * @return bool True if the class exists, false otherwise
  789. */
  790. protected function ClassExists(DOMNode $oClassNode)
  791. {
  792. assert(false);
  793. if ($oClassNode->hasAttribute('id'))
  794. {
  795. $sClassName = $oClassNode->GetAttribute('id');
  796. }
  797. else
  798. {
  799. throw new Exception('ModelFactory::AddClass: Cannot add a class with no name');
  800. }
  801. return (array_key_exists($sClassName, self::$aLoadedClasses));
  802. }
  803. /**
  804. * Check if the class specified by the given name already exists in the loaded DOM
  805. * @param string $sClassName The node corresponding to the class to load
  806. * @throws Exception
  807. * @return bool True if the class exists, false otherwise
  808. */
  809. protected function ClassNameExists($sClassName)
  810. {
  811. return !is_null($this->GetClass($sClassName));
  812. }
  813. /**
  814. * Add the given class to the DOM
  815. * @param DOMNode $oClassNode
  816. * @param string $sModuleName The name of the module in which this class is declared
  817. * @throws Exception
  818. */
  819. public function AddClass(DOMNode $oClassNode, $sModuleName)
  820. {
  821. if ($oClassNode->hasAttribute('id'))
  822. {
  823. $sClassName = $oClassNode->GetAttribute('id');
  824. }
  825. else
  826. {
  827. throw new Exception('ModelFactory::AddClass: Cannot add a class with no name');
  828. }
  829. if ($this->ClassNameExists($oClassNode->getAttribute('id')))
  830. {
  831. throw new Exception("ModelFactory::AddClass: Cannot add the already existing class $sClassName");
  832. }
  833. $sParentClass = $oClassNode->GetChildText('parent', '');
  834. $oParentNode = $this->GetClass($sParentClass);
  835. if ($oParentNode == null)
  836. {
  837. throw new Exception("ModelFactory::AddClass: Cannot find the parent class of '$sClassName': '$sParentClass'");
  838. }
  839. else
  840. {
  841. if ($sModuleName != '')
  842. {
  843. $oClassNode->SetAttribute('_created_in', $sModuleName);
  844. }
  845. $oParentNode->AddChildNode($this->oDOMDocument->importNode($oClassNode, true));
  846. if (substr($sParentClass, 0, 1) == '/') // Convention for well known parent classes
  847. {
  848. // Remove the leading slash character
  849. $oParentNameNode = $oClassNode->GetOptionalElement('parent')->firstChild; // Get the DOMCharacterData node
  850. $oParentNameNode->data = substr($sParentClass, 1);
  851. }
  852. }
  853. }
  854. public function GetClassXMLTemplate($sName, $sIcon)
  855. {
  856. $sHeader = '<?'.'xml version="1.0" encoding="utf-8"?'.'>';
  857. return
  858. <<<EOF
  859. $sHeader
  860. <class id="$sName">
  861. <comment/>
  862. <properties>
  863. </properties>
  864. <naming format=""><attributes/></naming>
  865. <reconciliation><attributes/></reconciliation>
  866. <display_template/>
  867. <icon>$sIcon</icon>
  868. </properties>
  869. <fields/>
  870. <lifecycle/>
  871. <methods/>
  872. <presentation>
  873. <details><items/></details>
  874. <search><items/></search>
  875. <list><items/></list>
  876. </presentation>
  877. </class>
  878. EOF
  879. ;
  880. }
  881. /**
  882. * List all constants from the DOM, for a given module
  883. * @param string $sModuleName
  884. * @throws Exception
  885. */
  886. public function ListConstants($sModuleName)
  887. {
  888. return $this->GetNodes("/itop_design/constants/constant[@_created_in='$sModuleName']");
  889. }
  890. /**
  891. * List all classes from the DOM, for a given module
  892. * @param string $sModuleName
  893. * @throws Exception
  894. */
  895. public function ListClasses($sModuleName)
  896. {
  897. return $this->GetNodes("/itop_design/classes//class[@_created_in='$sModuleName']");
  898. }
  899. /**
  900. * List all classes from the DOM
  901. * @throws Exception
  902. */
  903. public function ListAllClasses()
  904. {
  905. return $this->GetNodes("/itop_design/classes//class");
  906. }
  907. /**
  908. * List top level (non abstract) classes having child classes
  909. * @throws Exception
  910. */
  911. public function ListRootClasses()
  912. {
  913. return $this->GetNodes("/itop_design/classes/class/class[class]");
  914. }
  915. public function GetClass($sClassName)
  916. {
  917. $oClassNode = $this->GetNodes("/itop_design/classes//class[@id='$sClassName']")->item(0);
  918. return $oClassNode;
  919. }
  920. public function AddWellKnownParent($sWellKnownParent)
  921. {
  922. $oWKClass = $this->oDOMDocument->CreateElement('class');
  923. $oWKClass->setAttribute('id', $sWellKnownParent);
  924. $this->oClasses->AppendChild($oWKClass);
  925. return $oWKClass;
  926. }
  927. public function GetChildClasses($oClassNode)
  928. {
  929. return $this->GetNodes("class", $oClassNode);
  930. }
  931. public function GetField($sClassName, $sAttCode)
  932. {
  933. if (!$this->ClassNameExists($sClassName))
  934. {
  935. return null;
  936. }
  937. $oClassNode = self::$aLoadedClasses[$sClassName];
  938. $oFieldNode = $this->GetNodes("fields/field[@id='$sAttCode']", $oClassNode)->item(0);
  939. if (($oFieldNode == null) && ($sParentClass = $oClassNode->GetChildText('parent')))
  940. {
  941. return $this->GetField($sParentClass, $sAttCode);
  942. }
  943. return $oFieldNode;
  944. }
  945. /**
  946. * List all classes from the DOM
  947. * @throws Exception
  948. */
  949. public function ListFields(DOMNode $oClassNode)
  950. {
  951. return $this->GetNodes("fields/field", $oClassNode);
  952. }
  953. /**
  954. * List all transitions from a given state
  955. * @param DOMNode $oStateNode The state
  956. * @throws Exception
  957. */
  958. public function ListTransitions(DOMNode $oStateNode)
  959. {
  960. return $this->GetNodes("transitions/transition", $oStateNode);
  961. }
  962. /**
  963. * List all states of a given class
  964. * @param DOMNode $oClassNode The class
  965. * @throws Exception
  966. */
  967. public function ListStates(DOMNode $oClassNode)
  968. {
  969. return $this->GetNodes("lifecycle/states/state", $oClassNode);
  970. }
  971. public function ApplyChanges()
  972. {
  973. return $this->oRoot->ApplyChanges();
  974. }
  975. public function ListChanges()
  976. {
  977. return $this->oRoot->ListChanges();
  978. }
  979. /**
  980. * Import the node into the delta
  981. */
  982. protected function SetDeltaFlags($oNodeClone)
  983. {
  984. $sAlteration = $oNodeClone->getAttribute('_alteration');
  985. $oNodeClone->removeAttribute('_alteration');
  986. if ($oNodeClone->hasAttribute('_old_id'))
  987. {
  988. $oNodeClone->setAttribute('_rename_from', $oNodeClone->getAttribute('_old_id'));
  989. $oNodeClone->removeAttribute('_old_id');
  990. }
  991. switch ($sAlteration)
  992. {
  993. case '':
  994. if ($oNodeClone->hasAttribute('id'))
  995. {
  996. $oNodeClone->setAttribute('_delta', 'must_exist');
  997. }
  998. break;
  999. case 'added':
  1000. $oNodeClone->setAttribute('_delta', 'define');
  1001. break;
  1002. case 'replaced':
  1003. $oNodeClone->setAttribute('_delta', 'redefine');
  1004. break;
  1005. case 'removed':
  1006. $oNodeClone->setAttribute('_delta', 'delete');
  1007. break;
  1008. case 'needed':
  1009. $oNodeClone->setAttribute('_delta', 'define_if_not_exists');
  1010. break;
  1011. case 'forced':
  1012. $oNodeClone->setAttribute('_delta', 'force');
  1013. break;
  1014. }
  1015. return $oNodeClone;
  1016. }
  1017. /**
  1018. * Create path for the delta
  1019. * @param Array aMovedClasses The classes that have been moved in the hierarchy (deleted + created elsewhere)
  1020. * @param DOMDocument oTargetDoc Where to attach the top of the hierarchy
  1021. * @param MFElement oNode The node to import with its path
  1022. */
  1023. protected function ImportNodeAndPathDelta($aMovedClasses, $oTargetDoc, $oNode)
  1024. {
  1025. // Preliminary: skip the parent if this node is organized hierarchically into the DOM
  1026. // Only class nodes are organized this way
  1027. $oParent = $oNode->parentNode;
  1028. if ($oNode->IsClassNode())
  1029. {
  1030. while (($oParent instanceof DOMElement) && ($oParent->IsClassNode()))
  1031. {
  1032. $oParent = $oParent->parentNode;
  1033. }
  1034. }
  1035. // Recursively create the path for the parent
  1036. if ($oParent instanceof DOMElement)
  1037. {
  1038. $oParentClone = $this->ImportNodeAndPathDelta($aMovedClasses, $oTargetDoc, $oParent);
  1039. }
  1040. else
  1041. {
  1042. // We've reached the top let's add the node into the root recipient
  1043. $oParentClone = $oTargetDoc;
  1044. }
  1045. $sAlteration = $oNode->getAttribute('_alteration');
  1046. if ($oNode->IsClassNode() && ($sAlteration != ''))
  1047. {
  1048. // Handle the moved classes
  1049. //
  1050. // Import the whole root node
  1051. $oNodeClone = $oTargetDoc->importNode($oNode->cloneNode(true), true);
  1052. $oParentClone->appendChild($oNodeClone);
  1053. $this->SetDeltaFlags($oNodeClone);
  1054. // Handle the moved classes found under the root node (or the root node itself)
  1055. foreach($oNodeClone->GetNodes("descendant-or-self::class[@id]", false) as $oClassNode)
  1056. {
  1057. if (array_key_exists($oClassNode->getAttribute('id'), $aMovedClasses))
  1058. {
  1059. if ($sAlteration == 'removed')
  1060. {
  1061. // Remove that node: this specification will be overriden by the 'replaced' spec (see below)
  1062. $oClassNode->parentNode->removeChild($oClassNode);
  1063. }
  1064. else
  1065. {
  1066. // Move the class at the root, with the flag 'modified'
  1067. $oParentClone->appendChild($oClassNode);
  1068. $oClassNode->setAttribute('_alteration', 'replaced');
  1069. $this->SetDeltaFlags($oClassNode);
  1070. }
  1071. }
  1072. }
  1073. }
  1074. else
  1075. {
  1076. // Look for the node into the parent node
  1077. // Note: this is an identified weakness of the algorithm,
  1078. // because for each node modified, and each node of its path
  1079. // we will have to lookup for the existing entry
  1080. // Anyhow, this loop is quite quick to execute because in the delta
  1081. // the number of nodes is limited
  1082. $oNodeClone = null;
  1083. foreach ($oParentClone->childNodes as $oChild)
  1084. {
  1085. if (($oChild instanceof DOMElement) && ($oChild->tagName == $oNode->tagName))
  1086. {
  1087. if (!$oNode->hasAttribute('id') || ($oNode->getAttribute('id') == $oChild->getAttribute('id')))
  1088. {
  1089. $oNodeClone = $oChild;
  1090. break;
  1091. }
  1092. }
  1093. }
  1094. if (!$oNodeClone)
  1095. {
  1096. $bCopyContents = ($sAlteration == 'replaced') || ($sAlteration == 'added') || ($sAlteration == 'needed') || ($sAlteration == 'forced');
  1097. $oNodeClone = $oTargetDoc->importNode($oNode->cloneNode($bCopyContents), $bCopyContents);
  1098. $this->SetDeltaFlags($oNodeClone);
  1099. $oParentClone->appendChild($oNodeClone);
  1100. }
  1101. }
  1102. return $oNodeClone;
  1103. }
  1104. /**
  1105. * Set the value for a given trace attribute
  1106. * See MFElement::SetTrace to enable/disable change traces
  1107. */
  1108. public function SetTraceValue($sAttribute, $sPreviousValue, $sNewValue)
  1109. {
  1110. // Search into the deleted node as well!
  1111. $oNodeSet = $this->oDOMDocument->GetNodes("//*[@$sAttribute='$sPreviousValue']", null, false);
  1112. foreach($oNodeSet as $oTouchedNode)
  1113. {
  1114. $oTouchedNode->setAttribute($sAttribute, $sNewValue);
  1115. }
  1116. }
  1117. /**
  1118. * Get the document version of the delta
  1119. */
  1120. public function GetDeltaDocument($aNodesToIgnore = array(), $aAttributes = null)
  1121. {
  1122. $oDelta = new MFDocument();
  1123. // Handle classes moved from one parent to another
  1124. // This will be done in two steps:
  1125. // 1) Identify the moved classes (marked as deleted under the original parent, and created under the new parent)
  1126. // 2) When importing those "moved" classes into the delta (see ImportNodeAndPathDelta), extract them from the hierarchy (the alteration can be done at an upper level in the hierarchy) and mark them as "modified"
  1127. $aMovedClasses = array();
  1128. foreach($this->GetNodes("/itop_design/classes//class/class[@_alteration='removed']", null, false) as $oNode)
  1129. {
  1130. $sId = $oNode->getAttribute('id');
  1131. if ($this->GetNodes("/itop_design/classes//class/class[@id='$sId']/properties", null, false)->length > 0)
  1132. {
  1133. $aMovedClasses[$sId] = true;
  1134. }
  1135. }
  1136. foreach($this->ListChanges() as $oAlteredNode)
  1137. {
  1138. $this->ImportNodeAndPathDelta($aMovedClasses, $oDelta, $oAlteredNode);
  1139. }
  1140. foreach($aNodesToIgnore as $sXPath)
  1141. {
  1142. $oNodesToRemove = $oDelta->GetNodes($sXPath);
  1143. foreach($oNodesToRemove as $oNode)
  1144. {
  1145. if ($oNode instanceof DOMAttr)
  1146. {
  1147. $oNode->ownerElement->removeAttributeNode($oNode);
  1148. }
  1149. else
  1150. {
  1151. $oNode->parentNode->removeChild($oNode);
  1152. }
  1153. }
  1154. }
  1155. $oNodesToClean = $oDelta->GetNodes('/itop_design//*[@_altered_in]');
  1156. foreach($oNodesToClean as $oNode)
  1157. {
  1158. $oNode->removeAttribute('_altered_in');
  1159. }
  1160. if ($aAttributes != null)
  1161. {
  1162. foreach ($aAttributes as $sAttribute => $value)
  1163. {
  1164. if ($oDelta->documentElement) // yes, this may happen when still no change has been performed (and a module has been selected for installation)
  1165. {
  1166. $oDelta->documentElement->setAttribute($sAttribute, $value);
  1167. }
  1168. }
  1169. }
  1170. return $oDelta;
  1171. }
  1172. /**
  1173. * Get the text/XML version of the delta
  1174. */
  1175. public function GetDelta($aNodesToIgnore = array(), $aAttributes = null)
  1176. {
  1177. $oDelta = $this->GetDeltaDocument($aNodesToIgnore, $aAttributes);
  1178. return $oDelta->saveXML();
  1179. }
  1180. /**
  1181. * Searches on disk in the root directories for module description files
  1182. * and returns an array of MFModules
  1183. * @return array Array of MFModules
  1184. */
  1185. public function FindModules()
  1186. {
  1187. $aAvailableModules = ModuleDiscovery::GetAvailableModules($this->aRootDirs);
  1188. $aResult = array();
  1189. foreach($aAvailableModules as $sId => $aModule)
  1190. {
  1191. $oModule = new MFModule($sId, $aModule['root_dir'], $aModule['label'], isset($aModule['auto_select']));
  1192. if (isset($aModule['auto_select']))
  1193. {
  1194. $oModule->SetAutoSelect($aModule['auto_select']);
  1195. }
  1196. if (isset($aModule['datamodel']) && is_array($aModule['datamodel']))
  1197. {
  1198. $oModule->SetFilesToInclude($aModule['datamodel'], 'business');
  1199. }
  1200. if (isset($aModule['webservice']) && is_array($aModule['webservice']))
  1201. {
  1202. $oModule->SetFilesToInclude($aModule['webservice'], 'webservices');
  1203. }
  1204. if (isset($aModule['addons']) && is_array($aModule['addons']))
  1205. {
  1206. $oModule->SetFilesToInclude($aModule['addons'], 'addons');
  1207. }
  1208. $aResult[] = $oModule;
  1209. }
  1210. return $aResult;
  1211. }
  1212. public function TestAlteration()
  1213. {
  1214. try
  1215. {
  1216. $sHeader = '<?xml version="1.0" encoding="utf-8"?'.'>';
  1217. $sOriginalXML =
  1218. <<<EOF
  1219. $sHeader
  1220. <itop_design>
  1221. <a id="first a">
  1222. <b>Text</b>
  1223. <c id="1">
  1224. <d>D1</d>
  1225. <d>D2</d>
  1226. </c>
  1227. </a>
  1228. <a id="second a">
  1229. <parent>first a</parent>
  1230. </a>
  1231. <a id="third a">
  1232. <parent>first a</parent>
  1233. <x>blah</x>
  1234. </a>
  1235. </itop_design>
  1236. EOF;
  1237. $this->oDOMDocument = new MFDocument();
  1238. $this->oDOMDocument->loadXML($sOriginalXML);
  1239. // DOM Get the original values, then modify its contents by the mean of the API
  1240. $oRoot = $this->GetNodes('//itop_design')->item(0);
  1241. //$oRoot->Dump();
  1242. $sDOMOriginal = $oRoot->Dump(true);
  1243. $oNode = $oRoot->GetNodes('a/b')->item(0);
  1244. $oNew = $this->oDOMDocument->CreateElement('b', 'New text');
  1245. $oNode->parentNode->RedefineChildNode($oNew);
  1246. $oNode = $oRoot->GetNodes('a/c')->item(0);
  1247. $oNewC = $this->oDOMDocument->CreateElement('c');
  1248. $oNewC->setAttribute('id', '1');
  1249. $oNode->parentNode->RedefineChildNode($oNewC);
  1250. $oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'x'));
  1251. $oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'y'));
  1252. $oNewC->appendChild($this->oDOMDocument->CreateElement('d', 'z'));
  1253. $oNamedNode = $this->oDOMDocument->CreateElement('z');
  1254. $oNamedNode->setAttribute('id', 'abc');
  1255. $oNewC->AddChildNode($oNamedNode);
  1256. $oNewC->AddChildNode($this->oDOMDocument->CreateElement('r', 'to be replaced'));
  1257. // Alter this "modified node", no flag should be set in its subnodes
  1258. $oNewC->Rename('blah');
  1259. $oNewC->Rename('foo');
  1260. $oNewC->AddChildNode($this->oDOMDocument->CreateElement('y', '(no flag)'));
  1261. $oNewC->AddChildNode($this->oDOMDocument->CreateElement('x', 'To delete programatically'));
  1262. $oSubNode = $oNewC->GetUniqueElement('z');
  1263. $oSubNode->Rename('abcdef');
  1264. $oSubNode = $oNewC->GetUniqueElement('x');
  1265. $oSubNode->Delete();
  1266. $oNewC->RedefineChildNode($this->oDOMDocument->CreateElement('r', 'replacement'));
  1267. $oNode = $oRoot->GetNodes("//a[@id='second a']")->item(0);
  1268. $oNode->Rename('el 2o A');
  1269. $oNode->Rename('el secundo A');
  1270. $oNew = $this->oDOMDocument->CreateElement('e', 'Something new here');
  1271. $oNode->AddChildNode($oNew);
  1272. $oNewA = $this->oDOMDocument->CreateElement('a');
  1273. $oNewA->setAttribute('id', 'new a');
  1274. $oNode->AddChildNode($oNewA);
  1275. $oSubnode = $this->oDOMDocument->CreateElement('parent', 'el secundo A');
  1276. $oSubnode->setAttribute('id', 'to be changed');
  1277. $oNewA->AddChildNode($oSubnode);
  1278. $oNewA->AddChildNode($this->oDOMDocument->CreateElement('f', 'Welcome to the newcomer'));
  1279. $oNewA->AddChildNode($this->oDOMDocument->CreateElement('x', 'To delete programatically'));
  1280. // Alter this "new a", as it is new, no flag should be set
  1281. $oNewA->Rename('new_a');
  1282. $oSubNode = $oNewA->GetUniqueElement('parent');
  1283. $oSubNode->Rename('alter ego');
  1284. $oNewA->RedefineChildNode($this->oDOMDocument->CreateElement('f', 'dummy data'));
  1285. $oSubNode = $oNewA->GetUniqueElement('x');
  1286. $oSubNode->Delete();
  1287. $oNode = $oRoot->GetNodes("//a[@id='third a']")->item(0);
  1288. $oNode->Delete();
  1289. //$oRoot->Dump();
  1290. $sDOMModified = $oRoot->Dump(true);
  1291. // Compute the delta
  1292. //
  1293. $sDeltaXML = $this->GetDelta();
  1294. //echo "<pre>\n";
  1295. //echo htmlentities($sDeltaXML);
  1296. //echo "</pre>\n";
  1297. // Reiterating - try to remake the DOM by applying the computed delta
  1298. //
  1299. $this->oDOMDocument = new MFDocument();
  1300. $this->oDOMDocument->loadXML($sOriginalXML);
  1301. $oRoot = $this->GetNodes('//itop_design')->item(0);
  1302. //$oRoot->Dump();
  1303. echo "<h4>Rebuild the DOM - Delta applied...</h4>\n";
  1304. $oDeltaDoc = new MFDocument();
  1305. $oDeltaDoc->loadXML($sDeltaXML);
  1306. //$oDeltaDoc->Dump();
  1307. //$this->oDOMDocument->Dump();
  1308. $oDeltaRoot = $oDeltaDoc->childNodes->item(0);
  1309. $this->LoadDelta($oDeltaRoot, $this->oDOMDocument);
  1310. //$oRoot->Dump();
  1311. $sDOMRebuilt = $oRoot->Dump(true);
  1312. }
  1313. catch (Exception $e)
  1314. {
  1315. echo "<h1>Exception: ".$e->getMessage()."</h1>\n";
  1316. echo "<pre>\n";
  1317. debug_print_backtrace();
  1318. echo "</pre>\n";
  1319. }
  1320. $sArrStyle = "font-size: 40;";
  1321. echo "<table>\n";
  1322. echo " <tr>\n";
  1323. echo " <td width=\"50%\">\n";
  1324. echo " <h4>DOM - Original values</h4>\n";
  1325. echo " <pre>".htmlentities($sDOMOriginal)."</pre>\n";
  1326. echo " </td>\n";
  1327. echo " <td width=\"50%\" align=\"left\" valign=\"center\"><span style=\"$sArrStyle\">&rArr; &rArr; &rArr;</span></td>\n";
  1328. echo " </tr>\n";
  1329. echo " <tr><td align=\"center\"><span style=\"$sArrStyle\">&dArr;</div></td><td align=\"center\"><span style=\"$sArrStyle\"><span style=\"$sArrStyle\">&dArr;</div></div></td></tr>\n";
  1330. echo " <tr>\n";
  1331. echo " <td width=\"50%\">\n";
  1332. echo " <h4>DOM - Altered with various changes</h4>\n";
  1333. echo " <pre>".htmlentities($sDOMModified)."</pre>\n";
  1334. echo " </td>\n";
  1335. echo " <td width=\"50%\">\n";
  1336. echo " <h4>DOM - Rebuilt from the Delta</h4>\n";
  1337. echo " <pre>".htmlentities($sDOMRebuilt)."</pre>\n";
  1338. echo " </td>\n";
  1339. echo " </tr>\n";
  1340. echo " <tr><td align=\"center\"><span style=\"$sArrStyle\">&dArr;</div></td><td align=\"center\"><span style=\"$sArrStyle\">&uArr;</div></td></tr>\n";
  1341. echo " <td width=\"50%\">\n";
  1342. echo " <h4>Delta (Computed by ModelFactory)</h4>\n";
  1343. echo " <pre>".htmlentities($sDeltaXML)."</pre>\n";
  1344. echo " </td>\n";
  1345. echo " <td width=\"50%\" align=\"left\" valign=\"center\"><span style=\"$sArrStyle\">&rArr; &rArr; &rArr;</span></td>\n";
  1346. echo " </tr>\n";
  1347. echo "</table>\n";
  1348. } // TEST !
  1349. /**
  1350. * Extracts some nodes from the DOM
  1351. * @param string $sXPath A XPath expression
  1352. * @return DOMNodeList
  1353. */
  1354. public function GetNodes($sXPath, $oContextNode = null, $bSafe = true)
  1355. {
  1356. return $this->oDOMDocument->GetNodes($sXPath, $oContextNode, $bSafe);
  1357. }
  1358. }
  1359. /**
  1360. * Allow the setup page to load and perform its checks (including the check about the required extensions)
  1361. */
  1362. if (!class_exists('DOMElement'))
  1363. {
  1364. class DOMElement {function __construct(){throw new Exception('The dom extension is not enabled');}}
  1365. }
  1366. /**
  1367. * MFElement: helper to read/change the DOM
  1368. * @package ModelFactory
  1369. */
  1370. class MFElement extends Combodo\iTop\DesignElement
  1371. {
  1372. /**
  1373. * Extracts some nodes from the DOM
  1374. * @param string $sXPath A XPath expression
  1375. * @return DOMNodeList
  1376. */
  1377. public function GetNodes($sXPath, $bSafe = true)
  1378. {
  1379. return $this->ownerDocument->GetNodes($sXPath, $this, $bSafe);
  1380. }
  1381. /**
  1382. * Extracts some nodes from the DOM (active nodes only !!!)
  1383. * @param string $sXPath A XPath expression
  1384. * @return DOMNodeList
  1385. */
  1386. public function GetNodeById($sXPath, $sId)
  1387. {
  1388. return $this->ownerDocument->GetNodeById($sXPath, $sId, $this);
  1389. }
  1390. /**
  1391. * Returns the node directly under the given node
  1392. */
  1393. public function GetUniqueElement($sTagName, $bMustExist = true)
  1394. {
  1395. $oNode = null;
  1396. foreach($this->childNodes as $oChildNode)
  1397. {
  1398. if (($oChildNode->nodeName == $sTagName) && (($oChildNode->getAttribute('_alteration') != 'removed')))
  1399. {
  1400. $oNode = $oChildNode;
  1401. break;
  1402. }
  1403. }
  1404. if ($bMustExist && is_null($oNode))
  1405. {
  1406. throw new DOMFormatException('Missing unique tag: '.$sTagName);
  1407. }
  1408. return $oNode;
  1409. }
  1410. /**
  1411. * Assumes the current node to be either a text or
  1412. * <items>
  1413. * <item [key]="..."]>value<item>
  1414. * <item [key]="..."]>value<item>
  1415. * </items>
  1416. * where value can be the either a text or an array of items... recursively
  1417. * Returns a PHP array
  1418. */
  1419. public function GetNodeAsArrayOfItems($sElementName = 'items')
  1420. {
  1421. $oItems = $this->GetOptionalElement($sElementName);
  1422. if ($oItems)
  1423. {
  1424. $res = array();
  1425. $aRanks = array();
  1426. foreach($oItems->childNodes as $oItem)
  1427. {
  1428. if ($oItem instanceof DOMElement)
  1429. {
  1430. // When an attribute is missing
  1431. if ($oItem->hasAttribute('id'))
  1432. {
  1433. $key = $oItem->getAttribute('id');
  1434. if (array_key_exists($key, $res))
  1435. {
  1436. // Houston!
  1437. throw new DOMFormatException("id '$key' already used", null, null, $oItem);
  1438. }
  1439. $res[$key] = $oItem->GetNodeAsArrayOfItems();
  1440. }
  1441. else
  1442. {
  1443. $res[] = $oItem->GetNodeAsArrayOfItems();
  1444. }
  1445. $sRank = $oItem->GetChildText('rank');
  1446. if ($sRank != '')
  1447. {
  1448. $aRanks[] = (float) $sRank;
  1449. }
  1450. else
  1451. {
  1452. $aRanks[] = count($aRanks) > 0 ? max($aRanks) + 1 : 0;
  1453. }
  1454. array_multisort($aRanks, $res);
  1455. }
  1456. }
  1457. }
  1458. else
  1459. {
  1460. $res = $this->GetText();
  1461. }
  1462. return $res;
  1463. }
  1464. public function SetNodeAsArrayOfItems($aList)
  1465. {
  1466. $oNewNode = $this->ownerDocument->CreateElement($this->tagName);
  1467. if ($this->getAttribute('id') != '')
  1468. {
  1469. $oNewNode->setAttribute('id', $this->getAttribute('id'));
  1470. }
  1471. self::AddItemToNode($this->ownerDocument, $oNewNode, $aList);
  1472. $this->parentNode->RedefineChildNode($oNewNode);
  1473. }
  1474. protected static function AddItemToNode($oXmlDoc, $oXMLNode, $itemValue)
  1475. {
  1476. if (is_array($itemValue))
  1477. {
  1478. $oXmlItems = $oXmlDoc->CreateElement('items');
  1479. $oXMLNode->AppendChild($oXmlItems);
  1480. foreach($itemValue as $key => $item)
  1481. {
  1482. $oXmlItem = $oXmlDoc->CreateElement('item');
  1483. $oXmlItems->AppendChild($oXmlItem);
  1484. if (is_string($key))
  1485. {
  1486. $oXmlItem->SetAttribute('key', $key);
  1487. }
  1488. self::AddItemToNode($oXmlDoc, $oXmlItem, $item);
  1489. }
  1490. }
  1491. else
  1492. {
  1493. $oXmlText = $oXmlDoc->CreateTextNode((string) $itemValue);
  1494. $oXMLNode->AppendChild($oXmlText);
  1495. }
  1496. }
  1497. /**
  1498. * Helper to remove child nodes
  1499. */
  1500. protected function DeleteChildren()
  1501. {
  1502. while (isset($this->firstChild))
  1503. {
  1504. if ($this->firstChild instanceof MFElement)
  1505. {
  1506. $this->firstChild->DeleteChildren();
  1507. }
  1508. $this->removeChild($this->firstChild);
  1509. }
  1510. }
  1511. /**
  1512. * Find the child node matching the given node.
  1513. * UNSAFE: may return nodes marked as _alteration="removed"
  1514. * A method with the same signature MUST exist in MFDocument for the recursion to work fine
  1515. * @param MFElement $oRefNode The node to search for
  1516. * @param string $sSearchId substitutes to the value of the 'id' attribute
  1517. */
  1518. public function _FindChildNode(MFElement $oRefNode, $sSearchId = null)
  1519. {
  1520. return self::_FindNode($this, $oRefNode, $sSearchId);
  1521. }
  1522. /**
  1523. * Find the child node matching the given node under the specified parent.
  1524. * UNSAFE: may return nodes marked as _alteration="removed"
  1525. * @param DOMNode $oParent
  1526. * @param MFElement $oRefNode
  1527. * @param string $sSearchId
  1528. * @throws Exception
  1529. */
  1530. public static function _FindNode(DOMNode $oParent, MFElement $oRefNode, $sSearchId = null)
  1531. {
  1532. $oRes = null;
  1533. if ($oParent instanceof DOMDocument)
  1534. {
  1535. $oDoc = $oParent->firstChild->ownerDocument;
  1536. $oRoot = $oParent;
  1537. }
  1538. else
  1539. {
  1540. $oDoc = $oParent->ownerDocument;
  1541. $oRoot = $oParent;
  1542. }
  1543. $oXPath = new DOMXPath($oDoc);
  1544. if ($oRefNode->hasAttribute('id'))
  1545. {
  1546. // Find the first element having the same tag name and id
  1547. if (!$sSearchId)
  1548. {
  1549. $sSearchId = $oRefNode->getAttribute('id');
  1550. }
  1551. $sXPath = './'.$oRefNode->tagName."[@id='$sSearchId']";
  1552. $oRes = $oXPath->query($sXPath, $oRoot)->item(0);
  1553. }
  1554. else
  1555. {
  1556. // Get the first one having the same tag name (ignore others)
  1557. $sXPath = './'.$oRefNode->tagName;
  1558. $oRes = $oXPath->query($sXPath, $oRoot)->item(0);
  1559. }
  1560. return $oRes;
  1561. }
  1562. /**
  1563. * Check if the current node is under a node 'added' or 'altered'
  1564. * Usage: In such a case, the change must not be tracked
  1565. */
  1566. public function IsInDefinition()
  1567. {
  1568. // Iterate through the parents: reset the flag if any of them has a flag set
  1569. for($oParent = $this ; $oParent instanceof MFElement ; $oParent = $oParent->parentNode)
  1570. {
  1571. if ($oParent->getAttribute('_alteration') != '')
  1572. {
  1573. return true;
  1574. }
  1575. }
  1576. return false;
  1577. }
  1578. /**
  1579. * Check if the given node is (a child of a node) altered by one of the supplied modules
  1580. * @param array $aModules The list of module codes to consider
  1581. * @return boolean
  1582. */
  1583. public function IsAlteredByModule($aModules)
  1584. {
  1585. // Iterate through the parents: reset the flag if any of them has a flag set
  1586. for($oParent = $this ; $oParent instanceof MFElement ; $oParent = $oParent->parentNode)
  1587. {
  1588. if (in_array($oParent->getAttribute('_altered_in'), $aModules))
  1589. {
  1590. return true;
  1591. }
  1592. }
  1593. return false;
  1594. }
  1595. static $aTraceAttributes = null;
  1596. /**
  1597. * Enable/disable the trace on changed nodes
  1598. *
  1599. *@param aAttributes array Array of attributes (key => value) to be added onto any changed node
  1600. */
  1601. static public function SetTrace($aAttributes = null)
  1602. {
  1603. self::$aTraceAttributes = $aAttributes;
  1604. }
  1605. /**
  1606. * Mark the node as touched (if tracing is active)
  1607. */
  1608. public function AddTrace()
  1609. {
  1610. if (!is_null(self::$aTraceAttributes))
  1611. {
  1612. foreach (self::$aTraceAttributes as $sAttribute => $value)
  1613. {
  1614. $this->setAttribute($sAttribute, $value);
  1615. }
  1616. }
  1617. }
  1618. /**
  1619. * Add a node and set the flags that will be used to compute the delta
  1620. * @param MFElement $oNode The node (including all subnodes) to add
  1621. */
  1622. public function AddChildNode(MFElement $oNode)
  1623. {
  1624. // First: cleanup any flag behind the new node, and eventually add trace data
  1625. $oNode->ApplyChanges();
  1626. $oNode->AddTrace();
  1627. $oExisting = $this->_FindChildNode($oNode);
  1628. if ($oExisting)
  1629. {
  1630. if ($oExisting->getAttribute('_alteration') != 'removed')
  1631. {
  1632. $sPath = MFDocument::GetItopNodePath($oNode);
  1633. $iLine = $oNode->getLineNo();
  1634. throw new MFException($sPath.' at line '.$iLine.": could not be added (already exists)", MFException::COULD_NOT_BE_ADDED, $iLine, $sPath);
  1635. }
  1636. $oExisting->ReplaceWith($oNode);
  1637. $sFlag = 'replaced';
  1638. }
  1639. else
  1640. {
  1641. $this->appendChild($oNode);
  1642. $sFlag = 'added';
  1643. }
  1644. if (!$this->IsInDefinition())
  1645. {
  1646. $oNode->setAttribute('_alteration', $sFlag);
  1647. }
  1648. }
  1649. /**
  1650. * Modify a node and set the flags that will be used to compute the delta
  1651. * @param MFElement $oNode The node (including all subnodes) to set
  1652. */
  1653. public function RedefineChildNode(MFElement $oNode, $sSearchId = null)
  1654. {
  1655. // First: cleanup any flag behind the new node, and eventually add trace data
  1656. $oNode->ApplyChanges();
  1657. $oNode->AddTrace();
  1658. $oExisting = $this->_FindChildNode($oNode, $sSearchId);
  1659. if (!$oExisting)
  1660. {
  1661. $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
  1662. $iLine = $oNode->getLineNo();
  1663. throw new MFException($sPath." at line $iLine: could not be modified (not found)", MFException::COULD_NOT_BE_MODIFIED_NOT_FOUND, $sPath, $iLine);
  1664. }
  1665. $sPrevFlag = $oExisting->getAttribute('_alteration');
  1666. if ($sPrevFlag == 'removed')
  1667. {
  1668. $sPath = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(empty($sSearchId) ? '' : "[$sSearchId]");
  1669. $iLine = $oNode->getLineNo();
  1670. $sSourceNode = MFDocument::GetItopNodePath($this)."/".$oNode->tagName.(is_null($sSearchId) ? '' : "[$sSearchId]").' at line '.$this->getLineNo();
  1671. throw new MFException($sPath." at line $iLine: could not be modified (marked as deleted)", MFException::COULD_NOT_BE_MODIFIED_ALREADY_DELETED, $sPath, $iLine);
  1672. }
  1673. $oExisting->ReplaceWith($oNode);
  1674. if (!$this->IsInDefinition())
  1675. {
  1676. if ($sPrevFlag == '')
  1677. {
  1678. $sPrevFlag = 'replaced';
  1679. }
  1680. $oNode->setAttribute('_alteration', $sPrevFlag);
  1681. }
  1682. }
  1683. /**
  1684. * Combination of AddChildNode or RedefineChildNode... it depends
  1685. * This should become the preferred way of doing things (instead of implementing a test + the call to one of the APIs!
  1686. * @param MFElement $oNode The node (including all subnodes) to set
  1687. * @param string $sSearchId Optional Id of the node to SearchMenuNode
  1688. * @param bool $bForce Force mode to dynamically add or replace nodes
  1689. */
  1690. public function SetChildNode(MFElement $oNode, $sSearchId = null, $bForce = false)
  1691. {
  1692. // First: cleanup any flag behind the new node, and eventually add trace data
  1693. $oNode->ApplyChanges();
  1694. $oNode->AddTrace();
  1695. $oExisting = $this->_FindChildNode($oNode, $sSearchId);
  1696. if ($oExisting)
  1697. {
  1698. $sPrevFlag = $oExisting->getAttribute('_alteration');
  1699. if ($sPrevFlag == 'removed')
  1700. {
  1701. $sFlag = $bForce ? 'forced': 'replaced';
  1702. }
  1703. else
  1704. {
  1705. $sFlag = $sPrevFlag; // added, replaced or ''
  1706. }
  1707. $oExisting->ReplaceWith($oNode);
  1708. }
  1709. else
  1710. {
  1711. $this->appendChild($oNode);
  1712. $sFlag = $bForce ? 'forced': 'added';
  1713. }
  1714. if (!$this->IsInDefinition())
  1715. {
  1716. if ($sFlag == '')
  1717. {
  1718. $sFlag = $bForce ? 'forced': 'replaced';
  1719. }
  1720. $oNode->setAttribute('_alteration', $sFlag);
  1721. }
  1722. }
  1723. /**
  1724. * Check that the current node is actually a class node, under classes
  1725. */
  1726. public function IsClassNode()
  1727. {
  1728. if ($this->tagName == 'class')
  1729. {
  1730. if (($this->parentNode->tagName == 'classes') && ($this->parentNode->parentNode->tagName == 'itop_design') ) // Beware: classes/class also exists in the group definition
  1731. {
  1732. return true;
  1733. }
  1734. return $this->parentNode->IsClassNode();
  1735. }
  1736. else
  1737. {
  1738. return false;
  1739. }
  1740. }
  1741. /**
  1742. * Replaces a node by another one, making sure that recursive nodes are preserved
  1743. * @param MFElement $oNewNode The replacement
  1744. */
  1745. protected function ReplaceWith($oNewNode)
  1746. {
  1747. // Move the classes from the old node into the new one
  1748. if ($this->IsClassNode())
  1749. {
  1750. foreach($this->GetNodes('class') as $oChild)
  1751. {
  1752. $oNewNode->appendChild($oChild);
  1753. }
  1754. }
  1755. $oParentNode = $this->parentNode;
  1756. $oParentNode->replaceChild($oNewNode, $this);
  1757. }
  1758. /**
  1759. * Remove a node and set the flags that will be used to compute the delta
  1760. */
  1761. public function Delete()
  1762. {
  1763. $oParent = $this->parentNode;
  1764. switch ($this->getAttribute('_alteration'))
  1765. {
  1766. case 'replaced':
  1767. $sFlag = 'removed';
  1768. break;
  1769. case 'added':
  1770. case 'needed':
  1771. $sFlag = null;
  1772. break;
  1773. case 'removed':
  1774. throw new Exception("Attempting to remove a deleted node: $this->tagName (id: ".$this->getAttribute('id')."");
  1775. default:
  1776. $sFlag = 'removed';
  1777. if ($this->IsInDefinition())
  1778. {
  1779. $sFlag = null;
  1780. break;
  1781. }
  1782. }
  1783. if ($sFlag)
  1784. {
  1785. $this->setAttribute('_alteration', $sFlag);
  1786. $this->DeleteChildren();
  1787. // Add trace data
  1788. $this->AddTrace();
  1789. }
  1790. else
  1791. {
  1792. // Remove the node entirely
  1793. $this->parentNode->removeChild($this);
  1794. }
  1795. }
  1796. /**
  1797. * Merge the current node into the given container
  1798. *
  1799. * @param DOMNode $oContainer An element or a document
  1800. * @param string $sSearchId The id to consider (could be blank)
  1801. * @param bool $bMustExist Throw an exception if the node must already be found (and not marked as deleted!)
  1802. * @param bool $bIfExists Return null if the node does not exists (or is marked as deleted)
  1803. * @return DOMNode|null
  1804. */
  1805. public function MergeInto($oContainer, $sSearchId, $bMustExist, $bIfExists = false)
  1806. {
  1807. $oTargetNode = $oContainer->_FindChildNode($this, $sSearchId);
  1808. if ($oTargetNode)
  1809. {
  1810. if ($oTargetNode->getAttribute('_alteration') == 'removed')
  1811. {
  1812. if ($bMustExist)
  1813. {
  1814. throw new Exception(MFDocument::GetItopNodePath($this).' at line '.$this->getLineNo().": could not be found (marked as deleted)");
  1815. }
  1816. // Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17)
  1817. // So use this workaround to import a node and its attributes on *any* PHP version
  1818. $oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true);
  1819. $oContainer->AddChildNode($oTargetNode);
  1820. }
  1821. }
  1822. else
  1823. {
  1824. if ($bMustExist)
  1825. {
  1826. echo "Dumping parent node<br/>\n";
  1827. $oContainer->Dump();
  1828. throw new Exception(MFDocument::GetItopNodePath($this).' at line '.$this->getLineNo().": could not be found");
  1829. }
  1830. if (!$bIfExists)
  1831. {
  1832. // Beware: ImportNode(xxx, false) DOES NOT copy the node's attribute on *some* PHP versions (<5.2.17)
  1833. // So use this workaround to import a node and its attributes on *any* PHP version
  1834. $oTargetNode = $oContainer->ownerDocument->ImportNode($this->cloneNode(false), true);
  1835. $oContainer->AddChildNode($oTargetNode);
  1836. }
  1837. }
  1838. return $oTargetNode;
  1839. }
  1840. /**
  1841. * Renames a node and set the flags that will be used to compute the delta
  1842. * @param String $sNewId The new id
  1843. */
  1844. public function Rename($sId)
  1845. {
  1846. if (($this->getAttribute('_alteration') == 'replaced') || !$this->IsInDefinition())
  1847. {
  1848. $sOriginalId = $this->getAttribute('_old_id');
  1849. if ($sOriginalId == '')
  1850. {
  1851. $this->setAttribute('_old_id', $this->getAttribute('id'));
  1852. }
  1853. else if($sOriginalId == $sId)
  1854. {
  1855. $this->removeAttribute('_old_id');
  1856. }
  1857. }
  1858. $this->setAttribute('id', $sId);
  1859. // Leave a trace of this change
  1860. $this->AddTrace();
  1861. }
  1862. /**
  1863. * Apply extensibility rules onto this node
  1864. * @param array aRules Array of rules (strings)
  1865. * @return void
  1866. */
  1867. public function RestrictExtensibility($aRules)
  1868. {
  1869. $oRulesNode = $this->GetOptionalElement('rules');
  1870. if ($oRulesNode)
  1871. {
  1872. $aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems();
  1873. $aCurrentRules = array_merge($aCurrentRules, $aRules);
  1874. $oRulesNode->SetNodeAsArrayOfItems($aCurrentRules);
  1875. }
  1876. else
  1877. {
  1878. $oNewNode = $this->ownerDocument->CreateElement('rules');
  1879. $this->appendChild($oNewNode);
  1880. $oNewNode->SetNodeAsArrayOfItems($aRules);
  1881. }
  1882. }
  1883. /**
  1884. * Read extensibility rules for this node
  1885. * @return Array of rules (strings)
  1886. */
  1887. public function GetExtensibilityRules()
  1888. {
  1889. $aCurrentRules = array();
  1890. $oRulesNode = $this->GetOptionalElement('rules');
  1891. if ($oRulesNode)
  1892. {
  1893. $aCurrentRules = $oRulesNode->GetNodeAsArrayOfItems();
  1894. }
  1895. return $aCurrentRules;
  1896. }
  1897. /**
  1898. * List changes below a given node (see also MFDocument::ListChanges)
  1899. */
  1900. public function ListChanges()
  1901. {
  1902. // Note: omitting the dot will make the query be global to the whole document!!!
  1903. return $this->ownerDocument->GetNodes('.//*[@_alteration or @_old_id]', $this, false);
  1904. }
  1905. /**
  1906. * List changes below a given node (see also MFDocument::ApplyChanges)
  1907. */
  1908. public function ApplyChanges()
  1909. {
  1910. $oNodes = $this->ListChanges();
  1911. foreach($oNodes as $oNode)
  1912. {
  1913. $sOperation = $oNode->GetAttribute('_alteration');
  1914. switch($sOperation)
  1915. {
  1916. case 'added':
  1917. case 'replaced':
  1918. case 'needed':
  1919. // marked as added or modified, just reset the flag
  1920. $oNode->removeAttribute('_alteration');
  1921. break;
  1922. case 'removed':
  1923. // marked as deleted, let's remove the node from the tree
  1924. $oNode->parentNode->removeChild($oNode);
  1925. break;
  1926. }
  1927. if ($oNode->hasAttribute('_old_id'))
  1928. {
  1929. $oNode->removeAttribute('_old_id');
  1930. }
  1931. }
  1932. }
  1933. }
  1934. /**
  1935. * Allow the setup page to load and perform its checks (including the check about the required extensions)
  1936. */
  1937. if (!class_exists('DOMDocument'))
  1938. {
  1939. class DOMDocument {function __construct(){throw new Exception('The dom extension is not enabled');}}
  1940. }
  1941. /**
  1942. * MFDocument - formating rules for XML input/output
  1943. * @package ModelFactory
  1944. */
  1945. class MFDocument extends \Combodo\iTop\DesignDocument
  1946. {
  1947. /**
  1948. * Overloadable. Called prior to data loading.
  1949. */
  1950. protected function Init()
  1951. {
  1952. parent::Init();
  1953. $this->registerNodeClass('DOMElement', 'MFElement');
  1954. }
  1955. /**
  1956. * Overload the standard API
  1957. */
  1958. public function saveXML(DOMNode $node = null, $options = 0)
  1959. {
  1960. $oRootNode = $this->firstChild;
  1961. if (!$oRootNode)
  1962. {
  1963. $oRootNode = $this->createElement('itop_design'); // make sure that the document is not empty
  1964. $oRootNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
  1965. $oRootNode->setAttribute('version', ITOP_DESIGN_LATEST_VERSION);
  1966. $this->appendChild($oRootNode);
  1967. }
  1968. return parent::saveXML($node);
  1969. }
  1970. /**
  1971. * Overload createElement to make sure (via new DOMText) that the XML entities are
  1972. * always properly escaped
  1973. * (non-PHPdoc)
  1974. * @see DOMDocument::createElement()
  1975. */
  1976. function createElement($sName, $value = null, $namespaceURI = null)
  1977. {
  1978. $oElement = $this->importNode(new MFElement($sName, null, $namespaceURI));
  1979. if (($value !== '') && ($value !== null))
  1980. {
  1981. $oElement->appendChild(new DOMText($value));
  1982. }
  1983. return $oElement;
  1984. }
  1985. /**
  1986. * Find the child node matching the given node
  1987. * A method with the same signature MUST exist in MFElement for the recursion to work fine
  1988. * @param MFElement $oRefNode The node to search for
  1989. * @param string $sSearchId substitutes to the value of the 'id' attribute
  1990. */
  1991. public function _FindChildNode(MFElement $oRefNode, $sSearchId = null)
  1992. {
  1993. return MFElement::_FindNode($this, $oRefNode, $sSearchId);
  1994. }
  1995. /**
  1996. * Extracts some nodes from the DOM
  1997. * @param string $sXPath A XPath expression
  1998. * @return DOMNodeList
  1999. */
  2000. public function GetNodes($sXPath, $oContextNode = null, $bSafe = true)
  2001. {
  2002. $oXPath = new DOMXPath($this);
  2003. if ($bSafe)
  2004. {
  2005. $sXPath .= "[not(@_alteration) or @_alteration!='removed']";
  2006. }
  2007. if (is_null($oContextNode))
  2008. {
  2009. $oResult = $oXPath->query($sXPath);
  2010. }
  2011. else
  2012. {
  2013. $oResult = $oXPath->query($sXPath, $oContextNode);
  2014. }
  2015. return $oResult;
  2016. }
  2017. public function GetNodeById($sXPath, $sId, $oContextNode = null)
  2018. {
  2019. $oXPath = new DOMXPath($this);
  2020. $sQuotedId = self::XPathQuote($sId);
  2021. $sXPath .= "[@id=$sQuotedId and(not(@_alteration) or @_alteration!='removed')]";
  2022. if (is_null($oContextNode))
  2023. {
  2024. return $oXPath->query($sXPath);
  2025. }
  2026. else
  2027. {
  2028. return $oXPath->query($sXPath, $oContextNode);
  2029. }
  2030. }
  2031. }
  2032. /**
  2033. * Helper class manage parameters stored as XML nodes
  2034. * to be converted to a PHP structure during compilation
  2035. * Values can be either a hash, an array, a string, a boolean, an int or a float
  2036. */
  2037. class MFParameters
  2038. {
  2039. protected $aData = null;
  2040. public function __construct(DOMNode $oNode)
  2041. {
  2042. $this->aData = array();
  2043. $this->LoadFromDOM($oNode);
  2044. }
  2045. public function Get($sCode, $default = '')
  2046. {
  2047. if (array_key_exists($sCode, $this->aData))
  2048. {
  2049. return $this->aData[$sCode];
  2050. }
  2051. return $default;
  2052. }
  2053. public function GetAll()
  2054. {
  2055. return $this->aData;
  2056. }
  2057. public function LoadFromDOM(DOMNode $oNode)
  2058. {
  2059. $this->aData = array();
  2060. foreach($oNode->childNodes as $oChildNode)
  2061. {
  2062. if ($oChildNode instanceof DOMElement)
  2063. {
  2064. $this->aData[$oChildNode->nodeName] = $this->ReadElement($oChildNode);
  2065. }
  2066. }
  2067. }
  2068. protected function ReadElement(DOMNode $oNode)
  2069. {
  2070. if ($oNode instanceof DOMElement)
  2071. {
  2072. $sDefaultNodeType = ($this->HasChildNodes($oNode)) ? 'hash' : 'string';
  2073. $sNodeType = $oNode->getAttribute('type');
  2074. if ($sNodeType == '')
  2075. {
  2076. $sNodeType = $sDefaultNodeType;
  2077. }
  2078. switch($sNodeType)
  2079. {
  2080. case 'array':
  2081. $value = array();
  2082. // Treat the current element as zero based array, child tag names are NOT meaningful
  2083. $sFirstTagName = null;
  2084. foreach($oNode->childNodes as $oChildElement)
  2085. {
  2086. if ($oChildElement instanceof DOMElement)
  2087. {
  2088. if ($sFirstTagName == null)
  2089. {
  2090. $sFirstTagName = $oChildElement->nodeName;
  2091. }
  2092. else if ($sFirstTagName != $oChildElement->nodeName)
  2093. {
  2094. throw new Exception("Invalid Parameters: mixed tags ('$sFirstTagName' and '".$oChildElement->nodeName."') inside array '".$oNode->nodeName."'");
  2095. }
  2096. $val = $this->ReadElement($oChildElement);
  2097. // No specific Id, just push the value at the end of the array
  2098. $value[] = $val;
  2099. }
  2100. }
  2101. ksort($value, SORT_NUMERIC);
  2102. break;
  2103. case 'hash':
  2104. $value = array();
  2105. // Treat the current element as a hash, child tag names are keys
  2106. foreach($oNode->childNodes as $oChildElement)
  2107. {
  2108. if ($oChildElement instanceof DOMElement)
  2109. {
  2110. if (array_key_exists($oChildElement->nodeName, $value))
  2111. {
  2112. throw new Exception("Invalid Parameters file: duplicate tags '".$oChildElement->nodeName."' inside hash '".$oNode->nodeName."'");
  2113. }
  2114. $val = $this->ReadElement($oChildElement);
  2115. $value[$oChildElement->nodeName] = $val;
  2116. }
  2117. }
  2118. break;
  2119. case 'int':
  2120. case 'integer':
  2121. $value = (int)$this->GetText($oNode);
  2122. break;
  2123. case 'bool':
  2124. case 'boolean':
  2125. if (($this->GetText($oNode) == 'true') || ($this->GetText($oNode) == 1))
  2126. {
  2127. $value = true;
  2128. }
  2129. else
  2130. {
  2131. $value = false;
  2132. }
  2133. break;
  2134. case 'string':
  2135. default:
  2136. $value = str_replace('\n', "\n", (string)$this->GetText($oNode));
  2137. }
  2138. }
  2139. else if ($oNode instanceof DOMText)
  2140. {
  2141. $value = $oNode->wholeText;
  2142. }
  2143. return $value;
  2144. }
  2145. protected function GetAttribute($sAttName, $oNode, $sDefaultValue)
  2146. {
  2147. $sRet = $sDefaultValue;
  2148. foreach($oNode->attributes as $oAttribute)
  2149. {
  2150. if ($oAttribute->nodeName == $sAttName)
  2151. {
  2152. $sRet = $oAttribute->nodeValue;
  2153. break;
  2154. }
  2155. }
  2156. return $sRet;
  2157. }
  2158. /**
  2159. * Returns the TEXT of the current node (possibly from several subnodes)
  2160. */
  2161. public function GetText($oNode, $sDefault = null)
  2162. {
  2163. $sText = null;
  2164. foreach($oNode->childNodes as $oChildNode)
  2165. {
  2166. if ($oChildNode instanceof DOMText)
  2167. {
  2168. if (is_null($sText)) $sText = '';
  2169. $sText .= $oChildNode->wholeText;
  2170. }
  2171. }
  2172. if (is_null($sText))
  2173. {
  2174. return $sDefault;
  2175. }
  2176. else
  2177. {
  2178. return $sText;
  2179. }
  2180. }
  2181. /**
  2182. * Check if a node has child nodes (apart from text nodes)
  2183. */
  2184. public function HasChildNodes($oNode)
  2185. {
  2186. if ($oNode instanceof DOMElement)
  2187. {
  2188. foreach($oNode->childNodes as $oChildNode)
  2189. {
  2190. if ($oChildNode instanceof DOMElement)
  2191. {
  2192. return true;
  2193. }
  2194. }
  2195. }
  2196. return false;
  2197. }
  2198. function Merge(XMLParameters $oTask)
  2199. {
  2200. //todo: clarify the usage of this function that CANNOT work
  2201. $this->aData = $this->array_merge_recursive_distinct($this->aData, $oTask->aData);
  2202. }
  2203. /**
  2204. * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
  2205. * keys to arrays rather than overwriting the value in the first array with the duplicate
  2206. * value in the second array, as array_merge does. I.e., with array_merge_recursive,
  2207. * this happens (documented behavior):
  2208. *
  2209. * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
  2210. * => array('key' => array('org value', 'new value'));
  2211. *
  2212. * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
  2213. * Matching keys' values in the second array overwrite those in the first array, as is the
  2214. * case with array_merge, i.e.:
  2215. *
  2216. * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
  2217. * => array('key' => array('new value'));
  2218. *
  2219. * Parameters are passed by reference, though only for performance reasons. They're not
  2220. * altered by this function.
  2221. *
  2222. * @param array $array1
  2223. * @param array $array2
  2224. * @return array
  2225. * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
  2226. * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
  2227. */
  2228. protected function array_merge_recursive_distinct ( array &$array1, array &$array2 )
  2229. {
  2230. $merged = $array1;
  2231. foreach ( $array2 as $key => &$value )
  2232. {
  2233. if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) )
  2234. {
  2235. $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value );
  2236. }
  2237. else
  2238. {
  2239. $merged [$key] = $value;
  2240. }
  2241. }
  2242. return $merged;
  2243. }
  2244. }