Completed
Push — 16.1 ( 96a09f...9e378a )
by Nathan
36:36 queued 19:17
created

Merge   F

Complexity

Total Complexity 481

Size/Duplication

Total Lines 2065
Duplicated Lines 7.89 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
dl 163
loc 2065
rs 0.6314
c 0
b 0
f 0
wmc 481
lcom 1
cbo 16

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 2
A hook_export_limit_excepted() 0 7 1
get_replacements() 0 1 ?
D is_implemented() 0 45 21
F contact_replacements() 6 98 42
D get_links() 14 45 17
C get_all_links() 14 65 21
A format_datetime() 0 7 2
B is_export_limit_excepted() 0 19 5
C getExportLimit() 0 34 7
B hasExportLimit() 0 7 7
C merge() 0 29 8
F apply_styles() 7 95 19
F merge_string() 9 235 64
F replace() 27 247 72
C format_spreadsheet_numbers() 24 34 11
C increase_backtrack_limit() 6 25 7
A download_by_request() 0 13 4
C get_documents() 7 42 16
F document_action() 7 161 29
B document_mail_action() 0 24 1
D check_document() 0 26 10
A get_file_extensions() 0 4 1
A number_format() 0 12 4
C format_spreadsheet_dates() 16 75 15
C cf_link_to_expand() 0 67 14
B get_app_replacements() 0 29 4
B process_commands() 24 33 6
F replace_callback() 2 80 35
F download() 0 153 36

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Merge often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Merge, and based on these observations, apply Extract Interface, too.

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
				$date = Api\DateTime::createFromUserFormat($values[$key]);
1289
				if($mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
1290
					$mimetype == 'application/vnd.ms-excel.sheet.macroenabled.12')//Excel WTF
1291
				{
1292
					$interval = $date->diff(new Api\DateTime('1900-01-00 0:00'));
1293
					$values[$key] = $interval->format('%a')+1;// 1900-02-29 did not exist
1294
					// 1440 minutes in a day - fractional part
1295
					$values[$key] += ($date->format('H') * 60 + $date->format('i'))/1440;
1296
				}
1297
				else
1298
				{
1299
					$values[$key] = date('Y-m-d\TH:i:s',Api\DateTime::to($date,'ts'));
1300
				}
1301
			}
1302
			else
1303
			{
1304
				unset($names[$idx]);
1305
			}
1306
		}
1307
1308
		switch($mimetype)
1309
		{
1310
			case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1311
				// Removing these forces calc to respect our set value-type
1312
				$content = str_ireplace('calcext:value-type="string"','',$content);
1313
1314
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>\$\$('.implode('|',$names).')\$\$<\/\3>.?<\/table:table-cell>/s';
1315
				$replacement = '<table:table-cell$1office:value-type="date" office:date-value="\$\$$4\$\$"$2><$3>\$\$$4\$\$</$3></table:table-cell>';
1316
				break;
1317 View Code Duplication
			case 'application/xmlExcel.Sheet':	// Excel 2003
1318
				$format = '/'.preg_quote('<Data ss:Type="String">','/').'..('.implode('|',$names).')..'.preg_quote('</Data>','/').'/';
1319
				$replacement = '<Data ss:Type="DateTime">\$\$$1\$\$</Data>';
1320
1321
				break;
1322
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1323
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1324
				break;
1325
		}
1326 View Code Duplication
		if($format && $names)
1327
		{
1328
			// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
1329
			do {
1330
				$result = preg_replace($format, $replacement, $content, -1);
1331
			}
1332
			// try to increase/double pcre.backtrack_limit failure
1333
			while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit());
1334
1335
			if ($result) $content = $result;  // On failure $result would be NULL
1336
		}
1337
	}
1338
1339
	/**
1340
	 * Expand link_to custom fields with the merge replacements from the app
1341
	 * but only if the template uses them.
1342
	 */
1343
	public function cf_link_to_expand($values, $content, &$replacements, $app = null)
1344
	{
1345
		if($app == null)
1346
		{
1347
			$app = str_replace('_merge','',get_class($this));
1348
		}
1349
		$cfs = Api\Storage\Customfields::get($app);
1350
1351
		// Cache, in case more than one sub-placeholder is used
1352
		$app_replacements = array();
1353
1354
		// Custom field placeholders look like {{#name}}
1355
		// Placeholders that need expanded will look like {{#name/placeholder}}
1356
		$matches = null;
1357
		preg_match_all('/\${2}(([^\/#]*?\/)?)#([^$\/]+)\/(.*?)[$}]{2}/', $content, $matches);
1358
		list($placeholders, , , $cf, $sub) = $matches;
1359
1360
		// Collect any used custom fields from entries so you can do
1361
		// {{#other_app/#other_app_cf/n_fn}}
1362
		$expand_sub_cfs = [];
1363
		foreach($sub as $index => $cf_sub)
1364
		{
1365
			if(strpos($cf_sub, '#') === 0)
1366
			{
1367
				$expand_sub_cfs[$cf[$index]] .= '$$'.$cf_sub . '$$ ';
1368
			}
1369
		}
1370
		$expand_sub_cfs = array_unique($expand_sub_cfs);
1371
1372
		foreach($cf as $index => $field)
1373
		{
1374
			if($cfs[$field])
1375
			{
1376
				if(in_array($cfs[$field]['type'],array_keys($GLOBALS['egw_info']['apps'])))
1377
				{
1378
					$field_app = $cfs[$field]['type'];
1379
				}
1380
				else if ($cfs[$field]['type'] == 'api-accounts' || $cfs[$field]['type'] == 'select-account')
1381
				{
1382
					// Special case for api-accounts -> contact
1383
					$field_app = 'addressbook';
1384
					$account = $GLOBALS['egw']->accounts->read($values['#'.$field]);
1385
					$app_replacements[$field] = $this->contact_replacements($account['person_id']);
1386
				}
1387
				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...
1388
				{
1389
					// Sub-type - use app
1390
					$field_app = $list[0];
1391
				}
1392
				else
1393
				{
1394
					continue;
1395
				}
1396
1397
				// Get replacements for that application
1398
				if(!$app_replacements[$field])
1399
				{
1400
					$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...
1401
				}
1402
				$replacements[$placeholders[$index]] = $app_replacements[$field]['$$'.$sub[$index].'$$'];
1403
			}
1404
			else
1405
			{
1406
				if ($cfs[$field]['type'] == 'date' || $cfs[$field]['type'] == 'date-time') $this->date_fields[] = '#'.$field;
1407
			}
1408
		}
1409
	}
1410
1411
	/**
1412
	 * Get the replacements for any entry specified by app & id
1413
	 *
1414
	 * @param stribg $app
1415
	 * @param string $id
1416
	 * @param string $content
1417
	 * @return array
1418
	 */
1419
	protected function get_app_replacements($app, $id, $content, $prefix)
1420
	{
1421
		$replacements = array();
1422
		if($app == 'addressbook')
1423
		{
1424
			return $this->contact_replacements($id, $prefix);
1425
		}
1426
1427
		try
1428
		{
1429
			$classname = "{$app}_merge";
1430
			$class = new $classname();
1431
			$method = $app.'_replacements';
1432
			if(method_exists($class,$method))
1433
			{
1434
				$replacements = $class->$method($id, $prefix, $content);
1435
			}
1436
			else
1437
			{
1438
				$replacements = $class->get_replacements($id, $content);
1439
			}
1440
		}
1441
		catch (\Exception $e)
1442
		{
1443
			// Don't break merge, just log it
1444
			error_log($e->getMessage());
1445
		}
1446
		return $replacements;
1447
	}
1448
1449
	/**
1450
	 * Process special flags, such as IF or NELF
1451
	 *
1452
	 * @param content Text to be examined and changed
1453
	 * @param replacements array of markers => replacement
1454
	 *
1455
	 * @return changed content
1456
	 */
1457
	private function process_commands($content, $replacements)
1458
	{
1459 View Code Duplication
		if (strpos($content,'$$IF') !== false)
1460
		{	//Example use to use: $$IF n_prefix~Herr~Sehr geehrter~Sehr geehrte$$
1461
			$this->replacements =& $replacements;
1462
			$content = preg_replace_callback('/\$\$IF ([#0-9a-z_\/-]+)~(.*)~(.*)~(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1463
			unset($this->replacements);
1464
		}
1465 View Code Duplication
		if (strpos($content,'$$NELF') !== false)
1466
		{	//Example: $$NEPBR org_unit$$ sets a LF and value of org_unit, only if there is a value
1467
			$this->replacements =& $replacements;
1468
			$content = preg_replace_callback('/\$\$NELF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1469
			unset($this->replacements);
1470
		}
1471 View Code Duplication
		if (strpos($content,'$$NENVLF') !== false)
1472
		{	//Example: $$NEPBRNV org_unit$$ sets only a LF if there is a value for org_units, but did not add any value
1473
			$this->replacements =& $replacements;
1474
			$content = preg_replace_callback('/\$\$NENVLF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1475
			unset($this->replacements);
1476
		}
1477
		if (strpos($content,'$$LETTERPREFIX$$') !== false)
1478
		{	//Example use to use: $$LETTERPREFIX$$
1479
			$LETTERPREFIXCUSTOM = '$$LETTERPREFIXCUSTOM n_prefix title n_family$$';
1480
			$content = str_replace('$$LETTERPREFIX$$',$LETTERPREFIXCUSTOM,$content);
1481
		}
1482 View Code Duplication
		if (strpos($content,'$$LETTERPREFIXCUSTOM') !== false)
1483
		{	//Example use to use for a custom Letter Prefix: $$LETTERPREFIX n_prefix title n_family$$
1484
			$this->replacements =& $replacements;
1485
			$content = preg_replace_callback('/\$\$LETTERPREFIXCUSTOM ([#0-9a-z_-]+)(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1486
			unset($this->replacements);
1487
		}
1488
		return $content;
1489
	}
1490
1491
	/**
1492
	 * Callback for preg_replace to process $$IF
1493
	 *
1494
	 * @param array $param
1495
	 * @return string
1496
	 */
1497
	private function replace_callback($param)
1498
	{
1499 View Code Duplication
		if (array_key_exists('$$'.$param[4].'$$',$this->replacements)) $param[4] = $this->replacements['$$'.$param[4].'$$'];
1500 View Code Duplication
		if (array_key_exists('$$'.$param[3].'$$',$this->replacements)) $param[3] = $this->replacements['$$'.$param[3].'$$'];
1501
1502
		$pattern = '/'.preg_quote($param[2], '/').'/';
1503
		if (strpos($param[0],'$$IF') === 0 && (trim($param[2]) == "EMPTY" || $param[2] === ''))
1504
		{
1505
			$pattern = '/^$/';
1506
		}
1507
		$replace = preg_match($pattern,$this->replacements['$$'.$param[1].'$$']) ? $param[3] : $param[4];
1508
		switch($this->mimetype)
1509
		{
1510
			case 'application/vnd.oasis.opendocument.text':		// open office
1511
			case 'application/vnd.oasis.opendocument.spreadsheet':
1512
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1513
			case 'application/vnd.ms-word.document.macroenabled.12':
1514
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1515
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1516
			case 'application/xml':
1517
			case 'text/xml':
1518
			case 'text/html':
1519
				$is_xml = true;
1520
				break;
1521
		}
1522
1523
		switch($this->mimetype)
1524
			{
1525
				case 'application/rtf':
1526
				case 'text/rtf':
1527
					$LF = '}\par \pard\plain{';
1528
					break;
1529
				case 'application/vnd.oasis.opendocument.text':
1530
					$LF ='<text:line-break/>';
1531
					break;
1532
				case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1533
					$LF = '</text:p><text:p>';
1534
					break;
1535
				case 'application/xmlExcel.Sheet':	// Excel 2003
1536
					$LF = '&#10;';
1537
					break;
1538
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1539
				case 'application/vnd.ms-word.document.macroenabled.12':
1540
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1541
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1542
					$LF ='</w:t></w:r></w:p><w:p><w:r><w:t>';
1543
					break;
1544
				case 'application/xml';
1545
					$LF ='</w:t></w:r><w:r><w:br w:type="text-wrapping" w:clear="all"/></w:r><w:r><w:t>';
1546
					break;
1547
				case 'text/html':
1548
					$LF = "<br/>";
1549
					break;
1550
				default:
1551
					$LF = "\n";
1552
			}
1553
		if($is_xml) {
1554
			$this->replacements = str_replace(array('&','&amp;amp;','<','>',"\r","\n"),array('&amp;','&amp;','&lt;','&gt;','',$LF),$this->replacements);
1555
		}
1556
		if (strpos($param[0],'$$NELF') === 0)
1557
		{	//sets a Pagebreak and value, only if the field has a value
1558
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF.$this->replacements['$$'.$param[1].'$$'];
1559
		}
1560
		if (strpos($param[0],'$$NENVLF') === 0)
1561
		{	//sets a Pagebreak without any value, only if the field has a value
1562
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF;
1563
		}
1564
		if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0)
1565
		{	//sets a Letterprefix
1566
			$replaceprefixsort = array();
1567
			// ToDo Stefan: $contentstart is NOT defined here!!!
1568
			$replaceprefix = explode(' ',substr($param[0],21,-2));
1569
			foreach ($replaceprefix as $nameprefix)
1570
			{
1571
				if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$'];
1572
			}
1573
			$replace = implode($replaceprefixsort,' ');
1574
		}
1575
		return $replace;
1576
	}
1577
1578
	/**
1579
	 * Download document merged with contact(s)
1580
	 *
1581
	 * @param string $document vfs-path of document
1582
	 * @param array $ids array with contact id(s)
1583
	 * @param string $name ='' name to use for downloaded document
1584
	 * @param string $dirs comma or whitespace separated directories, used if $document is a relative path
1585
	 * @return string with error-message on error, otherwise it does NOT return
1586
	 */
1587
	public function download($document, $ids, $name='', $dirs='')
1588
	{
1589
		//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs') ->".function_backtrace());
1590
		if (($error = $this->check_document($document, $dirs)))
1591
		{
1592
			return $error;
1593
		}
1594
		$content_url = Api\Vfs::PREFIX.$document;
1595
		switch (($mimetype = Api\Vfs::mime_content_type($document)))
1596
		{
1597
			case 'message/rfc822':
1598
				//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs')=>$content_url ->".function_backtrace());
1599
				$mail_bo = Api\Mail::getInstance();
1600
				$mail_bo->openConnection();
1601
				try
1602
				{
1603
					$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...
1604
				}
1605
				catch (Api\Exception\WrongUserinput $e)
1606
				{
1607
					// if this returns with an exeption, something failed big time
1608
					return $e->getMessage();
1609
				}
1610
				//error_log(__METHOD__.__LINE__.' Message after importMessageToMergeAndSend:'.array2string($msgs));
1611
				$retString = '';
1612
				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']);
1613
				//if (strlen($retString)>0) $retString .= '<br />';
1614
				foreach($msgs['failed'] as $c =>$e)
1615
				{
1616
					$errorString .= lang('contact').' '.lang('id').':'.$c.'->'.$e.'.';
1617
				}
1618
				if (count($msgs['failed'])>0) $retString .= count($msgs['failed']).' '.lang('Message(s) send failed!').'=>'.$errorString;
1619
				return $retString;
1620
			case 'application/vnd.oasis.opendocument.text':
1621
			case 'application/vnd.oasis.opendocument.spreadsheet':
1622
				$ext = $mimetype == 'application/vnd.oasis.opendocument.text' ? '.odt' : '.ods';
1623
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,$ext).'-').$ext;
1624
				copy($content_url,$archive);
1625
				$content_url = 'zip://'.$archive.'#'.($content_file = 'content.xml');
1626
				$this->parse_html_styles = true;
1627
				break;
1628
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d':	// mimetypes in vfs are limited to 64 chars
1629
				$mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
1630
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1631
			case 'application/vnd.ms-word.document.macroenabled.12':
1632
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.docx').'-').'.docx';
1633
				copy($content_url,$archive);
1634
				$content_url = 'zip://'.$archive.'#'.($content_file = 'word/document.xml');
1635
				$fix = array(		// regular expression to fix garbled placeholders
1636
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1637
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$$\\1$$',
1638
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="','/').
1639
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>','/').'([a-z0-9_]+)'.
1640
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="','/').
1641
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>$$','/').'/i' => '$$\\2$$',
1642
					'/'.preg_quote('$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1643
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$\\1$',
1644
					'/'.preg_quote('$ $</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1645
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$ $\\1$ $',
1646
				);
1647
				break;
1648
			case 'application/xml':
1649
				$fix = array(	// hack to get Excel 2003 to display additional rows in tables
1650
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1651
				);
1652
				break;
1653
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee':
1654
				$mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
1655
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1656
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1657
				$fix = array(	// hack to get Excel 2007 to display additional rows in tables
1658
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1659
				);
1660
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.xlsx').'-').'.xlsx';
1661
				copy($content_url,$archive);
1662
				$content_url = 'zip://'.$archive.'#'.($content_file = 'xl/sharedStrings.xml');
1663
				break;
1664
		}
1665
		$err = null;
1666
		if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1667
		{
1668
			//error_log(__METHOD__."() !this->merge() err=$err");
1669
			return $err;
1670
		}
1671
		// Apply HTML formatting to target document, if possible
1672
		// check if we can use the XSL extension, to not give a fatal error and rendering whole merge-print non-functional
1673
		if (class_exists(XSLTProcessor) && class_exists(DOMDocument) && $this->parse_html_styles)
1674
		{
1675
			try
1676
			{
1677
				$this->apply_styles($merged, $mimetype);
1678
			}
1679
			catch (\Exception $e)
1680
			{
1681
				// Error converting HTML styles over
1682
				error_log($e->getMessage());
1683
				error_log("Target document: $content_url, IDs: ". array2string($ids));
1684
1685
				// Try again, but strip HTML so user gets something
1686
				$this->parse_html_styles = false;
1687
				if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1688
				{
1689
					return $err;
1690
				}
1691
			}
1692
			if ($this->report_memory_usage) error_log(__METHOD__."() after HTML processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1693
		}
1694
		if(!empty($name))
1695
		{
1696
			if(empty($ext))
1697
			{
1698
				$ext = '.'.pathinfo($document,PATHINFO_EXTENSION);
1699
			}
1700
			$name .= $ext;
1701
		}
1702
		else
1703
		{
1704
			$name = basename($document);
1705
		}
1706
		if (isset($archive))
1707
		{
1708
			$zip = new ZipArchive;
1709
			if ($zip->open($archive, ZipArchive::CHECKCONS) !== true)
1710
			{
1711
				error_log(__METHOD__.__LINE__." !ZipArchive::open('$archive',ZIPARCHIVE"."::CHECKCONS) failed. Trying open without validating");
1712
				if ($zip->open($archive) !== true) throw new Api\Exception("!ZipArchive::open('$archive',|ZIPARCHIVE::CHECKCONS)");
1713
			}
1714
			if ($zip->addFromString($content_file,$merged) !== true) throw new Api\Exception("!ZipArchive::addFromString('$content_file',\$merged)");
1715
			if ($zip->close() !== true) throw new Api\Exception("!ZipArchive::close()");
1716
			unset($zip);
1717
			unset($merged);
1718
			if ($this->report_memory_usage) error_log(__METHOD__."() after ZIP processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1719
			Api\Header\Content::type($name,$mimetype,filesize($archive));
1720
			readfile($archive,'r');
1721
		}
1722
		else
1723
		{
1724
			if ($mimetype == 'application/xml')
1725
			{
1726
				if (strpos($merged,'<?mso-application progid="Word.Document"?>') !== false)
1727
				{
1728
					$mimetype = 'application/msword';	// to open it automatically in word or oowriter
1729
				}
1730
				elseif (strpos($merged,'<?mso-application progid="Excel.Sheet"?>') !== false)
1731
				{
1732
					$mimetype = 'application/vnd.ms-excel';	// to open it automatically in excel or oocalc
1733
				}
1734
			}
1735
			Api\Header\Content::type($name, $mimetype);
1736
			echo $merged;
1737
		}
1738
		exit;
1739
	}
1740
1741
	/**
1742
	 * Download document merged with contact(s)
1743
	 * frontend for HTTP POST requests
1744
	 * accepts POST vars and calls internal function download()
1745
	 *   string data_document_name: the document name
1746
	 *   string data_document_dir: the document vfs directory
1747
	 *   string data_checked: contact id(s) to merge with (can be comma separated)
1748
	 *
1749
	 * @return string with error-message on error, otherwise it does NOT return
1750
	 */
1751
	public function download_by_request()
1752
	{
1753
		if(empty($_POST['data_document_name'])) return false;
1754
		if(empty($_POST['data_document_dir'])) return false;
1755
		if(empty($_POST['data_checked'])) return false;
1756
1757
		return $this->download(
1758
			$_POST['data_document_name'],
1759
			explode(',',$_POST['data_checked']),
1760
			'',
1761
			$_POST['data_document_dir']
1762
		);
1763
	}
1764
1765
	/**
1766
	 * Get a list of document actions / files from the given directory
1767
	 *
1768
	 * @param string $dirs Directory(s comma or space separated) to search
1769
	 * @param string $prefix='document_' prefix for array keys
1770
	 * @param array|string $mime_filter=null allowed mime type(s), default all, negative filter if $mime_filter[0] === '!'
1771
	 * @return array List of documents, suitable for a selectbox.  The key is document_<filename>.
1772
	 */
1773
	public static function get_documents($dirs, $prefix='document_', $mime_filter=null, $app='')
1774
	{
1775
		$export_limit=self::getExportLimit($app);
1776
		if (!$dirs || (!self::hasExportLimit($export_limit,'ISALLOWED') && !self::is_export_limit_excepted())) return array();
1777
1778
		// split multiple comma or whitespace separated directories
1779
		// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
1780 View Code Duplication
		if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
1781
		{
1782
			foreach($dirs as $n => &$d)
1783
			{
1784
				if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
1785
			}
1786
		}
1787
		if ($mime_filter && ($negativ_filter = $mime_filter[0] === '!'))
1788
		{
1789
			if (is_array($mime_filter))
1790
			{
1791
				unset($mime_filter[0]);
1792
			}
1793
			else
1794
			{
1795
				$mime_filter = substr($mime_filter, 1);
1796
			}
1797
		}
1798
		$list = array();
1799
		foreach($dirs as $dir)
1800
		{
1801
			if (($files = Api\Vfs::find($dir,array('need_mime'=>true),true)))
1802
			{
1803
				foreach($files as $file)
1804
				{
1805
					// return only the mime-types we support
1806
					$parts = explode('.',$file['name']);
1807
					if (!self::is_implemented($file['mime'],'.'.array_pop($parts))) continue;
1808
					if ($mime_filter && $negativ_filter === in_array($file['mime'], (array)$mime_filter)) continue;
1809
					$list[$prefix.$file['name']] = Api\Vfs::decodePath($file['name']);
1810
				}
1811
			}
1812
		}
1813
		return $list;
1814
	}
1815
1816
	/**
1817
	 * From this number of documents, show them in submenus by mime type
1818
	 */
1819
	const SHOW_DOCS_BY_MIME_LIMIT = 10;
1820
1821
	/**
1822
	 * Get insert-in-document action with optional default document on top
1823
	 *
1824
	 * If more than SHOW_DOCS_BY_MIME_LIMIT=10 documents found, they are displayed in submenus by mime type.
1825
	 *
1826
	 * @param string $dirs Directory(s comma or space separated) to search
1827
	 * @param int $group see nextmatch_widget::egw_actions
1828
	 * @param string $caption ='Insert in document'
1829
	 * @param string $prefix ='document_'
1830
	 * @param string $default_doc ='' full path to default document to show on top with action == 'document'!
1831
	 * @param int|string $export_limit =null export-limit, default $GLOBALS['egw_info']['server']['export_limit']
1832
	 * @return array see nextmatch_widget::egw_actions
1833
	 */
1834
	public static function document_action($dirs, $group=0, $caption='Insert in document', $prefix='document_', $default_doc='',
1835
		$export_limit=null)
1836
	{
1837
		$documents = array();
1838
		if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting
1839
		if ($default_doc && ($file = Api\Vfs::stat($default_doc)))	// put default document on top
1840
		{
1841
			if(!$file['mime'])
1842
			{
1843
				$file['mime'] = Api\Vfs::mime_content_type($default_doc);
1844
				$file['path'] = $default_doc;
1845
			}
1846
			$documents['document'] = array(
1847
				'icon' => Api\Vfs::mime_icon($file['mime']),
1848
				'caption' => Api\Vfs::decodePath(Api\Vfs::basename($default_doc)),
1849
				'group' => 1,
1850
				'postSubmit' => true,	// download needs post submit (not Ajax) to work
1851
			);
1852
			if ($file['mime'] == 'message/rfc822')
1853
			{
1854
				self::document_mail_action($documents['document'], $file);
1855
			}
1856
		}
1857
1858
		$files = array();
1859
		if ($dirs)
1860
		{
1861
			// split multiple comma or whitespace separated directories
1862
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
1863 View Code Duplication
			if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
1864
			{
1865
				foreach($dirs as $n => &$d)
1866
				{
1867
					if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
1868
				}
1869
			}
1870
			foreach($dirs as $dir)
1871
			{
1872
				$files += Api\Vfs::find($dir,array(
1873
					'need_mime' => true,
1874
					'order' => 'fs_name',
1875
					'sort' => 'ASC',
1876
				),true);
1877
			}
1878
		}
1879
1880
		$dircount = array();
1881
		foreach($files as $key => $file)
1882
		{
1883
			// use only the mime-types we support
1884
			$parts = explode('.',$file['name']);
1885
			if (!self::is_implemented($file['mime'],'.'.array_pop($parts)) ||
1886
				!Api\Vfs::check_access($file['path'], Api\Vfs::READABLE, $file) ||	// remove files not readable by user
1887
				$file['path'] === $default_doc)	// default doc already added
1888
			{
1889
				unset($files[$key]);
1890
			}
1891
			else
1892
			{
1893
				$dirname = Api\Vfs::dirname($file['path']);
1894
				if(!isset($dircount[$dirname]))
1895
				{
1896
					$dircount[$dirname] = 1;
1897
				}
1898
				else
1899
				{
1900
					$dircount[$dirname] ++;
1901
				}
1902
			}
1903
		}
1904
		foreach($files as $file)
1905
		{
1906
			if (count($dircount) > 1)
1907
			{
1908
				$name_arr = explode('/', $file['name']);
1909
				$current_level = &$documents;
1910
				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...
1911
				{
1912
					if($count == 0)
1913
					{
1914
						$current_level = &$documents;
1915
					}
1916
					else
1917
					{
1918
						$current_level = &$current_level[$prefix.$name_arr[($count-1)]]['children'];
1919
					}
1920
					switch($count)
1921
					{
1922
						case (count($name_arr)-1):
1923
							$current_level[$prefix.$file['name']] = array(
1924
								'icon'		=> Api\Vfs::mime_icon($file['mime']),
1925
								'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
1926
								'group'		=> 2,
1927
								'postSubmit' => true,	// download needs post submit (not Ajax) to work
1928
							);
1929
							if ($file['mime'] == 'message/rfc822')
1930
							{
1931
								self::document_mail_action($current_level[$prefix.$file['name']], $file);
1932
							}
1933
							break;
1934
1935
						default:
1936
							if(!is_array($current_level[$prefix.$name_arr[$count]]))
1937
							{
1938
								// create parent folder
1939
								$current_level[$prefix.$name_arr[$count]] = array(
1940
									'icon'		=> 'phpgwapi/foldertree_folder',
1941
									'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
1942
									'group'		=> 2,
1943
									'children'	=> array(),
1944
								);
1945
							}
1946
							break;
1947
					}
1948
				}
1949
			}
1950
			else if (count($files) >= self::SHOW_DOCS_BY_MIME_LIMIT)
1951
			{
1952
				if (!isset($documents[$file['mime']]))
1953
				{
1954
					$documents[$file['mime']] = array(
1955
						'icon' => Api\Vfs::mime_icon($file['mime']),
1956
						'caption' => Api\MimeMagic::mime2label($file['mime']),
1957
						'group' => 2,
1958
						'children' => array(),
1959
					);
1960
				}
1961
				$documents[$file['mime']]['children'][$prefix.$file['name']] = array(
1962
					'caption' => Api\Vfs::decodePath($file['name']),
1963
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
1964
				);
1965
				if ($file['mime'] == 'message/rfc822')
1966
				{
1967
					self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
1968
				}
1969
			}
1970
			else
1971
			{
1972
				$documents[$prefix.$file['name']] = array(
1973
					'icon' => Api\Vfs::mime_icon($file['mime']),
1974
					'caption' => Api\Vfs::decodePath($file['name']),
1975
					'group' => 2,
1976
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
1977
				);
1978
				if ($file['mime'] == 'message/rfc822')
1979
				{
1980
					self::document_mail_action($documents[$prefix.$file['name']], $file);
1981
				}
1982
			}
1983
		}
1984
1985
		return array(
1986
			'icon' => 'etemplate/merge',
1987
			'caption' => $caption,
1988
			'children' => $documents,
1989
			// disable action if no document or export completly forbidden for non-admins
1990
			'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit,'ISALLOWED') || self::is_export_limit_excepted()),
1991
			'hideOnDisabled' => true,	// do not show 'Insert in document', if no documents defined or no export allowed
1992
			'group' => $group,
1993
		);
1994
	}
1995
1996
	/**
1997
	 * Set up a document action for an eml (email) document
1998
	 *
1999
	 * Email (.eml) documents get special action handling.  They don't send a file
2000
	 * back to the client like the other documents.  Merging for a single selected
2001
	 * contact opens a compose window, multiple contacts just sends.
2002
	 *
2003
	 * @param Array &$action Action to be modified for mail
2004
	 * @param Array $file Array of information about the document from Api\Vfs::find
2005
	 * @return void
2006
	 */
2007
	private static function document_mail_action(Array &$action, $file)
2008
	{
2009
		unset($action['postSubmit']);
2010
2011
		// Lots takes a while, confirm
2012
		$action['confirm_multiple'] = lang('Do you want to send the message to all selected entries, WITHOUT further editing?');
2013
2014
		// These parameters trigger compose + merge - only if 1 row
2015
		$extra = array(
2016
			'from=merge',
2017
			'document='.$file['path'],
2018
			'merge='.get_called_class()
2019
		);
2020
2021
		// egw.open() used if only 1 row selected
2022
		$action['egw_open'] = 'edit-mail--'.implode('&',$extra);
2023
		$action['target'] = 'compose_' .$file['path'];
2024
2025
		// long_task runs menuaction once for each selected row
2026
		$action['nm_action'] = 'long_task';
2027
		$action['popup'] = Api\Link::get_registry('mail', 'edit_popup');
2028
		$action['message'] = lang('insert in %1',Api\Vfs::decodePath($file['name']));
2029
		$action['menuaction'] = 'mail.mail_compose.ajax_merge&document='.$file['path'].'&merge='. get_called_class();
2030
	}
2031
2032
	/**
2033
	 * Check if given document (relative path from document_actions()) exists in one of the given dirs
2034
	 *
2035
	 * @param string &$document maybe relative path of document, on return true absolute path to existing document
2036
	 * @param string $dirs comma or whitespace separated directories
2037
	 * @return string|boolean false if document exists, otherwise string with error-message
2038
	 */
2039
	public static function check_document(&$document, $dirs)
2040
	{
2041
		if($document[0] !== '/')
2042
		{
2043
			// split multiple comma or whitespace separated directories
2044
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
2045
			if ($dirs && ($dirs = preg_split('/[,\s]+\//', $dirs)))
2046
			{
2047
				foreach($dirs as $n => $dir)
2048
				{
2049
					if ($n) $dir = '/'.$dir;	// re-adding trailing slash removed by split
2050
					if (Api\Vfs::stat($dir.'/'.$document) && Api\Vfs::is_readable($dir.'/'.$document))
2051
					{
2052
						$document = $dir.'/'.$document;
2053
						return false;
2054
					}
2055
				}
2056
			}
2057
		}
2058
		elseif (Api\Vfs::stat($document) && Api\Vfs::is_readable($document))
2059
		{
2060
			return false;
2061
		}
2062
		//error_log(__METHOD__."('$document', dirs='$dirs') returning 'Document '$document' does not exist or is not readable for you!'");
2063
		return lang("Document '%1' does not exist or is not readable for you!",$document);
2064
	}
2065
2066
	/**
2067
	 * Get a list of supported extentions
2068
	 */
2069
	public static function get_file_extensions()
2070
	{
2071
		return array('txt', 'rtf', 'odt', 'ods', 'docx', 'xml', 'eml');
2072
	}
2073
2074
	/**
2075
	 * Format a number according to user prefs with decimal and thousands separator
2076
	 *
2077
	 * Reimplemented from etemplate to NOT use user prefs for Excel 2003, which gives an xml error
2078
	 *
2079
	 * @param int|float|string $number
2080
	 * @param int $num_decimal_places =2
2081
	 * @param string $_mimetype =''
2082
	 * @return string
2083
	 */
2084
	static public function number_format($number,$num_decimal_places=2,$_mimetype='')
2085
	{
2086
		if ((string)$number === '') return '';
2087
		//error_log(__METHOD__.$_mimetype);
2088
		switch($_mimetype)
2089
		{
2090
			case 'application/xml':	// Excel 2003
2091
			case 'application/vnd.oasis.opendocument.spreadsheet': // OO.o spreadsheet
2092
				return number_format(str_replace(' ','',$number),$num_decimal_places,'.','');
2093
		}
2094
		return Api\Etemplate::number_format($number,$num_decimal_places);
2095
	}
2096
}
2097