Completed
Push — 16.1 ( 342cbb...265813 )
by Nathan
50:13 queued 33:19
created

Merge::get_app_replacements()   B

Complexity

Conditions 4
Paths 7

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 7
nop 4
dl 0
loc 29
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - Document merge print
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package api
8
 * @subpackage storage
9
 * @copyright (c) 2007-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Storage;
15
16
use EGroupware\Api;
17
use EGroupware\Stylite;
18
19
use DOMDocument;
20
use XSLTProcessor;
21
use tidy;
22
use ZipArchive;
23
24
// explicit import old, non-namespaced phpgwapi classes
25
use uiaccountsel;
26
27
/**
28
 * Document merge print
29
 *
30
 * @todo move apply_styles call into merge_string to run for each entry merged and not all together to lower memory requirements
31
 */
32
abstract class Merge
33
{
34
	/**
35
	 * Instance of the addressbook_bo class
36
	 *
37
	 * @var addressbook_bo
38
	 */
39
	var $contacts;
40
41
	/**
42
	 * Datetime format according to user preferences
43
	 *
44
	 * @var string
45
	 */
46
	var $datetime_format = 'Y-m-d H:i';
47
48
	/**
49
	 * Fields that are to be treated as datetimes, when merged into spreadsheets
50
	 */
51
	var $date_fields = array();
52
53
	/**
54
	 * Mimetype of document processed by merge
55
	 *
56
	 * @var string
57
	 */
58
	var $mimetype;
59
60
	/**
61
	 * Plugins registered by extending class to create a table with multiple rows
62
	 *
63
	 * $$table/$plugin$$ ... $$endtable$$
64
	 *
65
	 * Callback returns replacements for row $n (stringing with 0) or null if no further rows
66
	 *
67
	 * @var array $plugin => array callback($plugin,$id,$n)
68
	 */
69
	var $table_plugins = array();
70
71
	/**
72
	 * Export limit in number of entries or some non-numerical value, if no export allowed at all, empty means no limit
73
	 *
74
	 * Set by constructor to $GLOBALS[egw_info][server][export_limit]
75
	 *
76
	 * @var int|string
77
	 */
78
	public $export_limit;
79
80
81
	/**
82
	 * Configuration for HTML Tidy to clean up any HTML content that is kept
83
	 */
84
	public static $tidy_config = array(
85
		'output-xml'		=> true,	// Entity encoding
86
		'show-body-only'	=> true,
87
		'output-encoding'	=> 'utf-8',
88
		'input-encoding'	=> 'utf-8',
89
		'quote-ampersand'	=> false,	// Prevent double encoding
90
		'quote-nbsp'		=> true,	// XSLT can handle spaces easier
91
		'preserve-entities'	=> true,
92
		'wrap'			=> 0,		// Wrapping can break output
93
	);
94
95
	/**
96
	 * Parse HTML styles into target document style, if possible
97
	 *
98
	 * Apps not using html in there own data should set this with Customfields::use_html($app)
99
	 * to avoid memory and time consuming html processing.
100
	 */
101
	protected $parse_html_styles = true;
102
103
	/**
104
	 * Enable this to report memory_usage to error_log
105
	 *
106
	 * @var boolean
107
	 */
108
	public $report_memory_usage = false;
109
110
	/**
111
	 * Constructor
112
	 */
113
	function __construct()
114
	{
115
		// Common messages are in preferences
116
		Api\Translation::add_app('preferences');
117
		// All contact fields are in addressbook
118
		Api\Translation::add_app('addressbook');
119
120
		$this->contacts = new Api\Contacts();
121
122
		$this->datetime_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'].' '.
123
			($GLOBALS['egw_info']['user']['preferences']['common']['timeformat']==12 ? 'h:i a' : 'H:i');
124
125
		$this->export_limit = self::getExportLimit();
126
	}
127
128
	/**
129
	 * Hook returning options for export_limit_excepted groups
130
	 *
131
	 * @param array $config
132
	 */
133
	public static function hook_export_limit_excepted($config)
134
	{
135
		$accountsel = new uiaccountsel();
136
137
		return '<input type="hidden" value="" name="newsettings[export_limit_excepted]" />'.
138
			$accountsel->selection('newsettings[export_limit_excepted]','export_limit_excepted',$config['export_limit_excepted'],'both',4);
139
	}
140
141
	/**
142
	 * Get all replacements, must be implemented in extending class
143
	 *
144
	 * Can use eg. the following high level methods:
145
	 * - contact_replacements($contact_id,$prefix='')
146
	 * - format_datetime($time,$format=null)
147
	 *
148
	 * @param int $id id of entry
149
	 * @param string &$content=null content to create some replacements only if they are use
150
	 * @return array|boolean array with replacements or false if entry not found
151
	 */
152
	abstract protected function get_replacements($id,&$content=null);
153
154
	/**
155
	 * Return if merge-print is implemented for given mime-type (and/or extension)
156
	 *
157
	 * @param string $mimetype eg. text/plain
158
	 * @param string $extension only checked for applications/msword and .rtf
159
	 */
160
	static public function is_implemented($mimetype,$extension=null)
161
	{
162
		static $zip_available=null;
163
		if (is_null($zip_available))
164
		{
165
			$zip_available = check_load_extension('zip') &&
166
				class_exists('ZipArchive');	// some PHP has zip extension, but no ZipArchive (eg. RHEL5!)
167
		}
168
		switch ($mimetype)
169
		{
170
			case 'application/msword':
171
				if (strtolower($extension) != '.rtf') break;
172
			case 'application/rtf':
173
			case 'text/rtf':
174
				return true;	// rtf files
175
			case 'application/vnd.oasis.opendocument.text':	// oo text
176
			case 'application/vnd.oasis.opendocument.spreadsheet':	// oo spreadsheet
177
				if (!$zip_available) break;
178
				return true;	// open office write xml files
179
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms word 2007 xml format
180
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d':	// mimetypes in vfs are limited to 64 chars
181
			case 'application/vnd.ms-word.document.macroenabled.12':
182
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':	// ms excel 2007 xml format
183
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee':
184
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
185
				if (!$zip_available) break;
186
				return true;	// ms word xml format
187
			case 'application/xml':
188
				return true;	// alias for text/xml, eg. ms office 2003 word format
189
			case 'message/rfc822':
190
				return true; // ToDo: check if you are theoretical able to send mail
191
			case 'application/x-yaml':
192
				return true;	// yaml file, plain text with marginal syntax support for multiline replacements
193
			default:
194
				if (substr($mimetype,0,5) == 'text/')
195
				{
196
					return true;	// text files
197
				}
198
				break;
199
		}
200
		return false;
201
202
		// As browsers not always return correct mime types, one could use a negative list instead
203
		//return !($mimetype == Api\Vfs::DIR_MIME_TYPE || substr($mimetype,0,6) == 'image/');
204
	}
205
206
	/**
207
	 * Return replacements for a contact
208
	 *
209
	 * @param int|string|array $contact contact-array or id
210
	 * @param string $prefix ='' prefix like eg. 'user'
211
	 * @param boolean $ignore_acl =false true: no acl check
212
	 * @return array
213
	 */
214
	public function contact_replacements($contact,$prefix='',$ignore_acl=false)
215
	{
216
		if (!is_array($contact))
217
		{
218
			$contact = $this->contacts->read($contact, $ignore_acl);
219
		}
220
		if (!is_array($contact)) return array();
221
222
		$replacements = array();
223
		foreach(array_keys($this->contacts->contact_fields) as $name)
224
		{
225
			$value = $contact[$name];
226
			switch($name)
227
			{
228
				case 'created': case 'modified':
229
					if($value) $value = Api\DateTime::to($value);
230
					break;
231
				case 'bday':
232
					if ($value)
233
					{
234
						try {
235
							$value = Api\DateTime::to($value, true);
236
						}
237
						catch (\Exception $e) {
238
							unset($e);	// ignore exception caused by wrongly formatted date
239
						}
240
					}
241
					break;
242
				case 'owner': case 'creator': case 'modifier':
243
					$value = Api\Accounts::username($value);
244
					break;
245
				case 'cat_id':
246
					if ($value)
247
					{
248
						// if cat-tree is displayed, we return a full category path not just the name of the cat
249
						$use = $GLOBALS['egw_info']['server']['cat_tab'] == 'Tree' ? 'path' : 'name';
250
						$cats = array();
251
						foreach(is_array($value) ? $value : explode(',',$value) as $cat_id)
252
						{
253
							$cats[] = $GLOBALS['egw']->categories->id2name($cat_id,$use);
254
						}
255
						$value = implode(', ',$cats);
256
					}
257
					break;
258
				case 'jpegphoto':	// returning a link might make more sense then the binary photo
259 View Code Duplication
					if ($contact['photo'])
260
					{
261
						$value = ($GLOBALS['egw_info']['server']['webserver_url'][0] == '/' ?
262
							($_SERVER['HTTPS'] ? 'https://' : 'http://').$_SERVER['HTTP_HOST'] : '').
263
							$GLOBALS['egw']->link('/index.php',$contact['photo']);
264
					}
265
					break;
266
				case 'tel_prefer':
267
					if ($value && $contact[$value])
268
					{
269
						$value = $contact[$value];
270
					}
271
					break;
272
				case 'account_id':
273
					if ($value)
274
					{
275
						$replacements['$$'.($prefix ? $prefix.'/':'').'account_lid$$'] = $GLOBALS['egw']->accounts->id2name($value);
276
					}
277
					break;
278
			}
279
			if ($name != 'photo') $replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = $value;
280
		}
281
		// set custom fields, should probably go to a general method all apps can use
282
		// need to load all cfs for $ignore_acl=true
283
		foreach($ignore_acl ? Customfields::get('addressbook', true) : $this->contacts->customfields as $name => $field)
284
		{
285
			$name = '#'.$name;
286
			$replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] =
287
				// use raw data for yaml, no user-preference specific formatting
288
				$this->mimetype == 'application/x-yaml' ? (string)$contact[$name] :
289
				Customfields::format($field, (string)$contact[$name]);
290
		}
291
292
		// Add in extra cat field
293
		$cats = array();
294
		foreach(is_array($contact['cat_id']) ? $contact['cat_id'] : explode(',',$contact['cat_id']) as $cat_id)
295
		{
296
			if(!$cat_id) continue;
297
			if($GLOBALS['egw']->categories->id2name($cat_id,'main') != $cat_id)
298
			{
299
				$path = explode(' / ', $GLOBALS['egw']->categories->id2name($cat_id, 'path'));
300
				unset($path[0]); // Drop main
301
				$cats[$GLOBALS['egw']->categories->id2name($cat_id,'main')][] = implode(' / ', $path);
302
			} elseif($cat_id) {
303
				$cats[$cat_id] = array();
304
			}
305
		}
306
		foreach($cats as $main => $cat) {
307
			$replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] .= $GLOBALS['egw']->categories->id2name($main,'name')
308
				. (count($cat) > 0 ? ': ' : '') . implode(', ', $cats[$main]) . "\n";
309
		}
310
		return $replacements;
311
	}
312
313
	/**
314
	 * Get links for the given record
315
	 *
316
	 * Uses egw_link system to get link titles
317
	 *
318
	 * @param app Name of current app
319
	 * @param id ID of current entry
320
	 * @param only_app Restrict links to only given application
321
	 * @param exclude Exclude links to these applications
322
	 * @param style String One of:
323
	 * 	'title' - plain text, just the title of the link
324
	 * 	'link' - URL to the entry
325
	 * 	'href' - HREF tag wrapped around the title
326
	 */
327
	protected function get_links($app, $id, $only_app='', $exclude = array(), $style = 'title')
328
	{
329
		$links = Api\Link::get_links($app, $id, $only_app);
330
		$link_titles = array();
331
		foreach($links as $link_info)
332
		{
333
			// Using only_app only returns the ID
334
			if(!is_array($link_info) && $only_app && $only_app[0] !== '!')
335
			{
336
				$link_info = array(
337
					'app'	=> $only_app,
338
					'id'	=> $link_info
339
				);
340
			}
341
			if($exclude && in_array($link_info['id'], $exclude)) continue;
342
343
			$title = Api\Link::title($link_info['app'], $link_info['id']);
344
			if(class_exists('EGroupware\Stylite\Vfs\Links\StreamWrapper') && $link_info['app'] != Api\Link::VFS_APPNAME)
345
			{
346
				$title = Stylite\Vfs\Links\StreamWrapper::entry2name($link_info['app'], $link_info['id'], $title);
347
			}
348
			if($style == 'href' || $style == 'link')
349
			{
350
				$link = Api\Link::view($link_info['app'], $link_info['id'], $link_info);
351 View Code Duplication
				if($link_info['app'] != Api\Link::VFS_APPNAME)
352
				{
353
					// Set app to false so we always get an external link
354
					$link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false));
355
				}
356
				else
357
				{
358
					$link = Api\Framework::link($link, array());
359
				}
360
				// Prepend site
361 View Code Duplication
				if ($link{0} == '/')
362
				{
363
					$link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://').
364
						($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link;
365
				}
366
				$title = $style == 'href' ? Api\Html::a_href(Api\Html::htmlspecialchars($title), $link) : $link;
367
			}
368
			$link_titles[] = $title;
369
		}
370
		return implode("\n",$link_titles);
371
	}
372
373
	/**
374
	 * Get all link placeholders
375
	 *
376
	 * Calls get_links() repeatedly to get all the combinations for the content.
377
	 *
378
	 * @param $app String appname
379
	 * @param $id String ID of record
380
	 * @param $prefix
381
	 * @param $content String document content
382
	 */
383
	protected function get_all_links($app, $id, $prefix, &$content)
384
	{
385
		$array = array();
386
		$pattern = '@\$(links_attachments|links|attachments|link)\/?(title|href|link)?\/?([a-z]*)\$@';
387
		static $link_cache=null;
388
		$matches = null;
389
		if(preg_match_all($pattern, $content, $matches))
390
		{
391
			foreach($matches[0] as $i => $placeholder)
392
			{
393
				$placeholder = substr($placeholder, 1, -1);
394
				if($link_cache[$id][$placeholder])
395
				{
396
					$array[$placeholder] = $link_cache[$id][$placeholder];
397
					continue;
398
				}
399
				switch($matches[1][$i])
400
				{
401
					case 'link':
402
						// Link to current record
403
						$title = Api\Link::title($app, $id);
404
						if(class_exists('EGroupware\Stylite\Vfs\Links\StreamWrapper') && $app != Api\Link::VFS_APPNAME)
405
						{
406
							$title = Stylite\Vfs\Links\StreamWrapper::entry2name($app, $id, $title);
407
						}
408
409
						$link = Api\Link::view($app, $id);
410 View Code Duplication
						if($app != Api\Link::VFS_APPNAME)
411
						{
412
							// Set app to false so we always get an external link
413
							$link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false));
414
						}
415
						else
416
						{
417
							$link = Api\Framework::link($link, array());
418
						}
419
						// Prepend site
420 View Code Duplication
						if ($link{0} == '/')
421
						{
422
							$link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://').
423
								($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link;
424
						}
425
						$array[($prefix?$prefix.'/':'').$placeholder] = Api\Html::a_href(Api\Html::htmlspecialchars($title), $link);
426
						break;
427
					case 'links':
428
						$link_app = $matches[3][$i] ? $matches[3][$i] :  '!'.Api\Link::VFS_APPNAME;
429
						$array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, $link_app, array(),$matches[2][$i]);
430
						break;
431
					case 'attachments':
432
						$array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, Api\Link::VFS_APPNAME,array(),$matches[2][$i]);
433
						break;
434
					default:
435
						$array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, $matches[3][$i], array(), $matches[2][$i]);
436
						break;
437
				}
438
				$link_cache[$id][$placeholder] = $array[$placeholder];
439
			}
440
		}
441
		// Need to set each app, to make sure placeholders are removed
442
		foreach(array_keys($GLOBALS['egw_info']['user']['apps']) as $_app)
443
		{
444
			$array[($prefix?$prefix.'/':'')."links/$app"] = $this->get_links($app,$id,$_app);
445
		}
446
		return $array;
447
	}
448
449
	/**
450
	 * Format a datetime
451
	 *
452
	 * @param int|string|DateTime $time unix timestamp or Y-m-d H:i:s string (in user time!)
453
	 * @param string $format =null format string, default $this->datetime_format
454
	 * @deprecated use Api\DateTime::to($time='now',$format='')
455
	 * @return string
456
	 */
457
	protected function format_datetime($time,$format=null)
458
	{
459
		trigger_error(__METHOD__ . ' is deprecated, use Api\DateTime::to($time, $format)', E_USER_DEPRECATED);
460
		if (is_null($format)) $format = $this->datetime_format;
461
462
		return Api\DateTime::to($time,$format);
463
	}
464
465
	/**
466
	 * Checks if current user is excepted from the export-limit:
467
	 * a) access to admin application
468
	 * b) he or one of his memberships is named in export_limit_excepted config var
469
	 *
470
	 * @return boolean
471
	 */
472
	public static function is_export_limit_excepted()
473
	{
474
		static $is_excepted=null;
475
476
		if (is_null($is_excepted))
477
		{
478
			$is_excepted = isset($GLOBALS['egw_info']['user']['apps']['admin']);
479
480
			// check export-limit and fail if user tries to export more entries then allowed
481
			if (!$is_excepted && (is_array($export_limit_excepted = $GLOBALS['egw_info']['server']['export_limit_excepted']) ||
482
				is_array($export_limit_excepted = unserialize($export_limit_excepted))))
483
			{
484
				$id_and_memberships = $GLOBALS['egw']->accounts->memberships($GLOBALS['egw_info']['user']['account_id'],true);
485
				$id_and_memberships[] = $GLOBALS['egw_info']['user']['account_id'];
486
				$is_excepted = (bool) array_intersect($id_and_memberships, $export_limit_excepted);
487
			}
488
		}
489
		return $is_excepted;
490
	}
491
492
	/**
493
	 * Checks if there is an exportlimit set, and returns
494
	 *
495
	 * @param string $app ='common' checks and validates app_limit, if not set returns the global limit
496
	 * @return mixed - no if no export is allowed, false if there is no restriction and int as there is a valid restriction
497
	 *		you may have to cast the returned value to int, if you want to use it as number
498
	 */
499
	public static function getExportLimit($app='common')
500
	{
501
		static $exportLimitStore=array();
502
		if (empty($app)) $app='common';
503
		//error_log(__METHOD__.__LINE__.' called with app:'.$app);
504
		if (!array_key_exists($app,$exportLimitStore))
505
		{
506
			//error_log(__METHOD__.__LINE__.' -> '.$app_limit.' '.function_backtrace());
507
			$exportLimitStore[$app] = $GLOBALS['egw_info']['server']['export_limit'];
508
			if ($app !='common')
509
			{
510
				$app_limit = Api\Hooks::single('export_limit',$app);
511
				if ($app_limit) $exportLimitStore[$app] = $app_limit;
512
			}
513
			//error_log(__METHOD__.__LINE__.' building cache for app:'.$app.' -> '.$exportLimitStore[$app]);
514
			if (empty($exportLimitStore[$app]))
515
			{
516
				$exportLimitStore[$app] = false;
517
				return false;
518
			}
519
520
			if (is_numeric($exportLimitStore[$app]))
521
			{
522
				$exportLimitStore[$app] = (int)$exportLimitStore[$app];
523
			}
524
			else
525
			{
526
				$exportLimitStore[$app] = 'no';
527
			}
528
			//error_log(__METHOD__.__LINE__.' -> '.$exportLimit);
529
		}
530
		//error_log(__METHOD__.__LINE__.' app:'.$app.' -> '.$exportLimitStore[$app]);
531
		return $exportLimitStore[$app];
532
	}
533
534
	/**
535
	 * hasExportLimit
536
	 * checks wether there is an exportlimit set, and returns true or false
537
	 * @param mixed $app_limit app_limit, if not set checks the global limit
538
	 * @param string $checkas [AND|ISALLOWED], AND default; if set to ISALLOWED it is checked if Export is allowed
539
	 *
540
	 * @return bool - true if no export is allowed or a limit is set, false if there is no restriction
541
	 */
542
	public static function hasExportLimit($app_limit,$checkas='AND')
543
	{
544
		if (strtoupper($checkas) == 'ISALLOWED') return (empty($app_limit) || ($app_limit !='no' && $app_limit > 0) );
545
		if (empty($app_limit)) return false;
546
		if ($app_limit == 'no') return true;
547
		if ($app_limit > 0) return true;
548
	}
549
550
	/**
551
	 * Merges a given document with contact data
552
	 *
553
	 * @param string $document path/url of document
554
	 * @param array $ids array with contact id(s)
555
	 * @param string &$err error-message on error
556
	 * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf
557
	 * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders
558
	 * @return string|boolean merged document or false on error
559
	 */
560
	public function &merge($document,$ids,&$err,$mimetype,array $fix=null)
561
	{
562
		if (!($content = file_get_contents($document)))
563
		{
564
			$err = lang("Document '%1' does not exist or is not readable for you!",$document);
565
			return false;
566
		}
567
568
		if (self::hasExportLimit($this->export_limit) && !self::is_export_limit_excepted() && count($ids) > (int)$this->export_limit)
569
		{
570
			$err = lang('No rights to export more than %1 entries!',(int)$this->export_limit);
571
			return false;
572
		}
573
574
		// fix application/msword mimetype for rtf files
575
		if ($mimetype == 'application/msword' && strtolower(substr($document,-4)) == '.rtf')
576
		{
577
			$mimetype = 'application/rtf';
578
		}
579
580
		try {
581
			$content = $this->merge_string($content,$ids,$err,$mimetype,$fix);
582
		} catch (\Exception $e) {
583
			_egw_log_exception($e);
584
			$err = $e->getMessage();
585
			return false;
586
		}
587
		return $content;
588
	}
589
590
	protected function apply_styles (&$content, $mimetype, $mso_application_progid=null)
591
	{
592 View Code Duplication
		if (!isset($mso_application_progid))
593
		{
594
			$matches = null;
595
			$mso_application_progid = $mimetype == 'application/xml' &&
596
				preg_match('/'.preg_quote('<?mso-application progid="').'([^"]+)'.preg_quote('"?>').'/',substr($content,0,200),$matches) ?
597
					$matches[1] : '';
598
		}
599
		// Tags we can replace with the target document's version
600
		$replace_tags = array();
601
		switch($mimetype.$mso_application_progid)
602
		{
603
			case 'application/vnd.oasis.opendocument.text':		// open office
604
			case 'application/vnd.oasis.opendocument.spreadsheet':
605
				// It seems easier to split the parent tags here
606
				$replace_tags = array(
607
					'/<(ol|ul|table)( [^>]*)?>/' => '</text:p><$1$2>',
608
					'/<\/(ol|ul|table)>/' => '</$1><text:p>',
609
					//'/<(li)(.*?)>(.*?)<\/\1>/' => '<$1 $2>$3</$1>',
610
				);
611
				$content = preg_replace(array_keys($replace_tags),array_values($replace_tags),$content);
612
613
				$doc = new DOMDocument();
614
				$xslt = new XSLTProcessor();
615
				$doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/openoffice.xslt');
616
				$xslt->importStyleSheet($doc);
617
618
//echo $content;die();
619
				break;
620
			case 'application/xmlWord.Document':	// Word 2003*/
621
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
622
			case 'application/vnd.ms-word.document.macroenabled.12':
623
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
624
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
625
				// It seems easier to split the parent tags here
626
				$replace_tags = array(
627
					// Tables, lists don't go inside <w:p>
628
					'/<(ol|ul|table)( [^>]*)?>/' => '</w:t></w:r></w:p><$1$2>',
629
					'/<\/(ol|ul|table)>/' => '</$1><w:p><w:r><w:t>',
630
					// Fix for things other than text (newlines) inside table row
631
					'/<(td)( [^>]*)?>((?!<w:t>))(.*?)<\/td>[\s]*?/' => '<$1$2><w:t>$4</w:t></td>',
632
					// Remove extra whitespace
633
					'/<li([^>]*?)>[^:print:]*?(.*?)<\/li>/' => '<li$1>$2</li>', // This doesn't get it all
634
					'/<w:t>[\s]+(.*?)<\/w:t>/' => '<w:t>$1</w:t>',
635
					// Remove spans with no attributes, linebreaks inside them cause problems
636
					'/<span>(.*?)<\/span>/' => '$1'
637
				);
638
				$content = preg_replace(array_keys($replace_tags),array_values($replace_tags),$content);
639
640
				/*
641
				In the case where you have something like <span><span></w:t><w:br/><w:t></span></span> (invalid - mismatched tags),
642
				it takes multiple runs to get rid of both spans.  So, loop.
643
				OO.o files have not yet been shown to have this problem.
644
				*/
645
				$count = $i = 0;
646
				do
647
				{
648
					$content = preg_replace('/<span>(.*?)<\/span>/','$1',$content, -1, $count);
649
					$i++;
650
				} while($count > 0 && $i < 10);
651
652
				$doc = new DOMDocument();
653
				$xslt = new XSLTProcessor();
654
				$xslt_file = $mimetype == 'application/xml' ? 'wordml.xslt' : 'msoffice.xslt';
655
				$doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/'.$xslt_file);
656
				$xslt->importStyleSheet($doc);
657
				break;
658
		}
659
660
		// XSLT transform known tags
661
		if($xslt)
662
		{
663
			// does NOT work with php 5.2.6: Catchable fatal error: argument 1 to transformToXml() must be of type DOMDocument
664
			//$element = new SimpleXMLelement($content);
665
			$element = new DOMDocument('1.0', 'utf-8');
666
			$result = $element->loadXML($content);
667
			if(!$result)
668
			{
669
				throw new Api\Exception('Unable to parse merged document for styles.  Check warnings in log for details.');
670
			}
671
			$content = $xslt->transformToXml($element);
672
673
			// Word 2003 needs two declarations, add extra declaration back in
674
			if($mimetype == 'application/xml' && $mso_application_progid == 'Word.Document' && strpos($content, '<?xml') !== 0) {
675
				$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'.$content;
676
			}
677
			// Validate
678
			/*
679
			$doc = new DOMDocument();
680
			$doc->loadXML($content);
681
			$doc->schemaValidate(*Schema (xsd) file*);
682
			*/
683
		}
684
	}
685
686
	/**
687
	 * Merges a given document with contact data
688
	 *
689
	 * @param string $_content
690
	 * @param array $ids array with contact id(s)
691
	 * @param string &$err error-message on error
692
	 * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf
693
	 * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders
694
	 * @param string $charset =null charset to override default set by mimetype or export charset
695
	 * @return string|boolean merged document or false on error
696
	 */
697
	public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null)
698
	{
699
		$matches = null;
700 View Code Duplication
		if ($mimetype == 'application/xml' &&
701
			preg_match('/'.preg_quote('<?mso-application progid="').'([^"]+)'.preg_quote('"?>').'/',substr($_content,0,200),$matches))
702
		{
703
			$mso_application_progid = $matches[1];
704
		}
705
		else
706
		{
707
			$mso_application_progid = '';
708
		}
709
		// alternative syntax using double curly brackets (eg. {{cat_id}} instead $$cat_id$$),
710
		// agressivly removing all xml-tags eg. Word adds within placeholders
711
		$content = preg_replace_callback('/{{[^}]+}}/i',create_function('$p','return \'$$\'.strip_tags(substr($p[0],2,-2)).\'$$\';'),$_content);
712
713
		// Handle escaped placeholder markers in RTF, they won't match when escaped
714
		if($mimetype == 'application/rtf')
715
		{
716
			$content = preg_replace('/\\\{\\\{([^\\}]+)\\\}\\\}/i','$$\1$$',$content);
717
		}
718
719
		// make currently processed mimetype available to class methods;
720
		$this->mimetype = $mimetype;
721
722
		// fix garbled placeholders
723
		if ($fix && is_array($fix))
724
		{
725
			$content = preg_replace(array_keys($fix),array_values($fix),$content);
726
			//die("<pre>".htmlspecialchars($content)."</pre>\n");
727
		}
728
		list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
729
		if ($mimetype == 'text/plain' && count($ids) > 1)
730
		{
731
			// textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag
732
			// header and footer should not hold any $$ tags; if we find $$ tags with the header, we assume it is the pagerepeatcontent
733
			$nohead = false;
734
			if (stripos($contentstart,'$$') !== false) $nohead = true;
735
			if ($nohead)
736
			{
737
				$contentend = $contentrepeat;
738
				$contentrepeat = $contentstart;
739
				$contentstart = '';
740
			}
741
742
		}
743
		if ($mimetype == 'application/vnd.oasis.opendocument.text' && count($ids) > 1)
744
		{
745
			if(strpos($content, '$$pagerepeat') === false)
746
			{
747
				//for odt files we have to split the content and add a style for page break to  the style area
748
				list($contentstart,$contentrepeat,$contentend) = preg_split('/office:body>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
0 ignored issues
show
Unused Code introduced by
The assignment to $contentend is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
749
				$contentstart = substr($contentstart,0,strlen($contentstart)-1);  //remove "<"
750
				$contentrepeat = substr($contentrepeat,0,strlen($contentrepeat)-2);  //remove "</";
751
				// need to add page-break style to the style list
752
				list($stylestart,$stylerepeat,$styleend) = preg_split('/<\/office:automatic-styles>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document style sheets
0 ignored issues
show
Unused Code introduced by
The assignment to $stylerepeat is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $styleend is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
753
				$contentstart = $stylestart.'<style:style style:name="P200" style:family="paragraph" style:parent-style-name="Standard"><style:paragraph-properties fo:break-before="page"/></style:style></office:automatic-styles>';
754
				$contentstart .= '<office:body>';
755
				$contentend = '</office:body></office:document-content>';
756
			}
757
			else
758
			{
759
				// Template specifies where to repeat
760
				list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get different parts of document, seperated by pagerepeat
761
			}
762
		}
763
		if (in_array($mimetype, array('application/vnd.ms-word.document.macroenabled.12', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) && count($ids) > 1)
764
		{
765
			//for Word 2007 XML files we have to split the content and add a style for page break to  the style area
766
			list($contentstart,$contentrepeat,$contentend) = preg_split('/w:body>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
0 ignored issues
show
Unused Code introduced by
The assignment to $contentend is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
767
			$contentstart = substr($contentstart,0,strlen($contentstart)-1);  //remove "</"
768
			$contentrepeat = substr($contentrepeat,0,strlen($contentrepeat)-2);  //remove "</";
769
			$contentstart .= '<w:body>';
770
			$contentend = '</w:body></w:document>';
771
		}
772
		list($Labelstart,$Labelrepeat,$Labeltend) = preg_split('/\$\$label\$\$/',$contentrepeat,-1, PREG_SPLIT_NO_EMPTY);  //get the Lable content
773
		preg_match_all('/\$\$labelplacement\$\$/',$contentrepeat,$countlables, PREG_SPLIT_NO_EMPTY);
774
		$countlables = count($countlables[0]);
775
		preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1);
776
		if ($countlables > 1) $lableprint = true;
777
		if (count($ids) > 1 && !$contentrepeat)
778
		{
779
			$err = lang('for more than one contact in a document use the tag pagerepeat!');
780
			return false;
781
		}
782
		if ($this->report_memory_usage) error_log(__METHOD__."(count(ids)=".count($ids).") strlen(contentrepeat)=".strlen($contentrepeat).', strlen(labelrepeat)='.strlen($Labelrepeat));
783
784
		if ($contentrepeat)
785
		{
786
			$content_stream = fopen('php://temp','r+');
787
			fwrite($content_stream, $contentstart);
788
			$joiner = '';
789
			switch($mimetype)
790
			{
791
				case 'application/rtf':
792
				case 'text/rtf':
793
					$joiner = '\\par \\page\\pard\\plain';
794
					break;
795
				case 'application/vnd.oasis.opendocument.text':
796
				case 'application/vnd.oasis.opendocument.spreadsheet':
797
				case 'application/xml':
798
				case 'text/html':
799
					$joiner = '';
800
					break;
801
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
802
				case 'application/vnd.ms-word.document.macroenabled.12':
803
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
804
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
805
					$joiner = '<w:br w:type="page" />';
806
					break;
807
				case 'text/plain':
808
					$joiner = "\r\n";
809
					break;
810
				default:
811
					$err = lang('%1 not implemented for %2!','$$pagerepeat$$',$mimetype);
812
					return false;
813
			}
814
		}
815
		foreach ((array)$ids as $n => $id)
816
		{
817
			if ($contentrepeat) $content = $contentrepeat;   //content to repeat
818
			if ($lableprint) $content = $Labelrepeat;
819
820
			// generate replacements; if exeption is thrown, catch it set error message and return false
821
			try
822
			{
823
				if(!($replacements = $this->get_replacements($id,$content)))
824
				{
825
					$err = lang('Entry not found!');
826
					return false;
827
				}
828
			}
829
			catch (Api\Exception\WrongUserinput $e)
830
			{
831
				// if this returns with an exeption, something failed big time
832
				$err = $e->getMessage();
833
				return false;
834
			}
835
			if ($this->report_memory_usage) error_log(__METHOD__."() $n: $id ".Api\Vfs::hsize(memory_get_usage(true)));
836
			// some general replacements: current user, date and time
837
			if (strpos($content,'$$user/') !== null && ($user = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id')))
838
			{
839
				$replacements += $this->contact_replacements($user,'user');
840
				$replacements['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'account_primary_group'));
841
			}
842
			$replacements['$$date$$'] = Api\DateTime::to('now',true);
843
			$replacements['$$datetime$$'] = Api\DateTime::to('now');
844
			$replacements['$$time$$'] = Api\DateTime::to('now',false);
845
846
			// does our extending class registered table-plugins AND document contains table tags
847
			if ($this->table_plugins && preg_match_all('/\\$\\$table\\/([A-Za-z0-9_]+)\\$\\$(.*?)\\$\\$endtable\\$\\$/s',$content,$matches,PREG_SET_ORDER))
848
			{
849
				// process each table
850
				foreach($matches as $match)
851
				{
852
					$plugin   = $match[1];	// plugin name
853
					$callback = $this->table_plugins[$plugin];
854
					$repeat   = $match[2];	// line to repeat
855
					$repeats = '';
856
					if (isset($callback))
857
					{
858
						for($n = 0; ($row_replacements = $this->$callback($plugin,$id,$n,$repeat)); ++$n)
859
						{
860
							$_repeat = $this->process_commands($repeat, $row_replacements);
861
							$repeats .= $this->replace($_repeat,$row_replacements,$mimetype,$mso_application_progid);
862
						}
863
					}
864
					$content = str_replace($match[0],$repeats,$content);
865
				}
866
			}
867
			$content = $this->process_commands($this->replace($content,$replacements,$mimetype,$mso_application_progid,$charset), $replacements);
868
869
			// remove not existing replacements (eg. from calendar array)
870
			if (strpos($content,'$$') !== null)
871
			{
872
				$content = preg_replace('/\$\$[a-z0-9_\/]+\$\$/i','',$content);
873
			}
874
			if ($contentrepeat)
875
			{
876
				fwrite($content_stream, ($n == 0 ? '' : $joiner) . $content);
877
			}
878
			if($lableprint)
879
			{
880
				$contentrep[is_array($id) ? implode(':',$id) : $id] = $content;
881
			}
882
		}
883
		if ($Labelrepeat)
884
		{
885
			$countpage=0;
886
			$count=0;
887
			$contentrepeatpages[$countpage] = $Labelstart.$Labeltend;
888
889
			foreach ($contentrep as $Label)
890
			{
891
				$contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/',$Label,$contentrepeatpages[$countpage],1);
892
				$count=$count+1;
893
				if (($count % $countlables) == 0 && count($contentrep)>$count)  //new page
894
				{
895
					$countpage = $countpage+1;
896
					$contentrepeatpages[$countpage] = $Labelstart.$Labeltend;
897
				}
898
			}
899
			$contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/','',$contentrepeatpages[$countpage],-1);  //clean empty fields
900
901
			switch($mimetype)
902
			{
903
				case 'application/rtf':
904
				case 'text/rtf':
905
					return $contentstart.implode('\\par \\page\\pard\\plain',$contentrepeatpages).$contentend;
906
				case 'application/vnd.oasis.opendocument.text':
907
					return $contentstart.implode('<text:line-break />',$contentrepeatpages).$contentend;
908
				case 'application/vnd.oasis.opendocument.spreadsheet':
909
					return $contentstart.implode('</text:p><text:p>',$contentrepeatpages).$contentend;
910
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
911
				case 'application/vnd.ms-word.document.macroenabled.12':
912
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
913
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
914
					return $contentstart.implode('<w:br w:type="page" />',$contentrepeatpages).$contentend;
915
				case 'text/plain':
916
					return $contentstart.implode("\r\n",$contentrep).$contentend;
917
			}
918
			$err = lang('%1 not implemented for %2!','$$labelplacement$$',$mimetype);
919
			return false;
920
		}
921
922
		if ($contentrepeat)
923
		{
924
			fwrite($content_stream, $contentend);
925
			rewind($content_stream);
926
			return stream_get_contents($content_stream);
927
		}
928
		if ($this->report_memory_usage) error_log(__METHOD__."() returning ".Api\Vfs::hsize(memory_get_peak_usage(true)));
929
930
		return $content;
931
	}
932
933
	/**
934
	 * Replace placeholders in $content of $mimetype with $replacements
935
	 *
936
	 * @param string $content
937
	 * @param array $replacements name => replacement pairs
938
	 * @param string $mimetype mimetype of content
939
	 * @param string $mso_application_progid ='' MS Office 2003: 'Excel.Sheet' or 'Word.Document'
940
	 * @param string $charset =null charset to override default set by mimetype or export charset
941
	 * @return string
942
	 */
943
	protected function replace($content,array $replacements,$mimetype,$mso_application_progid='',$charset=null)
944
	{
945
		switch($mimetype)
946
		{
947
			case 'application/vnd.oasis.opendocument.text':		// open office
948
			case 'application/vnd.oasis.opendocument.spreadsheet':
949
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
950
			case 'application/vnd.ms-word.document.macroenabled.12':
951
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
952
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
953
			case 'application/xml':
954
			case 'text/xml':
955
				$is_xml = true;
956
				$charset = 'utf-8';	// xml files --> always use utf-8
957
				break;
958
959
			case 'text/html':
960
				$is_xml = true;
961
				$matches = null;
962
				if (preg_match('/<meta http-equiv="content-type".*charset=([^;"]+)/i',$content,$matches))
963
				{
964
					$charset = $matches[1];
965
				}
966
				elseif (empty($charset))
967
				{
968
					$charset = 'utf-8';
969
				}
970
				break;
971
972
			default:	// div. text files --> use our export-charset, defined in addressbook prefs
973
				if (empty($charset)) $charset = $this->contacts->prefs['csv_charset'];
974
				break;
975
		}
976
		//error_log(__METHOD__."('$document', ... ,$mimetype) --> $charset (egw=".Api\Translation::charset().', export='.$this->contacts->prefs['csv_charset'].')');
977
978
		// do we need to convert charset
979
		if ($charset && $charset != Api\Translation::charset())
980
		{
981
			$replacements = Api\Translation::convert($replacements,Api\Translation::charset(),$charset);
982
		}
983
984
		// Date only placeholders for timestamps
985
		if(is_array($this->date_fields))
986
		{
987
			foreach($this->date_fields as $field)
988
			{
989
				if(($value = $replacements['$$'.$field.'$$']))
990
				{
991
					$time = Api\DateTime::createFromFormat('+'.Api\DateTime::$user_dateformat.' '.Api\DateTime::$user_timeformat.'*', $value);
992
					$replacements['$$'.$field.'/date$$'] = $time ? $time->format(Api\DateTime::$user_dateformat)  : '';
993
				}
994
			}
995
		}
996
		if ($is_xml)	// zip'ed xml document (eg. OO)
997
		{
998
			// Numeric fields
999
			$names = array();
1000
1001
			// Tags we can replace with the target document's version
1002
			$replace_tags = array();
1003
			// only keep tags, if we have xsl extension available
1004
			if (class_exists(XSLTProcessor) && class_exists(DOMDocument) && $this->parse_html_styles)
1005
			{
1006
				switch($mimetype.$mso_application_progid)
1007
				{
1008 View Code Duplication
					case 'text/html':
1009
						$replace_tags = array(
1010
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1011
							'<table>','<tr>','<td>','<a>','<style>',
1012
						);
1013
						break;
1014
					case 'application/vnd.oasis.opendocument.text':		// open office
1015 View Code Duplication
					case 'application/vnd.oasis.opendocument.spreadsheet':
1016
						$replace_tags = array(
1017
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1018
							'<table>','<tr>','<td>','<a>',
1019
						);
1020
						break;
1021
					case 'application/xmlWord.Document':	// Word 2003*/
1022
					case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1023
					case 'application/vnd.ms-word.document.macroenabled.12':
1024
					case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1025
					case 'application/vnd.ms-excel.sheet.macroenabled.12':
1026
						$replace_tags = array(
1027
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1028
							'<table>','<tr>','<td>',
1029
						);
1030
						break;
1031
				}
1032
			}
1033
			// clean replacements from array values and html or html-entities, which mess up xml
1034
			foreach($replacements as $name => &$value)
0 ignored issues
show
Bug introduced by
The expression $replacements of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1035
			{
1036
				// set unresolved array values to empty string
1037
				if(is_array($value))
1038
				{
1039
					$value = '';
1040
					continue;
1041
				}
1042
				// decode html entities back to utf-8
1043
1044
				if (is_string($value) && (strpos($value,'&') !== false) && $this->parse_html_styles)
1045
				{
1046
					$value = html_entity_decode($value,ENT_QUOTES,$charset);
0 ignored issues
show
Unused Code introduced by
The call to html_entity_decode() has too many arguments starting with $charset.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1047
1048
					// remove all non-decodable entities
1049
					if (strpos($value,'&') !== false)
1050
					{
1051
						$value = preg_replace('/&[^; ]+;/','',$value);
1052
					}
1053
				}
1054
				if(!$this->parse_html_styles || (
1055
					strpos($value, "\n") !== FALSE && strpos($value,'<br') === FALSE && strpos($value, '<span') === FALSE && strpos($value, '<p') === FALSE
1056
				))
1057
				{
1058
					// Encode special chars so they don't break the file
1059
					$value = htmlspecialchars($value,ENT_NOQUOTES);
1060
				}
1061
				else if (is_string($value) && (strpos($value,'<') !== false))
1062
				{
1063
					// Clean HTML, if it's being kept
1064
					if($replace_tags && extension_loaded('tidy')) {
1065
						$tidy = new tidy();
1066
						$cleaned = $tidy->repairString($value, self::$tidy_config);
1067
						// Found errors. Strip it all so there's some output
1068
						if($tidy->getStatus() == 2)
1069
						{
1070
							error_log($tidy->errorBuffer);
0 ignored issues
show
Bug introduced by
The property errorBuffer does not seem to exist in tidy.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1071
							$value = strip_tags($value);
1072
						}
1073
						else
1074
						{
1075
							$value = $cleaned;
1076
						}
1077
					}
1078
					// replace </p> and <br /> with CRLF (remove <p> and CRLF)
1079
					$value = strip_tags(str_replace(array("\r","\n",'<p>','</p>','<div>','</div>','<br />'),
1080
						array('','','',"\r\n",'',"\r\n","\r\n"), $value),
1081
						implode('', $replace_tags));
1082
1083
					// Change <tag>...\r\n</tag> to <tag>...</tag>\r\n or simplistic line break below will mangle it
1084
					// Loop to catch things like <b><span>Break:\r\n</span></b>
1085
					if($mso_application_progid)
1086
					{
1087
						$count = $i = 0;
1088
						do
1089
						{
1090
							$value = preg_replace('/<(b|strong|i|em|u|span)\b([^>]*?)>(.*?)'."\r\n".'<\/\1>/u', '<$1$2>$3</$1>'."\r\n",$value,-1,$count);
1091
							$i++;
1092
						} while($count > 0 && $i < 10); // Limit of 10 chosen arbitrarily just in case
1093
					}
1094
				}
1095
				// replace all control chars (C0+C1) but CR (\015), LF (\012) and TAB (\011) (eg. vertical tabulators) with space
1096
				// as they are not allowed in xml
1097
				$value = preg_replace('/[\000-\010\013\014\016-\037\177-\237]/u',' ',$value);
1098
				if(is_numeric($value) && $name != '$$user/account_id$$') // account_id causes problems with the preg_replace below
1099
				{
1100
					$names[] = preg_quote($name,'/');
1101
				}
1102
			}
1103
1104
			// Look for numbers, set their value if needed
1105 View Code Duplication
			if($this->numeric_fields || count($names))
1106
			{
1107
				foreach((array)$this->numeric_fields as $fieldname) {
1108
					$names[] = preg_quote($fieldname,'/');
1109
				}
1110
				$this->format_spreadsheet_numbers($content, $names, $mimetype.$mso_application_progid);
1111
			}
1112
1113
			// Look for dates, set their value if needed
1114 View Code Duplication
			if($this->date_fields || count($names))
1115
			{
1116
				$names = array();
1117
				foreach((array)$this->date_fields as $fieldname) {
1118
					$names[] = $fieldname;
1119
				}
1120
				$this->format_spreadsheet_dates($content, $names, $replacements, $mimetype.$mso_application_progid);
1121
			}
1122
1123
			// replace CRLF with linebreak tag of given type
1124
			switch($mimetype.$mso_application_progid)
1125
			{
1126
				case 'application/vnd.oasis.opendocument.text':		// open office writer
1127
					$break = '<text:line-break/>';
1128
					break;
1129
				case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1130
					$break = '</text:p><text:p>';
1131
					break;
1132
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms word 2007
1133
				case 'application/vnd.ms-word.document.macroenabled.12':
1134
					$break = '</w:t><w:br/><w:t>';
1135
					break;
1136
				case 'application/xmlExcel.Sheet':	// Excel 2003
1137
					$break = '&#10;';
1138
					break;
1139
				case 'application/xmlWord.Document':	// Word 2003*/
1140
					$break = '</w:t><w:br/><w:t>';
1141
					break;
1142
				case 'text/html':
1143
					$break = '<br/>';
1144
					break;
1145
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':	// ms excel 2007
1146
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1147
				default:
1148
					$break = "\r\n";
1149
					break;
1150
			}
1151
			// now decode &, < and >, which need to be encoded as entities in xml
1152
			// Check for encoded >< getting double-encoded
1153
			if($this->parse_html_styles)
1154
			{
1155
				$replacements = str_replace(array('&',"\r","\n",'&amp;lt;','&amp;gt;'),array('&amp;','',$break,'&lt;','&gt;'),$replacements);
1156
			}
1157
			else
1158
			{
1159
				// Need to at least handle new lines, or it'll be run together on one line
1160
				$replacements = str_replace(array("\r","\n"),array('',$break),$replacements);
1161
			}
1162
		}
1163
		if ($mimetype == 'application/x-yaml')
1164
		{
1165
			$content = preg_replace_callback('/^( +)([^$\n]*)(\$\$.+?\$\$)/m', function($matches) use ($replacements)
1166
			{
1167
				// allow with {{name/replace/with}} syntax to replace eg. commas with linebreaks: "{{name/, */\n}}"
1168
				$parts = null;
1169
				if (preg_match('|^\$\$([^/]+)/([^/]+)/([^$]*)\$\$$|', $matches[3], $parts) && isset($replacements['$$'.$parts[1].'$$']))
1170
				{
1171
					$replacement =& $replacements['$$'.$parts[1].'$$'];
1172
					$replacement = preg_replace('/'.$parts[2].'/', strtr($parts[3], array(
1173
						'\\n' => "\n", '\\r' => "\r", '\\t' => "\t", '\\v' => "\v", '\\\\' => '\\', '\\f' => "\f",
1174
					)), $replacement);
1175
				}
1176
				else
1177
				{
1178
					$replacement =& $replacements[$matches[3]];
1179
				}
1180
				// replacement with multiple lines --> add same number of space as before placeholder
1181
				if (isset($replacement))
1182
				{
1183
					return $matches[1].$matches[2].implode("\n".$matches[1], preg_split("/\r?\n/", $replacement));
1184
				}
1185
				return $matches[0];	// regular replacement below
1186
			}, $content);
1187
		}
1188
		return str_replace(array_keys($replacements),array_values($replacements),$content);
1189
	}
1190
1191
	/**
1192
	 * Convert numeric values in spreadsheets into actual numeric values
1193
	 */
1194
	protected function format_spreadsheet_numbers(&$content, $names, $mimetype)
1195
	{
1196
		foreach((array)$this->numeric_fields as $fieldname) {
1197
			$names[] = preg_quote($fieldname,'/');
1198
		}
1199
		switch($mimetype)
1200
		{
1201 View Code Duplication
			case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1202
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)(?:calcext:value-type="[^"]+")?>.?<([a-z].*?)[^>]*>('.implode('|',$names).')<\/\3>.?<\/table:table-cell>/s';
1203
				$replacement = '<table:table-cell$1office:value-type="float" office:value="$4"$2 calcext:value-type="float"><$3>$4</$3></table:table-cell>';
1204
				break;
1205 View Code Duplication
			case 'application/vnd.oasis.opendocument.text':		// tables in open office writer
1206
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>('.implode('|',$names).')<\/\3>.?<\/table:table-cell>/s';
1207
				$replacement = '<table:table-cell$1office:value-type="float" office:value="$4"$2><text:p text:style-name="Standard">$4</text:p></table:table-cell>';
1208
				break;
1209
			case 'application/vnd.oasis.opendocument.text':		// open office writer
1210 View Code Duplication
			case 'application/xmlExcel.Sheet':	// Excel 2003
1211
				$format = '/'.preg_quote('<Data ss:Type="String">','/').'('.implode('|',$names).')'.preg_quote('</Data>','/').'/';
1212
				$replacement = '<Data ss:Type="Number">$1</Data>';
1213
1214
				break;
1215
		}
1216 View Code Duplication
		if($format && $names)
1217
		{
1218
			// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
1219
			do {
1220
				$result = preg_replace($format, $replacement, $content, -1);
1221
			}
1222
			// try to increase/double pcre.backtrack_limit failure
1223
			while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit());
1224
1225
			if ($result) $content = $result;  // On failure $result would be NULL
1226
		}
1227
	}
1228
1229
	/**
1230
	 * Increase/double prce.backtrack_limit up to 1/4 of memory_limit
1231
	 *
1232
	 * @return boolean true: backtrack_limit increased, may try again, false limit already to high
1233
	 */
1234
	protected static function increase_backtrack_limit()
1235
	{
1236
		static $backtrack_limit=null,$memory_limit=null;
1237
		if (!isset($backtrack_limit))
1238
		{
1239
			$backtrack_limit = ini_get('pcre.backtrack_limit');
1240
		}
1241
		if (!isset($memory_limit))
1242
		{
1243
			$memory_limit = ini_get('memory_limit');
1244 View Code Duplication
			switch(strtoupper(substr($memory_limit, -1)))
1245
			{
1246
				case 'G': $memory_limit *= 1024;
1247
				case 'M': $memory_limit *= 1024;
1248
				case 'K': $memory_limit *= 1024;
1249
			}
1250
		}
1251
		if ($backtrack_limit < $memory_limit/8)
1252
		{
1253
			ini_set( 'pcre.backtrack_limit', $backtrack_limit*=2);
1254
			return true;
1255
		}
1256
		error_log("pcre.backtrack_limit exceeded @ $backtrack_limit, some cells left as text.");
1257
		return false;
1258
	}
1259
1260
	/**
1261
	 * Convert date / timestamp values in spreadsheets into actual date / timestamp values
1262
	 */
1263
	protected function format_spreadsheet_dates(&$content, $names, &$values, $mimetype)
1264
	{
1265
		if(!in_array($mimetype, array(
1266
			'application/vnd.oasis.opendocument.spreadsheet',		// open office calc
1267
			'application/xmlExcel.Sheet',					// Excel 2003
1268
			//'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'//Excel WTF
1269
		))) return;
1270
1271
		// Some different formats dates could be in, depending what they've been through
1272
		$formats = array(
1273
			'!'.Api\DateTime::$user_dateformat . ' ' .Api\DateTime::$user_timeformat.':s',
1274
			'!'.Api\DateTime::$user_dateformat . '*' .Api\DateTime::$user_timeformat.':s',
1275
			'!'.Api\DateTime::$user_dateformat . '* ' .Api\DateTime::$user_timeformat,
1276
			'!'.Api\DateTime::$user_dateformat . '*',
1277
			'!'.Api\DateTime::$user_dateformat,
1278
			'!Y-m-d\TH:i:s'
1279
		);
1280
1281
		// Properly format values for spreadsheet
1282
		foreach($names as $idx => &$field)
1283
		{
1284
			$key = '$$'.$field.'$$';
1285
			$field = preg_quote($field, '/');
1286
			if($values[$key])
1287
			{
1288
				if(!is_numeric($values[$key]))
1289
				{
1290
					// Try the different formats, stop when one works
1291
					foreach($formats as $f)
1292
					{
1293
						try {
1294
							$date = Api\DateTime::createFromFormat(
1295
								$f,
1296
								$values[$key],
1297
								Api\DateTime::$user_timezone
1298
							);
1299
							if($date) break;
1300
						} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1301
1302
						}
1303
					}
1304
					if(!$date)
1305
					{
1306
						// Couldn't get a date out of it... skip it
1307
						trigger_error("Unable to parse date $key = '{$values[$key]}' - left as text", E_USER_NOTICE);
1308
						unset($names[$idx]);
1309
						continue;
1310
					}
1311
				}
1312
				else
1313
				{
1314
					$date = new Api\DateTime($values[$key]);
1315
				}
1316
				if($mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
1317
					$mimetype == 'application/vnd.ms-excel.sheet.macroenabled.12')//Excel WTF
1318
				{
1319
					$interval = $date->diff(new Api\DateTime('1900-01-00 0:00'));
1320
					$values[$key] = $interval->format('%a')+1;// 1900-02-29 did not exist
1321
					// 1440 minutes in a day - fractional part
1322
					$values[$key] += ($date->format('H') * 60 + $date->format('i'))/1440;
1323
				}
1324
				else
1325
				{
1326
					$values[$key] = date('Y-m-d\TH:i:s',Api\DateTime::to($date,'ts'));
1327
				}
1328
			}
1329
			else
1330
			{
1331
				unset($names[$idx]);
1332
			}
1333
		}
1334
1335
		switch($mimetype)
1336
		{
1337
			case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1338
				// Removing these forces calc to respect our set value-type
1339
				$content = str_ireplace('calcext:value-type="string"','',$content);
1340
1341
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>\$\$('.implode('|',$names).')\$\$<\/\3>.?<\/table:table-cell>/s';
1342
				$replacement = '<table:table-cell$1office:value-type="date" office:date-value="\$\$$4\$\$"$2><$3>\$\$$4\$\$</$3></table:table-cell>';
1343
				break;
1344 View Code Duplication
			case 'application/xmlExcel.Sheet':	// Excel 2003
1345
				$format = '/'.preg_quote('<Data ss:Type="String">','/').'..('.implode('|',$names).')..'.preg_quote('</Data>','/').'/';
1346
				$replacement = '<Data ss:Type="DateTime">\$\$$1\$\$</Data>';
1347
1348
				break;
1349
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1350
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1351
				break;
1352
		}
1353 View Code Duplication
		if($format && $names)
1354
		{
1355
			// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
1356
			do {
1357
				$result = preg_replace($format, $replacement, $content, -1);
1358
			}
1359
			// try to increase/double pcre.backtrack_limit failure
1360
			while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit());
1361
1362
			if ($result) $content = $result;  // On failure $result would be NULL
1363
		}
1364
	}
1365
1366
	/**
1367
	 * Expand link_to custom fields with the merge replacements from the app
1368
	 * but only if the template uses them.
1369
	 */
1370
	public function cf_link_to_expand($values, $content, &$replacements, $app = null)
1371
	{
1372
		if($app == null)
1373
		{
1374
			$app = str_replace('_merge','',get_class($this));
1375
		}
1376
		$cfs = Api\Storage\Customfields::get($app);
1377
1378
		// Cache, in case more than one sub-placeholder is used
1379
		$app_replacements = array();
1380
1381
		// Custom field placeholders look like {{#name}}
1382
		// Placeholders that need expanded will look like {{#name/placeholder}}
1383
		$matches = null;
1384
		preg_match_all('/\${2}(([^\/#]*?\/)?)#([^$\/]+)\/(.*?)[$}]{2}/', $content, $matches);
1385
		list($placeholders, , , $cf, $sub) = $matches;
1386
1387
		// Collect any used custom fields from entries so you can do
1388
		// {{#other_app/#other_app_cf/n_fn}}
1389
		$expand_sub_cfs = [];
1390
		foreach($sub as $index => $cf_sub)
1391
		{
1392
			if(strpos($cf_sub, '#') === 0)
1393
			{
1394
				$expand_sub_cfs[$cf[$index]] .= '$$'.$cf_sub . '$$ ';
1395
			}
1396
		}
1397
		$expand_sub_cfs = array_unique($expand_sub_cfs);
1398
1399
		foreach($cf as $index => $field)
1400
		{
1401
			if($cfs[$field])
1402
			{
1403
				if(in_array($cfs[$field]['type'],array_keys($GLOBALS['egw_info']['apps'])))
1404
				{
1405
					$field_app = $cfs[$field]['type'];
1406
				}
1407
				else if ($cfs[$field]['type'] == 'api-accounts' || $cfs[$field]['type'] == 'select-account')
1408
				{
1409
					// Special case for api-accounts -> contact
1410
					$field_app = 'addressbook';
1411
					$account = $GLOBALS['egw']->accounts->read($values['#'.$field]);
1412
					$app_replacements[$field] = $this->contact_replacements($account['person_id']);
1413
				}
1414
				else if (($list = explode('-',$cfs[$field]['type']) && in_array($list[0], array_keys($GLOBALS['egw_info']['apps']))))
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $list = (explode('-', $c...['egw_info']['apps']))), Probably Intended Meaning: ($list = explode('-', $c...S['egw_info']['apps']))
Loading history...
1415
				{
1416
					// Sub-type - use app
1417
					$field_app = $list[0];
1418
				}
1419
				else
1420
				{
1421
					continue;
1422
				}
1423
1424
				// Get replacements for that application
1425
				if(!$app_replacements[$field])
1426
				{
1427
					$app_replacements[$field] = $this->get_app_replacements($field_app, $values['#'.$field], $content);
0 ignored issues
show
Bug introduced by
The call to get_app_replacements() misses a required argument $prefix.

This check looks for function calls that miss required arguments.

Loading history...
1428
				}
1429
				$replacements[$placeholders[$index]] = $app_replacements[$field]['$$'.$sub[$index].'$$'];
1430
			}
1431
			else
1432
			{
1433
				if ($cfs[$field]['type'] == 'date' || $cfs[$field]['type'] == 'date-time') $this->date_fields[] = '#'.$field;
1434
			}
1435
		}
1436
	}
1437
1438
	/**
1439
	 * Get the replacements for any entry specified by app & id
1440
	 *
1441
	 * @param stribg $app
1442
	 * @param string $id
1443
	 * @param string $content
1444
	 * @return array
1445
	 */
1446
	protected function get_app_replacements($app, $id, $content, $prefix)
1447
	{
1448
		$replacements = array();
1449
		if($app == 'addressbook')
1450
		{
1451
			return $this->contact_replacements($id, $prefix);
1452
		}
1453
1454
		try
1455
		{
1456
			$classname = "{$app}_merge";
1457
			$class = new $classname();
1458
			$method = $app.'_replacements';
1459
			if(method_exists($class,$method))
1460
			{
1461
				$replacements = $class->$method($id, $prefix, $content);
1462
			}
1463
			else
1464
			{
1465
				$replacements = $class->get_replacements($id, $content);
1466
			}
1467
		}
1468
		catch (\Exception $e)
1469
		{
1470
			// Don't break merge, just log it
1471
			error_log($e->getMessage());
1472
		}
1473
		return $replacements;
1474
	}
1475
1476
	/**
1477
	 * Process special flags, such as IF or NELF
1478
	 *
1479
	 * @param content Text to be examined and changed
1480
	 * @param replacements array of markers => replacement
1481
	 *
1482
	 * @return changed content
1483
	 */
1484
	private function process_commands($content, $replacements)
1485
	{
1486 View Code Duplication
		if (strpos($content,'$$IF') !== false)
1487
		{	//Example use to use: $$IF n_prefix~Herr~Sehr geehrter~Sehr geehrte$$
1488
			$this->replacements =& $replacements;
1489
			$content = preg_replace_callback('/\$\$IF ([#0-9a-z_\/-]+)~(.*)~(.*)~(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1490
			unset($this->replacements);
1491
		}
1492 View Code Duplication
		if (strpos($content,'$$NELF') !== false)
1493
		{	//Example: $$NEPBR org_unit$$ sets a LF and value of org_unit, only if there is a value
1494
			$this->replacements =& $replacements;
1495
			$content = preg_replace_callback('/\$\$NELF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1496
			unset($this->replacements);
1497
		}
1498 View Code Duplication
		if (strpos($content,'$$NENVLF') !== false)
1499
		{	//Example: $$NEPBRNV org_unit$$ sets only a LF if there is a value for org_units, but did not add any value
1500
			$this->replacements =& $replacements;
1501
			$content = preg_replace_callback('/\$\$NENVLF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1502
			unset($this->replacements);
1503
		}
1504
		if (strpos($content,'$$LETTERPREFIX$$') !== false)
1505
		{	//Example use to use: $$LETTERPREFIX$$
1506
			$LETTERPREFIXCUSTOM = '$$LETTERPREFIXCUSTOM n_prefix title n_family$$';
1507
			$content = str_replace('$$LETTERPREFIX$$',$LETTERPREFIXCUSTOM,$content);
1508
		}
1509 View Code Duplication
		if (strpos($content,'$$LETTERPREFIXCUSTOM') !== false)
1510
		{	//Example use to use for a custom Letter Prefix: $$LETTERPREFIX n_prefix title n_family$$
1511
			$this->replacements =& $replacements;
1512
			$content = preg_replace_callback('/\$\$LETTERPREFIXCUSTOM ([#0-9a-z_-]+)(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1513
			unset($this->replacements);
1514
		}
1515
		return $content;
1516
	}
1517
1518
	/**
1519
	 * Callback for preg_replace to process $$IF
1520
	 *
1521
	 * @param array $param
1522
	 * @return string
1523
	 */
1524
	private function replace_callback($param)
1525
	{
1526 View Code Duplication
		if (array_key_exists('$$'.$param[4].'$$',$this->replacements)) $param[4] = $this->replacements['$$'.$param[4].'$$'];
1527 View Code Duplication
		if (array_key_exists('$$'.$param[3].'$$',$this->replacements)) $param[3] = $this->replacements['$$'.$param[3].'$$'];
1528
1529
		$pattern = '/'.preg_quote($param[2], '/').'/';
1530
		if (strpos($param[0],'$$IF') === 0 && (trim($param[2]) == "EMPTY" || $param[2] === ''))
1531
		{
1532
			$pattern = '/^$/';
1533
		}
1534
		$replace = preg_match($pattern,$this->replacements['$$'.$param[1].'$$']) ? $param[3] : $param[4];
1535
		switch($this->mimetype)
1536
		{
1537
			case 'application/vnd.oasis.opendocument.text':		// open office
1538
			case 'application/vnd.oasis.opendocument.spreadsheet':
1539
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1540
			case 'application/vnd.ms-word.document.macroenabled.12':
1541
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1542
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1543
			case 'application/xml':
1544
			case 'text/xml':
1545
			case 'text/html':
1546
				$is_xml = true;
1547
				break;
1548
		}
1549
1550
		switch($this->mimetype)
1551
			{
1552
				case 'application/rtf':
1553
				case 'text/rtf':
1554
					$LF = '}\par \pard\plain{';
1555
					break;
1556
				case 'application/vnd.oasis.opendocument.text':
1557
					$LF ='<text:line-break/>';
1558
					break;
1559
				case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1560
					$LF = '</text:p><text:p>';
1561
					break;
1562
				case 'application/xmlExcel.Sheet':	// Excel 2003
1563
					$LF = '&#10;';
1564
					break;
1565
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1566
				case 'application/vnd.ms-word.document.macroenabled.12':
1567
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1568
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1569
					$LF ='</w:t></w:r></w:p><w:p><w:r><w:t>';
1570
					break;
1571
				case 'application/xml';
1572
					$LF ='</w:t></w:r><w:r><w:br w:type="text-wrapping" w:clear="all"/></w:r><w:r><w:t>';
1573
					break;
1574
				case 'text/html':
1575
					$LF = "<br/>";
1576
					break;
1577
				default:
1578
					$LF = "\n";
1579
			}
1580
		if($is_xml) {
1581
			$this->replacements = str_replace(array('&','&amp;amp;','<','>',"\r","\n"),array('&amp;','&amp;','&lt;','&gt;','',$LF),$this->replacements);
1582
		}
1583
		if (strpos($param[0],'$$NELF') === 0)
1584
		{	//sets a Pagebreak and value, only if the field has a value
1585
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF.$this->replacements['$$'.$param[1].'$$'];
1586
		}
1587
		if (strpos($param[0],'$$NENVLF') === 0)
1588
		{	//sets a Pagebreak without any value, only if the field has a value
1589
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF;
1590
		}
1591
		if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0)
1592
		{	//sets a Letterprefix
1593
			$replaceprefixsort = array();
1594
			// ToDo Stefan: $contentstart is NOT defined here!!!
1595
			$replaceprefix = explode(' ',substr($param[0],21,-2));
1596
			foreach ($replaceprefix as $nameprefix)
1597
			{
1598
				if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$'];
1599
			}
1600
			$replace = implode($replaceprefixsort,' ');
1601
		}
1602
		return $replace;
1603
	}
1604
1605
	/**
1606
	 * Download document merged with contact(s)
1607
	 *
1608
	 * @param string $document vfs-path of document
1609
	 * @param array $ids array with contact id(s)
1610
	 * @param string $name ='' name to use for downloaded document
1611
	 * @param string $dirs comma or whitespace separated directories, used if $document is a relative path
1612
	 * @return string with error-message on error, otherwise it does NOT return
1613
	 */
1614
	public function download($document, $ids, $name='', $dirs='')
1615
	{
1616
		//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs') ->".function_backtrace());
1617
		if (($error = $this->check_document($document, $dirs)))
1618
		{
1619
			return $error;
1620
		}
1621
		$content_url = Api\Vfs::PREFIX.$document;
1622
		switch (($mimetype = Api\Vfs::mime_content_type($document)))
1623
		{
1624
			case 'message/rfc822':
1625
				//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs')=>$content_url ->".function_backtrace());
1626
				$mail_bo = Api\Mail::getInstance();
1627
				$mail_bo->openConnection();
1628
				try
1629
				{
1630
					$msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, $_folder='');
0 ignored issues
show
Bug introduced by
$_folder = '' cannot be passed to importmessagetomergeandsend() as the parameter $_folder expects a reference.
Loading history...
1631
				}
1632
				catch (Api\Exception\WrongUserinput $e)
1633
				{
1634
					// if this returns with an exeption, something failed big time
1635
					return $e->getMessage();
1636
				}
1637
				//error_log(__METHOD__.__LINE__.' Message after importMessageToMergeAndSend:'.array2string($msgs));
1638
				$retString = '';
1639
				if (count($msgs['success'])>0) $retString .= count($msgs['success']).' '.(count($msgs['success'])+count($msgs['failed'])==1?lang('Message prepared for sending.'):lang('Message(s) send ok.'));//implode('<br />',$msgs['success']);
1640
				//if (strlen($retString)>0) $retString .= '<br />';
1641
				foreach($msgs['failed'] as $c =>$e)
1642
				{
1643
					$errorString .= lang('contact').' '.lang('id').':'.$c.'->'.$e.'.';
1644
				}
1645
				if (count($msgs['failed'])>0) $retString .= count($msgs['failed']).' '.lang('Message(s) send failed!').'=>'.$errorString;
1646
				return $retString;
1647
			case 'application/vnd.oasis.opendocument.text':
1648
			case 'application/vnd.oasis.opendocument.spreadsheet':
1649
				$ext = $mimetype == 'application/vnd.oasis.opendocument.text' ? '.odt' : '.ods';
1650
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,$ext).'-').$ext;
1651
				copy($content_url,$archive);
1652
				$content_url = 'zip://'.$archive.'#'.($content_file = 'content.xml');
1653
				$this->parse_html_styles = true;
1654
				break;
1655
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d':	// mimetypes in vfs are limited to 64 chars
1656
				$mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
1657
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1658
			case 'application/vnd.ms-word.document.macroenabled.12':
1659
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.docx').'-').'.docx';
1660
				copy($content_url,$archive);
1661
				$content_url = 'zip://'.$archive.'#'.($content_file = 'word/document.xml');
1662
				$fix = array(		// regular expression to fix garbled placeholders
1663
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1664
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$$\\1$$',
1665
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="','/').
1666
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>','/').'([a-z0-9_]+)'.
1667
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="','/').
1668
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>$$','/').'/i' => '$$\\2$$',
1669
					'/'.preg_quote('$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1670
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$\\1$',
1671
					'/'.preg_quote('$ $</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1672
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$ $\\1$ $',
1673
				);
1674
				break;
1675
			case 'application/xml':
1676
				$fix = array(	// hack to get Excel 2003 to display additional rows in tables
1677
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1678
				);
1679
				break;
1680
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee':
1681
				$mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
1682
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1683
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1684
				$fix = array(	// hack to get Excel 2007 to display additional rows in tables
1685
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1686
				);
1687
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.xlsx').'-').'.xlsx';
1688
				copy($content_url,$archive);
1689
				$content_url = 'zip://'.$archive.'#'.($content_file = 'xl/sharedStrings.xml');
1690
				break;
1691
		}
1692
		$err = null;
1693
		if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1694
		{
1695
			//error_log(__METHOD__."() !this->merge() err=$err");
1696
			return $err;
1697
		}
1698
		// Apply HTML formatting to target document, if possible
1699
		// check if we can use the XSL extension, to not give a fatal error and rendering whole merge-print non-functional
1700
		if (class_exists(XSLTProcessor) && class_exists(DOMDocument) && $this->parse_html_styles)
1701
		{
1702
			try
1703
			{
1704
				$this->apply_styles($merged, $mimetype);
1705
			}
1706
			catch (\Exception $e)
1707
			{
1708
				// Error converting HTML styles over
1709
				error_log($e->getMessage());
1710
				error_log("Target document: $content_url, IDs: ". array2string($ids));
1711
1712
				// Try again, but strip HTML so user gets something
1713
				$this->parse_html_styles = false;
1714
				if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1715
				{
1716
					return $err;
1717
				}
1718
			}
1719
			if ($this->report_memory_usage) error_log(__METHOD__."() after HTML processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1720
		}
1721
		if(!empty($name))
1722
		{
1723
			if(empty($ext))
1724
			{
1725
				$ext = '.'.pathinfo($document,PATHINFO_EXTENSION);
1726
			}
1727
			$name .= $ext;
1728
		}
1729
		else
1730
		{
1731
			$name = basename($document);
1732
		}
1733
		if (isset($archive))
1734
		{
1735
			$zip = new ZipArchive;
1736
			if ($zip->open($archive, ZipArchive::CHECKCONS) !== true)
1737
			{
1738
				error_log(__METHOD__.__LINE__." !ZipArchive::open('$archive',ZIPARCHIVE"."::CHECKCONS) failed. Trying open without validating");
1739
				if ($zip->open($archive) !== true) throw new Api\Exception("!ZipArchive::open('$archive',|ZIPARCHIVE::CHECKCONS)");
1740
			}
1741
			if ($zip->addFromString($content_file,$merged) !== true) throw new Api\Exception("!ZipArchive::addFromString('$content_file',\$merged)");
1742
			if ($zip->close() !== true) throw new Api\Exception("!ZipArchive::close()");
1743
			unset($zip);
1744
			unset($merged);
1745
			if ($this->report_memory_usage) error_log(__METHOD__."() after ZIP processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1746
			Api\Header\Content::type($name,$mimetype,filesize($archive));
1747
			readfile($archive,'r');
1748
		}
1749
		else
1750
		{
1751
			if ($mimetype == 'application/xml')
1752
			{
1753
				if (strpos($merged,'<?mso-application progid="Word.Document"?>') !== false)
1754
				{
1755
					$mimetype = 'application/msword';	// to open it automatically in word or oowriter
1756
				}
1757
				elseif (strpos($merged,'<?mso-application progid="Excel.Sheet"?>') !== false)
1758
				{
1759
					$mimetype = 'application/vnd.ms-excel';	// to open it automatically in excel or oocalc
1760
				}
1761
			}
1762
			Api\Header\Content::type($name, $mimetype);
1763
			echo $merged;
1764
		}
1765
		exit;
1766
	}
1767
1768
	/**
1769
	 * Download document merged with contact(s)
1770
	 * frontend for HTTP POST requests
1771
	 * accepts POST vars and calls internal function download()
1772
	 *   string data_document_name: the document name
1773
	 *   string data_document_dir: the document vfs directory
1774
	 *   string data_checked: contact id(s) to merge with (can be comma separated)
1775
	 *
1776
	 * @return string with error-message on error, otherwise it does NOT return
1777
	 */
1778
	public function download_by_request()
1779
	{
1780
		if(empty($_POST['data_document_name'])) return false;
1781
		if(empty($_POST['data_document_dir'])) return false;
1782
		if(empty($_POST['data_checked'])) return false;
1783
1784
		return $this->download(
1785
			$_POST['data_document_name'],
1786
			explode(',',$_POST['data_checked']),
1787
			'',
1788
			$_POST['data_document_dir']
1789
		);
1790
	}
1791
1792
	/**
1793
	 * Get a list of document actions / files from the given directory
1794
	 *
1795
	 * @param string $dirs Directory(s comma or space separated) to search
1796
	 * @param string $prefix='document_' prefix for array keys
1797
	 * @param array|string $mime_filter=null allowed mime type(s), default all, negative filter if $mime_filter[0] === '!'
1798
	 * @return array List of documents, suitable for a selectbox.  The key is document_<filename>.
1799
	 */
1800
	public static function get_documents($dirs, $prefix='document_', $mime_filter=null, $app='')
1801
	{
1802
		$export_limit=self::getExportLimit($app);
1803
		if (!$dirs || (!self::hasExportLimit($export_limit,'ISALLOWED') && !self::is_export_limit_excepted())) return array();
1804
1805
		// split multiple comma or whitespace separated directories
1806
		// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
1807 View Code Duplication
		if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
1808
		{
1809
			foreach($dirs as $n => &$d)
1810
			{
1811
				if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
1812
			}
1813
		}
1814
		if ($mime_filter && ($negativ_filter = $mime_filter[0] === '!'))
1815
		{
1816
			if (is_array($mime_filter))
1817
			{
1818
				unset($mime_filter[0]);
1819
			}
1820
			else
1821
			{
1822
				$mime_filter = substr($mime_filter, 1);
1823
			}
1824
		}
1825
		$list = array();
1826
		foreach($dirs as $dir)
1827
		{
1828
			if (($files = Api\Vfs::find($dir,array('need_mime'=>true),true)))
1829
			{
1830
				foreach($files as $file)
1831
				{
1832
					// return only the mime-types we support
1833
					$parts = explode('.',$file['name']);
1834
					if (!self::is_implemented($file['mime'],'.'.array_pop($parts))) continue;
1835
					if ($mime_filter && $negativ_filter === in_array($file['mime'], (array)$mime_filter)) continue;
1836
					$list[$prefix.$file['name']] = Api\Vfs::decodePath($file['name']);
1837
				}
1838
			}
1839
		}
1840
		return $list;
1841
	}
1842
1843
	/**
1844
	 * From this number of documents, show them in submenus by mime type
1845
	 */
1846
	const SHOW_DOCS_BY_MIME_LIMIT = 10;
1847
1848
	/**
1849
	 * Get insert-in-document action with optional default document on top
1850
	 *
1851
	 * If more than SHOW_DOCS_BY_MIME_LIMIT=10 documents found, they are displayed in submenus by mime type.
1852
	 *
1853
	 * @param string $dirs Directory(s comma or space separated) to search
1854
	 * @param int $group see nextmatch_widget::egw_actions
1855
	 * @param string $caption ='Insert in document'
1856
	 * @param string $prefix ='document_'
1857
	 * @param string $default_doc ='' full path to default document to show on top with action == 'document'!
1858
	 * @param int|string $export_limit =null export-limit, default $GLOBALS['egw_info']['server']['export_limit']
1859
	 * @return array see nextmatch_widget::egw_actions
1860
	 */
1861
	public static function document_action($dirs, $group=0, $caption='Insert in document', $prefix='document_', $default_doc='',
1862
		$export_limit=null)
1863
	{
1864
		$documents = array();
1865
		if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting
1866
		if ($default_doc && ($file = Api\Vfs::stat($default_doc)))	// put default document on top
1867
		{
1868
			if(!$file['mime'])
1869
			{
1870
				$file['mime'] = Api\Vfs::mime_content_type($default_doc);
1871
				$file['path'] = $default_doc;
1872
			}
1873
			$documents['document'] = array(
1874
				'icon' => Api\Vfs::mime_icon($file['mime']),
1875
				'caption' => Api\Vfs::decodePath(Api\Vfs::basename($default_doc)),
1876
				'group' => 1,
1877
				'postSubmit' => true,	// download needs post submit (not Ajax) to work
1878
			);
1879
			if ($file['mime'] == 'message/rfc822')
1880
			{
1881
				self::document_mail_action($documents['document'], $file);
1882
			}
1883
		}
1884
1885
		$files = array();
1886
		if ($dirs)
1887
		{
1888
			// split multiple comma or whitespace separated directories
1889
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
1890 View Code Duplication
			if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
1891
			{
1892
				foreach($dirs as $n => &$d)
1893
				{
1894
					if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
1895
				}
1896
			}
1897
			foreach($dirs as $dir)
1898
			{
1899
				$files += Api\Vfs::find($dir,array(
1900
					'need_mime' => true,
1901
					'order' => 'fs_name',
1902
					'sort' => 'ASC',
1903
				),true);
1904
			}
1905
		}
1906
1907
		$dircount = array();
1908
		foreach($files as $key => $file)
1909
		{
1910
			// use only the mime-types we support
1911
			$parts = explode('.',$file['name']);
1912
			if (!self::is_implemented($file['mime'],'.'.array_pop($parts)) ||
1913
				!Api\Vfs::check_access($file['path'], Api\Vfs::READABLE, $file) ||	// remove files not readable by user
1914
				$file['path'] === $default_doc)	// default doc already added
1915
			{
1916
				unset($files[$key]);
1917
			}
1918
			else
1919
			{
1920
				$dirname = Api\Vfs::dirname($file['path']);
1921
				if(!isset($dircount[$dirname]))
1922
				{
1923
					$dircount[$dirname] = 1;
1924
				}
1925
				else
1926
				{
1927
					$dircount[$dirname] ++;
1928
				}
1929
			}
1930
		}
1931
		foreach($files as $file)
1932
		{
1933
			if (count($dircount) > 1)
1934
			{
1935
				$name_arr = explode('/', $file['name']);
1936
				$current_level = &$documents;
1937
				for($count = 0; $count < count($name_arr); $count++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1938
				{
1939
					if($count == 0)
1940
					{
1941
						$current_level = &$documents;
1942
					}
1943
					else
1944
					{
1945
						$current_level = &$current_level[$prefix.$name_arr[($count-1)]]['children'];
1946
					}
1947
					switch($count)
1948
					{
1949
						case (count($name_arr)-1):
1950
							$current_level[$prefix.$file['name']] = array(
1951
								'icon'		=> Api\Vfs::mime_icon($file['mime']),
1952
								'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
1953
								'group'		=> 2,
1954
								'postSubmit' => true,	// download needs post submit (not Ajax) to work
1955
							);
1956
							if ($file['mime'] == 'message/rfc822')
1957
							{
1958
								self::document_mail_action($current_level[$prefix.$file['name']], $file);
1959
							}
1960
							break;
1961
1962
						default:
1963
							if(!is_array($current_level[$prefix.$name_arr[$count]]))
1964
							{
1965
								// create parent folder
1966
								$current_level[$prefix.$name_arr[$count]] = array(
1967
									'icon'		=> 'phpgwapi/foldertree_folder',
1968
									'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
1969
									'group'		=> 2,
1970
									'children'	=> array(),
1971
								);
1972
							}
1973
							break;
1974
					}
1975
				}
1976
			}
1977
			else if (count($files) >= self::SHOW_DOCS_BY_MIME_LIMIT)
1978
			{
1979
				if (!isset($documents[$file['mime']]))
1980
				{
1981
					$documents[$file['mime']] = array(
1982
						'icon' => Api\Vfs::mime_icon($file['mime']),
1983
						'caption' => Api\MimeMagic::mime2label($file['mime']),
1984
						'group' => 2,
1985
						'children' => array(),
1986
					);
1987
				}
1988
				$documents[$file['mime']]['children'][$prefix.$file['name']] = array(
1989
					'caption' => Api\Vfs::decodePath($file['name']),
1990
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
1991
				);
1992
				if ($file['mime'] == 'message/rfc822')
1993
				{
1994
					self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
1995
				}
1996
			}
1997
			else
1998
			{
1999
				$documents[$prefix.$file['name']] = array(
2000
					'icon' => Api\Vfs::mime_icon($file['mime']),
2001
					'caption' => Api\Vfs::decodePath($file['name']),
2002
					'group' => 2,
2003
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
2004
				);
2005
				if ($file['mime'] == 'message/rfc822')
2006
				{
2007
					self::document_mail_action($documents[$prefix.$file['name']], $file);
2008
				}
2009
			}
2010
		}
2011
2012
		return array(
2013
			'icon' => 'etemplate/merge',
2014
			'caption' => $caption,
2015
			'children' => $documents,
2016
			// disable action if no document or export completly forbidden for non-admins
2017
			'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit,'ISALLOWED') || self::is_export_limit_excepted()),
2018
			'hideOnDisabled' => true,	// do not show 'Insert in document', if no documents defined or no export allowed
2019
			'group' => $group,
2020
		);
2021
	}
2022
2023
	/**
2024
	 * Set up a document action for an eml (email) document
2025
	 *
2026
	 * Email (.eml) documents get special action handling.  They don't send a file
2027
	 * back to the client like the other documents.  Merging for a single selected
2028
	 * contact opens a compose window, multiple contacts just sends.
2029
	 *
2030
	 * @param Array &$action Action to be modified for mail
2031
	 * @param Array $file Array of information about the document from Api\Vfs::find
2032
	 * @return void
2033
	 */
2034
	private static function document_mail_action(Array &$action, $file)
2035
	{
2036
		unset($action['postSubmit']);
2037
2038
		// Lots takes a while, confirm
2039
		$action['confirm_multiple'] = lang('Do you want to send the message to all selected entries, WITHOUT further editing?');
2040
2041
		// These parameters trigger compose + merge - only if 1 row
2042
		$extra = array(
2043
			'from=merge',
2044
			'document='.$file['path'],
2045
			'merge='.get_called_class()
2046
		);
2047
2048
		// egw.open() used if only 1 row selected
2049
		$action['egw_open'] = 'edit-mail--'.implode('&',$extra);
2050
		$action['target'] = 'compose_' .$file['path'];
2051
2052
		// long_task runs menuaction once for each selected row
2053
		$action['nm_action'] = 'long_task';
2054
		$action['popup'] = Api\Link::get_registry('mail', 'edit_popup');
2055
		$action['message'] = lang('insert in %1',Api\Vfs::decodePath($file['name']));
2056
		$action['menuaction'] = 'mail.mail_compose.ajax_merge&document='.$file['path'].'&merge='. get_called_class();
2057
	}
2058
2059
	/**
2060
	 * Check if given document (relative path from document_actions()) exists in one of the given dirs
2061
	 *
2062
	 * @param string &$document maybe relative path of document, on return true absolute path to existing document
2063
	 * @param string $dirs comma or whitespace separated directories
2064
	 * @return string|boolean false if document exists, otherwise string with error-message
2065
	 */
2066
	public static function check_document(&$document, $dirs)
2067
	{
2068
		if($document[0] !== '/')
2069
		{
2070
			// split multiple comma or whitespace separated directories
2071
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
2072
			if ($dirs && ($dirs = preg_split('/[,\s]+\//', $dirs)))
2073
			{
2074
				foreach($dirs as $n => $dir)
2075
				{
2076
					if ($n) $dir = '/'.$dir;	// re-adding trailing slash removed by split
2077
					if (Api\Vfs::stat($dir.'/'.$document) && Api\Vfs::is_readable($dir.'/'.$document))
2078
					{
2079
						$document = $dir.'/'.$document;
2080
						return false;
2081
					}
2082
				}
2083
			}
2084
		}
2085
		elseif (Api\Vfs::stat($document) && Api\Vfs::is_readable($document))
2086
		{
2087
			return false;
2088
		}
2089
		//error_log(__METHOD__."('$document', dirs='$dirs') returning 'Document '$document' does not exist or is not readable for you!'");
2090
		return lang("Document '%1' does not exist or is not readable for you!",$document);
2091
	}
2092
2093
	/**
2094
	 * Get a list of supported extentions
2095
	 */
2096
	public static function get_file_extensions()
2097
	{
2098
		return array('txt', 'rtf', 'odt', 'ods', 'docx', 'xml', 'eml');
2099
	}
2100
2101
	/**
2102
	 * Format a number according to user prefs with decimal and thousands separator
2103
	 *
2104
	 * Reimplemented from etemplate to NOT use user prefs for Excel 2003, which gives an xml error
2105
	 *
2106
	 * @param int|float|string $number
2107
	 * @param int $num_decimal_places =2
2108
	 * @param string $_mimetype =''
2109
	 * @return string
2110
	 */
2111
	static public function number_format($number,$num_decimal_places=2,$_mimetype='')
2112
	{
2113
		if ((string)$number === '') return '';
2114
		//error_log(__METHOD__.$_mimetype);
2115
		switch($_mimetype)
2116
		{
2117
			case 'application/xml':	// Excel 2003
2118
			case 'application/vnd.oasis.opendocument.spreadsheet': // OO.o spreadsheet
2119
				return number_format(str_replace(' ','',$number),$num_decimal_places,'.','');
2120
		}
2121
		return Api\Etemplate::number_format($number,$num_decimal_places);
2122
	}
2123
}
2124