modelement.class.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. /**
  11. * Represents an element of source content managed by MODX.
  12. *
  13. * These elements are defined by some type of source content that when processed
  14. * will provide output or some type of logical result based on mutable
  15. * properties.
  16. *
  17. * This class creates an instance of a modElement object. This should not be
  18. * called directly, but rather extended for derivative modElement classes.
  19. *
  20. * @property modMediaSource $Source The associated Media Source, if any.
  21. *
  22. * @package modx
  23. * @abstract Implement a derivative of this class to represent an element which
  24. * can be processed within the MODX framework.
  25. * @extends modAccessibleSimpleObject
  26. */
  27. class modElement extends modAccessibleSimpleObject {
  28. /**
  29. * The property value array for the element.
  30. * @var array
  31. */
  32. public $_properties= null;
  33. /**
  34. * The string representation of the element properties.
  35. * @var string
  36. */
  37. public $_propertyString= '';
  38. /**
  39. * The source content of the element.
  40. * @var string
  41. */
  42. public $_content= '';
  43. /**
  44. * The source of the element.
  45. * @var string
  46. */
  47. public $_source= null;
  48. /**
  49. * The output of the element.
  50. * @var string
  51. */
  52. public $_output= '';
  53. /**
  54. * The boolean result of the element.
  55. *
  56. * This is typically only applicable to elements that use PHP source content.
  57. * @var boolean
  58. */
  59. public $_result= true;
  60. /**
  61. * The tag signature of the element instance.
  62. * @var string
  63. */
  64. public $_tag= null;
  65. /**
  66. * The character token which helps identify the element class in tag string.
  67. * @var string
  68. */
  69. public $_token= '';
  70. /**
  71. * @var boolean If the element is cacheable or not.
  72. */
  73. public $_cacheable= true;
  74. /**
  75. * @var boolean Indicates if the element was processed already.
  76. */
  77. public $_processed= false;
  78. /**
  79. * @var array Optional filters that can be used during processing.
  80. */
  81. public $_filters= array('input' => null, 'output' => null);
  82. /**
  83. * @var string Path to source file location when modElement->isStatic() === true.
  84. */
  85. protected $_sourcePath= "";
  86. /**
  87. * @var string Source file name when modElement->isStatic() === true.
  88. */
  89. protected $_sourceFile= "";
  90. /**
  91. * @var array A list of invalid characters in the name of an Element.
  92. */
  93. protected $_invalidCharacters = array('!','@','#','$','%','^','&','*',
  94. '(',')','+','=','[',']','{','}','\'','"',';',':','\\','/','<','>','?'
  95. ,' ',',','`','~');
  96. /**
  97. * Provides custom handling for retrieving the properties field of an Element.
  98. *
  99. * {@inheritdoc}
  100. */
  101. public function get($k, $format= null, $formatTemplate= null) {
  102. $value = parent :: get($k, $format, $formatTemplate);
  103. if ($k === 'properties' && $this->xpdo instanceof modX && $this->xpdo->getParser() && empty($value)) {
  104. $value = !empty($this->properties) && is_string($this->properties)
  105. ? $this->xpdo->parser->parsePropertyString($this->properties)
  106. : null;
  107. }
  108. /* automatically translate lexicon descriptions */
  109. if ($k == 'properties' && !empty($value) && is_array($value)
  110. && is_object($this->xpdo) && $this->xpdo instanceof modX && $this->xpdo->lexicon) {
  111. foreach ($value as &$property) {
  112. if (!empty($property['lexicon'])) {
  113. if (strpos($property['lexicon'],':') !== false) {
  114. $this->xpdo->lexicon->load('en:'.$property['lexicon']);
  115. } else {
  116. $this->xpdo->lexicon->load('en:core:'.$property['lexicon']);
  117. }
  118. $this->xpdo->lexicon->load($property['lexicon']);
  119. }
  120. $property['desc_trans'] = $this->xpdo->lexicon($property['desc']);
  121. $property['area'] = !empty($property['area']) ? $property['area'] : '';
  122. $property['area_trans'] = !empty($property['area']) ? $this->xpdo->lexicon($property['area']) : '';
  123. if (!empty($property['options'])) {
  124. foreach ($property['options'] as &$option) {
  125. if (empty($option['text']) && !empty($option['name'])) {
  126. $option['text'] = $option['name'];
  127. unset($option['name']);
  128. }
  129. if (empty($option['value']) && !empty($option[0])) {
  130. $option['value'] = $option[0];
  131. unset($option[0]);
  132. }
  133. $option['name'] = $this->xpdo->lexicon($option['text']);
  134. }
  135. }
  136. }
  137. }
  138. return $value;
  139. }
  140. /**
  141. * Overridden to handle changes to content managed in an external file.
  142. *
  143. * {@inheritdoc}
  144. */
  145. public function save($cacheFlag = null) {
  146. if (!$this->getOption(xPDO::OPT_SETUP)) {
  147. if ($this->staticSourceChanged()) {
  148. $staticContent = $this->getFileContent();
  149. if ($staticContent !== $this->get('content')) {
  150. if ($this->isStaticSourceMutable() && $staticContent === '') {
  151. $this->setDirty('content');
  152. } else {
  153. $this->setContent($staticContent);
  154. }
  155. }
  156. unset($staticContent);
  157. }
  158. $staticContentChanged = $this->staticContentChanged();
  159. if ($staticContentChanged && !$this->isStaticSourceMutable()) {
  160. $this->setContent($this->getFileContent());
  161. $staticContentChanged = false;
  162. }
  163. /* If element is empty, set to true in order to create an empty static file. */
  164. $content = $this->get('content');
  165. if (empty($content) && $this->isStatic()) {
  166. $staticContentChanged = true;
  167. }
  168. }
  169. /* Set oldPath before saving object. */
  170. $oldPath = $this->getOldStaticFilePath();
  171. $saved = parent::save($cacheFlag);
  172. if (!$this->getOption(xPDO::OPT_SETUP)) {
  173. if ($saved && $staticContentChanged) {
  174. $saved = $this->setFileContent($this->get('content'));
  175. }
  176. }
  177. /* Removing old static file when succesfull saved and oldPath has been set. */
  178. if ($saved && $oldPath && $this->isStaticFilesAutomated()) {
  179. if (@unlink($oldPath)) {
  180. $pathinfo = pathinfo($oldPath);
  181. $this->cleanupStaticFileDirectories($pathinfo['dirname']);
  182. }
  183. }
  184. return $saved;
  185. }
  186. /**
  187. * Determine if static files should be automated for current element class.
  188. *
  189. * @return bool
  190. */
  191. protected function isStaticFilesAutomated()
  192. {
  193. $elements = array(
  194. 'modTemplate' => 'templates',
  195. 'modTemplateVar' => 'tvs',
  196. 'modChunk' => 'chunks',
  197. 'modSnippet' => 'snippets',
  198. 'modPlugin' => 'plugins'
  199. );
  200. if (!array_key_exists($this->_class, $elements)) {
  201. return false;
  202. }
  203. return (bool) $this->xpdo->getOption('static_elements_automate_' . $elements[$this->_class], null, false);
  204. }
  205. /**
  206. * Constructs a valid tag representation of the element.
  207. *
  208. * @return string A tag representation of the element.
  209. */
  210. public function getTag() {
  211. if (empty($this->_tag)) {
  212. $propTemp = array();
  213. if (empty($this->_propertyString) && !empty($this->_properties)) {
  214. foreach ($this->_properties as $key => $value) {
  215. if (is_scalar($value)) {
  216. $propTemp[] = trim($key) . '=`' . $value . '`';
  217. }
  218. else {
  219. $propTemp[] = trim($key) . '=`' . md5(uniqid(rand(), true)) . '`';
  220. }
  221. }
  222. if (!empty($propTemp)) {
  223. $this->_propertyString = '?' . implode('&', $propTemp);
  224. }
  225. }
  226. $tag = '[[';
  227. $tag.= $this->getToken();
  228. $tag.= $this->get('name');
  229. if (!empty($this->_propertyString)) {
  230. $tag.= $this->_propertyString;
  231. }
  232. $tag.= ']]';
  233. $this->setTag($tag);
  234. }
  235. if (empty($this->_tag)) {
  236. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Instance of ' . get_class($this) . ' produced an empty tag!');
  237. }
  238. return $this->_tag;
  239. }
  240. /**
  241. * Accessor method for the token class var.
  242. *
  243. * @return string The token for this element tag.
  244. */
  245. public function getToken() {
  246. return $this->_token;
  247. }
  248. /**
  249. * Setter method for the token class var.
  250. *
  251. * @param string $token The token to use for this element tag.
  252. */
  253. public function setToken($token) {
  254. $this->_token = $token;
  255. }
  256. /**
  257. * Setter method for the tag class var.
  258. *
  259. * @param string $tag The tag to use for this element.
  260. */
  261. public function setTag($tag) {
  262. $this->_tag = $tag;
  263. }
  264. /**
  265. * Process the element source content to produce a result.
  266. *
  267. * @abstract Implement this to define behavior for a MODX content element.
  268. * @param array|string $properties A set of configuration properties for the
  269. * element.
  270. * @param string $content Optional content to use in place of any persistent
  271. * content associated with the element.
  272. * @return mixed The result of processing.
  273. */
  274. public function process($properties= null, $content= null) {
  275. $this->xpdo->getParser();
  276. $this->xpdo->parser->setProcessingElement(true);
  277. $this->getProperties($properties);
  278. $this->getTag();
  279. if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Processing Element: " . $this->get('name') . ($this->_tag ? "\nTag: {$this->_tag}" : "\n") . "\nProperties: " . print_r($this->_properties, true));
  280. if ($this->isCacheable() && isset ($this->xpdo->elementCache[$this->_tag])) {
  281. $this->_output = $this->xpdo->elementCache[$this->_tag];
  282. $this->_processed = true;
  283. } else {
  284. $this->filterInput();
  285. $this->getContent(is_string($content) ? array('content' => $content) : array());
  286. }
  287. return $this->_result;
  288. }
  289. /**
  290. * Cache the current output of this element instance by tag signature.
  291. */
  292. public function cache() {
  293. if ($this->isCacheable()) {
  294. $this->xpdo->elementCache[$this->_tag]= $this->_output;
  295. }
  296. }
  297. /**
  298. * Get an input filter instance configured for this Element.
  299. *
  300. * @return modInputFilter|null An input filter instance (or null if one cannot be loaded).
  301. */
  302. public function & getInputFilter() {
  303. if (!isset ($this->_filters['input']) || !($this->_filters['input'] instanceof modInputFilter)) {
  304. if (!$inputFilterClass= $this->get('input_filter')) {
  305. $inputFilterClass = $this->xpdo->getOption('input_filter',null,'filters.modInputFilter');
  306. }
  307. if ($filterClass= $this->xpdo->loadClass($inputFilterClass, '', false, true)) {
  308. if ($filter= new $filterClass($this->xpdo)) {
  309. $this->_filters['input']= $filter;
  310. }
  311. }
  312. }
  313. return $this->_filters['input'];
  314. }
  315. /**
  316. * Get an output filter instance configured for this Element.
  317. *
  318. * @return modOutputFilter|null An output filter instance (or null if one cannot be loaded).
  319. */
  320. public function & getOutputFilter() {
  321. if (!isset ($this->_filters['output']) || !($this->_filters['output'] instanceof modOutputFilter)) {
  322. if (!$outputFilterClass= $this->get('output_filter')) {
  323. $outputFilterClass = $this->xpdo->getOption('output_filter',null,'filters.modOutputFilter');
  324. }
  325. if ($filterClass= $this->xpdo->loadClass($outputFilterClass, '', false, true)) {
  326. if ($filter= new $filterClass($this->xpdo)) {
  327. $this->_filters['output']= $filter;
  328. }
  329. }
  330. }
  331. return $this->_filters['output'];
  332. }
  333. /**
  334. * Apply an input filter to an element.
  335. *
  336. * This is called by default in {@link modElement::process()} after the
  337. * element properties have been parsed.
  338. */
  339. public function filterInput() {
  340. $filter = $this->getInputFilter();
  341. if ($filter !== null && $filter instanceof modInputFilter) {
  342. $filter->filter($this);
  343. }
  344. }
  345. /**
  346. * Apply an output filter to an element.
  347. *
  348. * Call this method in your {modElement::process()} implementation when it
  349. * is appropriate, typically once all processing has been completed, but
  350. * before any caching takes place.
  351. */
  352. public function filterOutput() {
  353. $filter = $this->getOutputFilter();
  354. if ($filter !== null && $filter instanceof modOutputFilter) {
  355. $filter->filter($this);
  356. }
  357. }
  358. /**
  359. * Loads the access control policies applicable to this element.
  360. *
  361. * {@inheritdoc}
  362. */
  363. public function findPolicy($context = '') {
  364. $policy = array();
  365. $enabled = true;
  366. $context = !empty($context) ? $context : $this->xpdo->context->get('key');
  367. if ($context === $this->xpdo->context->get('key')) {
  368. $enabled = (boolean) $this->xpdo->getOption('access_category_enabled', null, true);
  369. } elseif ($this->xpdo->getContext($context)) {
  370. $enabled = (boolean) $this->xpdo->contexts[$context]->getOption('access_category_enabled', true);
  371. }
  372. if ($enabled) {
  373. if (empty($this->_policies) || !isset($this->_policies[$context])) {
  374. $accessTable = $this->xpdo->getTableName('modAccessCategory');
  375. $policyTable = $this->xpdo->getTableName('modAccessPolicy');
  376. $categoryClosureTable = $this->xpdo->getTableName('modCategoryClosure');
  377. $sql = "SELECT Acl.target, Acl.principal, Acl.authority, Acl.policy, Policy.data FROM {$accessTable} Acl " .
  378. "LEFT JOIN {$policyTable} Policy ON Policy.id = Acl.policy " .
  379. "JOIN {$categoryClosureTable} CategoryClosure ON CategoryClosure.descendant = :category " .
  380. "AND Acl.principal_class = 'modUserGroup' " .
  381. "AND CategoryClosure.ancestor = Acl.target " .
  382. "AND (Acl.context_key = :context OR Acl.context_key IS NULL OR Acl.context_key = '') " .
  383. "ORDER BY CategoryClosure.depth DESC, target, principal, authority ASC";
  384. $bindings = array(
  385. ':category' => $this->get('category'),
  386. ':context' => $context,
  387. );
  388. $query = new xPDOCriteria($this->xpdo, $sql, $bindings);
  389. if ($query->stmt && $query->stmt->execute()) {
  390. while ($row = $query->stmt->fetch(PDO::FETCH_ASSOC)) {
  391. $policy['modAccessCategory'][$row['target']][] = array(
  392. 'principal' => $row['principal'],
  393. 'authority' => $row['authority'],
  394. 'policy' => $row['data'] ? $this->xpdo->fromJSON($row['data'], true) : array(),
  395. );
  396. }
  397. }
  398. $this->_policies[$context] = $policy;
  399. } else {
  400. $policy = $this->_policies[$context];
  401. }
  402. }
  403. return $policy;
  404. }
  405. /**
  406. * Gets the raw, unprocessed source content for this element.
  407. *
  408. * @param array $options An array of options implementations can use to
  409. * accept language, revision identifiers, or other information to alter the
  410. * behavior of the method.
  411. * @return string The raw source content for the element.
  412. */
  413. public function getContent(array $options = array()) {
  414. if (!is_string($this->_content) || $this->_content === '') {
  415. if (isset($options['content'])) {
  416. $this->_content = $options['content'];
  417. } elseif ($this->isStatic()) {
  418. $this->_content = $this->getFileContent($options);
  419. if ($this->_content !== $this->_fields['content']) {
  420. $this->setContent($this->_content);
  421. if (!$this->isNew()) {
  422. $this->save();
  423. }
  424. }
  425. } else {
  426. $this->_content = $this->get('content');
  427. }
  428. }
  429. return $this->_content;
  430. }
  431. /**
  432. * Set the raw source content for this element.
  433. *
  434. * @param mixed $content The source content; implementations can decide if
  435. * it can only be a string, or some other source from which to retrieve it.
  436. * @param array $options An array of options implementations can use to
  437. * accept language, revision identifiers, or other information to alter the
  438. * behavior of the method.
  439. * @return boolean True indicates the content was set.
  440. */
  441. public function setContent($content, array $options = array()) {
  442. return $this->set('content', $content);
  443. }
  444. /**
  445. * Get the absolute path to the static source file for this instance.
  446. *
  447. * @param array $options An array of options.
  448. * @return string|boolean The absolute path to the static source file or false if not static.
  449. */
  450. public function getSourceFile(array $options = array()) {
  451. if ($this->isStatic() && (empty($this->_sourceFile) || $this->getOption('recalculate_source_file', $options, $this->staticSourceChanged()))) {
  452. $filename = $this->get('static_file');
  453. if (!empty($filename)) {
  454. $array = array();
  455. if ($this->xpdo->getParser() && $this->xpdo->parser->collectElementTags($filename, $array)) {
  456. $this->xpdo->parser->processElementTags('', $filename);
  457. }
  458. }
  459. if ($this->get('source') > 0) {
  460. /** @var modMediaSource $source */
  461. $source = $this->getOne('Source');
  462. if ($source && $source->get('is_stream')) {
  463. $source->initialize();
  464. $filename = $source->getBasePath().$filename;
  465. }
  466. }
  467. if (!file_exists($filename) && $this->get('source') < 1) {
  468. $this->getSourcePath($options);
  469. $this->_sourceFile= $this->_sourcePath . $filename;
  470. } else {
  471. $this->_sourceFile= $filename;
  472. }
  473. }
  474. return $this->isStatic() ? $this->_sourceFile : false;
  475. }
  476. /**
  477. * Get the absolute path location the source file is located relative to.
  478. *
  479. * @param array $options An array of options.
  480. * @return string The absolute path the sourceFile is relative to.
  481. */
  482. public function getSourcePath(array $options = array()) {
  483. $array = array();
  484. $this->_sourcePath= $this->xpdo->getOption('element_static_path', $options, $this->xpdo->getOption('components_path', $options, MODX_CORE_PATH . 'components/'));
  485. if ($this->xpdo->getParser() && $this->xpdo->parser->collectElementTags($this->_sourcePath, $array)) {
  486. $this->xpdo->parser->processElementTags('', $this->_sourcePath);
  487. }
  488. return $this->_sourcePath;
  489. }
  490. /**
  491. * Get the content stored in an external file for this instance.
  492. *
  493. * @param array $options An array of options.
  494. * @return bool|string The content or false if the content could not be retrieved.
  495. */
  496. public function getFileContent(array $options = array()) {
  497. $content = "";
  498. if ($this->isStatic()) {
  499. $sourceFile = $this->getSourceFile($options);
  500. if ($sourceFile && file_exists($sourceFile)) {
  501. $content = file_get_contents($sourceFile);
  502. }
  503. }
  504. return $content;
  505. }
  506. /**
  507. * Set external file content from this instance.
  508. *
  509. * @param string $content The content to set.
  510. * @param array $options An array of options.
  511. * @return bool|int The number of bytes written to file or false on failure.
  512. */
  513. public function setFileContent($content, array $options = array()) {
  514. $set = false;
  515. if ($this->isStatic()) {
  516. $sourceFile = $this->getSourceFile($options);
  517. if ($sourceFile) {
  518. $set = $this->xpdo->cacheManager->writeFile($sourceFile, $content);
  519. }
  520. }
  521. return $set;
  522. }
  523. /**
  524. * Get the properties for this element instance for processing.
  525. *
  526. * @param array|string $properties An array or string of properties to
  527. * apply.
  528. * @return array A simple array of properties ready to use for processing.
  529. */
  530. public function getProperties($properties = null) {
  531. $this->xpdo->getParser();
  532. $this->_properties= $this->xpdo->parser->parseProperties($this->get('properties'));
  533. $set= $this->getPropertySet();
  534. if (!empty($set)) {
  535. $this->_properties= array_merge($this->_properties, $set);
  536. }
  537. if ($this->get('property_preprocess')) {
  538. foreach ($this->_properties as $pKey => $pValue) {
  539. if ($this->xpdo->parser->processElementTags('', $pValue, $this->xpdo->parser->isProcessingUncacheable())) {
  540. $this->_properties[$pKey]= $pValue;
  541. }
  542. }
  543. }
  544. if (!empty($properties)) {
  545. $this->_properties= array_merge($this->_properties, $this->xpdo->parser->parseProperties($properties));
  546. }
  547. return $this->_properties;
  548. }
  549. /**
  550. * Gets a named property set related to this element instance.
  551. *
  552. * If a setName parameter is not provided, this function will attempt to
  553. * extract a setName from the element name using the @ symbol to delimit the
  554. * name of the property set.
  555. *
  556. * Here is an example of an element tag using the @ modifier to specify a
  557. * property set name:
  558. * [[ElementName@PropertySetName:FilterCommand=`FilterModifier`?
  559. * &PropertyKey1=`PropertyValue1`
  560. * &PropertyKey2=`PropertyValue2`
  561. * ]]
  562. *
  563. * @access public
  564. * @param string|null $setName An explicit property set name to search for.
  565. * @return array|null An array of properties or null if no set is found.
  566. */
  567. public function getPropertySet($setName = null) {
  568. $propertySet= null;
  569. $name = $this->get('name');
  570. if (strpos($name, '@') !== false) {
  571. $psName= '';
  572. $split= xPDO :: escSplit('@', $name);
  573. if ($split && isset($split[1])) {
  574. $name= $split[0];
  575. $psName= $split[1];
  576. $filters= xPDO :: escSplit(':', $setName);
  577. if ($filters && isset($filters[1]) && !empty($filters[1])) {
  578. $psName= $filters[0];
  579. $name.= ':' . $filters[1];
  580. }
  581. $this->set('name', $name);
  582. }
  583. if (!empty($psName)) {
  584. $psObj= $this->xpdo->getObjectGraph('modPropertySet', '{"Elements":{}}', array(
  585. 'Elements.element' => $this->id,
  586. 'Elements.element_class' => $this->_class,
  587. 'modPropertySet.name' => $psName
  588. ));
  589. if ($psObj) {
  590. $propertySet= $this->xpdo->parser->parseProperties($psObj->get('properties'));
  591. }
  592. }
  593. }
  594. if (!empty($setName)) {
  595. $propertySetObj= $this->xpdo->getObjectGraph('modPropertySet', '{"Elements":{}}', array(
  596. 'Elements.element' => $this->id,
  597. 'Elements.element_class' => $this->_class,
  598. 'modPropertySet.name' => $setName
  599. ));
  600. if ($propertySetObj) {
  601. if (is_array($propertySet)) {
  602. $propertySet= array_merge($propertySet, $this->xpdo->parser->parseProperties($propertySetObj->get('properties')));
  603. } else {
  604. $propertySet= $this->xpdo->parser->parseProperties($propertySetObj->get('properties'));
  605. }
  606. }
  607. }
  608. return $propertySet;
  609. }
  610. /**
  611. * Set default properties for this element instance.
  612. *
  613. * @access public
  614. * @param array|string $properties A property array or property string.
  615. * @param boolean $merge Indicates if properties should be merged with
  616. * existing ones.
  617. * @return boolean true if the properties are set.
  618. */
  619. public function setProperties($properties, $merge = false) {
  620. $set = false;
  621. $propertiesArray = array();
  622. if (is_string($properties)) {
  623. $properties = $this->xpdo->parser->parsePropertyString($properties);
  624. }
  625. if (is_array($properties)) {
  626. foreach ($properties as $propKey => $property) {
  627. if (is_array($property) && isset($property[5])) {
  628. $key = $property[0];
  629. $propertyArray = array(
  630. 'name' => $property[0],
  631. 'desc' => $property[1],
  632. 'type' => $property[2],
  633. 'options' => $property[3],
  634. 'value' => $property[4],
  635. 'lexicon' => !empty($property[5]) ? $property[5] : null,
  636. 'area' => !empty($property[6]) ? $property[6] : '',
  637. );
  638. } elseif (is_array($property) && isset($property['value'])) {
  639. $key = $property['name'];
  640. $propertyArray = array(
  641. 'name' => $property['name'],
  642. 'desc' => isset($property['description']) ? $property['description'] : (isset($property['desc']) ? $property['desc'] : ''),
  643. 'type' => isset($property['xtype']) ? $property['xtype'] : (isset($property['type']) ? $property['type'] : 'textfield'),
  644. 'options' => isset($property['options']) ? $property['options'] : array(),
  645. 'value' => $property['value'],
  646. 'lexicon' => !empty($property['lexicon']) ? $property['lexicon'] : null,
  647. 'area' => !empty($property['area']) ? $property['area'] : '',
  648. );
  649. } else {
  650. $key = $propKey;
  651. $propertyArray = array(
  652. 'name' => $propKey,
  653. 'desc' => '',
  654. 'type' => 'textfield',
  655. 'options' => array(),
  656. 'value' => $property,
  657. 'lexicon' => null,
  658. 'area' => '',
  659. );
  660. }
  661. if (!empty($propertyArray['options'])) {
  662. foreach ($propertyArray['options'] as $optionKey => &$option) {
  663. if (empty($option['text']) && !empty($option['name'])) $option['text'] = $option['name'];
  664. unset($option['menu'],$option['name']);
  665. }
  666. }
  667. if ($propertyArray['type'] == 'combo-boolean' && is_numeric($propertyArray['value'])) {
  668. $propertyArray['value'] = (boolean)$propertyArray['value'];
  669. }
  670. $propertiesArray[$key] = $propertyArray;
  671. }
  672. if ($merge && !empty($propertiesArray)) {
  673. $existing = $this->get('properties');
  674. if (is_array($existing) && !empty($existing)) {
  675. $propertiesArray = array_merge($existing, $propertiesArray);
  676. }
  677. }
  678. $set = $this->set('properties', $propertiesArray);
  679. }
  680. return $set;
  681. }
  682. /**
  683. * Add a property set to this element, making it available for use.
  684. *
  685. * @access public
  686. * @param string|modPropertySet $propertySet A modPropertySet object or the
  687. * name of a modPropertySet object to create a relationship with.
  688. * @return boolean True if a relationship was created or already exists.
  689. */
  690. public function addPropertySet($propertySet) {
  691. $added= false;
  692. if (!empty($propertySet)) {
  693. if (is_string($propertySet)) {
  694. $propertySet = $this->xpdo->getObject('modPropertySet', array('name' => $propertySet));
  695. }
  696. if (is_object($propertySet) && $propertySet instanceof modPropertySet) {
  697. if (!$this->isNew() && !$propertySet->isNew() && $this->xpdo->getCount('modElementPropertySet', array('element' => $this->get('id'), 'element_class' => $this->_class, 'property_set' => $propertySet->get('id')))) {
  698. $added = true;
  699. } else {
  700. if ($propertySet->isNew()) $propertySet->save();
  701. /** @var modElementPropertySet $link */
  702. $link= $this->xpdo->newObject('modElementPropertySet');
  703. $link->set('element', $this->get('id'));
  704. $link->set('element_class', $this->_class);
  705. $link->set('property_set', $propertySet->get('id'));
  706. $added = $link->save();
  707. }
  708. }
  709. }
  710. return $added;
  711. }
  712. /**
  713. * Remove a property set from this element, making it unavailable for use.
  714. *
  715. * @access public
  716. * @param string|modPropertySet $propertySet A modPropertySet object or the
  717. * name of a modPropertySet object to dissociate from.
  718. * @return boolean True if a relationship was destroyed.
  719. */
  720. public function removePropertySet($propertySet) {
  721. $removed = false;
  722. if (!empty($propertySet)) {
  723. if (is_string($propertySet)) {
  724. $propertySet = $this->xpdo->getObject('modPropertySet', array('name' => $propertySet));
  725. }
  726. if (is_object($propertySet) && $propertySet instanceof modPropertySet) {
  727. $removed = $this->xpdo->removeObject('modElementPropertySet', array('element' => $this->get('id'), 'element_class' => $this->_class, 'property_set' => $propertySet->get('id')));
  728. }
  729. }
  730. return $removed;
  731. }
  732. /**
  733. * Indicates if the element is cacheable.
  734. *
  735. * @access public
  736. * @return boolean True if the element can be stored to or retrieved from
  737. * the element cache.
  738. */
  739. public function isCacheable() {
  740. return $this->_cacheable;
  741. }
  742. /**
  743. * Sets the runtime cacheability of the element.
  744. *
  745. * @access public
  746. * @param boolean $cacheable Indicates the value to set for cacheability of
  747. * this element.
  748. */
  749. public function setCacheable($cacheable = true) {
  750. $this->_cacheable = (boolean) $cacheable;
  751. }
  752. /**
  753. * Get the Source for this Element
  754. *
  755. * @param string $contextKey
  756. * @param boolean $fallbackToDefault
  757. * @return modMediaSource|null
  758. */
  759. public function getSource($contextKey = '',$fallbackToDefault = true) {
  760. if (empty($contextKey)) $contextKey = $this->xpdo->context->get('key');
  761. $source = $this->_source;
  762. if (empty($source)) {
  763. $c = $this->xpdo->newQuery('sources.modMediaSource');
  764. $c->innerJoin('sources.modMediaSourceElement','SourceElement');
  765. $c->where(array(
  766. 'SourceElement.object' => $this->get('id'),
  767. 'SourceElement.object_class' => $this->_class,
  768. 'SourceElement.context_key' => $contextKey,
  769. ));
  770. $source = $this->xpdo->getObject('sources.modMediaSource',$c);
  771. if (!$source && $fallbackToDefault) {
  772. $source = modMediaSource::getDefaultSource($this->xpdo);
  773. }
  774. $this->setSource($source);
  775. }
  776. return $source;
  777. }
  778. /**
  779. * Setter method for the source class var.
  780. *
  781. * @param string $source The source to use for this element.
  782. */
  783. public function setSource($source) {
  784. $this->_source = $source;
  785. }
  786. /**
  787. * Get the stored sourceCache for a context
  788. *
  789. * @param string $contextKey
  790. * @param array $options
  791. * @return array
  792. */
  793. public function getSourceCache($contextKey = '',array $options = array()) {
  794. /** @var modCacheManager $cacheManager */
  795. $cacheManager = $this->xpdo->getCacheManager();
  796. if (!$cacheManager || !($cacheManager instanceof modCacheManager)) return array();
  797. if (empty($contextKey)) $contextKey = $this->xpdo->context->get('key');
  798. return $cacheManager->getElementMediaSourceCache($this,$contextKey,$options);
  799. }
  800. /**
  801. * Indicates if the instance has content in an external file.
  802. *
  803. * @return boolean True if the instance has content stored in an external file.
  804. */
  805. public function isStatic() {
  806. return $this->get('static');
  807. }
  808. /**
  809. * Indicates if the content has changed and the Element has a mutable static source.
  810. *
  811. * @return boolean
  812. */
  813. public function staticContentChanged() {
  814. return $this->isStatic() && $this->isDirty('content');
  815. }
  816. /**
  817. * Check if directories are empty after moving a static element and remove empty directories.
  818. *
  819. * @param $dirname
  820. */
  821. public function cleanupStaticFileDirectories($dirname) {
  822. $contents = array_diff(scandir($dirname), array('..', '.', '.DS_Store'));
  823. @unlink($dirname .'/.DS_Store');
  824. if (count($contents) === 0) {
  825. if (is_dir($dirname)) {
  826. if (rmdir($dirname)) {
  827. /* Check if parent directory is also empty. */
  828. $this->cleanupStaticFileDirectories(dirname($dirname));
  829. }
  830. }
  831. }
  832. }
  833. /**
  834. * Returns static file path if the file path or source has changed.
  835. */
  836. public function getOldStaticFilePath() {
  837. $oldFilePath = '';
  838. $sourceId = 0;
  839. $result = $this->xpdo->getObject($this->_class, array('id' => $this->_fields['id']));
  840. if ($result) {
  841. $staticFilePath = $result->get('static_file');
  842. $sourceId = $result->get('source');
  843. if ($staticFilePath !== $this->_fields['static_file'] || $sourceId !== $this->_fields['source']) {
  844. $oldFilePath = $staticFilePath;
  845. }
  846. }
  847. if (!empty($oldFilePath)) {
  848. if ($sourceId > 0) {
  849. /** @var modMediaSource $source */
  850. $source = $this->xpdo->getObject('sources.modFileMediaSource', array('id' => $sourceId));
  851. if ($source && $source->get('is_stream')) {
  852. $source->initialize();
  853. $oldFilePath = $source->getBasePath() . $oldFilePath;
  854. }
  855. }
  856. if (!file_exists($oldFilePath) && $this->get('source') < 1) {
  857. $this->getSourcePath();
  858. $oldFilePath = $this->_sourcePath . $oldFilePath;
  859. }
  860. }
  861. return $oldFilePath;
  862. }
  863. /**
  864. * Indicates if the static source has changed.
  865. *
  866. * @return boolean
  867. */
  868. public function staticSourceChanged() {
  869. return $this->isStatic() && ($this->isDirty('static') || $this->isDirty('static_file') || $this->isDirty('source'));
  870. }
  871. /**
  872. * Return if the static source is mutable.
  873. *
  874. * @return boolean True if the source file is mutable.
  875. */
  876. public function isStaticSourceMutable() {
  877. $isMutable = false;
  878. $sourceFile = $this->getSourceFile();
  879. if ($sourceFile) {
  880. if (file_exists($sourceFile)) {
  881. $isMutable = is_writable($sourceFile) && !is_dir($sourceFile);
  882. } else {
  883. $sourceDir = dirname($sourceFile);
  884. $i = 100;
  885. while (!empty($sourceDir)) {
  886. if (file_exists($sourceDir) && is_dir($sourceDir)) {
  887. $isMutable = is_writable($sourceDir);
  888. if ($isMutable) break;
  889. }
  890. if ($sourceDir != '/') {
  891. $sourceDir = dirname($sourceDir);
  892. } else {
  893. break;
  894. }
  895. $i--;
  896. if ($i < 0) break;
  897. }
  898. }
  899. }
  900. return $isMutable;
  901. }
  902. /**
  903. * Ensure the static source cannot browse the protected configuration directory
  904. *
  905. * @return boolean True if is a valid source path
  906. */
  907. public function isStaticSourceValidPath() {
  908. $isValid = true;
  909. $sourceFile = $this->getSourceFile();
  910. if ($sourceFile) {
  911. $sourceDirectory = rtrim(dirname($sourceFile),'/');
  912. $configDirectory = rtrim($this->xpdo->getOption('core_path',null,MODX_CORE_PATH).'config/','/');
  913. if ($sourceDirectory == $configDirectory) {
  914. $isValid = false;
  915. }
  916. }
  917. return $isValid;
  918. }
  919. }