/
modResource.php
1540 lines (1418 loc) · 61.7 KB
/
modResource.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
namespace MODX\Revolution;
use MODX\Revolution\Registry\modDbRegister;
use MODX\Revolution\Registry\modRegistry;
use PDO;
use xPDO\Cache\xPDOCache;
use xPDO\Cache\xPDOCacheManager;
use xPDO\Om\xPDOCriteria;
use xPDO\Om\xPDOObject;
use xPDO\xPDO;
/**
* Class modResource
*
* Represents a web resource managed by the MODX framework.
*
* @property int $id The ID of the Resource
* @property string $type The type of the resource; document/reference
* @property string $pagetitle The page title of the Resource
* @property string $longtitle The long title of the Resource
* @property string $description The description of the Resource
* @property string $alias The FURL alias of the resource
* @property boolean $aliasVisible Whether or not we should exclude the resource alias for children
* @property string $link_attributes Any link attributes for the URL generated for the Resource
* @property boolean $published Whether or not this Resource is published, or viewable by users without the 'view_unpublished' permission
* @property int $pub_date The UNIX time that this Resource will be automatically marked as published
* @property int $unpub_date The UNIX time that this Resource will be automatically marked as unpublished
* @property int $parent The parent ID of the Resource
* @property boolean $isfolder Whether or not this Resource is a container
* @property string $introtext The intro text of this Resource, often used as an excerpt
* @property string $content The actual content of this Resource
* @property boolean $richtext Whether or not this Resource is edited with a Rich Text Editor, if installed
* @property int $template The Template this Resource is tied to, or 0 to use an empty Template
* @property int $menuindex The menuindex, or rank, that this Resource shows in.
* @property boolean $searchable Whether or not this Resource should be searchable
* @property boolean $cacheable Whether or not this Resource should be cacheable
* @property int $createdby The ID of the User that created this Resource
* @property int $createdon The UNIX time of when this Resource was created
* @property int $editedby The ID of the User, if any, that last edited this Resource
* @property int $editedon The UNIX time, if set, of when this Resource was last edited
* @property boolean $deleted Whether or not this Resource is marked as deleted
* @property int $deletedon The UNIX time of when this Resource was deleted
* @property int $deletedby The User that deleted this Resource
* @property int $publishedon The UNIX time that this Resource was marked as published
* @property int $publishedby The User that published this Resource
* @property string $menutitle The title to show when this Resource is displayed in a menu
* @property boolean $donthit Deprecated.
* @property boolean $privateweb Deprecated.
* @property boolean $privatemgr Deprecated.
* @property int $content_dispo The type of Content Disposition that is used when displaying this Resource
* @property boolean $hidemenu Whether or not this Resource should show in menus
* @property string $class_key The Class Key of this Resource. Useful for derivative Resource types
* @property string $context_key The Context that this Resource resides in
* @property int $content_type The Content Type ID of this Resource
* @property string $uri The generated URI of this Resource
* @property boolean $uri_override Whether or not this URI is "frozen": where the URI will stay as specified and will not be regenerated
* @property boolean $hide_children_in_tree Whether or not this Resource should show in the mgr tree any of its children
* @property boolean $show_in_tree Whether or not this Resource should show in the mgr tree
* @property array $properties
*
* @property modResource[] $Children
* @property modTemplateVarResource[] $TemplateVarResources
* @property modResourceGroupResource[] $ResourceGroupResources
* @property modAccessResource[] $Acls
* @property modContextResource[] $ContextResources
*
* @package MODX\Revolution
*/
class modResource extends modAccessibleSimpleObject implements modResourceInterface
{
/**
* Represents the cacheable content for a resource.
*
* Note that this is not the raw source content, but the content that is the
* result of processing cacheable tags within the raw source content.
*
* @var string
*/
public $_content = '';
/**
* Represents the output the resource produces.
*
* @var string
*/
public $_output = '';
/**
* The context the resource is requested from.
*
* Note that this is different than the context_key field that describes a
* primary context for the resource.
*
* @var string
*/
public $_contextKey = null;
/**
* Indicates if the resource has already been processed.
*
* @var boolean
*/
protected $_processed = false;
/**
* The cache filename for the resource in the context.
*
* @var string
*/
protected $_cacheKey = null;
/**
* Indicates if the site cache should be refreshed when saving changes.
*
* @var boolean
*/
protected $_refreshCache = true;
/**
* Indicates if this Resource was generated from a forward.
*
* @var boolean
*/
public $_isForward = false;
/**
* An array of Javascript/CSS to be appended to the footer of this Resource
*
* @var array $_jscripts
*/
public $_jscripts = [];
/**
* An array of Javascript/CSS to be appended to the HEAD of this Resource
*
* @var array $_sjscripts
*/
public $_sjscripts = [];
/**
* All loaded Javascript/CSS that has been calculated to be loaded
*
* @var array
*/
public $_loadedjscripts = [];
/**
* Use if extending modResource to state whether or not to show the extended class in the tree context menu
*
* @var boolean
*/
public $showInContextMenu = false;
/**
* Use if extending modResource to state whether or not to allow drop on extended class in the resource tree
* Set 1 for allow drop, 0 for disable drop or -1 for default behavior
*
* @var int
*/
public $allowDrop = -1;
/**
* Use if extending modResource to state whether or not the derivative class can be listed in the class_key
* dropdown users can change when editing a resource.
*
* @var boolean
*/
public $allowListingInClassKeyDropdown = true;
/**
* Whether or not to allow creation of children resources in tree. Can be overridden in a derivative Resource class.
*
* @var boolean
*/
public $allowChildrenResources = true;
/** @var modX|xPDO $xpdo */
public $xpdo;
/**
* Filter a string for use as a URL path segment.
*
* @param modX|xPDO &$xpdo A reference to a modX or xPDO instance.
* @param string $segment The string to filter into a path segment.
* @param array $options Local options to override global filter settings.
*
* @return string The filtered string ready to use as a path segment.
*/
public static function filterPathSegment(&$xpdo, $segment, array $options = [])
{
/* setup the various options */
$iconv = function_exists('iconv');
$mbext = function_exists('mb_strlen') && (boolean)$xpdo->getOption('use_multibyte', $options, false);
$charset = strtoupper((string)$xpdo->getOption('modx_charset', $options, 'UTF-8'));
$delimiter = $xpdo->getOption('friendly_alias_word_delimiter', $options, '-');
$delimiters = $xpdo->getOption('friendly_alias_word_delimiters', $options, '-_');
$maxlength = (integer)$xpdo->getOption('friendly_alias_max_length', $options, 0);
$stripElementTags = (boolean)$xpdo->getOption('friendly_alias_strip_element_tags', $options, true);
$trimchars = $xpdo->getOption('friendly_alias_trim_chars', $options, '/.' . $delimiters);
$restrictchars = $xpdo->getOption('friendly_alias_restrict_chars', $options, 'pattern');
$restrictcharspattern = $xpdo->getOption('friendly_alias_restrict_chars_pattern', $options,
'/[\0\x0B\t\n\r\f\a&=+%#<>"~`@\?\[\]\{\}\|\^\'\\\\]/');
$lowercase = (boolean)$xpdo->getOption('friendly_alias_lowercase_only', $options, true);
$translit = $xpdo->getOption('friendly_alias_translit', $options, $iconv ? 'iconv' : 'none');
$translitClass = $xpdo->getOption('friendly_alias_translit_class', $options, 'translit.modTransliterate');
/* strip html and optionally MODX element tags (stripped by default) */
if ($xpdo instanceof modX) {
$segment = $xpdo->stripTags($segment, '', $stripElementTags ? [] : null);
}
/* replace with the specified word delimiter */
$segment = str_replace(' ', $delimiter, $segment);
/* decode named entities to the appropriate character for the character set */
$segment = html_entity_decode($segment, ENT_QUOTES, $charset);
/* prepare '&' replacement */
if ($xpdo instanceof modX && $xpdo->getService('lexicon', modLexicon::class) && $xpdo->lexicon('and')) {
$ampersand = ' ' . $xpdo->lexicon('and') . ' ';
} else {
$ampersand = ' and ';
}
/* apply transliteration as configured */
switch ($translit) {
case '':
case 'none':
/* no transliteration */
break;
case 'iconv':
/* if iconv is available, use the built-in transliteration it provides */
$segment = iconv($mbext ? mb_detect_encoding($segment) : $charset, $charset . '//TRANSLIT//IGNORE',
$segment);
$ampersand = iconv($mbext ? mb_detect_encoding($segment) : $charset, $charset . '//TRANSLIT//IGNORE',
$ampersand);
break;
case 'iconv_ascii':
/* if iconv is available, use the built-in transliteration to ASCII it provides */
$segment = iconv(($mbext) ? mb_detect_encoding($segment) : $charset, 'ASCII//TRANSLIT//IGNORE',
$segment);
break;
default:
/* otherwise look for a transliteration service class that will accept named transliteration tables */
if ($xpdo instanceof modX) {
$translitClassPath = $xpdo->getOption('friendly_alias_translit_class_path', $options,
$xpdo->getOption('core_path', $options, MODX_CORE_PATH) . 'components/');
if ($xpdo->getService('translit', $translitClass, $translitClassPath, $options)) {
$segment = $xpdo->translit->translate($segment, $translit);
$ampersand = $xpdo->translit->translate($ampersand, $translit);
}
}
break;
}
/* replace any remaining '&' with a translit ampersand */
$segment = str_replace('&', $ampersand, $segment);
/* restrict characters as configured */
switch ($restrictchars) {
case 'alphanumeric':
/* restrict segment to alphanumeric characters only */
$segment = preg_replace('/[^\.%A-Za-z0-9 _-]/', '', $segment);
break;
case 'alpha':
/* restrict segment to alpha characters only */
$segment = preg_replace('/[^\.%A-Za-z _-]/', '', $segment);
break;
case 'legal':
/* restrict segment to legal URL characters only */
$segment = preg_replace('/[\0\x0B\t\n\r\f\a&=+%#<>"~`@\?\[\]\{\}\|\^\'\\\\]/', '', $segment);
break;
case 'pattern':
default:
/* restrict segment using regular expression pattern configured (same as legal by default) */
if (!empty($restrictcharspattern)) {
$segment = preg_replace($restrictcharspattern, '', $segment);
}
}
/* replace one or more space characters with word delimiter */
$segment = preg_replace('/\s+/u', $delimiter, $segment);
/* replace one or more instances of word delimiters with word delimiter */
$delimiterTokens = [];
for ($d = 0; $d < strlen($delimiters); $d++) {
$delimiterTokens[] = preg_quote($delimiters[$d], '/');
}
if (!empty($delimiterTokens)) {
$delimiterPattern = '/[' . implode('|', $delimiterTokens) . ']+/';
$segment = preg_replace($delimiterPattern, $delimiter, $segment);
}
/* unless lowercase_only preference is explicitly off, change case to lowercase */
if ($lowercase) {
if ($mbext) {
/* if the mb extension is available use it to protect multi-byte chars */
$segment = mb_convert_case($segment, MB_CASE_LOWER, $charset);
} else {
/* otherwise, just use strtolower */
$segment = strtolower($segment);
}
}
/* trim specified chars from both ends of the segment */
$segment = trim($segment, $trimchars);
/* get the strlen of the segment (use mb extension if available) */
$length = $mbext ? mb_strlen($segment, $charset) : strlen($segment);
/* if maxlength is specified and exceeded, return substr with additional trim applied */
if ($maxlength > 0 && $length > $maxlength) {
$segment = substr($segment, 0, $maxlength);
$segment = trim($segment, $trimchars);
}
return $segment;
}
/**
* Get a sortable, limitable collection (and total count) of Resource Groups for a given Resource.
*
* @static
*
* @param modResource &$resource A reference to the modResource to get the groups from.
* @param array $sort An array of sort columns in column => direction format.
* @param int $limit A limit of records to retrieve in the collection.
* @param int $offset A record offset for a limited collection.
*
* @return array An array containing the collection and total.
*/
public static function listGroups(modResource &$resource, array $sort = ['id' => 'ASC'], $limit = 0, $offset = 0)
{
$result = ['collection' => [], 'total' => 0];
$c = $resource->xpdo->newQuery(modResourceGroup::class);
$c->leftJoin(modResourceGroupResource::class, 'ResourceGroupResource', [
"ResourceGroupResource.document_group = modResourceGroup.id",
'ResourceGroupResource.document' => $resource->get('id'),
]);
$result['total'] = $resource->xpdo->getCount(modResourceGroup::class, $c);
$c->select($resource->xpdo->getSelectColumns(modResourceGroup::class, 'modResourceGroup'));
$c->select(["IF(ISNULL(ResourceGroupResource.document),0,1) AS access"]);
foreach ($sort as $sortKey => $sortDir) {
$c->sortby($resource->xpdo->escape('modResourceGroup') . '.' . $resource->xpdo->escape($sortKey), $sortDir);
}
if ($limit > 0) {
$c->limit($limit, $offset);
}
$result['collection'] = $resource->xpdo->getCollection(modResourceGroup::class, $c);
return $result;
}
/**
* Retrieve a collection of Template Variables for a Resource.
*
* @static
*
* @param modResource &$resource A reference to the modResource to retrieve TemplateVars for.
*
* @return modTemplateVar[] A collection of modTemplateVar instances for the modResource.
*/
public static function getTemplateVarCollection(modResource &$resource)
{
$c = $resource->xpdo->newQuery(modTemplateVar::class);
$c->query['distinct'] = 'DISTINCT';
$c->select($resource->xpdo->getSelectColumns(modTemplateVar::class, 'modTemplateVar'));
$c->select($resource->xpdo->getSelectColumns(modTemplateVarTemplate::class, 'tvtpl', '', ['rank']));
if ($resource->isNew()) {
$c->select([
'modTemplateVar.default_text AS value',
'0 AS resourceId',
]);
} else {
$c->select([
'IF(ISNULL(tvc.value),modTemplateVar.default_text,tvc.value) AS value',
$resource->get('id') . ' AS resourceId',
]);
}
$c->innerJoin(modTemplateVarTemplate::class, 'tvtpl', [
'tvtpl.tmplvarid = modTemplateVar.id',
'tvtpl.templateid' => $resource->get('template'),
]);
if (!$resource->isNew()) {
$c->leftJoin(modTemplateVarResource::class, 'tvc', [
'tvc.tmplvarid = modTemplateVar.id',
'tvc.contentid' => $resource->get('id'),
]);
}
$c->sortby('tvtpl.rank,modTemplateVar.rank');
return $resource->xpdo->getCollection(modTemplateVar::class, $c);
}
/**
* Refresh Resource URI fields for children of the specified parent.
*
* @static
*
* @param modX &$modx A reference to a valid modX instance.
* @param int $parent The id of a Resource parent to start from (default is 0, the root)
* @param array $options An array of various options for the method:
* - resetOverrides: if true, Resources with uri_override set to true will be included
* - contexts: an optional array of context keys to limit the refresh scope
*
* @return void
*/
public static function refreshURIs(modX &$modx, $parent = 0, array $options = [])
{
$resetOverrides = array_key_exists('resetOverrides', $options) ? (boolean)$options['resetOverrides'] : false;
$contexts = array_key_exists('contexts', $options) ? explode(',', $options['contexts']) : null;
$criteria = $modx->newQuery(modResource::class, ['parent' => $parent]);
if (!$resetOverrides) {
$criteria->where(['uri_override' => false]);
}
if (!empty($contexts)) {
$criteria->where(['context_key:IN' => $contexts]);
}
$criteria->sortby('menuindex', 'ASC');
/** @var modResource $resource */
foreach ($modx->getIterator(modResource::class, $criteria) as $resource) {
$resource->set('refreshURIs', true);
if ($resetOverrides) {
$resource->set('uri_override', false);
}
if (!$resource->get('uri_override')) {
$resource->set('uri', '');
}
$resource->save();
}
}
/**
* Updates the Context of all Children recursively to that of the parent.
*
* @static
*
* @param modX & $modx A reference to an initialized modX instance.
* @param modResource $parent The parent modResource instance.
* @param array $options An array of options.
*
* @return int The number of children updated.
*/
public static function updateContextOfChildren(modX &$modx, $parent, array $options = [])
{
$count = 0;
/** @var modResource $child */
foreach ($parent->getIterator('Children') as $child) {
$child->set('context_key', $parent->get('context_key'));
if ($child->save()) {
$count++;
} else {
$modx->log(modX::LOG_LEVEL_ERROR, "Could not change Context of child resource {$child->get('id')}", '',
__METHOD__, __FILE__, __LINE__);
}
}
return $count;
}
/**
* @param xPDO $xpdo A reference to the xPDO|modX instance
*/
function __construct(xPDO & $xpdo)
{
parent:: __construct($xpdo);
$this->_contextKey = isset ($this->xpdo->context) ? $this->xpdo->context->get('key') : 'web';
$this->_cacheKey = "[contextKey]/resources/[id]";
}
/**
* Prepare the resource for output.
*/
public function prepare()
{
# 1. Parse cacheable elements if exist.
$this->process();
# 2. Copy registered scripts added by the cacheable elements.
$this->syncScripts();
# 3. Parse uncacheable elements.
$this->parseContent();
}
/**
* Process a resource, transforming source content to output.
*
* @return string The processed cacheable content of a resource.
*/
public function process()
{
if (!$this->get('cacheable') || !$this->_processed || !$this->_content) {
$this->_content = '';
$this->_output = '';
$this->xpdo->getParser();
/** @var modTemplate $baseElement */
if ($baseElement = $this->getOne('Template')) {
if ($baseElement->process()) {
$this->_content = $baseElement->_output;
$this->_processed = true;
}
} else {
$this->_content = $this->getContent();
$maxIterations = intval($this->xpdo->getOption('parser_max_iterations', 10));
$this->xpdo->parser->processElementTags('', $this->_content, false, false, '[[', ']]', [],
$maxIterations);
$this->_processed = true;
}
}
return $this->_content;
}
/**
* @param array $data Data for placeholders
*
* @return string
*/
public function parseContent($data = [])
{
$this->xpdo->getParser();
$maxIterations = intval($this->xpdo->getOption('parser_max_iterations', null, 10));
$oldResource = $this->xpdo->resource;
$this->xpdo->resource = $this;
if (!empty($data)) {
$scope = $this->xpdo->toPlaceholders($data, '', '.', true);
}
if (!$this->_processed) {
$this->_content = $this->getContent();
$this->xpdo->parser->processElementTags('', $this->_content, false, false, '[[', ']]', [], $maxIterations);
$this->_processed = true;
}
$this->_output = $this->_content;
$this->xpdo->parser->processElementTags('', $this->_output, true, false, '[[', ']]', [], $maxIterations);
$this->xpdo->parser->processElementTags('', $this->_output, true, true, '[[', ']]', [], $maxIterations);
$this->xpdo->resource = $oldResource;
if (isset($scope['keys'])) {
$this->xpdo->unsetPlaceholders($scope['keys']);
}
if (isset($scope['restore'])) {
$this->xpdo->toPlaceholders($scope['restore']);
}
return $this->_output;
}
/**
* Store scripts registered by cached elements.
*/
public function syncScripts()
{
$this->_jscripts = $this->xpdo->jscripts;
$this->_sjscripts = $this->xpdo->sjscripts;
$this->_loadedjscripts = $this->xpdo->loadedjscripts;
}
/**
* Gets the raw, unprocessed source content for a resource.
*
* @param array $options An array of options implementations can use to
* accept language, revision identifiers, or other information to alter the
* behavior of the method.
*
* @return string The raw source content for the resource.
*/
public function getContent(array $options = [])
{
if (isset($options['content'])) {
$content = $options['content'];
} else {
$content = $this->get('content');
}
return $content;
}
/**
* Set the raw source content for this element.
*
* @param mixed $content The source content; implementations can decide if
* it can only be a string, or some other source from which to retrieve it.
* @param array $options An array of options implementations can use to
* accept language, revision identifiers, or other information to alter the
* behavior of the method.
*
* @return boolean True indicates the content was set.
*/
public function setContent($content, array $options = [])
{
return $this->set('content', $content);
}
/**
* Returns the cache key for this instance in the specified or current context.
*
* @param string $context A specific Context to get the cache key from.
*
* @return string The cache key.
*/
public function getCacheKey($context = '')
{
$id = $this->get('id') ? (string)$this->get('id') : '0';
if (!is_string($context) || $context === '') {
$context = !empty($this->_contextKey)
? $this->_contextKey
: $this->get('context_key');
}
$cacheKey = $this->_cacheKey;
if (strpos($cacheKey, '[') !== false) {
$cacheKey = str_replace('[contextKey]', $context, $cacheKey);
$cacheKey = str_replace('[id]', $id, $cacheKey);
}
return $cacheKey;
}
/**
* Gets a collection of objects related by aggregate or composite relations.
*
* {@inheritdoc}
*
* Includes special handling for related objects with alias {@link
* modTemplateVar}, respecting framework security unless specific criteria
* are provided.
*
* @todo Refactor to use the new ABAC security model.
*/
public function & getMany($alias, $criteria = null, $cacheFlag = false)
{
if ($alias === 'TemplateVars' || $alias === 'modTemplateVar' && ($criteria === null || strtolower($criteria) === 'all')) {
$collection = $this->getTemplateVars();
} else {
$collection = parent:: getMany($alias, $criteria, $cacheFlag);
}
return $collection;
}
/**
* Get a collection of the Template Variable values for the Resource.
*
* @return array A collection of TemplateVar values for this Resource.
*/
public function getTemplateVars()
{
return $this->xpdo->call(modResource::class, 'getTemplateVarCollection', [&$this]);
}
/**
* Set a field value by the field key or name.
*
* {@inheritdoc}
*
* Additional logic added for the following fields:
* -alias: Applies {@link modResource::cleanAlias()}
*/
public function set($k, $v = null, $vType = '')
{
switch ($k) {
case 'alias' :
$v = $this->cleanAlias($v);
break;
}
return parent:: set($k, $v, $vType);
}
/**
* Transforms a string to form a valid URL representation.
*
* @param string $alias A string to transform into a valid URL representation.
* @param array $options Options to append to or override configuration settings.
*
* @return string The transformed string.
*/
public function cleanAlias($alias, array $options = [])
{
if ($this->xpdo instanceof modX && $ctx = $this->xpdo->getContext($this->get('context_key'))) {
$options = array_merge($ctx->config, $options);
}
return $this->xpdo->call($this->_class, 'filterPathSegment', [&$this->xpdo, $alias, $options]);
}
/**
* Persist new or changed modResource instances to the database container.
*
* If the modResource is new, the createdon and createdby fields will be set
* using the current time and user authenticated in the context.
*
* If uri is empty or uri_overridden is not set and something has been changed which
* might affect the Resource's uri, it is (re-)calculated using getAliasPath(). This
* can be forced recursively by setting refreshURIs to true before calling save().
*
* @param boolean $cacheFlag
*
* @return boolean
*/
public function save($cacheFlag = null)
{
if ($this->isNew()) {
if (!$this->get('createdon')) {
$this->set('createdon', time());
}
if (!$this->get('createdby') && $this->xpdo instanceof modX) {
$this->set('createdby', $this->xpdo->getLoginUserID());
}
}
$refreshChildURIs = false;
if ($this->xpdo instanceof modX && $this->xpdo->getOption('friendly_urls')) {
$refreshChildURIs = ($this->get('refreshURIs') || $this->isDirty('uri') || $this->isDirty('alias') || $this->isDirty('alias_visible') || $this->isDirty('parent') || $this->isDirty('context_key'));
if ($this->get('uri') == '' || (!$this->get('uri_override') && ($this->isDirty('uri_override') || $this->isDirty('content_type') || $this->isDirty('isfolder') || $refreshChildURIs))) {
$this->set('uri', $this->getAliasPath($this->get('alias')));
}
}
$changeContext = false;
if ($this->xpdo instanceof modX) {
$changeContext = $this->isDirty('context_key');
}
$rt = parent:: save($cacheFlag);
if ($rt && $refreshChildURIs) {
$this->xpdo->call(modResource::class, 'refreshURIs', [
&$this->xpdo,
$this->get('id'),
]);
}
if ($rt && $changeContext) {
$this->xpdo->call($this->_class, 'updateContextOfChildren', [&$this->xpdo, $this]);
}
return $rt;
}
/**
* Return whether or not the resource has been processed.
*
* @access public
* @return boolean
*/
public function getProcessed()
{
return $this->_processed;
}
/**
* Set the field indicating the resource has been processed.
*
* @param boolean $processed Pass true to indicate the Resource has been processed.
*/
public function setProcessed($processed)
{
$this->_processed = (boolean)$processed;
}
/**
* Adds a lock on the Resource
*
* @access public
*
* @param integer $user
* @param array $options An array of options for the lock.
*
* @return boolean True if the lock was successful.
*/
public function addLock($user = 0, array $options = [])
{
$locked = false;
if ($this->xpdo instanceof modX) {
if (!$user) {
$user = $this->xpdo->user->get('id');
}
$lockedBy = $this->getLock();
if (empty($lockedBy) || ($lockedBy == $user)) {
$this->xpdo->registry->locks->subscribe('/resource/');
$this->xpdo->registry->locks->send('/resource/', [md5($this->get('id')) => $user],
['ttl' => $this->xpdo->getOption('lock_ttl', $options, 360)]);
$locked = true;
} elseif ($lockedBy != $user) {
$locked = $lockedBy;
}
}
return $locked;
}
/**
* Gets the lock on the Resource.
*
* @access public
* @return int
*/
public function getLock()
{
$lock = 0;
if ($this->xpdo instanceof modX) {
if ($this->xpdo->getService('registry', modRegistry::class)) {
$this->xpdo->registry->addRegister('locks', modDbRegister::class, ['directory' => 'locks']);
$this->xpdo->registry->locks->connect();
$this->xpdo->registry->locks->subscribe('/resource/' . md5($this->get('id')));
if ($msgs = $this->xpdo->registry->locks->read(['remove_read' => false, 'poll_limit' => 1])) {
$msg = reset($msgs);
$lock = intval($msg);
}
}
}
return $lock;
}
/**
* Removes all locks on a Resource.
*
* @access public
*
* @param int $user
*
* @return boolean True if locks were removed.
*/
public function removeLock($user = 0)
{
$removed = false;
if ($this->xpdo instanceof modX) {
if (!$user) {
$user = $this->xpdo->user->get('id');
}
$lockedBy = $this->getLock();
if (empty($lockedBy) || $lockedBy == $user) {
if ($this->xpdo->getService('registry', modRegistry::class)) {
$this->xpdo->registry->addRegister('locks', modDbRegister::class, ['directory' => 'locks']);
$this->xpdo->registry->locks->connect();
$this->xpdo->registry->locks->subscribe('/resource/' . md5($this->get('id')));
$this->xpdo->registry->locks->read(['remove_read' => true, 'poll_limit' => 1]);
$removed = true;
}
}
}
return $removed;
}
/**
* Loads the access control policies applicable to this resource.
*
* {@inheritdoc}
*/
public function findPolicy($context = '')
{
$policy = [];
$enabled = true;
$context = !empty($context) ? $context : $this->xpdo->context->get('key');
if ($context === $this->xpdo->context->get('key')) {
$enabled = (boolean)$this->xpdo->getOption('access_resource_group_enabled', null, true);
} elseif ($this->xpdo->getContext($context)) {
$enabled = (boolean)$this->xpdo->contexts[$context]->getOption('access_resource_group_enabled', true);
}
if ($enabled) {
if (empty($this->_policies) || !isset($this->_policies[$context])) {
$accessTable = $this->xpdo->getTableName(modAccessResourceGroup::class);
$policyTable = $this->xpdo->getTableName(modAccessPolicy::class);
$resourceGroupTable = $this->xpdo->getTableName(modResourceGroupResource::class);
$sql = "SELECT Acl.target, Acl.principal, Acl.authority, Acl.policy, Policy.data FROM {$accessTable} Acl " .
"LEFT JOIN {$policyTable} Policy ON Policy.id = Acl.policy " .
"JOIN {$resourceGroupTable} ResourceGroup ON Acl.principal_class = {$this->xpdo->quote(modUserGroup::class)} " .
"AND (Acl.context_key = :context OR Acl.context_key IS NULL OR Acl.context_key = '') " .
"AND ResourceGroup.document = :resource " .
"AND ResourceGroup.document_group = Acl.target " .
"GROUP BY Acl.target, Acl.principal, Acl.authority, Acl.policy";
$bindings = [
':resource' => $this->get('id'),
':context' => $context,
];
$query = new xPDOCriteria($this->xpdo, $sql, $bindings);
if ($query->stmt && $query->stmt->execute()) {
while ($row = $query->stmt->fetch(PDO::FETCH_ASSOC)) {
$policy[modAccessResourceGroup::class][$row['target']][] = [
'principal' => $row['principal'],
'authority' => $row['authority'],
'policy' => $row['data'] ? $this->xpdo->fromJSON($row['data'], true) : [],
];
}
}
$this->_policies[$context] = $policy;
} else {
$policy = $this->_policies[$context];
}
}
return $policy;
}
/**
* Checks to see if the Resource has children or not. Returns the number of
* children.
*
* @access public
* @return integer The number of children of the Resource
*/
public function hasChildren()
{
$c = $this->xpdo->newQuery(modResource::class);
$c->where([
'parent' => $this->get('id'),
]);
return $this->xpdo->getCount(modResource::class, $c);
}
/**
* Gets the value of a TV for the Resource.
*
* @access public
*
* @param mixed $pk Either the ID of the TV, or the name of the TV.
*
* @return null/mixed The value of the TV for the Resource, or null if the
* TV is not found.
*/
public function getTVValue($pk)
{
$byName = !is_numeric($pk);
/** @var modTemplateVar $tv */
if ($byName && $this->xpdo instanceof modX) {
$tv = $this->xpdo->getParser()->getElement(modTemplateVar::class, $pk);
} else {
$tv = $this->xpdo->getObject(modTemplateVar::class, $byName ? ['name' => $pk] : $pk);
}
return $tv == null ? null : $tv->renderOutput($this->get('id'));
}
/**
* Sets a value for a TV for this Resource
*
* @param mixed $pk The TV name or ID to set
* @param string $value The value to set for the TV
*
* @return bool Whether or not the TV saved successfully
*/
public function setTVValue($pk, $value)
{
$success = false;
if (is_numeric($pk)) {
$pk = intval($pk);
} elseif (is_string($pk)) {
$pk = ['name' => $pk];
}
/** @var modTemplateVar $tv */
$tv = $this->xpdo->getObject(modTemplateVar::class, $pk);
if ($tv) {
$tv->setValue($this->get('id'), $value);
$success = $tv->save();
}
return $success;
}
/**
* Get the Resource's full alias path.
*
* @param string $alias Optional. The alias to check. If not set, will
* then build it from the pagetitle if automatic_alias is set to true.
* @param array $fields Optional. An array of field values to use instead of
* using the current modResource fields.
*
* @return string
*/
public function getAliasPath($alias = '', array $fields = [])
{
if (empty($fields)) {
$fields = $this->toArray();
}
$workingContext = $this->xpdo->getContext($fields['context_key']);
if (empty($fields['uri_override']) || empty($fields['uri'])) {
/* auto assign alias if using automatic_alias */
if (empty($alias) && $workingContext->getOption('automatic_alias', false)) {
$alias = $this->cleanAlias($fields['pagetitle']);
} elseif (empty($alias) && isset($fields['id']) && !empty($fields['id'])) {
$alias = $this->cleanAlias($fields['id']);
} else {
$alias = $this->cleanAlias($alias);
}
$fullAlias = $alias;
$isHtml = true;
$extension = '';
$containerSuffix = $workingContext->getOption('container_suffix', '');
/* @var modContentType $contentType process content type */
if (!empty($fields['content_type']) && $contentType = $this->xpdo->getObject(modContentType::class, $fields['content_type'])) {
$extension = $contentType->getExtension();
$isHtml = (strpos($contentType->get('mime_type'), 'html') !== false);
}
/* set extension to container suffix if Resource is a folder, HTML content type, and the container suffix is set */
if (!empty($fields['isfolder']) && $isHtml && !empty ($containerSuffix)) {
$extension = $containerSuffix;
}
$aliasPath = '';
/* if using full alias paths, calculate here */
if ($workingContext->getOption('use_alias_path', false)) {
$useFrozenPathUris = $workingContext->getOption('use_frozen_parent_uris', false);
$pathParentId = $fields['parent'];
$parentResources = [];
$query = $this->xpdo->newQuery(modResource::class);
$query->select($this->xpdo->getSelectColumns(modResource::class, '', '',
['parent', 'alias', 'alias_visible', 'uri', 'uri_override']));
$query->where("{$this->xpdo->escape('id')} = ?");
$query->prepare();
$query->stmt->execute([$pathParentId]);
$currResource = $query->stmt->fetch(PDO::FETCH_ASSOC);
while ($currResource) {
// If the use_frozen_parent_uris setting is enabled, we will look at the parent frozen uri instead
// of building the full uri from all parents. This makes sure children will have an uri relative