1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8/****
9 * Initially just a collection of the functions dotted around tiki-editpage.php for v4.0
10 * Started in the edit_fixup experimental branch - jonnybradley Aug 2009
11 *
12 */
13
14class EditLib
15{
16	private $tracesOn = false;
17
18	// Fields for translation related methods.
19	public $sourcePageName = null;
20	public $targetPageName = null;
21	public $oldSourceVersion = null;
22	public $newSourceVersion = null;
23
24	// Fields for handling links to external wiki pages
25	private $external_wikis = null;
26
27
28	// general
29
30	function make_sure_page_to_be_created_is_not_an_alias($page, $page_info)
31	{
32		$access = TikiLib::lib('access');
33		$tikilib = TikiLib::lib('tiki');
34		$wikilib = TikiLib::lib('wiki');
35		$semanticlib = TikiLib::lib('semantic');
36
37		$aliases = $semanticlib->getAliasContaining($page, true);
38		if (! $page_info && count($aliases) > 0) {
39			$error_title = tra("Cannot create aliased page");
40			$error_msg = tra("You attempted to create the following page:") . " " .
41						 "<b>$page</b>.\n<p>\n";
42			$error_msg .= tra("That page is an alias for the following pages") . ": ";
43			foreach ($aliases as $an_alias) {
44				$error_msg .= '<a href="' . $wikilib->editpage_url($an_alias['fromPage'], false) . '">' . $an_alias['fromPage'] . '</a>, ';
45			}
46			$error_msg .= "\n<p>\n";
47			$error_msg .= tra("If you want to create the page, you must first edit each of the pages above to remove the alias link they may contain. This link should look something like this");
48			$error_msg .= ": <b>(alias($page))</b>";
49
50			$access->display_error(page, $error_title, "", true, $error_msg);
51		}
52	}
53
54	function user_needs_to_specify_language_of_page_to_be_created($page, $page_info, $new_page_inherited_attributes = null)
55	{
56		global $prefs;
57		if (isset($_REQUEST['need_lang']) && $_REQUEST['need_lang'] == 'n') {
58			return false;
59		}
60		if ($prefs['feature_multilingual'] == 'n') {
61			return false;
62		}
63		if ($page_info && isset($page_info['lang']) && $page_info['lang'] != '') {
64			return false;
65		}
66		if (isset($_REQUEST['lang']) && $_REQUEST['lang'] != '') {
67			return false;
68		}
69		if ($new_page_inherited_attributes != null &&
70			isset($new_page_inherited_attributes['lang']) &&
71			$new_page_inherited_attributes['lang'] != '') {
72			return false;
73		}
74
75		return true;
76	}
77
78	// translation functions
79
80	function isTranslationMode()
81	{
82		return $this->isUpdateTranslationMode() || $this->isNewTranslationMode();
83	}
84
85	function isNewTranslationMode()
86	{
87		global $prefs;
88
89		if ($prefs['feature_multilingual'] != 'y') {
90			return false;
91		}
92		if (isset($_REQUEST['translationOf'])
93			&& ! empty($_REQUEST['translationOf'])) {
94			return true;
95		}
96		if (isset($_REQUEST['is_new_translation'])
97			&& $_REQUEST['is_new_translation'] == 'y') {
98			return true;
99		}
100		return false;
101	}
102
103	function isUpdateTranslationMode()
104	{
105		return isset($_REQUEST['source_page'])
106			&& isset($_REQUEST['oldver'])
107			&& (! isset($_REQUEST['is_new_translation']) || $_REQUEST['is_new_translation'] == 'n')
108			&& isset($_REQUEST['newver']);
109	}
110
111	function prepareTranslationData()
112	{
113		$this->setTranslationSourceAndTargetPageNames();
114		$this->setTranslationSourceAndTargetVersions();
115	}
116
117	private function setTranslationSourceAndTargetPageNames()
118	{
119		$smarty = TikiLib::lib('smarty');
120
121		if (! $this->isTranslationMode()) {
122			return;
123		}
124
125		$this->targetPageName = null;
126		if (isset($_REQUEST['target_page'])) {
127			$this->targetPageName = $_REQUEST['target_page'];
128		} elseif (isset($_REQUEST['page'])) {
129			$this->targetPageName = $_REQUEST['page'];
130		}
131		$smarty->assign('target_page', $this->targetPageName);
132
133		$this->sourcePageName = null;
134		if (isset($_REQUEST['translationOf']) && $_REQUEST['translationOf']) {
135			$this->sourcePageName = $_REQUEST['translationOf'];
136		} elseif (isset($_REQUEST['source_page'])) {
137			$this->sourcePageName = $_REQUEST['source_page'];
138		}
139		$smarty->assign('source_page', $this->sourcePageName);
140
141		if ($this->isNewTranslationMode()) {
142			$smarty->assign('translationIsNew', 'y');
143		} else {
144			$smarty->assign('translationIsNew', 'n');
145		}
146	}
147
148	private function setTranslationSourceAndTargetVersions()
149	{
150		global $_REQUEST, $tikilib;
151
152		if (isset($_REQUEST['oldver'])) {
153			$this->oldSourceVersion = $_REQUEST['oldver'];
154		} else {
155			// Note: -1 means a "virtual" empty version.
156			$this->oldsourceVersion = -1;
157		}
158
159		if (isset($_REQUEST['newver'])) {
160			$this->newSourceVersion = $_REQUEST['newver'];
161		} else {
162			// Note: version number of 0 means the most recent version.
163			$this->newSourceVersion = 0;
164		}
165	}
166
167	function aTranslationWasSavedAs($complete_or_partial)
168	{
169		if (! $this->isTranslationMode() ||
170			! isset($_REQUEST['save'])) {
171			return false;
172		}
173
174		// We are saving a translation. Is it partial or complete?
175		if ($complete_or_partial == 'complete' && isset($_REQUEST['partial_save'])) {
176			return false;
177		} elseif ($complete_or_partial == 'partial' && ! isset($_REQUEST['partial_save'])) {
178			return false;
179		}
180		return true;
181	}
182
183
184	/**
185	 * Convert rgb() color definiton to hex color definiton
186	 *
187	 * @param unknown_type $col
188	 * @return The hex representation
189	 */
190	function parseColor(&$col)
191	{
192
193		if (preg_match("/^rgb\( *(\d+) *, *(\d+) *, *(\d+) *\)$/", $col, $parts)) {
194			$hex = str_pad(dechex($parts[1]), 2, '0', STR_PAD_LEFT)
195				 . str_pad(dechex($parts[2]), 2, '0', STR_PAD_LEFT)
196				 . str_pad(dechex($parts[3]), 2, '0', STR_PAD_LEFT);
197			$hex = '#' . TikiLib::strtoupper($hex);
198		} else {
199			$hex = $col;
200		}
201
202		return $hex;
203	}
204
205
206	/**
207	 * Utility for walk_and_parse to process links
208	 *
209	 * @param array $args the attributes of the link
210	 * @param array $text the link text
211	 * @param string $src output string
212	 * @param array $p ['stack'] = closing strings stack
213	 */
214	private function parseLinkTag(&$args, &$text, &$src, &$p)
215	{
216
217		global $prefs;
218
219		$link = '';
220		$link_open = '';
221		$link_close = '';
222
223		/*
224		 * parse the link classes
225		 */
226		$cl_wiki = false;
227		$cl_wiki_page = false; // Wiki page
228		$cl_ext_page = false; // external Wiki page
229		$cl_external = false; // external web page
230		$cl_semantic = ''; // semantic link
231		if ($prefs['feature_semantic'] === 'y') {
232			$semantic_tokens = TikiLib::lib('semantic')->getAllTokens();
233		} else {
234			$semantic_tokens = [];
235		}
236
237		$ext_wiki_name = '';
238
239		if (isset($args['class']) && isset($args['href'])) {
240			$matches = [];
241			preg_match_all('/([^ ]+)/', $args['class']['value'], $matches);
242			$classes = $matches[0];
243
244			for ($i = 0, $count_classes = count($classes); $i < $count_classes; $i++) {
245				$cl = $classes[$i];
246
247				switch ($cl) {
248					case 'wiki':
249						$cl_wiki = true;
250						break;
251					case 'wiki_page':
252						$cl_wiki_page = true;
253						break;
254					case 'ext_page':
255						$cl_ext_page = true;
256						break;
257					case 'external':
258						$cl_external = true;
259						break;
260					default:
261						// if the preceding class was 'ext_page', then we have the name of the external Wiki
262						if ($i > 0 && $classes[$i - 1] == 'ext_page') {
263							$ext_wiki_name = $cl;
264						}
265						if (in_array($cl, $semantic_tokens)) {
266							$cl_semantic = $cl;
267						}
268				}
269			}
270		}
271
272
273		/*
274		 * extract the target and the anchor from the href
275		 */
276		if (isset($args['href'])) {
277			$href = urldecode($args['href']['value']);
278			// replace pipe chars as that's the delimiter in a wiki/external link
279			$href = str_replace('|', '%7C', $href);
280			$matches = explode('#', $href);
281			if (count($matches) == 2) {
282				$target = $matches[0];
283				$anchor = '#' . $matches[1];
284			} else {
285				$target = $href;
286				$anchor = '';
287			}
288		} else {
289			$target = '';
290			$anchor = '';
291		}
292
293
294		/*
295		 * treat invalid external Wikis as web links
296		 */
297		if ($cl_ext_page) {
298			// retrieve the definitions from the database
299			if ($this->external_wikis == null) {
300				global $tikilib;
301				$query = 'SELECT `name`, `extwiki` FROM `tiki_extwiki`';
302				$this->external_wikis = $tikilib->fetchMap($query);
303			}
304
305			// name must be set and defined
306			if (! $ext_wiki_name || ! isset($this->external_wikis[$ext_wiki_name])) {
307				$cl_ext_page = false;
308				$cl_wiki_page = false;
309			}
310		};
311
312
313		/*
314		 * construct the links according to the defined classes
315		 */
316		if ($cl_wiki_page) {
317			/*
318			 * link to wiki page -> (( ))
319			 */
320			$link_open = "($cl_semantic(";
321			$link_close = '))';
322
323			// remove the html part of the target
324			$target = preg_replace('/tiki\-index\.php\?page\=/', '', $target);
325
326			// construct the link
327			$link = $target;
328			if ($anchor) {
329				$link .= '|' . $anchor;
330			}
331		} elseif ($cl_ext_page) {
332			/*
333			 * link to external Wiki page ((:))
334			 */
335			$link_open = '((';
336			$link_close = '))';
337
338			// remove the extwiki definition from the target
339			$def = preg_replace('/\$page/', '', $this->external_wikis[$ext_wiki_name]);
340			$def = preg_quote($def, '/');
341			$target = preg_replace('/^' . $def . '/', '', $target);
342
343			// construct the link
344			$link = $ext_wiki_name . ':' . $target;
345			if ($anchor) {
346				$link .= '|' . $anchor;
347			}
348		} elseif ($cl_wiki && ! $cl_external && ! $target && strlen($anchor) > 0  && substr($anchor, 0, 1) == '#') {
349			/*
350			 * inpage links [#]
351			 */
352			$link_open = '[';
353			$link_close = ']';
354
355			// construct the link
356			$link = $target = $anchor;
357			$anchor = '';
358		} elseif ($cl_wiki && ! $cl_external) {
359			/*
360			 * other tiki resources []
361			 * -> articles, ...
362			 */
363			$link_open = '[';
364			$link_close = ']';
365
366			// construct the link
367			$link = $target;
368		} elseif (! $cl_wiki && ! $cl_external && ! $text && isset($args['id']) && isset($args['id']['value'])) {
369			/*
370			 * anchor
371			 */
372			 $link_open = '{ANAME()}';
373			 $link_close = '{ANAME}';
374			 $link = $args['id']['value'];
375		} else {
376			/*
377			 * other links []
378			 */
379			$link_open = '[';
380			$link_close = ']';
381
382
383			/*
384			 * parse the rel attribute
385			 */
386			$box = '';
387
388			if (isset($args['class']) && isset($args['rel'])) {
389				$matches = [];
390				preg_match_all('/([^ ]+)/', $args['rel']['value'], $matches);
391				$rels = $matches[0];
392
393				for ($i = 0, $count_rels = count($rels); $i < $count_rels; $i++) {
394					$r = $rels[$i];
395
396					if (preg_match('/^box/', $r)) {
397						$box = $r;
398					}
399				}
400			}
401
402			// construct the link
403			$link = $target;
404			if ($anchor) {
405				$link .= $anchor;
406			}
407			// the box must be appended to the text
408			if ($box) {
409				$text .= '|' . $box;
410			}
411		} // convert links
412
413
414
415		/*
416		 * flush the constructed link
417		 */
418		if ($link_open && $link_close) {
419			$p['wiki_lbr']++; // force wiki line break mode
420
421			// does the link text match the target?
422			if ($target == trim($text)) {
423				$text = '';	 // make sure walk_and_parse() does not append any text.
424			} else {
425				$link .= '|'; // the text will be appended by walk_and_parse()
426			}
427
428			// process the tag and update the output
429			$this->processWikiTag('a', $src, $p, $link_open, $link_close, true);
430			$src .= $link;
431		}
432	}
433
434
435	/**
436	 * Utility for walk_and_parse to process p and div tags
437	 *
438	 * @param bool $isPar True if we process a <p>, false if a <div>
439	 * @param array $args the attributes of the tag
440	 * @param string $src output string
441	 * @param array $p ['stack'] = closing strings stack
442	 */
443	private function parseParDivTag($isPar, &$args, &$src, &$p)
444	{
445
446		global $prefs;
447
448		if (isset($args['style']) || isset($args['align'])) {
449			$tag_name = $isPar ? 'p' : 'div'; // key for the $p[stack]
450			$type = $isPar ? 'type="p", ' : ''; // used for {DIV()}
451
452			$style = [];
453			$this->parseStyleAttribute($args['style']['value'], $style);
454
455
456			/*
457			 * convert 'align' to 'style' definitions
458			*/
459			if (isset($args['align'])) {
460				$style['text-align'] = $args['align']['value'];
461			}
462
463
464			/*
465			 * process all the defined styles
466			 */
467			foreach (array_keys($style) as $format) {
468				switch ($format) {
469					case 'text-align':
470						if ($style[$format] == 'left') {
471							$src .= "{DIV(${type}align=\"left\")}";
472							$p['stack'][] = ['tag' => $tag_name, 'string' => '{DIV}'];
473						} elseif ($style[$format] == 'center') {
474							$markup = ($prefs['feature_use_three_colon_centertag'] == 'y') ? ':::' : '::';
475							$this->processWikiTag($tag_name, $src, $p, $markup, $markup, false);
476						} elseif ($style[$format] == 'right') {
477							$src .= "{DIV(${type}align=\"right\")}";
478							$p['stack'][] = ['tag' => $tag_name, 'string' => '{DIV}'];
479						} elseif ($style[$format] == 'justify') {
480							$src .= "{DIV(${type}align=\"justify\")}";
481							$p['stack'][] = ['tag' => $tag_name, 'string' => '{DIV}'];
482						}
483						break;
484				} // switch format
485			} // foreach style
486		}
487	}
488
489
490	/**
491	 * Utility for walk_and_parse to process span arguments
492	 *
493	 * @param array $args the attributes of the span
494	 * @param string $src output string
495	 * @param array $p ['stack'] = closing strings stack
496	 */
497	private function parseSpanTag(&$args, &$src, &$p)
498	{
499
500		if (isset($args['style'])) {
501			$style = [];
502			$this->parseStyleAttribute($args['style']['value'], $style);
503
504			$this->processWikiTag('span', $src, $p, '', '', true); // prepare the stacks, later we will append
505
506
507			/*
508			 * The colors need to be handeled separatly; two style definitions become
509			 * one single wiki markup.
510			 */
511			$fcol = '';
512			$bcol = '';
513
514			if (isset($style['color'])) {
515				$fcol = $this->parseColor($style['color']);
516				unset($style['color']);
517			}
518			if (isset($style['background-color'])) { // background: color-def have been converted to background-color
519				$bcol = $this->parseColor($style['background-color']);
520				unset($style['background-color']);
521			}
522
523			if ($fcol || $bcol) {
524				$col  = "~~";
525				$col .= ($fcol ? $fcol : ' ');
526				$col .= ($bcol ? ',' . $bcol : '');
527				$col .= ':';
528
529				$this->processWikiTag('span', $src, $p, $col, '~~', true, true);
530			}
531
532
533			/*
534			 * Process the remaining format definitions
535			 */
536			foreach (array_keys($style) as $format) {
537				switch ($format) {
538					case 'font-weight':
539						if ($style[$format] == 'bold') {
540							$this->processWikiTag('span', $src, $p, '__', '__', true, true);
541						}
542						break;
543					case 'font-style':
544						if ($style[$format] == 'italic') {
545							$this->processWikiTag('span', $src, $p, '\'\'', '\'\'', true, true);
546						}
547					case 'text-decoration':
548						if ($style[$format] == 'line-through') {
549							$this->processWikiTag('span', $src, $p, '--', '--', true, true);
550						} elseif ($style[$format] == 'underline') {
551							$this->processWikiTag('span', $src, $p, '===', '===', true, true);
552						}
553				} // switch format
554			} // foreach style
555		} // style
556	}
557
558
559	/**
560	 * Parse a html style definition into an array
561	 *
562	 * This method tries to expand the shorthand definitions, such as 'background:',
563	 * to the correspoinding key/value paris. If a definition is unknown, it is kept.
564	 *
565	 * @param string $style The value of the style attribute
566	 * @param array $parsed key/value pairs
567	 */
568	function parseStyleAttribute(&$style, &$parsed)
569	{
570
571		$matches = [];
572		preg_match_all('/ *([^ :]+) *: *([^;]+) *;?/', $style, $matches);
573
574		for ($i = 0, $count_matches = count($matches[0]); $i < $count_matches; $i++) {
575			$key = $matches[1][$i];
576			$value = trim($matches[2][$i]);
577
578			/*
579			 * shortand list 'background:'
580			 * - set 'background-color'
581			 */
582			if ($key == 'background') {
583				$unprocessed = '';
584				$shorthand = [];
585				$this->parseStyleList($value, $shorthand);
586
587				foreach ($shorthand as $s) {
588					switch ($s) {
589						case preg_match('/^#(\w{3,6})$/', $s) > 0:
590							$parsed['background-color'] = $s;
591							break;
592						case preg_match('/^rgb\(.*\)$/', $s) > 0:
593							$parsed['background-color'] = $s;
594							break;
595						default:
596							$unprocessed .= ' ' . $s;
597					}
598				} // foreach shorthand
599
600				// keep unprocessed list entries
601				$value = trim($unprocessed);
602			} // background:
603
604			// save the result
605			if ($value) {
606				$parsed[$key] = $value;
607			}
608		} // style definitions
609	}
610
611
612	/**
613	 * Parse a space separated list of html styles
614	 *
615	 * Example: "rgb( 1, 2, 3) url(background.gif)"
616	 *
617	 * @param string $list List of styles
618	 * @param array $parsed The parsed list
619	 */
620	function parseStyleList(&$list, &$parsed)
621	{
622
623		$matches = [];
624		preg_match_all('/(?:[[:graph:]]+\([^\)]*\))|(?:[^ ]+)/', $list, $matches);
625		$parsed = $matches[0];
626	}
627
628
629	/**
630	 * Utility of walk_and_parse to process a wiki tag
631	 *
632	 * Wiki tags need a special treatment: In html, a tag may contain
633	 * several line breaks. In Wiki however, line breaks are often not allowed
634	 * and sometimes additional line breaks are required.
635	 *
636	 * This method saves Wiki tags an line break information in separate stacks.
637	 * These stackes are used in walk_and_parse to:
638	 * - Output the required markup before and after the linebreaks (<br />).
639	 * - To ensure that linebreaks are, if required, inserted at the correct place (\n).
640	 *
641	 * @param string $tag The name of the html tag
642	 * @param string $src Th Output string
643	 * @param array  $p   ['stack'] = closing strings stack,
644	 * @param string $begin The wiki markup that begins the tag
645	 * @param string $end The wiki markup that ends the tag
646	 * @param bool $is_inline True if the tag is inline, false if the tag must span exacly one line.
647	 * @param bool $append True = append to the topmost element on the stack, false = create a new element on the stack
648	 */
649	private function processWikiTag($tag, &$src, &$p, $begin, $end, $is_inline, $append = false)
650	{
651
652		// append=false, create new entries on the stack
653		if (! $append) {
654			$p['stack'][] = ['tag' => $tag, 'string' => '', 'wikitags' => 0 ];
655			$p['wikistack'][] = [ 'begin' => [], 'end' => [] ];
656		};
657
658		// get the entry points on the stacks
659		$keys = array_keys($p['wikistack']);
660		$key = end($keys);
661		$wiki = &$p['wikistack'][$key];
662
663		$keys = array_keys($p['stack']);
664		$key = end($keys);
665		$stack = &$p['stack'][$key];
666		$string = &$stack['string'];
667
668		// append to the stacks
669		$wiki['begin'][] = $begin;
670		$wiki['end'][] = $end;
671		$string = $end . $string;
672		$stack['wikitags']++;
673
674		// update the output string
675		if (! $is_inline) {
676			$this->startNewLine($src);
677		}
678		$src .= $begin;
679	}
680
681
682	function saveCompleteTranslation()
683	{
684		$multilinguallib = TikiLib::lib('multilingual');
685		$tikilib = TikiLib::lib('tiki');
686
687		$sourceInfo = $tikilib->get_page_info($this->sourcePageName);
688		$targetInfo = $tikilib->get_page_info($this->targetPageName);
689
690		$multilinguallib->propagateTranslationBits(
691			'wiki page',
692			$sourceInfo['page_id'],
693			$targetInfo['page_id'],
694			$sourceInfo['version'],
695			$targetInfo['version']
696		);
697		$multilinguallib->deleteTranslationInProgressFlags($targetInfo['page_id'], $sourceInfo['lang']);
698	}
699
700	function savePartialTranslation()
701	{
702		$tikilib = TikiLib::lib('tiki');
703		$multilinguallib = TikiLib::lib('multilingual');
704
705		$sourceInfo = $tikilib->get_page_info($this->sourcePageName);
706		$targetInfo = $tikilib->get_page_info($this->targetPageName);
707
708		$multilinguallib->addTranslationInProgressFlags($targetInfo['page_id'], $sourceInfo['lang']);
709	}
710
711	/**
712	 * Function to take html from ckeditor and parse back to wiki markup
713	 * Used by "switch editor" and when saving in wysiwyg_htmltowiki mode
714	 * When saving in mixed "html" mode the "unparsing" is done in JavaScript client-side
715	 *
716	 * @param $inData string	editor content
717	 * @return string			wiki markup
718	 */
719
720	function parseToWiki($inData)
721	{
722
723		global $prefs;
724
725		$parsed = $this->partialParseWysiwygToWiki($inData);	// remove cke type plugin wrappers
726
727		$parsed = html_entity_decode($parsed, ENT_QUOTES, 'UTF-8');
728		$parsed = preg_replace('/\t/', '', $parsed); // remove all tabs inserted by the CKE
729
730		$parsed = preg_replace_callback('/<pre class=["\']tiki_plugin["\']>(.*?)<\/pre>/ims', [$this, 'parseToWikiPlugin'], $parsed);	// rempve plugin wrappers
731
732		$parsed = $this->parse_html($parsed);
733		$parsed = preg_replace('/\{img\(? src=.*?img\/smiles\/icon_([\w\-]*?)\..*\}/im', '(:$1:)', $parsed);	// "unfix" smilies
734		$parsed = preg_replace('/&nbsp;/m', ' ', $parsed);												// spaces
735		$parsed = preg_replace('/!(?:\d\.)+/', '!#', $parsed); // numbered headings
736		if ($prefs['feature_use_three_colon_centertag'] == 'y') { // numbered and centerd headings
737			$parsed = preg_replace('/!:::(?:\d\.)+ *(.*):::/', '!#:::\1:::', $parsed);
738		} else {
739			$parsed = preg_replace('/!::(?:\d\.)+ *(.*)::/', '!#::\1::', $parsed);
740		}
741
742		// remove empty center tags
743		if ($prefs['feature_use_three_colon_centertag'] == 'y') { // numbered and centerd headings
744			$parsed = preg_replace('/::: *:::\n/', '', $parsed);
745		} else {
746			$parsed = preg_replace('/:: *::\n/', '', $parsed);
747		}
748
749		// Put back htmlentities as normal char
750		$parsed = htmlspecialchars_decode($parsed, ENT_QUOTES);
751		return $parsed;
752	}
753
754	function parseToWikiPlugin($matches)
755	{
756		if (count($matches) > 1) {
757			return nl2br($matches[1]);
758		}
759	}
760
761	/**
762	 * Render html to send to ckeditor, including parsing plugins for wysiwyg editing
763	 * From both wiki page source (for wysiwyg_htmltowiki) and "html" modes
764	 *
765	 * @param $inData string	page data, can be wiki or mixed html/wiki
766	 * @param bool $fromWiki	set if converting from wiki page using "switch editor"
767	 * @param bool $isHtml 		true if $inData is HTML, false if wiki
768	 * @return string			html to send to ckeditor
769	 */
770
771	function parseToWysiwyg($inData, $fromWiki = false, $isHtml = false, $options = [])
772	{
773		global $tikiroot, $prefs;
774
775		$allowImageLazyLoad = $prefs['allowImageLazyLoad'];
776		$prefs['allowImageLazyLoad'] = 'n';
777
778		// Parsing page data for wysiwyg editor
779		$inData = $this->partialParseWysiwygToWiki($inData);	// remove any wysiwyg plugins so they don't get double parsed
780		$parsed = preg_replace('/(!!*)[\+\-]/m', '$1', $inData);		// remove show/hide headings
781		$parsed = preg_replace('/&#039;/', '\'', $parsed);			// catch single quotes at html entities
782
783		if (preg_match('/^\|\|.*\|\|$/', $parsed)) {	// new tables get newlines converted to <br> then to %%%
784			$parsed = str_replace('<br>', "\n", $parsed);
785		}
786
787
788		$parsed = TikiLib::lib('parser')->parse_data(
789			$parsed,
790			array_merge([
791				'absolute_links' => true,
792				'noheaderinc' => true,
793				'suppress_icons' => true,
794				'ck_editor' => true,
795				'is_html' => ($isHtml && ! $fromWiki),
796				'process_wiki_paragraphs' => (! $isHtml || $fromWiki),
797				'process_double_brackets' => 'n'
798			], $options)
799		);
800
801		if ($fromWiki) {
802			$parsed = preg_replace('/^\s*<p>&nbsp;[\s]*<\/p>\s*/iu', '', $parsed);						// remove added empty <p>
803		}
804		$parsed = preg_replace('/<span class=\"img\">(.*?)<\/span>/im', '$1', $parsed);					// remove spans round img's
805		// Workaround Wysiwyg Image Plugin Editor in IE7 erases image on insert http://dev.tiki.org/item3615
806		$parsed2 = preg_replace('/(<span class=\"tiki_plugin\".*?plugin=\"img\".*?><\/span>)<\/p>/is', '$1<span>&nbsp;</span></p>', $parsed);
807		if ($parsed2 !== null) {
808			$parsed = $parsed2;
809		}
810		// Fix IE7 wysiwyg editor always adding absolute path
811		if (isset($_SERVER['HTTP_HOST'])) {
812			$search = '/(<a[^>]+href=\")https?\:\/\/' . preg_quote($_SERVER['HTTP_HOST'] . $tikiroot, '/') . '([^>]+_cke_saved_href)/i';
813		} else {
814			$search = '/(<a[^>]+href=\")https?\:\/\/' . preg_quote($_SERVER['SERVER_NAME'] . $tikiroot, '/') . '([^>]+_cke_saved_href)/i';
815		}
816		$parsed = preg_replace($search, '$1$2', $parsed);
817
818		if (! $isHtml) {
819			// Fix for plugin being the last item in a page making it impossible to add new lines (new text ends up inside the plugin)
820			$parsed = preg_replace('/<!-- end tiki_plugin --><\/(span|div)>(<\/p>)?$/', '<!-- end tiki_plugin --></$1>&nbsp;$2', $parsed);
821			// also if first
822			$parsed = preg_replace('/^<(div|span) class="tiki_plugin"/', '&nbsp;<$1 class="tiki_plugin"', $parsed);
823		}
824
825		$prefs['allowImageLazyLoad'] = $allowImageLazyLoad;
826
827		return $parsed;
828	}
829
830	/**
831	 * Converts wysiwyg plugins into wiki.
832	 * Also processes headings by removing surrounding <p>
833	 * Also used by ajax preview in Services_Edit_Controller
834	 *
835	 * @param string $inData	page data - mostly html but can have a bit of wiki in it
836	 * @return string			html with wiki plugins
837	 */
838
839	static function partialParseWysiwygToWiki($inData)
840	{
841		if (empty($inData)) {
842			return '';
843		}
844		// de-protect ck_protected comments
845		$ret = preg_replace('/<!--{cke_protected}{C}%3C!%2D%2D%20end%20tiki_plugin%20%2D%2D%3E-->/i', '<!-- end tiki_plugin -->', $inData);
846		if (! $ret) {
847			$ret = $inData;
848			trigger_error(tr('Parse To Wiki %0: preg_replace error #%1', 1, preg_last_error()));
849		}
850		// remove the wysiwyg plugin elements leaving the syntax only remaining
851		$ret2 = preg_replace('/<(?:div|span)[^>]*syntax="(.*)".*end tiki_plugin --><\/(?:div|span)>/Umis', "$1", $ret);
852		// preg_replace blows up here with a PREG_BACKTRACK_LIMIT_ERROR on pages with "corrupted" plugins
853		if (! $ret2) {
854			trigger_error(tr('Parse To Wiki %0: preg_replace error #%1', 2, preg_last_error()));
855		} else {
856			$ret = $ret2;
857		}
858
859		// take away the <p> that f/ck introduces around wiki heading ! to have maketoc/edit section working
860		$ret2 = preg_replace('/<p>\s*!(.*)<\/p>/Umis', "!$1\n", $ret);
861		if (! $ret2) {
862			trigger_error(tr('Parse To Wiki %0: preg_replace error #%1', 3, preg_last_error()));
863		} else {
864			$ret = $ret2;
865		}
866
867		// strip the last empty <p> tag generated somewhere (ckeditor 3.6, Tiki 10)
868		$ret2 = preg_replace('/\s*<p>\s*<\/p>\s*$/Umis', "$1\n", $ret);
869		if (! $ret2) {
870			trigger_error(tr('Parse To Wiki %0: preg_replace error #%1', 4, preg_last_error()));
871		} else {
872			$ret = $ret2;
873		}
874
875		// convert tikicomment tags back to ~tc~tiki comments~/tc~
876		$ret2 = preg_replace('/<tikicomment>(.*)<\/tikicomment>/Umis', '~tc~$1~/tc~', $ret);
877		if (! $ret2) {
878			trigger_error(tr('Parse To Wiki %0: preg_replace error #%1', 5, preg_last_error()));
879		} else {
880			$ret = $ret2;
881		}
882
883		return $ret;
884	}
885
886	// parse HTML functions
887
888	/**
889	 * \brief Parsed HTML tree walker (used by HTML sucker)
890	 *
891	 * This is initial implementation (stupid... w/o any intellegence (almost :))
892	 * It is rapidly designed version... just for test: 'can this feature be useful'.
893	 * Later it should be replaced by well designed one :) don't bash me now :)
894	 *
895	 * \param &$c array -- parsed HTML
896	 * \param &$src string -- output string
897	 * \param &$p array -- ['stack'] = closing strings stack,
898						   ['listack'] = stack of list types currently opened
899						   ['first_td'] = flag: 'is <tr> was just before this <td>'
900						   ['first_tr'] = flag: 'is <table> was just before this <tr>'
901	 */
902	function walk_and_parse(&$c, &$src, &$p, $head_url)
903	{
904		global $prefs;
905		// If no string
906		if (! $c) {
907			return;
908		}
909		$parserlib = TikiLib::lib('parser');
910
911		for ($i = 0; $i <= $c['contentpos']; $i++) {
912			$node = $c[$i];
913			// If content type 'text' output it to destination...
914
915			if ($node['type'] == 'text') {
916				if (! ctype_space($node['data'])) {
917					$add = $node['data'];
918					$noparsed = [];
919					$parserlib->plugins_remove($add, $noparsed);
920					$add = str_replace(["\r","\n"], '', $add);
921					$add = str_replace('&nbsp;', ' ', $add);
922					$add = str_replace('[', '[[', $add);			// escape square brackets to prevent accidental wiki links
923					$parserlib->plugins_replace($add, $noparsed, true);
924					$src .= $add;
925				} else {
926					$src .= str_replace(["\n", "\r"], '', $node['data']);	// keep the spaces
927				}
928			} elseif ($node['type'] == 'comment') {
929				$src .= preg_replace('/<!--/', "\n~hc~", preg_replace('/-->/', "~/hc~\n", $node['data']));
930			} elseif ($node['type'] == 'tag') {
931				if ($node['data']['type'] == 'open') {
932					// Open tag type
933
934					// deal with plugins - could be either span of div so process before the switch statement
935					if (isset($node['pars']['plugin']) && isset($node['pars']['syntax'])) {	// handling for tiki plugins
936						$src .= html_entity_decode($node['pars']['syntax']['value']);
937						$more_spans = 1;
938						$elem_type = $node['data']['name'];
939						$other_elements = 0;
940						$j = $i + 1;
941						while ($j < $c['contentpos']) {	// loop through contents of this span and discard everything
942							if ($c[$j]['data']['name'] == $elem_type && $c[$j]['data']['type'] == 'close') {
943								$more_spans--;
944								if ($more_spans === 0) {
945									break;
946								}
947//							} else if ($c[$j]['data']['name'] == 'br' && $more_spans === 1 && $other_elements === 0) {
948							} elseif ($c[$j]['data']['name'] == $elem_type && $c[$j]['data']['type'] == 'open') {
949								$more_spans++;
950							} elseif ($c[$j]['data']['type'] == 'open' && $c[$j]['data']['name'] != 'br' && $c[$j]['data']['name'] != 'img' && $c[$j]['data']['name'] != 'input') {
951								$other_elements++;
952							} elseif ($c[$j]['data']['type'] == 'close') {
953								$other_elements--;
954							}
955							$j++;
956						}
957						$i = $j;	// skip everything that was inside this span
958					}
959
960					$isPar = false; // assuming "div" when calling parseParDivTag()
961
962					switch ($node['data']['name']) {
963						// Tags we don't want at all.
964						case 'meta':
965						case 'link':
966							$node['content'] = '';
967							break;
968						case 'script':
969							$node['content'] = '';
970							if (! isset($node['pars']['src'])) {
971								$i++;
972								while ($node['type'] === 'text' || ($node['data']['name'] !== 'script' && $node['data']['type'] !== 'close' && $i <= $c['contentpos'])) {
973									$i++;    // skip contents of script tag
974								}
975							}
976							break;
977						case 'style':
978							$node['content'] = '';
979							$i++;
980							while ($node['data']['name'] !== 'style' && $node['data']['type'] !== 'close' && $i <= $c['contentpos']) {
981								$i++;    // skip contents of script tag
982							}
983							break;
984
985						// others we do want
986						case 'br':
987							if ($p['wiki_lbr']) { // "%%%" or "\n" ?
988								$src .= ' %%% ';
989							} else {
990								// close all wiki tags
991								foreach (array_reverse($p['wikistack']) as $wiki_arr) {
992									foreach (array_reverse($wiki_arr['end']) as $end) {
993										$src .= $end;
994									}
995								}
996								$src .= "\n";
997
998								// for lists, we must prepend '+' to keep the indentation
999								if ($p['listack']) {
1000									$src .= str_repeat('+', count($p['listack']));
1001								}
1002
1003								// reopen all wikitags
1004								foreach ($p['wikistack'] as $wiki_arr) {
1005									foreach ($wiki_arr['begin'] as $begin) {
1006										$src .= $begin;
1007									}
1008								}
1009							}
1010							break;
1011						case 'hr':
1012							$src .= $this->startNewLine($src) . '---';
1013							break;
1014						case 'title':
1015							$src .= "\n!";
1016							$p['stack'][] = ['tag' => 'title', 'string' => "\n"];
1017							break;
1018						case 'p':
1019							$isPar = true;
1020							if ($src && $prefs['feature_wiki_paragraph_formatting'] !== 'y') {
1021								$src .= "\n";
1022							}
1023						case 'div': // Wiki parsing creates divs for center
1024							if (isset($node['pars'])) {
1025								$this->parseParDivTag($isPar, $node['pars'], $src, $p);
1026							} elseif (! empty($p['table'])) {
1027								$src .= '%%%';
1028							} else {	// normal para or div
1029								$src .= $this->startNewLine($src);
1030								$p['stack'][] = ['tag' => $node['data']['name'], 'string' => "\n\n"];
1031							}
1032							break;
1033						case 'span':
1034							if (isset($node['pars'])) {
1035								$this->parseSpanTag($node['pars'], $src, $p);
1036							}
1037							break;
1038						case 'b':
1039							$this->processWikiTag('b', $src, $p, '__', '__', true);
1040							break;
1041						case 'i':
1042							$this->processWikiTag('i', $src, $p, '\'\'', '\'\'', true);
1043							break;
1044						case 'em':
1045							$this->processWikiTag('em', $src, $p, '\'\'', '\'\'', true);
1046							break;
1047						case 'strong':
1048							$this->processWikiTag('strong', $src, $p, '__', '__', true);
1049							break;
1050						case 'u':
1051							$this->processWikiTag('u', $src, $p, '===', '===', true);
1052							break;
1053						case 'strike':
1054							$this->processWikiTag('strike', $src, $p, '--', '--', true);
1055							break;
1056						case 'del':
1057							$this->processWikiTag('del', $src, $p, '--', '--', true);
1058							break;
1059						case 'center':
1060							if ($prefs['feature_use_three_colon_centertag'] == 'y') {
1061								$src .= ':::';
1062								$p['stack'][] = ['tag' => 'center', 'string' => ':::'];
1063							} else {
1064								$src .= '::';
1065								$p['stack'][] = ['tag' => 'center', 'string' => '::'];
1066							}
1067							break;
1068						case 'code':
1069							$src .= '-+';
1070							$p['stack'][] = ['tag' => 'code', 'string' => '+-'];
1071							break;
1072						case 'dd':
1073							$src .= ':';
1074							$p['stack'][] = ['tag' => 'dd', 'string' => "\n"];
1075							break;
1076						case 'dt':
1077							$src .= ';';
1078							$p['stack'][] = ['tag' => 'dt', 'string' => ''];
1079							break;
1080
1081						case 'h1':
1082						case 'h2':
1083						case 'h3':
1084						case 'h4':
1085						case 'h5':
1086						case 'h6':
1087							$p['wiki_lbr']++; // force wiki line break mode
1088							$hlevel = (int) $node['data']['name']{1};
1089							if (isset($node['pars']['style']['value']) && strpos($node['pars']['style']['value'], 'text-align: center;') !== false) {
1090								if ($prefs['feature_use_three_colon_centertag'] == 'y') {
1091									$src .= $this->startNewLine($src) . str_repeat('!', $hlevel) . ':::';
1092									$p['stack'][] = ['tag' => $node['data']['name'], 'string' => ":::\n"];
1093								} else {
1094									$src .= $this->startNewLine($src) . str_repeat('!', $hlevel) . '::';
1095									$p['stack'][] = ['tag' => $node['data']['name'], 'string' => "::\n"];
1096								}
1097							} else {	// normal para or div
1098								$src .= $this->startNewLine($src) . str_repeat('!', $hlevel);
1099								$p['stack'][] = ['tag' => $node['data']['name'], 'string' => "\n"];
1100							}
1101							break;
1102						case 'pre':
1103							$src .= "~pre~\n";
1104							$p['stack'][] = ['tag' => 'pre', 'string' => "~/pre~\n"];
1105							break;
1106						case 'sub':
1107							$src .= '{SUB()}';
1108							$p['stack'][] = ['tag' => 'sub', 'string' => '{SUB}'];
1109							break;
1110						case 'sup':
1111							$src .= '{SUP()}';
1112							$p['stack'][] = ['tag' => 'sup', 'string' => '{SUP}'];
1113							break;
1114						case 'tt':
1115							$src .= '{DIV(type="tt")}';
1116							$p['stack'][] = ['tag' => 'tt', 'string' => '{DIV}'];
1117							break;
1118						case 's':
1119							$src .= $this->processWikiTag('s', $src, $p, '--', '--', true);
1120							break;
1121						// Table parser
1122						case 'table':
1123							$src .= $this->startNewLine($src) . '||';
1124							$p['stack'][] = ['tag' => 'table', 'string' => '||'];
1125							$p['first_tr'] = true;
1126							$p['table'] = true;
1127							break;
1128						case 'tr':
1129							if (! $p['first_tr']) {
1130								$this->startNewLine($src);
1131							}
1132							$p['first_tr'] = false;
1133							$p['first_td'] = true;
1134							$p['wiki_lbr']++; // force wiki line break mode
1135							break;
1136						case 'td':
1137							if ($p['first_td']) {
1138								$src .= '';
1139							} else {
1140								$src .= '|';
1141							}
1142							$p['first_td'] = false;
1143							break;
1144						// Lists parser
1145						case 'ul':
1146							$p['listack'][] = '*';
1147							break;
1148						case 'ol':
1149							$p['listack'][] = '#';
1150							break;
1151						case 'li':
1152							// Generate wiki list item according to current list depth.
1153							$src .= $this->startNewLine($src) . str_repeat(end($p['listack']), count($p['listack']));
1154							break;
1155						case 'font':
1156							// If color attribute present in <font> tag
1157							if (isset($node['pars']['color']['value'])) {
1158								$src .= '~~' . $node['pars']['color']['value'] . ':';
1159								$p['stack'][] = ['tag' => 'font', 'string' => '~~'];
1160							}
1161							break;
1162						case 'img':
1163							// If src attribute present in <img> tag
1164							if (isset($node['pars']['src']['value'])) {
1165								// Note what it produce (img) not {img}! Will fix this below...
1166								if (strstr($node['pars']['src']['value'], 'http:')) {
1167									$src .= '{img src="' . $node['pars']['src']['value'] . '"}';
1168								} else {
1169									$src .= '{img src="' . $head_url . $node['pars']['src']['value'] . '"}';
1170								}
1171							}
1172							break;
1173						case 'a':
1174							if (isset($node['pars'])) {
1175								// get the link text
1176								$text = '';
1177								if ($i < count($c)) {
1178									$next_token = &$c[$i + 1];
1179									if (isset($next_token['type']) && $next_token['type'] == 'text' && isset($next_token['data'])) {
1180										$text = &$next_token['data'];
1181									}
1182								}
1183								// parse the link
1184								$this->parseLinkTag($node['pars'], $text, $src, $p);
1185							}
1186
1187							// deactivated by mauriz, will be replaced by the routine above
1188							// If href attribute present in <a> tag
1189							/*
1190							if (isset($c[$i]['pars']['href']['value'])) {
1191								if ( strstr( $c[$i]['pars']['href']['value'], 'http:' )) {
1192									$src .= '['.$c[$i]['pars']['href']['value'].'|';
1193								} else {
1194									$src .= '['.$head_url.$c[$i]['pars']['href']['value'].'|';
1195								}
1196								$p['stack'][] = array('tag' => 'a', 'string' => ']');
1197							}
1198							if ( isset($c[$i]['pars']['name']['value'])) {
1199								$src .= '{ANAME()}'.$c[$i]['pars']['name']['value'].'{ANAME}';
1200							}
1201							*/
1202
1203
1204							break;
1205					}	// end switch on tag name
1206				} else {
1207					// This is close tag type. Is that smth we r waiting for?
1208					switch ($node['data']['name']) {
1209						case 'ul':
1210							if (end($p['listack']) == '*') {
1211								array_pop($p['listack']);
1212							}
1213							if (empty($p['listack'])) {
1214								$src .= "\n";
1215							}
1216							break;
1217						case 'ol':
1218							if (end($p['listack']) == '#') {
1219								array_pop($p['listack']);
1220							}
1221							if (empty($p['listack'])) {
1222								$src .= "\n";
1223							}
1224							break;
1225						default:
1226							$e = end($p['stack']);
1227							if (isset($e['tag']) && $node['data']['name'] == $e['tag']) {
1228								$src .= $e['string'];
1229								array_pop($p['stack']);
1230							}
1231							if ($node['data']['name'] === 'table') {
1232								$p['table'] = false;
1233							}
1234							break;
1235					}
1236
1237					// update the wiki stack
1238					if (isset($e['wikitags']) && $e['wikitags']) {
1239						for ($i_wiki = 0; $i_wiki < $e['wikitags']; $i_wiki++) {
1240							array_pop($p['wikistack']);
1241						}
1242					}
1243
1244					// can we leave wiki line break mode ?
1245					switch ($node['data']['name']) {
1246						case 'a':
1247						case 'h1':
1248						case 'h2':
1249						case 'h3':
1250						case 'h4':
1251						case 'h5':
1252						case 'h6':
1253						case 'tr':
1254							$p['wiki_lbr']--;
1255							break;
1256					}
1257				}
1258			}
1259			// Recursive call on tags with content...
1260			if (! empty($node['content'])) {
1261				if (substr($src, -1) != ' ') {
1262					$src .= ' ';
1263				}
1264				$this->walk_and_parse($node['content'], $src, $p, $head_url);
1265			}
1266		}
1267		if (substr($src, -2) == "\n\n") {	// seem to always get too many line ends
1268			$src = substr($src, 0, -2);
1269		}
1270	}	// end walk_and_parse
1271
1272	function startNewLine(&$str)
1273	{
1274		if (strlen($str) && substr($str, -1) != "\n") {
1275			$str .= "\n";
1276		}
1277	}
1278
1279	/**
1280	 * wrapper around zaufi's HTML sucker code just to use the html to wiki bit
1281	 *
1282	 * @param string $inHtml -- HTML in
1283	 * @return null|string
1284	 * @throws Exception
1285	 */
1286
1287
1288	function parse_html(&$inHtml)
1289	{
1290		include(__DIR__ . '/../htmlparser/htmlparser.inc');
1291		// Read compiled (serialized) grammar
1292		$grammarfile = TIKI_PATH . '/lib/htmlparser/htmlgrammar.cmp';
1293		if (! $fp = @fopen($grammarfile, 'r')) {
1294			$smarty = TikiLib::lib('smarty');
1295			$smarty->assign('msg', tra("Can't parse HTML data - no grammar file"));
1296			$smarty->display("error.tpl");
1297			die;
1298		}
1299		$grammar = unserialize(fread($fp, filesize($grammarfile)));
1300		fclose($fp);
1301
1302		// process a few ckeditor artifacts
1303		$inHtml = str_replace('<p></p>', '', $inHtml);	// empty p tags are invisible
1304
1305		// create parser object, insert html code and parse it
1306		$htmlparser = new HtmlParser($inHtml, $grammar, '', 0);
1307		$htmlparser->Parse();
1308		// Should I try to convert HTML to wiki?
1309		$out_data = '';
1310		/*
1311		 * ['stack'] = array
1312		 * Speacial keys introduced to convert to Wiki
1313		 * - ['wikitags']     = the number of 'wikistack' entries produced by the html tag
1314		 *
1315		 * ['wikistack'] = array(), is used to save the wiki markup for the linebreak handling (1 array = 1 html tag)
1316		 * Each array entry contains the following keys:
1317		 * - ['begin']        = array() of begin markups (1 style definition = 1 array entry)
1318		 * - ['end']          = array() of end markups
1319		 *
1320		 * wiki_lbr  = true if we must use '%%%' for linebreaks instead of '\n'
1321		 */
1322		$p = ['stack' => [], 'listack' => [], 'wikistack' => [],
1323			'wiki_lbr' => 0, 'first_td' => false, 'first_tr' => false];
1324		$this->walk_and_parse($htmlparser->content, $out_data, $p, '');
1325		// Is some tags still opened? (It can be if HTML not valid, but this is not reason
1326		// to produce invalid wiki :)
1327		while (count($p['stack'])) {
1328			$e = end($p['stack']);
1329			$out_data .= $e['string'];
1330			array_pop($p['stack']);
1331		}
1332		// Unclosed lists r ignored... wiki have no special start/end lists syntax....
1333		// OK. Things remains to do:
1334		// 1) fix linked images
1335		$out_data = preg_replace(',\[(.*)\|\(img src=(.*)\)\],mU', '{img src=$2 link=$1}', $out_data);
1336		// 2) fix remains images (not in links)
1337		$out_data = preg_replace(',\(img src=(.*)\),mU', '{img src=$1}', $out_data);
1338		// 3) remove empty lines
1339		$out_data = preg_replace(",[\n]+,mU", "\n", $out_data);
1340		// 4) remove nbsp's
1341		$out_data = preg_replace(",&#160;,mU", " ", $out_data);
1342
1343		return $out_data;
1344	}	// end parse_html
1345
1346
1347	function get_new_page_attributes_from_parent_pages($page, $page_info)
1348	{
1349		$tikilib = TikiLib::lib('tiki');
1350		$wikilib = TikiLib::lib('wiki');
1351
1352		$new_page_attrs = [];
1353		$parent_pages = $wikilib->get_parent_pages($page);
1354		$parent_pages_info = [];
1355		foreach ($parent_pages as $a_parent_page_name) {
1356			$this_parent_page_info = $tikilib->get_page_info($a_parent_page_name);
1357			$parent_pages_info[] = $this_parent_page_info;
1358		}
1359		$new_page_attrs = $this->get_newpage_language_from_parent_page($page, $page_info, $parent_pages_info, $new_page_attrs);
1360		// Note: in the future, may add some methods below to guess things like
1361		//       categories, workspaces, etc...
1362
1363		return $new_page_attrs;
1364	}
1365
1366	function get_newpage_language_from_parent_page($page, $page_info, $parent_pages_info, $new_page_attrs)
1367	{
1368		if (! isset($page_info['lang'])) {
1369			$lang = null;
1370			foreach ($parent_pages_info as $this_parent_page_info) {
1371				if (isset($this_parent_page_info['lang'])) {
1372					if ($lang != null and $lang != $this_parent_page_info['lang']) {
1373						// If more than one parent pages and they have different languages
1374						// then we can't guess which  is the right one.
1375						$lang = null;
1376						break;
1377					} else {
1378						$lang = $this_parent_page_info['lang'];
1379					}
1380				}
1381			}
1382			if ($lang != null) {
1383				$new_page_attrs['lang'] = $lang;
1384			}
1385		}
1386		return $new_page_attrs;
1387	}
1388}
1389