modresource.class.php 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439
  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. * Interface for implementation on derivative Resource types. Please define the following methods in your derivative
  12. * class to properly implement a Custom Resource Type in MODX.
  13. *
  14. * @see modResource
  15. * @interface
  16. * @package modx
  17. */
  18. interface modResourceInterface {
  19. /**
  20. * Determine the controller path for this Resource class. Return an absolute path.
  21. *
  22. * @static
  23. * @param xPDO $modx A reference to the modX object
  24. * @return string The absolute path to the controller for this Resource class
  25. */
  26. public static function getControllerPath(xPDO &$modx);
  27. /**
  28. * Use this in your extended Resource class to display the text for the context menu item, if showInContextMenu is
  29. * set to true. Return in the following format:
  30. *
  31. * array(
  32. * 'text_create' => 'ResourceTypeName',
  33. * 'text_create_here' => 'Create ResourceTypeName Here',
  34. * );
  35. *
  36. * @return array
  37. */
  38. public function getContextMenuText();
  39. /**
  40. * Use this in your extended Resource class to return a translatable name for the Resource Type.
  41. * @return string
  42. */
  43. public function getResourceTypeName();
  44. /**
  45. * Allows you to manipulate the tree node for a Resource before it is sent
  46. * @abstract
  47. * @param array $node
  48. */
  49. public function prepareTreeNode(array $node = array());
  50. }
  51. /**
  52. * Represents a web resource managed by the MODX framework.
  53. *
  54. * @property int $id The ID of the Resource
  55. * @property string $type The type of the resource; document/reference
  56. * @property string $contentType The content type string of the Resource, such as text/html
  57. * @property string $pagetitle The page title of the Resource
  58. * @property string $longtitle The long title of the Resource
  59. * @property string $description The description of the Resource
  60. * @property string $alias The FURL alias of the resource
  61. * @property boolean $aliasVisible Whether or not we should exclude the resource alias for children
  62. * @property string $link_attributes Any link attributes for the URL generated for the Resource
  63. * @property boolean $published Whether or not this Resource is published, or viewable by users without the 'view_unpublished' permission
  64. * @property int $pub_date The UNIX time that this Resource will be automatically marked as published
  65. * @property int $unpub_date The UNIX time that this Resource will be automatically marked as unpublished
  66. * @property int $parent The parent ID of the Resource
  67. * @property boolean $isfolder Whether or not this Resource is a container
  68. * @property string $introtext The intro text of this Resource, often used as an excerpt
  69. * @property string $content The actual content of this Resource
  70. * @property boolean $richtext Whether or not this Resource is edited with a Rich Text Editor, if installed
  71. * @property int $template The Template this Resource is tied to, or 0 to use an empty Template
  72. * @property int $menuindex The menuindex, or rank, that this Resource shows in.
  73. * @property boolean $searchable Whether or not this Resource should be searchable
  74. * @property boolean $cacheable Whether or not this Resource should be cacheable
  75. * @property int $createdby The ID of the User that created this Resource
  76. * @property int $createdon The UNIX time of when this Resource was created
  77. * @property int $editedby The ID of the User, if any, that last edited this Resource
  78. * @property int $editedon The UNIX time, if set, of when this Resource was last edited
  79. * @property boolean $deleted Whether or not this Resource is marked as deleted
  80. * @property int $deletedon The UNIX time of when this Resource was deleted
  81. * @property int $deletedby The User that deleted this Resource
  82. * @property int $publishedon The UNIX time that this Resource was marked as published
  83. * @property int $publishedby The User that published this Resource
  84. * @property string $menutitle The title to show when this Resource is displayed in a menu
  85. * @property boolean $donthit Deprecated.
  86. * @property boolean $privateweb Deprecated.
  87. * @property boolean $privatemgr Deprecated.
  88. * @property int $content_dispo The type of Content Disposition that is used when displaying this Resource
  89. * @property boolean $hidemenu Whether or not this Resource should show in menus
  90. * @property string $class_key The Class Key of this Resource. Useful for derivative Resource types
  91. * @property string $context_key The Context that this Resource resides in
  92. * @property int $content_type The Content Type ID of this Resource
  93. * @property string $uri The generated URI of this Resource
  94. * @property boolean $uri_override Whether or not this URI is "frozen": where the URI will stay as specified and will not be regenerated
  95. * @property boolean $hide_children_in_tree Whether or not this Resource should show in the mgr tree any of its children
  96. * @property boolean $show_in_tree Whether or not this Resource should show in the mgr tree
  97. * @see modTemplate
  98. * @see modContentType
  99. * @package modx
  100. */
  101. class modResource extends modAccessibleSimpleObject implements modResourceInterface {
  102. /**
  103. * Represents the cacheable content for a resource.
  104. *
  105. * Note that this is not the raw source content, but the content that is the
  106. * result of processing cacheable tags within the raw source content.
  107. * @var string
  108. */
  109. public $_content= '';
  110. /**
  111. * Represents the output the resource produces.
  112. * @var string
  113. */
  114. public $_output= '';
  115. /**
  116. * The context the resource is requested from.
  117. *
  118. * Note that this is different than the context_key field that describes a
  119. * primary context for the resource.
  120. * @var string
  121. */
  122. public $_contextKey= null;
  123. /**
  124. * Indicates if the resource has already been processed.
  125. * @var boolean
  126. */
  127. protected $_processed= false;
  128. /**
  129. * The cache filename for the resource in the context.
  130. * @var string
  131. */
  132. protected $_cacheKey= null;
  133. /**
  134. * Indicates if the site cache should be refreshed when saving changes.
  135. * @var boolean
  136. */
  137. protected $_refreshCache= true;
  138. /**
  139. * Indicates if this Resource was generated from a forward.
  140. * @var boolean
  141. */
  142. public $_isForward= false;
  143. /**
  144. * An array of Javascript/CSS to be appended to the footer of this Resource
  145. * @var array $_jscripts
  146. */
  147. public $_jscripts = array();
  148. /**
  149. * An array of Javascript/CSS to be appended to the HEAD of this Resource
  150. * @var array $_sjscripts
  151. */
  152. public $_sjscripts = array();
  153. /**
  154. * All loaded Javascript/CSS that has been calculated to be loaded
  155. * @var array
  156. */
  157. public $_loadedjscripts = array();
  158. /**
  159. * Use if extending modResource to state whether or not to show the extended class in the tree context menu
  160. * @var boolean
  161. */
  162. public $showInContextMenu = false;
  163. /**
  164. * Use if extending modResource to state whether or not to allow drop on extended class in the resource tree
  165. * Set 1 for allow drop, 0 for disable drop or -1 for default behavior
  166. * @var int
  167. */
  168. public $allowDrop = -1;
  169. /**
  170. * Use if extending modResource to state whether or not the derivative class can be listed in the class_key
  171. * dropdown users can change when editing a resource.
  172. * @var boolean
  173. */
  174. public $allowListingInClassKeyDropdown = true;
  175. /**
  176. * Whether or not to allow creation of children resources in tree. Can be overridden in a derivative Resource class.
  177. * @var boolean
  178. */
  179. public $allowChildrenResources = true;
  180. /** @var modX $xpdo */
  181. public $xpdo;
  182. /**
  183. * Filter a string for use as a URL path segment.
  184. *
  185. * @param modX|xPDO &$xpdo A reference to a modX or xPDO instance.
  186. * @param string $segment The string to filter into a path segment.
  187. * @param array $options Local options to override global filter settings.
  188. *
  189. * @return string The filtered string ready to use as a path segment.
  190. */
  191. public static function filterPathSegment(&$xpdo, $segment, array $options = array()) {
  192. /* setup the various options */
  193. $iconv = function_exists('iconv');
  194. $mbext = function_exists('mb_strlen') && (boolean) $xpdo->getOption('use_multibyte', false);
  195. $charset = strtoupper((string) $xpdo->getOption('modx_charset', $options, 'UTF-8'));
  196. $delimiter = $xpdo->getOption('friendly_alias_word_delimiter', $options, '-');
  197. $delimiters = $xpdo->getOption('friendly_alias_word_delimiters', $options, '-_');
  198. $maxlength = (integer) $xpdo->getOption('friendly_alias_max_length', $options, 0);
  199. $stripElementTags = (boolean) $xpdo->getOption('friendly_alias_strip_element_tags', $options, true);
  200. $trimchars = $xpdo->getOption('friendly_alias_trim_chars', $options, '/.' . $delimiters);
  201. $restrictchars = $xpdo->getOption('friendly_alias_restrict_chars', $options, 'pattern');
  202. $restrictcharspattern = $xpdo->getOption('friendly_alias_restrict_chars_pattern', $options, '/[\0\x0B\t\n\r\f\a&=+%#<>"~`@\?\[\]\{\}\|\^\'\\\\]/');
  203. $lowercase = (boolean) $xpdo->getOption('friendly_alias_lowercase_only', $options, true);
  204. $translit = $xpdo->getOption('friendly_alias_translit', $options, $iconv ? 'iconv' : 'none');
  205. $translitClass = $xpdo->getOption('friendly_alias_translit_class', $options, 'translit.modTransliterate');
  206. /* strip html and optionally MODX element tags (stripped by default) */
  207. if ($xpdo instanceof modX) {
  208. $segment = $xpdo->stripTags($segment, '', $stripElementTags ? array() : null);
  209. }
  210. /* replace &nbsp; with the specified word delimiter */
  211. $segment = str_replace('&nbsp;', $delimiter, $segment);
  212. /* decode named entities to the appropriate character for the character set */
  213. $segment = html_entity_decode($segment, ENT_QUOTES, $charset);
  214. /* prepare '&' replacement */
  215. if ($xpdo instanceof modX && $xpdo->getService('lexicon','modLexicon') && $xpdo->lexicon('and')) {
  216. $ampersand = ' ' . $xpdo->lexicon('and') . ' ';
  217. } else {
  218. $ampersand = ' and ';
  219. }
  220. /* apply transliteration as configured */
  221. switch ($translit) {
  222. case '':
  223. case 'none':
  224. /* no transliteration */
  225. break;
  226. case 'iconv':
  227. /* if iconv is available, use the built-in transliteration it provides */
  228. $segment = iconv($mbext ? mb_detect_encoding($segment) : $charset, $charset . '//TRANSLIT//IGNORE', $segment);
  229. $ampersand = iconv($mbext ? mb_detect_encoding($segment) : $charset, $charset . '//TRANSLIT//IGNORE', $ampersand);
  230. break;
  231. case 'iconv_ascii':
  232. /* if iconv is available, use the built-in transliteration to ASCII it provides */
  233. $segment = iconv(($mbext) ? mb_detect_encoding($segment) : $charset, 'ASCII//TRANSLIT//IGNORE', $segment);
  234. break;
  235. default:
  236. /* otherwise look for a transliteration service class that will accept named transliteration tables */
  237. if ($xpdo instanceof modX) {
  238. $translitClassPath = $xpdo->getOption('friendly_alias_translit_class_path', $options, $xpdo->getOption('core_path', $options, MODX_CORE_PATH) . 'components/');
  239. if ($xpdo->getService('translit', $translitClass, $translitClassPath, $options)) {
  240. $segment = $xpdo->translit->translate($segment, $translit);
  241. $ampersand = $xpdo->translit->translate($ampersand, $translit);
  242. }
  243. }
  244. break;
  245. }
  246. /* replace any remaining '&' with a translit ampersand */
  247. $segment = str_replace('&', $ampersand, $segment);
  248. /* restrict characters as configured */
  249. switch ($restrictchars) {
  250. case 'alphanumeric':
  251. /* restrict segment to alphanumeric characters only */
  252. $segment = preg_replace('/[^\.%A-Za-z0-9 _-]/', '', $segment);
  253. break;
  254. case 'alpha':
  255. /* restrict segment to alpha characters only */
  256. $segment = preg_replace('/[^\.%A-Za-z _-]/', '', $segment);
  257. break;
  258. case 'legal':
  259. /* restrict segment to legal URL characters only */
  260. $segment = preg_replace('/[\0\x0B\t\n\r\f\a&=+%#<>"~`@\?\[\]\{\}\|\^\'\\\\]/', '', $segment);
  261. break;
  262. case 'pattern':
  263. default:
  264. /* restrict segment using regular expression pattern configured (same as legal by default) */
  265. if (!empty($restrictcharspattern)) {
  266. $segment = preg_replace($restrictcharspattern, '', $segment);
  267. }
  268. }
  269. /* replace one or more space characters with word delimiter */
  270. $segment = preg_replace('/\s+/u', $delimiter, $segment);
  271. /* replace one or more instances of word delimiters with word delimiter */
  272. $delimiterTokens = array();
  273. for ($d = 0; $d < strlen($delimiters); $d++) {
  274. $delimiterTokens[] = preg_quote($delimiters{$d}, '/');
  275. }
  276. if (!empty($delimiterTokens)) {
  277. $delimiterPattern = '/[' . implode('|', $delimiterTokens) . ']+/';
  278. $segment = preg_replace($delimiterPattern, $delimiter, $segment);
  279. }
  280. /* unless lowercase_only preference is explicitly off, change case to lowercase */
  281. if ($lowercase) {
  282. if ($mbext) {
  283. /* if the mb extension is available use it to protect multi-byte chars */
  284. $segment = mb_convert_case($segment, MB_CASE_LOWER, $charset);
  285. } else {
  286. /* otherwise, just use strtolower */
  287. $segment = strtolower($segment);
  288. }
  289. }
  290. /* trim specified chars from both ends of the segment */
  291. $segment = trim($segment, $trimchars);
  292. /* get the strlen of the segment (use mb extension if available) */
  293. $length = $mbext ? mb_strlen($segment, $charset) : strlen($segment);
  294. /* if maxlength is specified and exceeded, return substr with additional trim applied */
  295. if ($maxlength > 0 && $length > $maxlength) {
  296. $segment = substr($segment, 0, $maxlength);
  297. $segment = trim($segment, $trimchars);
  298. }
  299. return $segment;
  300. }
  301. /**
  302. * Get a sortable, limitable collection (and total count) of Resource Groups for a given Resource.
  303. *
  304. * @static
  305. * @param modResource &$resource A reference to the modResource to get the groups from.
  306. * @param array $sort An array of sort columns in column => direction format.
  307. * @param int $limit A limit of records to retrieve in the collection.
  308. * @param int $offset A record offset for a limited collection.
  309. * @return array An array containing the collection and total.
  310. */
  311. public static function listGroups(modResource &$resource, array $sort = array('id' => 'ASC'), $limit = 0, $offset = 0) {
  312. $result = array('collection' => array(), 'total' => 0);
  313. $c = $resource->xpdo->newQuery('modResourceGroup');
  314. $c->leftJoin('modResourceGroupResource', 'ResourceGroupResource', array(
  315. "ResourceGroupResource.document_group = modResourceGroup.id",
  316. 'ResourceGroupResource.document' => $resource->get('id')
  317. ));
  318. $result['total'] = $resource->xpdo->getCount('modResourceGroup',$c);
  319. $c->select($resource->xpdo->getSelectColumns('modResourceGroup', 'modResourceGroup'));
  320. $c->select(array("IF(ISNULL(ResourceGroupResource.document),0,1) AS access"));
  321. foreach ($sort as $sortKey => $sortDir) {
  322. $c->sortby($resource->xpdo->escape('modResourceGroup') . '.' . $resource->xpdo->escape($sortKey), $sortDir);
  323. }
  324. if ($limit > 0) $c->limit($limit, $offset);
  325. $result['collection'] = $resource->xpdo->getCollection('modResourceGroup', $c);
  326. return $result;
  327. }
  328. /**
  329. * Retrieve a collection of Template Variables for a Resource.
  330. *
  331. * @static
  332. * @param modResource &$resource A reference to the modResource to retrieve TemplateVars for.
  333. * @return A collection of modTemplateVar instances for the modResource.
  334. */
  335. public static function getTemplateVarCollection(modResource &$resource) {
  336. $c = $resource->xpdo->newQuery('modTemplateVar');
  337. $c->query['distinct'] = 'DISTINCT';
  338. $c->select($resource->xpdo->getSelectColumns('modTemplateVar', 'modTemplateVar'));
  339. $c->select($resource->xpdo->getSelectColumns('modTemplateVarTemplate', 'tvtpl', '', array('rank')));
  340. if ($resource->isNew()) {
  341. $c->select(array(
  342. 'modTemplateVar.default_text AS value',
  343. '0 AS resourceId'
  344. ));
  345. } else {
  346. $c->select(array(
  347. 'IF(ISNULL(tvc.value),modTemplateVar.default_text,tvc.value) AS value',
  348. $resource->get('id').' AS resourceId'
  349. ));
  350. }
  351. $c->innerJoin('modTemplateVarTemplate','tvtpl',array(
  352. 'tvtpl.tmplvarid = modTemplateVar.id',
  353. 'tvtpl.templateid' => $resource->get('template'),
  354. ));
  355. if (!$resource->isNew()) {
  356. $c->leftJoin('modTemplateVarResource','tvc',array(
  357. 'tvc.tmplvarid = modTemplateVar.id',
  358. 'tvc.contentid' => $resource->get('id'),
  359. ));
  360. }
  361. $c->sortby('tvtpl.rank,modTemplateVar.rank');
  362. return $resource->xpdo->getCollection('modTemplateVar', $c);
  363. }
  364. /**
  365. * Refresh Resource URI fields for children of the specified parent.
  366. *
  367. * @static
  368. * @param modX &$modx A reference to a valid modX instance.
  369. * @param int $parent The id of a Resource parent to start from (default is 0, the root)
  370. * @param array $options An array of various options for the method:
  371. * - resetOverrides: if true, Resources with uri_override set to true will be included
  372. * - contexts: an optional array of context keys to limit the refresh scope
  373. * @return void
  374. */
  375. public static function refreshURIs(modX &$modx, $parent = 0, array $options = array()) {
  376. $resetOverrides = array_key_exists('resetOverrides', $options) ? (boolean) $options['resetOverrides'] : false;
  377. $contexts = array_key_exists('contexts', $options) ? explode(',', $options['contexts']) : null;
  378. $criteria = $modx->newQuery('modResource', array('parent' => $parent));
  379. if (!$resetOverrides) {
  380. $criteria->where(array('uri_override' => false));
  381. }
  382. if (!empty($contexts)) {
  383. $criteria->where(array('context_key:IN' => $contexts));
  384. }
  385. $criteria->sortby('menuindex', 'ASC');
  386. /** @var modResource $resource */
  387. foreach ($modx->getIterator('modResource', $criteria) as $resource) {
  388. $resource->set('refreshURIs', true);
  389. if ($resetOverrides) {
  390. $resource->set('uri_override', false);
  391. }
  392. if (!$resource->get('uri_override')) {
  393. $resource->set('uri', '');
  394. }
  395. $resource->save();
  396. }
  397. }
  398. /**
  399. * Updates the Context of all Children recursively to that of the parent.
  400. *
  401. * @static
  402. * @param modX &$modx A reference to an initialized modX instance.
  403. * @param modResource $parent The parent modResource instance.
  404. * @param array $options An array of options.
  405. * @return int The number of children updated.
  406. */
  407. public static function updateContextOfChildren(modX &$modx, $parent, array $options = array()) {
  408. $count = 0;
  409. /** @var modResource $child */
  410. foreach ($parent->getIterator('Children') as $child) {
  411. $child->set('context_key', $parent->get('context_key'));
  412. if ($child->save()) {
  413. $count++;
  414. } else {
  415. $modx->log(modX::LOG_LEVEL_ERROR, "Could not change Context of child resource {$child->get('id')}", '', __METHOD__, __FILE__, __LINE__);
  416. }
  417. }
  418. return $count;
  419. }
  420. /**
  421. * @param xPDO $xpdo A reference to the xPDO|modX instance
  422. */
  423. function __construct(xPDO & $xpdo) {
  424. parent :: __construct($xpdo);
  425. $this->_contextKey= isset ($this->xpdo->context) ? $this->xpdo->context->get('key') : 'web';
  426. $this->_cacheKey= "[contextKey]/resources/[id]";
  427. }
  428. /**
  429. * Prepare the resource for output.
  430. */
  431. public function prepare()
  432. {
  433. # 1. Parse cacheable elements if exist.
  434. $this->process();
  435. # 2. Copy registered scripts added by the cacheable elements.
  436. $this->syncScripts();
  437. # 3. Parse uncacheable elements.
  438. $this->parseContent();
  439. }
  440. /**
  441. * Process a resource, transforming source content to output.
  442. *
  443. * @return string The processed cacheable content of a resource.
  444. */
  445. public function process() {
  446. if (!$this->get('cacheable') || !$this->_processed || !$this->_content) {
  447. $this->_content= '';
  448. $this->_output= '';
  449. $this->xpdo->getParser();
  450. /** @var modTemplate $baseElement */
  451. if ($baseElement= $this->getOne('Template')) {
  452. if ($baseElement->process()) {
  453. $this->_content= $baseElement->_output;
  454. $this->_processed= true;
  455. }
  456. } else {
  457. $this->_content= $this->getContent();
  458. $maxIterations= intval($this->xpdo->getOption('parser_max_iterations',10));
  459. $this->xpdo->parser->processElementTags('', $this->_content, false, false, '[[', ']]', array(), $maxIterations);
  460. $this->_processed= true;
  461. }
  462. }
  463. return $this->_content;
  464. }
  465. /**
  466. * @param array $data Data for placeholders
  467. * @return string
  468. */
  469. public function parseContent($data = array())
  470. {
  471. $this->xpdo->getParser();
  472. $maxIterations = intval($this->xpdo->getOption('parser_max_iterations', null, 10));
  473. $oldResource = $this->xpdo->resource;
  474. $this->xpdo->resource = $this;
  475. if (!empty($data)) {
  476. $scope = $this->xpdo->toPlaceholders($data, '', '.', true);
  477. }
  478. if (!$this->_processed) {
  479. $this->_content = $this->getContent();
  480. $this->xpdo->parser->processElementTags('', $this->_content, false, false, '[[', ']]', array(), $maxIterations);
  481. $this->_processed = true;
  482. }
  483. $this->_output = $this->_content;
  484. $this->xpdo->parser->processElementTags('', $this->_output, true, false, '[[', ']]', array(), $maxIterations);
  485. $this->xpdo->parser->processElementTags('', $this->_output, true, true, '[[', ']]', array(), $maxIterations);
  486. $this->xpdo->resource = $oldResource;
  487. if (isset($scope['keys'])) $this->xpdo->unsetPlaceholders($scope['keys']);
  488. if (isset($scope['restore'])) $this->xpdo->toPlaceholders($scope['restore']);
  489. return $this->_output;
  490. }
  491. /**
  492. * Store scripts registered by cached elements.
  493. */
  494. public function syncScripts()
  495. {
  496. $this->_jscripts = $this->xpdo->jscripts;
  497. $this->_sjscripts = $this->xpdo->sjscripts;
  498. $this->_loadedjscripts = $this->xpdo->loadedjscripts;
  499. }
  500. /**
  501. * Gets the raw, unprocessed source content for a resource.
  502. *
  503. * @param array $options An array of options implementations can use to
  504. * accept language, revision identifiers, or other information to alter the
  505. * behavior of the method.
  506. * @return string The raw source content for the resource.
  507. */
  508. public function getContent(array $options = array()) {
  509. $content = '';
  510. if (isset($options['content'])) {
  511. $content = $options['content'];
  512. } else {
  513. $content = $this->get('content');
  514. }
  515. return $content;
  516. }
  517. /**
  518. * Set the raw source content for this element.
  519. *
  520. * @param mixed $content The source content; implementations can decide if
  521. * it can only be a string, or some other source from which to retrieve it.
  522. * @param array $options An array of options implementations can use to
  523. * accept language, revision identifiers, or other information to alter the
  524. * behavior of the method.
  525. * @return boolean True indicates the content was set.
  526. */
  527. public function setContent($content, array $options = array()) {
  528. return $this->set('content', $content);
  529. }
  530. /**
  531. * Returns the cache key for this instance in the specified or current context.
  532. *
  533. * @param string $context A specific Context to get the cache key from.
  534. *
  535. * @return string The cache key.
  536. */
  537. public function getCacheKey($context = '') {
  538. $id = $this->get('id') ? (string) $this->get('id') : '0';
  539. if (!is_string($context) || $context === '') {
  540. $context = !empty($this->_contextKey)
  541. ? $this->_contextKey
  542. : $this->get('context_key');
  543. }
  544. $cacheKey = $this->_cacheKey;
  545. if (strpos($cacheKey, '[') !== false) {
  546. $cacheKey= str_replace('[contextKey]', $context, $cacheKey);
  547. $cacheKey= str_replace('[id]', $id, $cacheKey);
  548. }
  549. return $cacheKey;
  550. }
  551. /**
  552. * Gets a collection of objects related by aggregate or composite relations.
  553. *
  554. * {@inheritdoc}
  555. *
  556. * Includes special handling for related objects with alias {@link
  557. * modTemplateVar}, respecting framework security unless specific criteria
  558. * are provided.
  559. *
  560. * @todo Refactor to use the new ABAC security model.
  561. */
  562. public function & getMany($alias, $criteria= null, $cacheFlag= false) {
  563. $collection= array ();
  564. if ($alias === 'TemplateVars' || $alias === 'modTemplateVar' && ($criteria === null || strtolower($criteria) === 'all')) {
  565. $collection= $this->getTemplateVars();
  566. } else {
  567. $collection= parent :: getMany($alias, $criteria, $cacheFlag);
  568. }
  569. return $collection;
  570. }
  571. /**
  572. * Get a collection of the Template Variable values for the Resource.
  573. *
  574. * @return array A collection of TemplateVar values for this Resource.
  575. */
  576. public function getTemplateVars() {
  577. return $this->xpdo->call('modResource', 'getTemplateVarCollection', array(&$this));
  578. }
  579. /**
  580. * Set a field value by the field key or name.
  581. *
  582. * {@inheritdoc}
  583. *
  584. * Additional logic added for the following fields:
  585. * -alias: Applies {@link modResource::cleanAlias()}
  586. * -contentType: Calls {@link modResource::addOne()} to sync contentType
  587. * -content_type: Sets the contentType field appropriately
  588. */
  589. public function set($k, $v= null, $vType= '') {
  590. $rt= false;
  591. switch ($k) {
  592. case 'alias' :
  593. $v= $this->cleanAlias($v);
  594. break;
  595. case 'contentType' :
  596. if ($v !== $this->get('contentType')) {
  597. if ($contentType= $this->xpdo->getObject('modContentType', array ('mime_type' => $v))) {
  598. if ($contentType->get('mime_type') != $this->get('contentType')) {
  599. $this->addOne($contentType, 'ContentType');
  600. }
  601. }
  602. }
  603. break;
  604. case 'content_type' :
  605. if ($v !== $this->get('content_type')) {
  606. /** @var modContentType $contentType */
  607. if ($contentType= $this->xpdo->getObject('modContentType', $v)) {
  608. if ($contentType->get('mime_type') != $this->get('contentType')) {
  609. $this->_fields['contentType']= $contentType->get('mime_type');
  610. $this->_dirty['contentType']= 'contentType';
  611. }
  612. }
  613. }
  614. break;
  615. }
  616. return parent :: set($k, $v, $vType);
  617. }
  618. /**
  619. * Adds an object related to this modResource by a foreign key relationship.
  620. *
  621. * {@inheritdoc}
  622. *
  623. * Adds legacy support for keeping the existing contentType field in sync
  624. * when a modContentType is set using this function.
  625. *
  626. * @param xPDOObject $obj
  627. * @param string $alias
  628. * @return boolean
  629. */
  630. public function addOne(& $obj, $alias= '') {
  631. $added= parent :: addOne($obj, $alias);
  632. if ($obj instanceof modContentType && $alias= 'ContentType') {
  633. $this->_fields['contentType']= $obj->get('mime_type');
  634. $this->_dirty['contentType']= 'contentType';
  635. }
  636. return $added;
  637. }
  638. /**
  639. * Transforms a string to form a valid URL representation.
  640. *
  641. * @param string $alias A string to transform into a valid URL representation.
  642. * @param array $options Options to append to or override configuration settings.
  643. * @return string The transformed string.
  644. */
  645. public function cleanAlias($alias, array $options = array()) {
  646. if ($this->xpdo instanceof modX && $ctx = $this->xpdo->getContext($this->get('context_key'))) {
  647. $options = array_merge($ctx->config, $options);
  648. }
  649. return $this->xpdo->call($this->_class, 'filterPathSegment', array(&$this->xpdo, $alias, $options));
  650. }
  651. /**
  652. * Persist new or changed modResource instances to the database container.
  653. *
  654. * If the modResource is new, the createdon and createdby fields will be set
  655. * using the current time and user authenticated in the context.
  656. *
  657. * If uri is empty or uri_overridden is not set and something has been changed which
  658. * might affect the Resource's uri, it is (re-)calculated using getAliasPath(). This
  659. * can be forced recursively by setting refreshURIs to true before calling save().
  660. *
  661. * @param boolean $cacheFlag
  662. * @return boolean
  663. */
  664. public function save($cacheFlag= null) {
  665. if ($this->isNew()) {
  666. if (!$this->get('createdon')) $this->set('createdon', time());
  667. if (!$this->get('createdby') && $this->xpdo instanceof modX) $this->set('createdby', $this->xpdo->getLoginUserID());
  668. }
  669. $refreshChildURIs = false;
  670. if ($this->xpdo instanceof modX && $this->xpdo->getOption('friendly_urls')) {
  671. $refreshChildURIs = ($this->get('refreshURIs') || $this->isDirty('uri') || $this->isDirty('alias') || $this->isDirty('alias_visible') || $this->isDirty('parent') || $this->isDirty('context_key'));
  672. if ($this->get('uri') == '' || (!$this->get('uri_override') && ($this->isDirty('uri_override') || $this->isDirty('content_type') || $this->isDirty('isfolder') || $refreshChildURIs))) {
  673. $this->set('uri', $this->getAliasPath($this->get('alias')));
  674. }
  675. }
  676. $changeContext = false;
  677. if ($this->xpdo instanceof modX) {
  678. $changeContext = $this->isDirty('context_key');
  679. }
  680. $rt= parent :: save($cacheFlag);
  681. if ($rt && $refreshChildURIs) {
  682. $this->xpdo->call('modResource', 'refreshURIs', array(
  683. &$this->xpdo,
  684. $this->get('id'),
  685. ));
  686. }
  687. if ($rt && $changeContext) {
  688. $this->xpdo->call($this->_class, 'updateContextOfChildren', array(&$this->xpdo, $this));
  689. }
  690. return $rt;
  691. }
  692. /**
  693. * Return whether or not the resource has been processed.
  694. *
  695. * @access public
  696. * @return boolean
  697. */
  698. public function getProcessed() {
  699. return $this->_processed;
  700. }
  701. /**
  702. * Set the field indicating the resource has been processed.
  703. *
  704. * @param boolean $processed Pass true to indicate the Resource has been processed.
  705. */
  706. public function setProcessed($processed) {
  707. $this->_processed= (boolean) $processed;
  708. }
  709. /**
  710. * Adds a lock on the Resource
  711. *
  712. * @access public
  713. * @param integer $user
  714. * @param array $options An array of options for the lock.
  715. * @return boolean True if the lock was successful.
  716. */
  717. public function addLock($user = 0, array $options = array()) {
  718. $locked = false;
  719. if ($this->xpdo instanceof modX) {
  720. if (!$user) {
  721. $user = $this->xpdo->user->get('id');
  722. }
  723. $lockedBy = $this->getLock();
  724. if (empty($lockedBy) || ($lockedBy == $user)) {
  725. $this->xpdo->registry->locks->subscribe('/resource/');
  726. $this->xpdo->registry->locks->send('/resource/', array(md5($this->get('id')) => $user), array('ttl' => $this->xpdo->getOption('lock_ttl', $options, 360)));
  727. $locked = true;
  728. } elseif ($lockedBy != $user) {
  729. $locked = $lockedBy;
  730. }
  731. }
  732. return $locked;
  733. }
  734. /**
  735. * Gets the lock on the Resource.
  736. *
  737. * @access public
  738. * @return int
  739. */
  740. public function getLock() {
  741. $lock = 0;
  742. if ($this->xpdo instanceof modX) {
  743. if ($this->xpdo->getService('registry', 'registry.modRegistry')) {
  744. $this->xpdo->registry->addRegister('locks', 'registry.modDbRegister', array('directory' => 'locks'));
  745. $this->xpdo->registry->locks->connect();
  746. $this->xpdo->registry->locks->subscribe('/resource/' . md5($this->get('id')));
  747. if ($msgs = $this->xpdo->registry->locks->read(array('remove_read' => false, 'poll_limit' => 1))) {
  748. $msg = reset($msgs);
  749. $lock = intval($msg);
  750. }
  751. }
  752. }
  753. return $lock;
  754. }
  755. /**
  756. * Removes all locks on a Resource.
  757. *
  758. * @access public
  759. * @param int $user
  760. * @return boolean True if locks were removed.
  761. */
  762. public function removeLock($user = 0) {
  763. $removed = false;
  764. if ($this->xpdo instanceof modX) {
  765. if (!$user) {
  766. $user = $this->xpdo->user->get('id');
  767. }
  768. $lockedBy = $this->getLock();
  769. if (empty($lockedBy) || $lockedBy == $user) {
  770. if ($this->xpdo->getService('registry', 'registry.modRegistry')) {
  771. $this->xpdo->registry->addRegister('locks', 'registry.modDbRegister', array('directory' => 'locks'));
  772. $this->xpdo->registry->locks->connect();
  773. $this->xpdo->registry->locks->subscribe('/resource/' . md5($this->get('id')));
  774. $this->xpdo->registry->locks->read(array('remove_read' => true, 'poll_limit' => 1));
  775. $removed = true;
  776. }
  777. }
  778. }
  779. return $removed;
  780. }
  781. /**
  782. * Loads the access control policies applicable to this resource.
  783. *
  784. * {@inheritdoc}
  785. */
  786. public function findPolicy($context = '') {
  787. $policy = array();
  788. $enabled = true;
  789. $context = !empty($context) ? $context : $this->xpdo->context->get('key');
  790. if ($context === $this->xpdo->context->get('key')) {
  791. $enabled = (boolean) $this->xpdo->getOption('access_resource_group_enabled', null, true);
  792. } elseif ($this->xpdo->getContext($context)) {
  793. $enabled = (boolean) $this->xpdo->contexts[$context]->getOption('access_resource_group_enabled', true);
  794. }
  795. if ($enabled) {
  796. if (empty($this->_policies) || !isset($this->_policies[$context])) {
  797. $accessTable = $this->xpdo->getTableName('modAccessResourceGroup');
  798. $policyTable = $this->xpdo->getTableName('modAccessPolicy');
  799. $resourceGroupTable = $this->xpdo->getTableName('modResourceGroupResource');
  800. $sql = "SELECT Acl.target, Acl.principal, Acl.authority, Acl.policy, Policy.data FROM {$accessTable} Acl " .
  801. "LEFT JOIN {$policyTable} Policy ON Policy.id = Acl.policy " .
  802. "JOIN {$resourceGroupTable} ResourceGroup ON Acl.principal_class = 'modUserGroup' " .
  803. "AND (Acl.context_key = :context OR Acl.context_key IS NULL OR Acl.context_key = '') " .
  804. "AND ResourceGroup.document = :resource " .
  805. "AND ResourceGroup.document_group = Acl.target " .
  806. "GROUP BY Acl.target, Acl.principal, Acl.authority, Acl.policy";
  807. $bindings = array(
  808. ':resource' => $this->get('id'),
  809. ':context' => $context
  810. );
  811. $query = new xPDOCriteria($this->xpdo, $sql, $bindings);
  812. if ($query->stmt && $query->stmt->execute()) {
  813. while ($row = $query->stmt->fetch(PDO::FETCH_ASSOC)) {
  814. $policy['modAccessResourceGroup'][$row['target']][] = array(
  815. 'principal' => $row['principal'],
  816. 'authority' => $row['authority'],
  817. 'policy' => $row['data'] ? $this->xpdo->fromJSON($row['data'], true) : array(),
  818. );
  819. }
  820. }
  821. $this->_policies[$context] = $policy;
  822. } else {
  823. $policy = $this->_policies[$context];
  824. }
  825. }
  826. return $policy;
  827. }
  828. /**
  829. * Checks to see if the Resource has children or not. Returns the number of
  830. * children.
  831. *
  832. * @access public
  833. * @return integer The number of children of the Resource
  834. */
  835. public function hasChildren() {
  836. $c = $this->xpdo->newQuery('modResource');
  837. $c->where(array(
  838. 'parent' => $this->get('id'),
  839. ));
  840. return $this->xpdo->getCount('modResource',$c);
  841. }
  842. /**
  843. * Gets the value of a TV for the Resource.
  844. *
  845. * @access public
  846. * @param mixed $pk Either the ID of the TV, or the name of the TV.
  847. * @return null/mixed The value of the TV for the Resource, or null if the
  848. * TV is not found.
  849. */
  850. public function getTVValue($pk) {
  851. $byName = !is_numeric($pk);
  852. /** @var modTemplateVar $tv */
  853. if ($byName && $this->xpdo instanceof modX) {
  854. $tv = $this->xpdo->getParser()->getElement('modTemplateVar', $pk);
  855. } else {
  856. $tv = $this->xpdo->getObject('modTemplateVar', $byName ? array('name' => $pk) : $pk);
  857. }
  858. return $tv == null ? null : $tv->renderOutput($this->get('id'));
  859. }
  860. /**
  861. * Sets a value for a TV for this Resource
  862. *
  863. * @param mixed $pk The TV name or ID to set
  864. * @param string $value The value to set for the TV
  865. * @return bool Whether or not the TV saved successfully
  866. */
  867. public function setTVValue($pk,$value) {
  868. $success = false;
  869. if (is_numeric($pk)) {
  870. $pk = intval($pk);
  871. } elseif (is_string($pk)) {
  872. $pk = array('name' => $pk);
  873. }
  874. /** @var modTemplateVar $tv */
  875. $tv = $this->xpdo->getObject('modTemplateVar',$pk);
  876. if ($tv) {
  877. $tv->setValue($this->get('id'),$value);
  878. $success = $tv->save();
  879. }
  880. return $success;
  881. }
  882. /**
  883. * Get the Resource's full alias path.
  884. *
  885. * @param string $alias Optional. The alias to check. If not set, will
  886. * then build it from the pagetitle if automatic_alias is set to true.
  887. * @param array $fields Optional. An array of field values to use instead of
  888. * using the current modResource fields.
  889. * @return string
  890. */
  891. public function getAliasPath($alias = '',array $fields = array()) {
  892. if (empty($fields)) $fields = $this->toArray();
  893. $workingContext = $this->xpdo->getContext($fields['context_key']);
  894. if (empty($fields['uri_override']) || empty($fields['uri'])) {
  895. /* auto assign alias if using automatic_alias */
  896. if (empty($alias) && $workingContext->getOption('automatic_alias', false)) {
  897. $alias = $this->cleanAlias($fields['pagetitle']);
  898. } elseif (empty($alias) && isset($fields['id']) && !empty($fields['id'])) {
  899. $alias = $this->cleanAlias($fields['id']);
  900. } else {
  901. $alias = $this->cleanAlias($alias);
  902. }
  903. $fullAlias= $alias;
  904. $isHtml= true;
  905. $extension= '';
  906. $containerSuffix= $workingContext->getOption('container_suffix', '');
  907. /* @var modContentType $contentType process content type */
  908. if (!empty($fields['content_type']) && $contentType= $this->xpdo->getObject('modContentType', $fields['content_type'])) {
  909. $extension= $contentType->getExtension();
  910. $isHtml= (strpos($contentType->get('mime_type'), 'html') !== false);
  911. }
  912. /* set extension to container suffix if Resource is a folder, HTML content type, and the container suffix is set */
  913. if (!empty($fields['isfolder']) && $isHtml && !empty ($containerSuffix)) {
  914. $extension= $containerSuffix;
  915. }
  916. $aliasPath= '';
  917. /* if using full alias paths, calculate here */
  918. if ($workingContext->getOption('use_alias_path', false)) {
  919. $useFrozenPathUris = $workingContext->getOption('use_frozen_parent_uris', false);
  920. $pathParentId= $fields['parent'];
  921. $parentResources= array ();
  922. $query = $this->xpdo->newQuery('modResource');
  923. $query->select($this->xpdo->getSelectColumns('modResource', '', '', array('parent', 'alias', 'alias_visible', 'uri', 'uri_override')));
  924. $query->where("{$this->xpdo->escape('id')} = ?");
  925. $query->prepare();
  926. $query->stmt->execute(array($pathParentId));
  927. $currResource= $query->stmt->fetch(PDO::FETCH_ASSOC);
  928. while ($currResource) {
  929. // If the use_frozen_parent_uris setting is enabled, we will look at the parent frozen uri instead
  930. // of building the full uri from all parents. This makes sure children will have an uri relative
  931. // from the parent at all times.
  932. if ($useFrozenPathUris && $currResource['uri_override'] && !empty($currResource['uri'])) {
  933. $parentResources[] = rtrim($currResource['uri'], '/');
  934. break;
  935. }
  936. $parentAlias= $currResource['alias'];
  937. if (empty ($parentAlias)) {
  938. $parentAlias= "{$pathParentId}";
  939. }
  940. // If we are ignoring the alias for this parent, simply skip adding it to the array for the alias
  941. // path.
  942. if ($currResource['alias_visible'] == 1) {
  943. $parentResources[]= "{$parentAlias}";
  944. }
  945. $pathParentId= $currResource['parent'];
  946. $query->stmt->execute(array($pathParentId));
  947. $currResource= $query->stmt->fetch(PDO::FETCH_ASSOC);
  948. }
  949. $aliasPath= !empty ($parentResources) ? implode('/', array_reverse($parentResources)) : '';
  950. if (strlen($aliasPath) > 0 && $aliasPath[strlen($aliasPath) - 1] !== '/') $aliasPath .= '/';
  951. }
  952. $fullAlias= $aliasPath . $fullAlias . $extension;
  953. } else {
  954. $fullAlias= $fields['uri'];
  955. }
  956. return $fullAlias;
  957. }
  958. /**
  959. * Tests to see if an alias is a duplicate.
  960. *
  961. * @param string $aliasPath The current full alias path. If none is passed,
  962. * will build it from the Resource's currently set alias.
  963. * @param string $contextKey The context to search for a duplicate alias in.
  964. * @return mixed The ID of the Resource using the alias, if a duplicate, otherwise false.
  965. */
  966. public function isDuplicateAlias($aliasPath = '', $contextKey = '') {
  967. if (empty($aliasPath)) $aliasPath = $this->getAliasPath($this->get('alias'));
  968. $criteria = $this->xpdo->newQuery('modResource');
  969. $where = array(
  970. 'id:!=' => $this->get('id'),
  971. 'uri' => $aliasPath,
  972. 'deleted' => false,
  973. );
  974. if (!empty($contextKey)) {
  975. $where['context_key'] = $contextKey;
  976. }
  977. $criteria->select('id');
  978. $criteria->where($where);
  979. $criteria->prepare();
  980. $duplicate = $this->xpdo->getValue($criteria->stmt);
  981. return $duplicate > 0 ? (integer) $duplicate : false;
  982. }
  983. /**
  984. * Duplicate the Resource.
  985. *
  986. * @param array $options An array of options.
  987. * @return mixed Returns either an error message, or the newly created modResource object.
  988. */
  989. public function duplicate(array $options = array()) {
  990. if (!($this->xpdo instanceof modX)) return false;
  991. /* duplicate resource */
  992. $prefixDuplicate = !empty($options['prefixDuplicate']) ? true : false;
  993. $newName = !empty($options['newName']) ? $options['newName'] : ($prefixDuplicate ? $this->xpdo->lexicon('duplicate_of', array(
  994. 'name' => $this->get('pagetitle'),
  995. )) : $this->get('pagetitle'));
  996. /** @var modResource $newResource */
  997. $newResource = $this->xpdo->newObject($this->get('class_key'));
  998. $newResource->fromArray($this->toArray('', true), '', false, true);
  999. $newResource->set('pagetitle', $newName);
  1000. /* do published status preserving */
  1001. $publishedMode = $this->getOption('publishedMode',$options,'preserve');
  1002. switch ($publishedMode) {
  1003. case 'unpublish':
  1004. $newResource->set('published',false);
  1005. $newResource->set('publishedon',0);
  1006. $newResource->set('publishedby',0);
  1007. break;
  1008. case 'publish':
  1009. $newResource->set('published',true);
  1010. $newResource->set('publishedon',time());
  1011. $newResource->set('publishedby',$this->xpdo->user->get('id'));
  1012. break;
  1013. case 'preserve':
  1014. default:
  1015. $newResource->set('published',$this->get('published'));
  1016. $newResource->set('publishedon',$this->get('publishedon'));
  1017. $newResource->set('publishedby',$this->get('publishedby'));
  1018. break;
  1019. }
  1020. /* allow overrides for every item */
  1021. if (!empty($options['overrides']) && is_array($options['overrides'])) {
  1022. $newResource->fromArray($options['overrides']);
  1023. }
  1024. $newResource->set('id',0);
  1025. /* make sure children get assigned to new parent */
  1026. $newResource->set('parent',isset($options['parent']) ? $options['parent'] : $this->get('parent'));
  1027. $newResource->set('createdby',$this->xpdo->user->get('id'));
  1028. $newResource->set('createdon',time());
  1029. $newResource->set('editedby',0);
  1030. $newResource->set('editedon',0);
  1031. /* get new alias */
  1032. $preserve_alias = $this->xpdo->getOption('preserve_alias', $options, false);
  1033. $alias = $newResource->cleanAlias($newName);
  1034. if ($this->xpdo->getOption('friendly_urls', $options, false)) {
  1035. if(!($preserve_alias)){
  1036. /* auto assign alias */
  1037. $aliasPath = $newResource->getAliasPath($newName);
  1038. $dupeContext = $this->xpdo->getOption('global_duplicate_uri_check', $options, false) ? '' : $newResource->get('context_key');
  1039. if ($newResource->isDuplicateAlias($aliasPath, $dupeContext)) {
  1040. $alias = '';
  1041. if ($newResource->get('uri_override')) {
  1042. $newResource->set('uri_override', false);
  1043. }
  1044. }
  1045. $newResource->set('alias',$alias);
  1046. }
  1047. }
  1048. $preserve_menuindex = $this->xpdo->getOption('preserve_menuindex', $options, false);
  1049. /* set new menuindex */
  1050. if(!$preserve_menuindex){
  1051. $menuindex = $this->xpdo->getCount('modResource',array('parent' => $this->get('parent')));
  1052. $newResource->set('menuindex',$menuindex);
  1053. }
  1054. /* save resource */
  1055. if (!$newResource->save()) {
  1056. return $this->xpdo->lexicon('resource_err_duplicate');
  1057. }
  1058. $tvds = $this->getMany('TemplateVarResources');
  1059. /** @var modTemplateVarResource $oldTemplateVarResource */
  1060. foreach ($tvds as $oldTemplateVarResource) {
  1061. /** @var modTemplateVarResource $newTemplateVarResource */
  1062. $newTemplateVarResource = $this->xpdo->newObject('modTemplateVarResource');
  1063. $newTemplateVarResource->set('contentid',$newResource->get('id'));
  1064. $newTemplateVarResource->set('tmplvarid',$oldTemplateVarResource->get('tmplvarid'));
  1065. $newTemplateVarResource->set('value',$oldTemplateVarResource->get('value'));
  1066. $newTemplateVarResource->save();
  1067. }
  1068. $groups = $this->getMany('ResourceGroupResources');
  1069. /** @var modResourceGroupResource $oldResourceGroupResource */
  1070. foreach ($groups as $oldResourceGroupResource) {
  1071. /** @var modResourceGroupResource $newResourceGroupResource */
  1072. $newResourceGroupResource = $this->xpdo->newObject('modResourceGroupResource');
  1073. $newResourceGroupResource->set('document_group',$oldResourceGroupResource->get('document_group'));
  1074. $newResourceGroupResource->set('document',$newResource->get('id'));
  1075. $newResourceGroupResource->save();
  1076. }
  1077. /* duplicate resource, recursively */
  1078. $duplicateChildren = isset($options['duplicateChildren']) ? $options['duplicateChildren'] : true;
  1079. if ($duplicateChildren) {
  1080. if (!$this->checkPolicy('add_children')) return $newResource;
  1081. $criteria = array(
  1082. 'context_key' => $this->get('context_key'),
  1083. 'parent' => $this->get('id')
  1084. );
  1085. $count = $this->xpdo->getCount('modResource',$criteria);
  1086. if ($count > 0) {
  1087. $children = $this->xpdo->getIterator('modResource',$criteria);
  1088. /** @var modResource $child */
  1089. foreach ($children as $child) {
  1090. $child->duplicate(array(
  1091. 'duplicateChildren' => true,
  1092. 'parent' => $newResource->get('id'),
  1093. 'prefixDuplicate' => $prefixDuplicate,
  1094. 'overrides' => !empty($options['overrides']) ? $options['overrides'] : false,
  1095. 'publishedMode' => $publishedMode,
  1096. 'preserve_alias' => $preserve_alias,
  1097. 'preserve_menuindex' => $preserve_menuindex
  1098. ));
  1099. }
  1100. }
  1101. }
  1102. return $newResource;
  1103. }
  1104. /**
  1105. * Joins a Resource to a Resource Group
  1106. *
  1107. * @access public
  1108. * @param mixed $resourceGroupPk Either the ID, name or object of the Resource Group
  1109. * @param boolean $byName Force the criteria to check by name for Numeric usergroup's name
  1110. * @return boolean True if successful.
  1111. */
  1112. public function joinGroup($resourceGroupPk, $byName = false) {
  1113. if (!is_object($resourceGroupPk) && !($resourceGroupPk instanceof modResourceGroup)) {
  1114. if ($byName) {
  1115. $c = array(
  1116. 'name' => $resourceGroupPk,
  1117. );
  1118. } else {
  1119. $c = array(
  1120. is_int($resourceGroupPk) ? 'id' : 'name' => $resourceGroupPk,
  1121. );
  1122. }
  1123. /** @var modResourceGroup $resourceGroup */
  1124. $resourceGroup = $this->xpdo->getObject('modResourceGroup',$c);
  1125. if (empty($resourceGroup) || !is_object($resourceGroup) || !($resourceGroup instanceof modResourceGroup)) {
  1126. $this->xpdo->log(modX::LOG_LEVEL_ERROR, __METHOD__ . ' - No resource group: ' . $resourceGroupPk);
  1127. return false;
  1128. }
  1129. } else {
  1130. $resourceGroup =& $resourceGroupPk;
  1131. }
  1132. if ($this->isMember($resourceGroup->get('name'))) {
  1133. $this->xpdo->log(modX::LOG_LEVEL_ERROR, __METHOD__ . ' - Resource '.$this->get('id') . ' already in resource group: ' . $resourceGroupPk);
  1134. return false;
  1135. }
  1136. /** @var modResourceGroupResource $resourceGroupResource */
  1137. $resourceGroupResource = $this->xpdo->newObject('modResourceGroupResource');
  1138. $resourceGroupResource->set('document',$this->get('id'));
  1139. $resourceGroupResource->set('document_group',$resourceGroup->get('id'));
  1140. return $resourceGroupResource->save();
  1141. }
  1142. /**
  1143. * Removes a Resource from a Resource Group
  1144. *
  1145. * @access public
  1146. * @param int|string|modResourceGroup $resourceGroupPk Either the ID, name or object of the Resource Group
  1147. * @return boolean True if successful.
  1148. */
  1149. public function leaveGroup($resourceGroupPk) {
  1150. if (!is_object($resourceGroupPk) && !($resourceGroupPk instanceof modResourceGroup)) {
  1151. $c = array(
  1152. is_int($resourceGroupPk) ? 'id' : 'name' => $resourceGroupPk,
  1153. );
  1154. /** @var modResourceGroup $resourceGroup */
  1155. $resourceGroup = $this->xpdo->getObject('modResourceGroup',$c);
  1156. if (empty($resourceGroup) || !is_object($resourceGroup) || !($resourceGroup instanceof modResourceGroup)) {
  1157. $this->xpdo->log(modX::LOG_LEVEL_ERROR, __METHOD__ . ' - No resource group: ' . (is_object($resourceGroupPk) ? $resourceGroupPk->get('name') : $resourceGroupPk));
  1158. return false;
  1159. }
  1160. } else {
  1161. $resourceGroup =& $resourceGroupPk;
  1162. }
  1163. if (!$this->isMember($resourceGroup->get('name'))) {
  1164. $this->xpdo->log(modX::LOG_LEVEL_ERROR, __METHOD__ . ' - Resource ' . $this->get('id') . ' is not in resource group: ' . (is_object($resourceGroupPk) ? $resourceGroupPk->get('name') : $resourceGroupPk));
  1165. return false;
  1166. }
  1167. /** @var modResourceGroupResource $resourceGroupResource */
  1168. $resourceGroupResource = $this->xpdo->getObject('modResourceGroupResource',array(
  1169. 'document' => $this->get('id'),
  1170. 'document_group' => $resourceGroup->get('id'),
  1171. ));
  1172. return $resourceGroupResource->remove();
  1173. }
  1174. /**
  1175. * Gets a sortable, limitable collection (and total count) of Resource Groups for the Resource.
  1176. *
  1177. * @param array $sort An array of sort columns in column => direction format.
  1178. * @param int $limit A limit of records to retrieve in the collection.
  1179. * @param int $offset A record offset for a limited collection.
  1180. * @return array An array containing the collection and total.
  1181. */
  1182. public function getGroupsList(array $sort = array('id' => 'ASC'), $limit = 0, $offset = 0) {
  1183. return $this->xpdo->call('modResource', 'listGroups', array(&$this, $sort, $limit, $offset));
  1184. }
  1185. /**
  1186. * Gets all the Resource Group names of the resource groups this resource is assigned to.
  1187. *
  1188. * @access public
  1189. * @return array An array of Resource Group names.
  1190. */
  1191. public function getResourceGroupNames() {
  1192. $resourceGroupNames= array();
  1193. $resourceGroups = $this->xpdo->getCollectionGraph('modResourceGroup', '{"ResourceGroupResources":{}}', array('ResourceGroupResources.document' => $this->get('id')));
  1194. if ($resourceGroups) {
  1195. /** @var modResourceGroup $resourceGroup */
  1196. foreach ($resourceGroups as $resourceGroup) {
  1197. $resourceGroupNames[] = $resourceGroup->get('name');
  1198. }
  1199. }
  1200. return $resourceGroupNames;
  1201. }
  1202. /**
  1203. * States whether a resource is a member of a resource group or groups. You may specify
  1204. * either a string name of the resource group, or an array of names.
  1205. *
  1206. * @access public
  1207. * @param string|array $groups Either a string of a resource group name or an array
  1208. * of names.
  1209. * @param boolean $matchAll If true, requires the resource to be a member of all
  1210. * the resource groups specified. If false, the resource can be a member of only one to
  1211. * pass. Defaults to false.
  1212. * @return boolean True if the resource is a member of any of the resource groups
  1213. * specified.
  1214. */
  1215. public function isMember($groups, $matchAll = false) {
  1216. $isMember = false;
  1217. $resourceGroupNames = $this->getResourceGroupNames();
  1218. if ($resourceGroupNames) {
  1219. if (is_array($groups)) {
  1220. if ($matchAll) {
  1221. $matches = array_diff($groups, $resourceGroupNames);
  1222. $isMember = empty($matches);
  1223. } else {
  1224. $matches = array_intersect($groups, $resourceGroupNames);
  1225. $isMember = !empty($matches);
  1226. }
  1227. } else {
  1228. $isMember = (array_search($groups, $resourceGroupNames) !== false);
  1229. }
  1230. }
  1231. return $isMember;
  1232. }
  1233. /**
  1234. * Determine the controller path for this Resource class
  1235. * @static
  1236. * @param xPDO $modx A reference to the modX object
  1237. * @return string The absolute path to the controller for this Resource class
  1238. */
  1239. public static function getControllerPath(xPDO &$modx) {
  1240. $theme = $modx->getOption('manager_theme',null,'default');
  1241. $controllersPath = $modx->getOption('manager_path',null,MODX_MANAGER_PATH).'controllers/'.$theme.'/';
  1242. return $controllersPath.'resource/';
  1243. }
  1244. /**
  1245. * Use this in your extended Resource class to display the text for the context menu item, if showInContextMenu is
  1246. * set to true.
  1247. * @return array
  1248. */
  1249. public function getContextMenuText() {
  1250. return array(
  1251. 'text_create' => $this->xpdo->lexicon('resource'),
  1252. 'text_create_here' => $this->xpdo->lexicon('resource_create_here'),
  1253. );
  1254. }
  1255. /**
  1256. * Use this in your extended Resource class to return a translatable name for the Resource Type.
  1257. * @return string
  1258. */
  1259. public function getResourceTypeName() {
  1260. $className = $this->_class;
  1261. if ($className == 'modDocument') $className = 'document';
  1262. return $this->xpdo->lexicon($className);
  1263. }
  1264. /**
  1265. * Use this in your extended Resource class to modify the tree node contents
  1266. * @param array $node
  1267. * @return array
  1268. */
  1269. public function prepareTreeNode(array $node = array()) {
  1270. return $node;
  1271. }
  1272. /**
  1273. * Get a namespaced property for the Resource
  1274. * @param string $key
  1275. * @param string $namespace
  1276. * @param null $default
  1277. * @return null
  1278. */
  1279. public function getProperty($key,$namespace = 'core',$default = null) {
  1280. $properties = $this->get('properties');
  1281. $properties = !empty($properties) ? $properties : array();
  1282. return array_key_exists($namespace,$properties) && array_key_exists($key,$properties[$namespace]) ? $properties[$namespace][$key] : $default;
  1283. }
  1284. /**
  1285. * Get the properties for the specific namespace for the Resource
  1286. * @param string $namespace
  1287. * @return array
  1288. */
  1289. public function getProperties($namespace = 'core') {
  1290. $properties = $this->get('properties');
  1291. $properties = !empty($properties) ? $properties : array();
  1292. return array_key_exists($namespace,$properties) ? $properties[$namespace] : array();
  1293. }
  1294. /**
  1295. * Set a namespaced property for the Resource
  1296. * @param string $key
  1297. * @param mixed $value
  1298. * @param string $namespace
  1299. * @return bool
  1300. */
  1301. public function setProperty($key,$value,$namespace = 'core') {
  1302. $properties = $this->get('properties');
  1303. $properties = !empty($properties) ? $properties : array();
  1304. if (!array_key_exists($namespace,$properties)) $properties[$namespace] = array();
  1305. $properties[$namespace][$key] = $value;
  1306. return $this->set('properties',$properties);
  1307. }
  1308. /**
  1309. * Set properties for a namespace on the Resource, optionally merging them with existing ones.
  1310. * @param array $newProperties
  1311. * @param string $namespace
  1312. * @param bool $merge
  1313. * @return boolean
  1314. */
  1315. public function setProperties(array $newProperties,$namespace = 'core',$merge = true) {
  1316. $properties = $this->get('properties');
  1317. $properties = !empty($properties) ? $properties : array();
  1318. if (!array_key_exists($namespace,$properties)) $properties[$namespace] = array();
  1319. $properties[$namespace] = $merge ? array_merge($properties[$namespace],$newProperties) : $newProperties;
  1320. return $this->set('properties',$properties);
  1321. }
  1322. /**
  1323. * Clear the cache of this resource in the current or specified Context.
  1324. *
  1325. * @param string $context Key of context for clearing
  1326. *
  1327. * @return void
  1328. */
  1329. public function clearCache($context = '') {
  1330. /** @var xPDOFileCache $cache */
  1331. $cache = $this->xpdo->cacheManager->getCacheProvider(
  1332. $this->xpdo->getOption('cache_resource_key', null, 'resource'),
  1333. array(
  1334. xPDO::OPT_CACHE_HANDLER => $this->xpdo->getOption('cache_resource_handler', null, $this->xpdo->getOption(xPDO::OPT_CACHE_HANDLER, null, 'xPDOFileCache')),
  1335. xPDO::OPT_CACHE_EXPIRES => (integer)$this->xpdo->getOption('cache_resource_expires', null, $this->xpdo->getOption(xPDO::OPT_CACHE_EXPIRES, null, 0)),
  1336. xPDO::OPT_CACHE_FORMAT => (integer)$this->xpdo->getOption('cache_resource_format', null, $this->xpdo->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)),
  1337. xPDO::OPT_CACHE_ATTEMPTS => (integer)$this->xpdo->getOption('cache_resource_attempts', null, $this->xpdo->getOption(xPDO::OPT_CACHE_ATTEMPTS, null, 10)),
  1338. xPDO::OPT_CACHE_ATTEMPT_DELAY => (integer)$this->xpdo->getOption('cache_resource_attempt_delay', null, $this->xpdo->getOption(xPDO::OPT_CACHE_ATTEMPT_DELAY, null, 1000)),
  1339. )
  1340. );
  1341. $key = $this->getCacheKey($context);
  1342. $cache->delete($key, array('deleteTop' => true));
  1343. $cache->delete($key);
  1344. if ($this->xpdo instanceof modX) {
  1345. $this->xpdo->invokeEvent('OnResourceCacheUpdate', array('id' => $this->get('id')));
  1346. }
  1347. }
  1348. }