/
Document.php
1437 lines (1257 loc) · 51.4 KB
/
Document.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 APF\core\pagecontroller;
/**
* <!--
* This file is part of the adventure php framework (APF) published under
* http://adventure-php-framework.org.
*
* The APF is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The APF is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the APF. If not, see http://www.gnu.org/licenses/lgpl-3.0.txt.
* -->
*/
use APF\core\benchmark\BenchmarkTimer;
use APF\core\expression\taglib\ExpressionEvaluationTag;
use APF\core\loader\RootClassLoader;
use APF\core\singleton\Singleton;
use Exception;
use InvalidArgumentException;
/**
* @package APF\core\pagecontroller
* @class Document
*
* Represents a node within the APF DOM tree. Each document can compose several other documents
* by use of the $children property (composite tree).
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
*/
class Document extends APFObject {
/**
* @const
* Attribute name for service name of document controller
*/
const CONTROLLER_ATTR_SERVICE_NAME = 'service';
/**
* @const
* Attribute name for service namespace of document controller
*/
const CONTROLLER_ATTR_SERVICE_NAMESPACE = 'namespace';
/**
* @const
* Attribute name for fully qualified class name of document controller
*/
const CONTROLLER_ATTR_CLASS = 'class';
/**
* @protected
* @var string Unique object identifier.
*/
protected $objectId = null;
/**
* @protected
* @var Document Reference to the parent object.
*/
protected $parentObject = null;
/**
* @protected
* @var string[] The attributes of an object (merely the XML tag attributes).
*/
protected $attributes = array();
/**
* @protected
* @var string The content of the tag. Example:
* <pre><foo:bar>This is the content of the tag.</foo:bar></pre>
*/
protected $content;
/**
* @protected
* @var DocumentController The instance of the document controller to use at transformation time.
*/
protected $documentController = null;
/**
* @protected
* @var Document[] List of the children of the current object.
*/
protected $children = array();
/**
* @protected
* @var string[][] Data attributes of the current DOM document (similar to Java Script).
*/
protected $data = array();
/**
* @var TagLib[] List of known tags the APF parser uses to create tag instances during analysis phase.
*/
protected static $knownTags = array();
/**
* @var TagLib[] List of known tags for a dedicated DOM node the APF parser uses to create tag instances during analysis phase.
*/
protected $knownInstanceTags = array();
/**
* @public
* @static
* @var int The maximum number of parser loops taken to analyze tags within a document. Used to protect against infinite loops.
*/
public static $maxParserLoops = 500;
/**
* @public
*
* Injects the parent node of the current APF object.
*
* @param Document $parentObject The parent node.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function setParentObject(Document &$parentObject) {
$this->parentObject = & $parentObject;
}
/**
* @public
*
* Returns the parent node of the current APF object.
*
* @return Document The parent node.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function &getParentObject() {
return $this->parentObject;
}
/**
* @public
*
* Sets the object id of the current APF object.
*
* @param string $objectId The object id.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function setObjectId($objectId) {
$this->objectId = $objectId;
}
/**
* @public
*
* Returns the object id of the current APF object.
*
* @return string The object id.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function getObjectId() {
return $this->objectId;
}
/**
* @public
*
* Returns the object's attribute.
*
* @param string $name The name of the desired attribute.
* @param string $default The default value for the attribute.
*
* @return string Returns the value or null in case of errors.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
* Version 0.2, 02.02.2007 (Added default value handling)<br />
*/
public function getAttribute($name, $default = null) {
return isset($this->attributes[$name]) ? $this->attributes[$name] : $default;
}
/**
* @public
*
* Let's you retrieve a tag attribute expressing to other developers that it is a mandatory attribute.
*
* @param string $name The name of the desired attribute.
*
* @return string Returns the value.
* @throws InvalidArgumentException In case the attribute is not present/defined.
*
* @author Christian Achatz
* @version
* Version 0.1, 27.05.2014<br />
*/
public function getRequiredAttribute($name) {
$attribute = $this->getAttribute($name);
if ($attribute === null) {
throw new InvalidArgumentException('[' . get_class($this) . '::getRequiredAttribute()] Attribute "' . $name
. '" has not been defined but is mandatory! Please re-check your template setup.');
}
return $attribute;
}
/**
* @public
*
* Sets an object's attribute.
*
* @param string $name Name of the attribute.
* @param string $value Value of the attribute.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
*/
public function setAttribute($name, $value) {
$this->attributes[$name] = $value;
}
/**
* @public
*
* Returns an object's attributes.
*
* @return string[] Returns the list of attributes of the current object.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
*/
public function getAttributes() {
return $this->attributes;
}
/**
* @public
*
* Deletes an attribute.
*
* @param string $name The name of the attribute to delete.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
*/
public function deleteAttribute($name) {
unset($this->attributes[$name]);
}
/**
* @public
*
* Sets an object's attributes.
*
* @param array $attributes The attributes list.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
*/
public function setAttributes(array $attributes = array()) {
if (count($attributes) > 0) {
if (!is_array($this->attributes)) {
$this->attributes = array();
}
$this->attributes = array_merge($this->attributes, $attributes);
}
}
/**
* @public
*
* Let's you add the applied value to the given attribute.
* <p/>
* Implicitly creates the attribute in case it doesn't exist.
*
* @param string $name The name of the attribute to add a value to.
* @param string $value The value to add to the current attribute value.
*
* @author Christian Schäfer
* @version
* Version 0.1, 09.01.2007<br />
* Version 0.2, 09.02.2013 (Moved to APFObject to avoid multiple implementations)<br />
*/
public function addAttribute($name, $value) {
if (isset($this->attributes[$name])) {
$this->attributes[$name] .= $value;
} else {
$this->attributes[$name] = $value;
}
}
/**
* @public
*
* Returns the textual content of the current node.
*
* @return string The content of the current node.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function getContent() {
return $this->content;
}
/**
* @public
*
* Sets the textual content of the current node.
*
* @param string $content The content of the current node.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function setContent($content) {
$this->content = $content;
}
/**
* @public
*
* Returns the list of the current node's children.
*
* @return Document[] The current node's children.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function &getChildren() {
return $this->children;
}
/**
* @protected
*
* Let's you retrieve a child node of the current document by specifying a selector
* (attribute name and attribute value) and the expected node type (name of the taglib
* class).
*
* @param string $attributeName The name of the attribute to match against the given value.
* @param string $value The value of the attribute to select the desired node.
* @param string $tagLibClass The expected class name of the node.
*
* @return Document The desired child node.
* @throws InvalidArgumentException In case the node has no children or no child node can be found with the given selectors.
*
* @author Christian Achatz
* @version
* Version 0.1, 11.12.2011<br />
* Version 0.2, 09.02.2013 (Now public access since DocumentController is now derived from APFObject instead of Document)<br />
*/
public function &getChildNode($attributeName, $value, $tagLibClass) {
$children = & $this->getChildren();
if (count($children) > 0) {
foreach ($children as $objectId => $DUMMY) {
if ($children[$objectId] instanceof $tagLibClass) {
if ($children[$objectId]->getAttribute($attributeName) == $value) {
return $children[$objectId];
}
}
}
} else {
throw new InvalidArgumentException('[' . get_class($this) . '::getChildNode()] Current node has no children!',
E_USER_ERROR);
}
throw new InvalidArgumentException('[' . get_class($this) . '::getChildNode()] No child node with type "'
. $tagLibClass . '" and attribute selector ' . $attributeName . '="' . $value . '" composed in current '
. 'document!', E_USER_ERROR);
}
/**
* @protected
*
* Let's you retrieve a list of child nodes of the current document by specifying a selector
* (attribute name and attribute value) and the expected node type (name of the taglib
* class).
*
* @param string $attributeName The name of the attribute to match against the given value.
* @param string $value The value of the attribute to select the desired node.
* @param string $tagLibClass The expected class name of the nodes.
*
* @return Document[] The desired list of child nodes.
* @throws InvalidArgumentException In case the node has no children or no child node can be found with the given selectors.
*
* @author Christian Achatz
* @version
* Version 0.1, 14.07.2012<br />
* Version 0.2, 09.02.2013 (Now public access since DocumentController is now derived from APFObject instead of Document)<br />
*/
public function &getChildNodes($attributeName, $value, $tagLibClass) {
$children = & $this->getChildren();
if (count($children) > 0) {
$result = array();
foreach ($children as $objectId => $DUMMY) {
if ($children[$objectId] instanceof $tagLibClass) {
if ($children[$objectId]->getAttribute($attributeName) == $value) {
$result[] = & $children[$objectId];
}
}
}
if (count($result) == 0) {
throw new InvalidArgumentException('[' . get_class($this) . '::getChildNodes()] No child nodes with type "'
. $tagLibClass . '" and attribute selector ' . $attributeName . '="' . $value . '" composed in current '
. 'document!', E_USER_ERROR);
} else {
return $result;
}
}
throw new InvalidArgumentException('[' . get_class($this) . '::getChildNodes()] Current node has no children!', E_USER_ERROR);
}
/**
* @public
*
* API method to set a place holder's content within a document.
*
* @param string $name name of the place holder.
* @param string $value value of the place holder.
* @param bool $append True in case the applied value should be appended, false otherwise.
*
* @return Document This instance for further usage.
* @throws InvalidArgumentException In case the place holder cannot be found.
*
* @author Christian Achatz, Jan Wiese
* @version
* Version 0.1, 29.12.2006<br />
* Version 0.2, 10.11.2008 (Removed check, if taglib class exists)<br />
* Version 0.3, 07.02.2013 (Moved to Document to avoid multiple implementations)<br />
* Version 0.4, 05.08.2013 (Added support to append content to place holders)<br />
*/
public function &setPlaceHolder($name, $value, $append = false) {
$count = 0;
foreach ($this->children as $objectId => $DUMMY) {
if ($this->children[$objectId] instanceof PlaceHolderTag
&& $this->children[$objectId]->getAttribute('name') === $name
) {
// false handled first, since most usages don't append --> slightly faster
if ($append === false) {
$this->children[$objectId]->setContent($value);
} else {
$this->children[$objectId]->setContent(
$this->children[$objectId]->getContent() . $value
);
}
$count++;
}
}
if ($count == 0) {
// Since this method is used within all derived classes the exception message is
// rather generic unfortunately. In order to be more precise, the convenience methods
// within BaseDocumentController catch and rethrow the exception enriched with further
// information.
$message = '[' . get_class($this) . '::setPlaceHolder()] No place holder with name "' . $name
. '" found within document with ';
$nodeName = $this->getAttribute('name');
if (!empty($nodeName)) {
$message .= 'name "' . $nodeName . '" and ';
}
$message .= 'node type "' . get_class($this) . '"! Please check your template code: ' . $this->getContent() . '.';
throw new InvalidArgumentException($message, E_USER_ERROR);
}
return $this;
}
/**
* @public
*
* This method is for conveniently setting of multiple place holders. The applied
* array must contain a structure like this:
* <code>
* array(
* 'key-a' => 'value-a',
* 'key-b' => 'value-b',
* 'key-c' => 'value-c',
* 'key-d' => 'value-d',
* 'key-e' => 'value-e',
* )
* </code>
* Thereby, the <em>key-*</em> offsets define the name of the place holders, their
* values are used as the place holder's values.
*
* @param string[] $placeHolderValues Key-value-couples to fill place holders.
* @param bool $append True in case the applied values should be appended, false otherwise.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.11.2010<br />
* Version 0.2, 09.02.2013 (Moved to Document to avoid multiple implementations)<br />
* Version 0.3, 06.08.2013 (Added support for appending content to place holders)<br />
*/
public function setPlaceHolders(array $placeHolderValues, $append = false) {
foreach ($placeHolderValues as $key => $value) {
$this->setPlaceHolder($key, $value, $append);
}
}
/**
* @protected
*
* Replaces string place holders in content of <*:placeholder /> tag.
* An example of a string place holder with key "url" is "{URL}"
* String place holders are always written in capital letters!
*
* @param string $name Place holder name.
* @param string $key Key name of string place holder.
* @param string $value Value, the string place holder is replaced with.
*
* @return Document This instance for further usage.
* @throws InvalidArgumentException In case no place holder has been found.
*
* @author Jan Wiese <jan.wiese@adventure-php-framework.org>
* @version
* Version 0.1, 03.10.2012<br />
*/
public function &setStringPlaceHolder($name, $key, $value) {
$nodes = & $this->getChildNodes('name', $name, 'APF\core\pagecontroller\PlaceHolderTag');
/* @var $nodes PlaceHolderTag[] */
foreach ($nodes as $node) {
$node->setStringReplacement($key, $value);
}
return $this;
}
/**
* @public
*
* Returns the name of the document controller in case the document should
* be transformed using an MVC controller. In case no controller is defined
* <em>null</em> is returned instead.
*
* @return string|null The name of the document controller.
*
* @author Christian Achatz
* @version
* Version 0.1, 20.02.2010<br />
*/
public function getDocumentController() {
return $this->documentController === null ? null : get_class($this->documentController);
}
/**
* This method adds a given tag to the <em>global</em> list of known tags for the APF parser.
*
* @param TagLib $tag The tag to add for the APF parser.
*
* @author Christian Schäfer, Christian Achatz
* @version
* Version 0.1, 28.12.2006<br />
* Version 0.2, 03.03.2007 (Removed the "&" in front of "new")<br />
* Version 0.3, 14.02.2011 (Refactored method signature to be more type safe)<br />
*/
public static function addTagLib(TagLib $tag) {
self::$knownTags[$tag->getPrefix() . ':' . $tag->getName()] = $tag;
}
/**
* This method adds a given tag to the <em>local</em> list of known tags for the APF parser.
* <p/>
* Using this method, you can override globally defined tags for this particular instance.
*
* @param TagLib $tag The tag to add for the APF parser used for this particular instance.
*
* @author Christian Achatz
* @version
* Version 0.1, 14.02.2011 (ID#185, ID#1786: introduced local override mechanism)<br />
*/
public function addInstanceTagLib(TagLib $tag) {
$this->knownInstanceTags[$tag->getPrefix() . ':' . $tag->getName()] = $tag;
}
/**
* This method adds a given list of tags to the list of known tags for the APF parser.
*
* @param TagLib[] $tags Tags to register for the APF parser.
*
* @author Christian Achatz
* @version
* Version 0.1, 18.06.2014<br />
*/
public static function addTagLibs(array $tags) {
foreach ($tags as $tag) {
self::addTagLib($tag);
}
}
/**
* This method adds a list of tag to the <em>local</em> list of known tags for the APF parser.
* <p/>
* Using this method, you can override globally defined tags for this particular instance.
*
* @param TagLib[] $tags Tag to add for the APF parser used for this particular instance.
*
* @author Christian Achatz
* @version
* Version 0.1, 14.02.2011 (ID#185, ID#1786: introduced local override mechanism)<br />
*/
public function addInstanceTagLibs(array $tags) {
foreach ($tags as $tag) {
$this->addInstanceTagLib($tag);
}
}
/**
* @public
*
* Allows you to set data attributes to the current DOM node (similar to Java Script for HTML nodes).
*
* @param string $name The reference name of the data field to set/add.
* @param mixed $data The data to inject to the current node.
*
* @author Christian Achatz
* @version
* Version 0.1, 29.01.2014<br />
*/
public function setData($name, $data) {
$this->data[$name] = $data;
}
/**
* @public
*
* Allows you to retrieve a data attribute from the current DOM node (similar to Java Script for HTML nodes).
*
* @param string $name The reference name of the data field to set/add.
* @param mixed $default The desired default value (optional).
*
* @return mixed The desired data field content or the default value.
*
* @author Christian Achatz
* @version
* Version 0.1, 29.01.2014<br />
*/
public function getData($name, $default = null) {
return isset($this->data[$name]) ? $this->data[$name] : $default;
}
/**
* @public
*
* Loads the initial template for the initial document. Can also be used to load
* content from files within sub taglibs.
*
* @param string $namespace Namespace of the initial templates.
* @param string $design Name of the initial template.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
* Version 0.2, 15.01.2007 (Now document controller are extracted first)<br />
*/
public function loadDesign($namespace, $design) {
// read the content of the template
$this->loadContentFromFile($namespace, $design);
// analyze document controller definition
$this->extractDocumentController();
// parse known tags
$this->extractTagLibTags();
}
/**
* @protected
*
* Loads a template from a given namespace. The convention says, that the name of the template
* is equal to the file body plus the ".html" extensions. The namespace is a APF namespace.
*
* @param string $namespace The namespace of the template.
* @param string $name The name of the template (a.k.a. design).
*
* @throws IncludeException In case the template file is not found.
*
* @author Christian Schäfer
* @version
* Version 0.1, 28.12.2006<br />
* Version 0.2, 01.01.2007<br />
* Version 0.3, 03.11.2008 (Added code of the responsible template to the error message to ease debugging)<br />
* Version 0.4, 31.10.2012 (Introduced the append feature to be able to preserve tag content created/added prior calling this method)<br />
*/
protected function loadContentFromFile($namespace, $name) {
// sanitize the design name to avoid xss or code injection
$name = preg_replace('/[^A-Za-z0-9\-_]/', '', $name);
try {
$file = $this->getTemplateFilePath($namespace, $name);
} catch (Exception $e) {
// rethrow exception with meaningful content (class loader exception would be too misleading)
throw new IncludeException('[' . get_class($this) . '::loadContentFromFile()] Template "' . $name
. '" not existent in namespace "' . $namespace . '"!', E_USER_ERROR, $e);
}
if (file_exists($file)) {
// Append the content to the current content buffer. In case the existing content should be
// overwritten by this method call, please clear it using $this->content = '' prior calling
// this method (normally not necessary).
$this->content .= file_get_contents($file);
} else {
// get template code from parent object, if the parent exists
$code = '';
if ($this->getParentObject() !== null) {
$code = ' Please check your template code (' . $this->getParentObject()->getContent() . ').';
}
throw new IncludeException('[' . get_class($this) . '::loadContentFromFile()] Template "' . $name
. '" not existent in namespace "' . $namespace . '" (file: "' . $file . '")!' . $code, E_USER_ERROR);
}
}
/**
* @protected
*
* Generates the file path of the desired template.
* <p/>
* Overwriting this method allows you to use a different algorithm of creating the
* path within your custom tag implementations.
*
* @param string $namespace The namespace of the template.
* @param string $name The (file) name of the template.
*
* @return string The template file path of the referred APF template.
*
* @author Christian Achatz
* @version
* Version 0.1, 31.10.2012<br />
*/
protected function getTemplateFilePath($namespace, $name) {
// gather namespace and full(!) template name and use class loader to determine root path
// ID#152: check whether we have a vendor-only namespace declaration to support
// Document::getTemplateFileName('APF', 'foo') calls
$vendorOnly = RootClassLoader::isVendorOnlyNamespace($namespace);
if ($vendorOnly === true) {
$classLoader = RootClassLoader::getLoaderByVendor($namespace);
} else {
$classLoader = RootClassLoader::getLoaderByNamespace($namespace);
}
$rootPath = $classLoader->getRootPath();
if ($vendorOnly === true) {
return $rootPath . '/' . $name . '.html';
} else {
$vendor = $classLoader->getVendorName();
return $rootPath . '/' . str_replace('\\', '/', str_replace($vendor . '\\', '', $namespace)) . '/' . $name . '.html';
}
}
/**
* @protected
*
* Parses the content of the current APF DOM node. Extracts all tags contained in the current
* document content. Each tag is converted into a child Document of the current tree element.
* The tag definition place is remembered by a marker tag using the internal id of the DOM node.
* <p/>
* Since release 1.17 nested tag structures are supported. This means, that the APF parser
* is able to handle symmetric structures like this:
* <code>
* <foo:bar>
* <foo:bar>
* </foo:bar>
* </foo:bar>
* </code>
* Besides, the APF parser is able to handle asymmetric structures like
* <code>
* <foo:bar />
* <foo:bar>
* <foo:bar>
* <foo:bar />
* </foo:bar>
* </foo:bar>
* </code>
* Please note that using nested structures must be supported by the tag implementations.
* <p/>
* The APF parser is able to handle nested tag structures with self-containing tags (directly of
* across multiple hierarchies) as of version 2.2. Thus, you can re-use tags across any hierarchy
* with the same prefix and name at your convenience or defined tags with different prefix and/or
* name as desired (e.g. using the same implementation).
* <p/>
* To protect against infinite loops with broken tag structures the parser uses <em>self::$maxParserLoops</em>
* to limit the parser cycles to a configurable amount of times. In case your project requires a
* higher value, please set <em>Document::$maxParserLoops</em> to an appropriate value.
*
* @author Christian Schäfer, Christian Achatz
* @version
* Version 0.1, 28.12.2006<br />
* Version 0.2, 21.01.2007 (Bug-fix: a mixture of self- and exclusively closing tags lead to wrong parsing)<br />
* Version 0.3, 31.01.2007 (Added context injection)<br />
* Version 0.4, 09.04.2007 (Removed double attributes setting, added language injection)<br />
* Version 0.5, 02.04.2008 (Bug-fix: the token is now displayed in the HTML error page)<br />
* Version 0.6, 06.06.2009 (Improvement: content is not copied during parsing any more)<br />
* Version 0.7, 30.12.2009 (Introduced benchmark marks for the onParseTime() event.)<br />
* Version 0.8, 25.01.2013 (Re-writing of the tag parser to support nested tags with the same tag prefix and name)<br />
* Version 0.9, 20.06.2014 (Re-writing entire parser for 2.2 to support nested, self-containing tags across multiple hierarchies)<br />
*/
protected function extractTagLibTags() {
/**
* @var array The list of parsed tags within the current document.
*/
$tags = array();
/**
* @var int The number of tokens within the current document (introduced also for performance reasons).
*/
$count = 0;
/* @var $t BenchmarkTimer */
$t = & Singleton::getInstance('APF\core\benchmark\BenchmarkTimer');
$benchId = '(' . get_class($this) . ') ' . $this->getObjectId() . '::onParseTime()';
$t->start($benchId);
/**
* @var int Position pointer for tag search. Introduced for performance reasons to skip stuff that we already searched in.
*/
$offset = 0;
while (($colon = strpos($this->content, ':', $offset)) !== false) {
// start tag searching goes as follows
// - tag sub string starting at the current position of the colon with 12 chars
// search for last < in that sub-string
// tag prefixes must not be longer than 10 characters - for security reasons
$area = $colon >= 12 ? 12 : $colon; // in case the tag starts right at the beginning, the area to search gets smaller!
$start = strrpos(substr($this->content, $colon - $area, $area), '<');
// no open tag found --> continue!
if ($start === false) {
$offset = $colon + 1;
continue;
}
// do offset correction due to internal
$start = $colon - $area + $start; // $area (12 by default) for the sub-string part
// avoid issue with "<li>FOO:" constructs that will be recognized as tag
if (strpos(substr($this->content, $start, $colon - $start), '>') !== false) {
$offset = $colon + 1;
continue;
}
// find out whether we have an opening or closing tag
$end = strpos($this->content, '>', $colon + 1);
if (substr($this->content, $start, 2) !== '</') {
// Determine whether we have a self-closing tag or not. This is important
// within the following lines how to handle the tag.
if (substr($this->content, $end - 1, 1) == '/') {
$selfClosing = true;
} else {
$selfClosing = false;
}
// s = tag start position
$tags[$count]['s'] = $start;
// p = tag prefix (e.g. "foo" with tag "<foo:bar />")
$tags[$count]['p'] = substr($this->content, $start + 1, $colon - $start - 1);
// search for next space to gather tag name
$space = strpos($this->content, ' ', $colon);
// in case we encounter tag definitions w/o spaces, reset the space position
// to the next ">".
if ($space === false) {
if ($selfClosing === true) {
$space = $end - 1;
} else {
$space = $end;
}
}
// in case we encounter a closing bracket first, this may be due to
// a tag without attributes <foo:bar>...
if ($end < $space) {
// reset space indicator to bracket position to support opening
// tags without attributes (<foo:bar> </foo:bar>)
if ($selfClosing) {
// Correct position by minus one due to "/>" at the end.
// This only holds true for "<foo:bar/>" tags (no space after tag name).
$space = $end - 1;
} else {
$space = $end;
}
}
// n = tag name (e.g. "bar" with tag "<foo:bar />")
$tags[$count]['n'] = trim(substr($this->content, $colon + 1, $space - $colon - 1)); // instead of trim, maybe search for a new line instead
// assemble the token to allow easier closing tag search
$token = $tags[$count]['p'] . ':' . $tags[$count]['n'];
if ($selfClosing === true) {
// e = tag end position
$tags[$count]['e'] = $end + 1;
// set offset to end of last tag before starting with new one
$offset = $end + 1;
} else {
// if we've got an opening/not self-closing tag, let's search for our pendent closing tag
$tokenLength = strlen($token);
$startTagLength = $tokenLength + 1; // 1 for "<"
$endTagLength = $tokenLength + 3; // 3 for "</" plus ">"
// initialize the token position cursor
$tokenPos = $space; // for performance reasons: start right after the space to save some chars
$tagEndPos = $end;
$openingTagCount = 1; // for performance reasons: start at 1 and skip current tag position to save some chars
$closingTagCount = 0;
while (true) {
$tokenPos = strpos($this->content, $token, $tokenPos);
if ($tokenPos === false) {
break;
}
if (substr($this->content, $tokenPos - 1, 1) == '<') {
// Check for explicitly closing tag, because self-closing tags
// do not count searching for a symmetric tag hierarchy included
// in another tag structure.
$bracket = strpos($this->content, '>', $tokenPos + $tokenLength);
if (substr($this->content, $bracket - 1, 1) !== '/') {
$openingTagCount++;
}
} else if (substr($this->content, $tokenPos - 1, 1) == '/') {
// ID#98: Check for explicit closing tag expressed by "</$token>" instead of relying
// on the previous check. Otherwise, an occurrence of "$token" solely will lead to
// a match for a closing tag which in fact is just an occurrence of the token.
$closingTagCount++;
}
// In case we have passed the first tag occurrence let's look for a symmetric
// tag structure. This check enables nesting tag structures with the same
// tag prefix and name.
if ($openingTagCount > 0 && $openingTagCount == $closingTagCount) {
$tagEndPos = $tokenPos - 2;
break;
}
// Shift cursor to start search after current token position to recursively
// search for the last symmetric end tag.
$tokenPos = $tokenPos + $startTagLength;
}
// e = tag end position
$tags[$count]['e'] = $tagEndPos + $endTagLength;
// set offset to end of last tag before starting with new one to skip already analyzed parts
$offset = $tags[$count]['e'];
}
}
$count++;
}
// Offset correction necessary, since previously gathered tag positions are manipulated
// during building-up the tag markers!