x Sorry, these patches are not available anymore due to data migration. Please run a fresh inspection.

Merge   F
last analyzed

Complexity

Total Complexity 560

Size/Duplication

Total Lines 2301
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1127
c 1
b 0
f 0
dl 0
loc 2301
rs 1.092
wmc 560

34 Methods

Rating   Name   Duplication   Size   Complexity  
F is_implemented() 0 45 25
A __construct() 0 13 2
F contact_replacements() 0 112 51
A hook_export_limit_excepted() 0 6 1
B hasExportLimit() 0 6 7
A is_export_limit_excepted() 0 18 5
A number_format() 0 11 4
F merge_string() 0 248 74
F apply_styles() 0 82 23
A get_app() 0 18 3
A share_placeholder() 0 25 5
A create_share() 0 20 3
A download_by_request() 0 11 4
B increase_backtrack_limit() 0 24 7
C format_spreadsheet_numbers() 0 36 15
F merge_file() 0 166 46
B process_commands() 0 32 6
F replace_callback() 0 87 43
A document_editable_action() 0 12 1
C format_spreadsheet_dates() 0 73 15
B getExportLimit() 0 33 7
C get_documents() 0 41 16
A document_mail_action() 0 23 1
A get_app_replacements() 0 28 4
C get_all_links() 0 56 16
F document_action() 0 192 38
A download() 0 12 3
A get_file_extensions() 0 3 1
F replace() 0 264 87
C cf_link_to_expand() 0 66 15
A format_datetime() 0 6 2
B merge() 0 28 8
B check_document() 0 25 10
C get_links() 0 38 12

How to fix   Complexity   

Complex Class

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.

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;
0 ignored issues
show
Bug introduced by
The type EGroupware\Stylite was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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;
0 ignored issues
show
Bug introduced by
The type uiaccountsel was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\addressbook_bo was not found. Did you mean addressbook_bo? If so, make sure to prefix the type with \.
Loading history...
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
	 * Save sent emails.  Used when merge template is an email.  Default is true,
112
	 * to save sent emails in your sent folder.
113
	 *
114
	 * @var boolean
115
	 */
116
	public $keep_emails = true;
117
118
	/**
119
	 * Constructor
120
	 */
121
	function __construct()
122
	{
123
		// Common messages are in preferences
124
		Api\Translation::add_app('preferences');
125
		// All contact fields are in addressbook
126
		Api\Translation::add_app('addressbook');
127
128
		$this->contacts = new Api\Contacts();
0 ignored issues
show
Documentation Bug introduced by
It seems like new EGroupware\Api\Contacts() of type EGroupware\Api\Contacts is incompatible with the declared type EGroupware\Api\Storage\addressbook_bo of property $contacts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
129
130
		$this->datetime_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'].' '.
131
			($GLOBALS['egw_info']['user']['preferences']['common']['timeformat']==12 ? 'h:i a' : 'H:i');
132
133
		$this->export_limit = self::getExportLimit();
0 ignored issues
show
Documentation Bug introduced by
It seems like self::getExportLimit() can also be of type false. However, the property $export_limit is declared as type integer|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
134
	}
135
136
	/**
137
	 * Hook returning options for export_limit_excepted groups
138
	 *
139
	 * @param array $config
140
	 */
141
	public static function hook_export_limit_excepted($config)
142
	{
143
		$accountsel = new uiaccountsel();
144
145
		return '<input type="hidden" value="" name="newsettings[export_limit_excepted]" />'.
146
			$accountsel->selection('newsettings[export_limit_excepted]','export_limit_excepted',$config['export_limit_excepted'],'both',4);
147
	}
148
149
	/**
150
	 * Get all replacements, must be implemented in extending class
151
	 *
152
	 * Can use eg. the following high level methods:
153
	 * - contact_replacements($contact_id,$prefix='')
154
	 * - format_datetime($time,$format=null)
155
	 *
156
	 * @param int $id id of entry
157
	 * @param string &$content=null content to create some replacements only if they are use
158
	 * @return array|boolean array with replacements or false if entry not found
159
	 */
160
	abstract protected function get_replacements($id,&$content=null);
161
162
	/**
163
	 * Return if merge-print is implemented for given mime-type (and/or extension)
164
	 *
165
	 * @param string $mimetype eg. text/plain
166
	 * @param string $extension only checked for applications/msword and .rtf
167
	 */
168
	static public function is_implemented($mimetype,$extension=null)
169
	{
170
		static $zip_available=null;
171
		if (is_null($zip_available))
172
		{
173
			$zip_available = check_load_extension('zip') &&
174
				class_exists('ZipArchive');	// some PHP has zip extension, but no ZipArchive (eg. RHEL5!)
175
		}
176
		switch ($mimetype)
177
		{
178
			case 'application/msword':
179
				if (strtolower($extension) != '.rtf') break;
180
			case 'application/rtf':
181
			case 'text/rtf':
182
				return true;	// rtf files
183
			case 'application/vnd.oasis.opendocument.text':	// oo text
184
			case 'application/vnd.oasis.opendocument.spreadsheet':	// oo spreadsheet
185
			case 'application/vnd.oasis.opendocument.presentation':
186
			case 'application/vnd.oasis.opendocument.text-template':
187
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
188
			case 'application/vnd.oasis.opendocument.presentation-template':
189
				if (!$zip_available) break;
190
				return true;	// open office write xml files
191
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms word 2007 xml format
192
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d':	// mimetypes in vfs are limited to 64 chars
193
			case 'application/vnd.ms-word.document.macroenabled.12':
194
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':	// ms excel 2007 xml format
195
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee':
196
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
197
				if (!$zip_available) break;
198
				return true;	// ms word xml format
199
			case 'application/xml':
200
				return true;	// alias for text/xml, eg. ms office 2003 word format
201
			case 'message/rfc822':
202
				return true; // ToDo: check if you are theoretical able to send mail
203
			case 'application/x-yaml':
204
				return true;	// yaml file, plain text with marginal syntax support for multiline replacements
205
			default:
206
				if (substr($mimetype,0,5) == 'text/')
207
				{
208
					return true;	// text files
209
				}
210
				break;
211
		}
212
		return false;
213
214
		// As browsers not always return correct mime types, one could use a negative list instead
215
		//return !($mimetype == Api\Vfs::DIR_MIME_TYPE || substr($mimetype,0,6) == 'image/');
216
	}
217
218
	/**
219
	 * Return replacements for a contact
220
	 *
221
	 * @param int|string|array $contact contact-array or id
222
	 * @param string $prefix ='' prefix like eg. 'user'
223
	 * @param boolean $ignore_acl =false true: no acl check
224
	 * @return array
225
	 */
226
	public function contact_replacements($contact,$prefix='',$ignore_acl=false, &$content = '')
227
	{
228
		if (!is_array($contact))
229
		{
230
			$contact = $this->contacts->read($contact, $ignore_acl);
231
		}
232
		if (!is_array($contact)) return array();
233
234
		$replacements = array();
235
		foreach(array_keys($this->contacts->contact_fields) as $name)
236
		{
237
			$value = $contact[$name];
238
			switch($name)
239
			{
240
				case 'created': case 'modified':
241
					if($value) $value = Api\DateTime::to($value);
242
					break;
243
				case 'bday':
244
					if ($value)
245
					{
246
						try {
247
							$value = Api\DateTime::to($value, true);
248
						}
249
						catch (\Exception $e) {
250
							unset($e);	// ignore exception caused by wrongly formatted date
251
						}
252
					}
253
					break;
254
				case 'owner': case 'creator': case 'modifier':
255
					$value = Api\Accounts::username($value);
256
					break;
257
				case 'cat_id':
258
					if ($value)
259
					{
260
						// if cat-tree is displayed, we return a full category path not just the name of the cat
261
						$use = $GLOBALS['egw_info']['server']['cat_tab'] == 'Tree' ? 'path' : 'name';
262
						$cats = array();
263
						foreach(is_array($value) ? $value : explode(',',$value) as $cat_id)
264
						{
265
							$cats[] = $GLOBALS['egw']->categories->id2name($cat_id,$use);
266
						}
267
						$value = implode(', ',$cats);
268
					}
269
					break;
270
				case 'jpegphoto':	// returning a link might make more sense then the binary photo
271
					if ($contact['photo'])
272
					{
273
						$value = Api\Framework::getUrl(Api\Framework::link('/index.php',$contact['photo']));
274
					}
275
					break;
276
				case 'tel_prefer':
277
					if ($value && $contact[$value])
278
					{
279
						$value = $contact[$value];
280
					}
281
					break;
282
				case 'account_id':
283
					if ($value)
284
					{
285
						$replacements['$$'.($prefix ? $prefix.'/':'').'account_lid$$'] = $GLOBALS['egw']->accounts->id2name($value);
286
					}
287
					break;
288
			}
289
			if ($name != 'photo') $replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = $value;
290
		}
291
		// set custom fields, should probably go to a general method all apps can use
292
		// need to load all cfs for $ignore_acl=true
293
		foreach($ignore_acl ? Customfields::get('addressbook', true) : $this->contacts->customfields as $name => $field)
294
		{
295
			$name = '#'.$name;
296
			if(!$contact[$name])
297
			{
298
				$replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = '';
299
				continue;
300
			}
301
			// Format date cfs per user Api\Preferences
302
			if($this->mimetype !== 'application/x-yaml' && $contact[$name] &&
303
					($field['type'] == 'date' || $field['type'] == 'date-time'))
304
			{
305
				$this->date_fields[] = '#'.$name;
306
				$replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = Api\DateTime::to($contact[$name], $field['type'] == 'date' ? true : '');
307
			}
308
			$replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] =
309
				// use raw data for yaml, no user-preference specific formatting
310
				$this->mimetype == 'application/x-yaml' || $field['type'] == 'htmlarea' ? (string)$contact[$name] :
311
				Customfields::format($field, (string)$contact[$name]);
312
		}
313
314
		if($content && strpos($content, '$$#') !== FALSE)
315
		{
316
			$this->cf_link_to_expand($contact, $content, $replacements, 'addressbook');
317
		}
318
319
		// Add in extra cat field
320
		$cats = array();
321
		foreach(is_array($contact['cat_id']) ? $contact['cat_id'] : explode(',',$contact['cat_id']) as $cat_id)
322
		{
323
			if(!$cat_id) continue;
324
			if($GLOBALS['egw']->categories->id2name($cat_id,'main') != $cat_id)
325
			{
326
				$path = explode(' / ', $GLOBALS['egw']->categories->id2name($cat_id, 'path'));
327
				unset($path[0]); // Drop main
328
				$cats[$GLOBALS['egw']->categories->id2name($cat_id,'main')][] = implode(' / ', $path);
329
			} elseif($cat_id) {
330
				$cats[$cat_id] = array();
331
			}
332
		}
333
		foreach($cats as $main => $cat) {
334
			$replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] .= $GLOBALS['egw']->categories->id2name($main,'name')
335
				. (count($cat) > 0 ? ': ' : '') . implode(', ', $cats[$main]) . "\n";
336
		}
337
		return $replacements;
338
	}
339
340
	/**
341
	 * Get links for the given record
342
	 *
343
	 * Uses egw_link system to get link titles
344
	 *
345
	 * @param app Name of current app
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\Name was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
346
	 * @param id ID of current entry
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\ID was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
347
	 * @param only_app Restrict links to only given application
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\Restrict was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
348
	 * @param exclude Exclude links to these applications
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\Exclude was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
349
	 * @param style String One of:
350
	 * 	'title' - plain text, just the title of the link
351
	 * 	'link' - URL to the entry
352
	 * 	'href' - HREF tag wrapped around the title
353
	 */
354
	protected function get_links($app, $id, $only_app='', $exclude = array(), $style = 'title')
355
	{
356
		$links = Api\Link::get_links($app, $id, $only_app);
357
		$link_titles = array();
358
		foreach($links as $link_info)
359
		{
360
			// Using only_app only returns the ID
361
			if(!is_array($link_info) && $only_app && $only_app[0] !== '!')
362
			{
363
				$link_info = array(
364
					'app'	=> $only_app,
365
					'id'	=> $link_info
366
				);
367
			}
368
			if($exclude && in_array($link_info['id'], $exclude)) continue;
369
370
			$title = Api\Link::title($link_info['app'], $link_info['id']);
371
			
372
			if($style == 'href' || $style == 'link')
373
			{
374
				$link = Api\Link::view($link_info['app'], $link_info['id'], $link_info);
375
				if($link_info['app'] != Api\Link::VFS_APPNAME)
376
				{
377
					// Set app to false so we always get an external link
378
					$link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false));
379
				}
380
				else
381
				{
382
					$link = Api\Framework::link($link, array());
383
				}
384
				// Prepend site
385
				if ($link[0] == '/') $link = Api\Framework::getUrl($link);
386
387
				$title = $style == 'href' ? Api\Html::a_href(Api\Html::htmlspecialchars($title), $link) : $link;
388
			}
389
			$link_titles[] = $title;
390
		}
391
		return implode("\n",$link_titles);
392
	}
393
394
	/**
395
	 * Get all link placeholders
396
	 *
397
	 * Calls get_links() repeatedly to get all the combinations for the content.
398
	 *
399
	 * @param $app String appname
400
	 * @param $id String ID of record
401
	 * @param $prefix
402
	 * @param $content String document content
403
	 */
404
	protected function get_all_links($app, $id, $prefix, &$content)
405
	{
406
		$array = array();
407
		$pattern = '@\$\$(links_attachments|links|attachments|link)\/?(title|href|link)?\/?([a-z]*)\$\$@';
408
		static $link_cache=null;
409
		$matches = null;
410
		if(preg_match_all($pattern, $content, $matches))
411
		{
412
			foreach($matches[0] as $i => $placeholder)
413
			{
414
				$placeholder = substr($placeholder, 2, -2);
415
				if($link_cache[$id][$placeholder])
416
				{
417
					$array[$placeholder] = $link_cache[$id][$placeholder];
418
					continue;
419
				}
420
				switch($matches[1][$i])
421
				{
422
					case 'link':
423
						// Link to current record
424
						$title = Api\Link::title($app, $id);
425
						if(class_exists('EGroupware\Stylite\Vfs\Links\StreamWrapper') && $app != Api\Link::VFS_APPNAME)
426
						{
427
							$title = Stylite\Vfs\Links\StreamWrapper::entry2name($app, $id, $title);
0 ignored issues
show
Bug introduced by
The type EGroupware\Stylite\Vfs\Links\StreamWrapper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
428
						}
429
430
						$link = Api\Link::view($app, $id);
431
						if($app != Api\Link::VFS_APPNAME)
432
						{
433
							// Set app to false so we always get an external link
434
							$link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false));
435
						}
436
						else
437
						{
438
							$link = Api\Framework::link($link, array());
439
						}
440
						// Prepend site
441
						if ($link[0] == '/') $link = Api\Framework::getUrl($link);
442
443
						$array['$$'.($prefix?$prefix.'/':'').$placeholder.'$$'] = Api\Html::a_href(Api\Html::htmlspecialchars($title), $link);
444
						break;
445
					case 'links':
446
						$link_app = $matches[3][$i] ? $matches[3][$i] :  '!'.Api\Link::VFS_APPNAME;
447
						$array['$$'.($prefix?$prefix.'/':'').$placeholder.'$$'] = $this->get_links($app, $id, $link_app, array(),$matches[2][$i]);
448
						break;
449
					case 'attachments':
450
						$array['$$'.($prefix?$prefix.'/':'').$placeholder.'$$'] = $this->get_links($app, $id, Api\Link::VFS_APPNAME,array(),$matches[2][$i]);
451
						break;
452
					default:
453
						$array['$$'.($prefix?$prefix.'/':'').$placeholder.'$$'] = $this->get_links($app, $id, $matches[3][$i], array(), $matches[2][$i]);
454
						break;
455
				}
456
				$link_cache[$id][$placeholder] = $array[$placeholder];
457
			}
458
		}
459
		return $array;
460
	}
461
462
	/**
463
	 * Get share placeholder
464
	 *
465
	 * If the placeholder is present in the content, the share will be automatically
466
	 * created.
467
	 */
468
	protected function share_placeholder($app, $id, $prefix, &$content)
469
	{
470
		$replacements = array();
471
472
		// Skip if no content or content has no share placeholder
473
		if(!$content || strpos($content, '$$share') === FALSE)
474
		{
475
			return $replacements;
476
		}
477
478
		if(!$GLOBALS['egw_info']['user']['apps']['stylite'])
479
		{
480
			$replacements['$$'.$prefix.'share$$'] = lang('EPL Only');
481
			return $replacements;
482
		}
483
484
		// Get or create the share
485
		$share = $this->create_share($app, $id, $content);
486
487
		if($share)
0 ignored issues
show
introduced by
$share is of type EGroupware\Api\Sharing, thus it always evaluated to true.
Loading history...
488
		{
489
			$replacements['$$'.$prefix.'share$$'] = $link = Api\Sharing::share2link($share);
0 ignored issues
show
Unused Code introduced by
The assignment to $link is dead and can be removed.
Loading history...
490
		}
491
492
		return $replacements;
493
	}
494
495
	/**
496
	 * Create a share for an entry
497
	 *
498
	 * @param string $app
499
	 * @param string $id
500
	 * @param String $content
501
	 * @return \EGroupware\Api\Sharing
502
	 */
503
	protected function create_share($app, $id, &$content)
504
	{
505
		// Check if some other process created the share (with custom options)
506
		// and put it in the session cache for us
507
		$path = "$app::$id";
508
		$session = \EGroupware\Api\Cache::getSession(Api\Sharing::class, $path);
509
		if($session && $session['share_path'] == $path)
510
		{
511
			return $session;
512
		}
513
514
		// Need to create the share here.
515
		// No way to know here if it should be writable, or who it's going to
516
		$mode = /* ?  ? Sharing::WRITABLE :*/ Api\Sharing::READONLY;
517
		$recipients = array();
518
		$extra = array();
519
520
		//$extra['share_writable'] |=  ($mode == Sharing::WRITABLE ? 1 : 0);
521
522
		return \EGroupware\Stylite\Link\Sharing::create($path, $mode, NULL, $recipients, $extra);
0 ignored issues
show
Bug introduced by
The type EGroupware\Stylite\Link\Sharing was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
523
	}
524
525
	/**
526
	 * Format a datetime
527
	 *
528
	 * @param int|string|DateTime $time unix timestamp or Y-m-d H:i:s string (in user time!)
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\DateTime was not found. Did you mean DateTime? If so, make sure to prefix the type with \.
Loading history...
529
	 * @param string $format =null format string, default $this->datetime_format
530
	 * @deprecated use Api\DateTime::to($time='now',$format='')
531
	 * @return string
532
	 */
533
	protected function format_datetime($time,$format=null)
534
	{
535
		trigger_error(__METHOD__ . ' is deprecated, use Api\DateTime::to($time, $format)', E_USER_DEPRECATED);
536
		if (is_null($format)) $format = $this->datetime_format;
537
538
		return Api\DateTime::to($time,$format);
539
	}
540
541
	/**
542
	 * Checks if current user is excepted from the export-limit:
543
	 * a) access to admin application
544
	 * b) he or one of his memberships is named in export_limit_excepted config var
545
	 *
546
	 * @return boolean
547
	 */
548
	public static function is_export_limit_excepted()
549
	{
550
		static $is_excepted=null;
551
552
		if (is_null($is_excepted))
553
		{
554
			$is_excepted = isset($GLOBALS['egw_info']['user']['apps']['admin']);
555
556
			// check export-limit and fail if user tries to export more entries then allowed
557
			if (!$is_excepted && (is_array($export_limit_excepted = $GLOBALS['egw_info']['server']['export_limit_excepted']) ||
558
				is_array($export_limit_excepted = unserialize($export_limit_excepted))))
559
			{
560
				$id_and_memberships = $GLOBALS['egw']->accounts->memberships($GLOBALS['egw_info']['user']['account_id'],true);
561
				$id_and_memberships[] = $GLOBALS['egw_info']['user']['account_id'];
562
				$is_excepted = (bool) array_intersect($id_and_memberships, $export_limit_excepted);
563
			}
564
		}
565
		return $is_excepted;
566
	}
567
568
	/**
569
	 * Checks if there is an exportlimit set, and returns
570
	 *
571
	 * @param string $app ='common' checks and validates app_limit, if not set returns the global limit
572
	 * @return mixed - no if no export is allowed, false if there is no restriction and int as there is a valid restriction
573
	 *		you may have to cast the returned value to int, if you want to use it as number
574
	 */
575
	public static function getExportLimit($app='common')
576
	{
577
		static $exportLimitStore=array();
578
		if (empty($app)) $app='common';
579
		//error_log(__METHOD__.__LINE__.' called with app:'.$app);
580
		if (!array_key_exists($app,$exportLimitStore))
581
		{
582
			//error_log(__METHOD__.__LINE__.' -> '.$app_limit.' '.function_backtrace());
583
			$exportLimitStore[$app] = $GLOBALS['egw_info']['server']['export_limit'];
584
			if ($app !='common')
585
			{
586
				$app_limit = Api\Hooks::single('export_limit',$app);
587
				if ($app_limit) $exportLimitStore[$app] = $app_limit;
588
			}
589
			//error_log(__METHOD__.__LINE__.' building cache for app:'.$app.' -> '.$exportLimitStore[$app]);
590
			if (empty($exportLimitStore[$app]))
591
			{
592
				$exportLimitStore[$app] = false;
593
				return false;
594
			}
595
596
			if (is_numeric($exportLimitStore[$app]))
597
			{
598
				$exportLimitStore[$app] = (int)$exportLimitStore[$app];
599
			}
600
			else
601
			{
602
				$exportLimitStore[$app] = 'no';
603
			}
604
			//error_log(__METHOD__.__LINE__.' -> '.$exportLimit);
605
		}
606
		//error_log(__METHOD__.__LINE__.' app:'.$app.' -> '.$exportLimitStore[$app]);
607
		return $exportLimitStore[$app];
608
	}
609
610
	/**
611
	 * hasExportLimit
612
	 * checks wether there is an exportlimit set, and returns true or false
613
	 * @param mixed $app_limit app_limit, if not set checks the global limit
614
	 * @param string $checkas [AND|ISALLOWED], AND default; if set to ISALLOWED it is checked if Export is allowed
615
	 *
616
	 * @return bool - true if no export is allowed or a limit is set, false if there is no restriction
617
	 */
618
	public static function hasExportLimit($app_limit,$checkas='AND')
619
	{
620
		if (strtoupper($checkas) == 'ISALLOWED') return (empty($app_limit) || ($app_limit !='no' && $app_limit > 0) );
621
		if (empty($app_limit)) return false;
622
		if ($app_limit == 'no') return true;
623
		if ($app_limit > 0) return true;
624
	}
625
626
	/**
627
	 * Merges a given document with contact data
628
	 *
629
	 * @param string $document path/url of document
630
	 * @param array $ids array with contact id(s)
631
	 * @param string &$err error-message on error
632
	 * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf
633
	 * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders
634
	 * @return string|boolean merged document or false on error
635
	 */
636
	public function &merge($document,$ids,&$err,$mimetype,array $fix=null)
637
	{
638
		if (!($content = file_get_contents($document)))
639
		{
640
			$err = lang("Document '%1' does not exist or is not readable for you!",$document);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $document. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

640
			$err = /** @scrutinizer ignore-call */ lang("Document '%1' does not exist or is not readable for you!",$document);

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. Please note the @ignore annotation hint above.

Loading history...
641
			return false;
642
		}
643
644
		if (self::hasExportLimit($this->export_limit) && !self::is_export_limit_excepted() && count($ids) > (int)$this->export_limit)
645
		{
646
			$err = lang('No rights to export more than %1 entries!',(int)$this->export_limit);
647
			return false;
648
		}
649
650
		// fix application/msword mimetype for rtf files
651
		if ($mimetype == 'application/msword' && strtolower(substr($document,-4)) == '.rtf')
652
		{
653
			$mimetype = 'application/rtf';
654
		}
655
656
		try {
657
			$content = $this->merge_string($content,$ids,$err,$mimetype,$fix);
658
		} catch (\Exception $e) {
659
			_egw_log_exception($e);
660
			$err = $e->getMessage();
661
			return false;
662
		}
663
		return $content;
664
	}
665
666
	protected function apply_styles (&$content, $mimetype, $mso_application_progid=null)
667
	{
668
		if (!isset($mso_application_progid))
669
		{
670
			$matches = null;
671
			$mso_application_progid = $mimetype == 'application/xml' &&
672
				preg_match('/'.preg_quote('<?mso-application progid="').'([^"]+)'.preg_quote('"?>').'/',substr($content,0,200),$matches) ?
673
					$matches[1] : '';
674
		}
675
		// Tags we can replace with the target document's version
676
		$replace_tags = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $replace_tags is dead and can be removed.
Loading history...
677
		switch($mimetype.$mso_application_progid)
678
		{
679
			case 'application/vnd.oasis.opendocument.text':	// oo text
680
			case 'application/vnd.oasis.opendocument.spreadsheet':	// oo spreadsheet
681
			case 'application/vnd.oasis.opendocument.presentation':
682
			case 'application/vnd.oasis.opendocument.text-template':
683
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
684
			case 'application/vnd.oasis.opendocument.presentation-template':
685
				$doc = new DOMDocument();
686
				$xslt = new XSLTProcessor();
687
				$doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/openoffice.xslt');
688
				$xslt->importStyleSheet($doc);
689
690
//echo $content;die();
691
				break;
692
			case 'application/xmlWord.Document':	// Word 2003*/
693
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
694
			case 'application/vnd.ms-word.document.macroenabled.12':
695
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
696
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
697
				// It seems easier to split the parent tags here
698
				$replace_tags = array(
699
					// Tables, lists don't go inside <w:p>
700
					'/<(ol|ul|table)( [^>]*)?>/' => '</w:t></w:r></w:p><$1$2>',
701
					'/<\/(ol|ul|table)>/' => '</$1><w:p><w:r><w:t>',
702
					// Fix for things other than text (newlines) inside table row
703
					'/<(td)( [^>]*)?>((?!<w:t>))(.*?)<\/td>[\s]*?/' => '<$1$2><w:t>$4</w:t></td>',
704
					// Remove extra whitespace
705
					'/<li([^>]*?)>[^:print:]*?(.*?)<\/li>/' => '<li$1>$2</li>', // This doesn't get it all
706
					'/<w:t>[\s]+(.*?)<\/w:t>/' => '<w:t>$1</w:t>',
707
					// Remove spans with no attributes, linebreaks inside them cause problems
708
					'/<span>(.*?)<\/span>/' => '$1'
709
				);
710
				$content = preg_replace(array_keys($replace_tags),array_values($replace_tags),$content);
711
712
				/*
713
				In the case where you have something like <span><span></w:t><w:br/><w:t></span></span> (invalid - mismatched tags),
714
				it takes multiple runs to get rid of both spans.  So, loop.
715
				OO.o files have not yet been shown to have this problem.
716
				*/
717
				$count = $i = 0;
718
				do
719
				{
720
					$content = preg_replace('/<span>(.*?)<\/span>/','$1',$content, -1, $count);
721
					$i++;
722
				} while($count > 0 && $i < 10);
723
724
				$doc = new DOMDocument();
725
				$xslt = new XSLTProcessor();
726
				$xslt_file = $mimetype == 'application/xml' ? 'wordml.xslt' : 'msoffice.xslt';
727
				$doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/'.$xslt_file);
728
				$xslt->importStyleSheet($doc);
729
				break;
730
		}
731
732
		// XSLT transform known tags
733
		if($xslt)
734
		{
735
			// does NOT work with php 5.2.6: Catchable fatal error: argument 1 to transformToXml() must be of type DOMDocument
736
			//$element = new SimpleXMLelement($content);
737
			$element = new DOMDocument('1.0', 'utf-8');
738
			$result = $element->loadXML($content);
739
			if(!$result)
740
			{
741
				throw new Api\Exception('Unable to parse merged document for styles.  Check warnings in log for details.');
742
			}
743
			$content = $xslt->transformToXml($element);
744
//echo $content;die();
745
			// Word 2003 needs two declarations, add extra declaration back in
746
			if($mimetype == 'application/xml' && $mso_application_progid == 'Word.Document' && strpos($content, '<?xml') !== 0) {
747
				$content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'.$content;
748
			}
749
			// Validate
750
			/*
751
			$doc = new DOMDocument();
752
			$doc->loadXML($content);
753
			$doc->schemaValidate(*Schema (xsd) file*);
754
			*/
755
		}
756
	}
757
758
	/**
759
	 * Merges a given document with contact data
760
	 *
761
	 * @param string $_content
762
	 * @param array $ids array with contact id(s)
763
	 * @param string &$err error-message on error
764
	 * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf
765
	 * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders
766
	 * @param string $charset =null charset to override default set by mimetype or export charset
767
	 * @return string|boolean merged document or false on error
768
	 */
769
	public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null)
770
	{
771
		$matches = null;
772
		if ($mimetype == 'application/xml' &&
773
			preg_match('/'.preg_quote('<?mso-application progid="').'([^"]+)'.preg_quote('"?>').'/',substr($_content,0,200),$matches))
774
		{
775
			$mso_application_progid = $matches[1];
776
		}
777
		else
778
		{
779
			$mso_application_progid = '';
780
		}
781
		// alternative syntax using double curly brackets (eg. {{cat_id}} instead $$cat_id$$),
782
		// agressivly removing all xml-tags eg. Word adds within placeholders
783
		$content = preg_replace_callback('/{{[^}]+}}/i', function($matches)
784
		{
785
			return '$$'.strip_tags(substr($matches[0], 2, -2)).'$$';
786
		}, $_content);
787
		// Handle escaped placeholder markers in RTF, they won't match when escaped
788
		if($mimetype == 'application/rtf')
789
		{
790
			$content = preg_replace('/\\\{\\\{([^\\}]+)\\\}\\\}/i','$$\1$$',$content);
791
		}
792
793
		// make currently processed mimetype available to class methods;
794
		$this->mimetype = $mimetype;
795
796
		// fix garbled placeholders
797
		if ($fix && is_array($fix))
798
		{
799
			$content = preg_replace(array_keys($fix),array_values($fix),$content);
800
			//die("<pre>".htmlspecialchars($content)."</pre>\n");
801
		}
802
		list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
803
		if ($mimetype == 'text/plain' && $ids && count($ids) > 1)
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
804
		{
805
			// textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag
806
			// header and footer should not hold any $$ tags; if we find $$ tags with the header, we assume it is the pagerepeatcontent
807
			$nohead = false;
808
			if (stripos($contentstart,'$$') !== false) $nohead = true;
809
			if ($nohead)
810
			{
811
				$contentend = $contentrepeat;
812
				$contentrepeat = $contentstart;
813
				$contentstart = '';
814
			}
815
816
		}
817
		if (in_array($mimetype, array('application/vnd.oasis.opendocument.text','application/vnd.oasis.opendocument.text-template')) && count($ids) > 1)
818
		{
819
			if(strpos($content, '$$pagerepeat') === false)
820
			{
821
				//for odt files we have to split the content and add a style for page break to  the style area
822
				list($contentstart,$contentrepeat,$contentend) = preg_split('/office:body>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
823
				$contentstart = substr($contentstart,0,strlen($contentstart)-1);  //remove "<"
0 ignored issues
show
Unused Code introduced by
The assignment to $contentstart is dead and can be removed.
Loading history...
824
				$contentrepeat = substr($contentrepeat,0,strlen($contentrepeat)-2);  //remove "</";
825
				// need to add page-break style to the style list
826
				list($stylestart,$stylerepeat,$styleend) = preg_split('/<\/office:automatic-styles>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document style sheets
827
				$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>';
828
				$contentstart .= '<office:body>';
829
				$contentend = '</office:body></office:document-content>';
830
			}
831
			else
832
			{
833
				// Template specifies where to repeat
834
				list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get different parts of document, seperated by pagerepeat
835
			}
836
		}
837
		if (in_array($mimetype, array('application/vnd.ms-word.document.macroenabled.12', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) && count($ids) > 1)
838
		{
839
			//for Word 2007 XML files we have to split the content and add a style for page break to  the style area
840
			list($contentstart,$contentrepeat,$contentend) = preg_split('/w:body>/',$content,-1, PREG_SPLIT_NO_EMPTY);  //get differt parts of document, seperatet by Pagerepeat
841
			$contentstart = substr($contentstart,0,strlen($contentstart)-1);  //remove "</"
842
			$contentrepeat = substr($contentrepeat,0,strlen($contentrepeat)-2);  //remove "</";
843
			$contentstart .= '<w:body>';
844
			$contentend = '</w:body></w:document>';
845
		}
846
		list($Labelstart,$Labelrepeat,$Labeltend) = preg_split('/\$\$label\$\$/',$contentrepeat,-1, PREG_SPLIT_NO_EMPTY);  //get the Lable content
847
		preg_match_all('/\$\$labelplacement\$\$/',$contentrepeat,$countlables, PREG_SPLIT_NO_EMPTY);
848
		$countlables = count($countlables[0]);
849
		preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1);
850
		if ($countlables > 1) $lableprint = true;
851
		if (count($ids) > 1 && !$contentrepeat)
852
		{
853
			$err = lang('for more than one contact in a document use the tag pagerepeat!');
854
			return false;
855
		}
856
		if ($this->report_memory_usage) error_log(__METHOD__."(count(ids)=".count($ids).") strlen(contentrepeat)=".strlen($contentrepeat).', strlen(labelrepeat)='.strlen($Labelrepeat));
857
858
		if ($contentrepeat)
859
		{
860
			$content_stream = fopen('php://temp','r+');
861
			fwrite($content_stream, $contentstart);
862
			$joiner = '';
863
			switch($mimetype)
864
			{
865
				case 'application/rtf':
866
				case 'text/rtf':
867
					$joiner = '\\par \\page\\pard\\plain';
868
					break;
869
				case 'application/vnd.oasis.opendocument.text':	// oo text
870
				case 'application/vnd.oasis.opendocument.spreadsheet':	// oo spreadsheet
871
				case 'application/vnd.oasis.opendocument.presentation':
872
				case 'application/vnd.oasis.opendocument.text-template':
873
				case 'application/vnd.oasis.opendocument.spreadsheet-template':
874
				case 'application/vnd.oasis.opendocument.presentation-template':
875
				case 'application/xml':
876
				case 'text/html':
877
				case 'text/csv':
878
					$joiner = '';
879
					break;
880
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
881
				case 'application/vnd.ms-word.document.macroenabled.12':
882
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
883
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
884
					$joiner = '<w:br w:type="page" />';
885
					break;
886
				case 'text/plain':
887
					$joiner = "\r\n";
888
					break;
889
				default:
890
					$err = lang('%1 not implemented for %2!','$$pagerepeat$$',$mimetype);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with '$$pagerepeat$$'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

890
					$err = /** @scrutinizer ignore-call */ lang('%1 not implemented for %2!','$$pagerepeat$$',$mimetype);

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. Please note the @ignore annotation hint above.

Loading history...
891
					return false;
892
			}
893
		}
894
		foreach ((array)$ids as $n => $id)
895
		{
896
			if ($contentrepeat) $content = $contentrepeat;   //content to repeat
897
			if ($lableprint) $content = $Labelrepeat;
898
899
			// generate replacements; if exeption is thrown, catch it set error message and return false
900
			try
901
			{
902
				if(!($replacements = $this->get_replacements($id,$content)))
903
				{
904
					$err = lang('Entry not found!');
905
					return false;
906
				}
907
			}
908
			catch (Api\Exception\WrongUserinput $e)
909
			{
910
				// if this returns with an exeption, something failed big time
911
				$err = $e->getMessage();
912
				return false;
913
			}
914
			if ($this->report_memory_usage) error_log(__METHOD__."() $n: $id ".Api\Vfs::hsize(memory_get_usage(true)));
915
			// some general replacements: current user, date and time
916
			if (strpos($content,'$$user/') !== null && ($user = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id')))
917
			{
918
				$replacements += $this->contact_replacements($user,'user', false, $content);
919
				$replacements['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'account_primary_group'));
920
			}
921
			$replacements['$$date$$'] = Api\DateTime::to('now',true);
922
			$replacements['$$datetime$$'] = Api\DateTime::to('now');
923
			$replacements['$$time$$'] = Api\DateTime::to('now',false);
924
925
			$app = $this->get_app();
926
			$replacements += $this->share_placeholder($app, $id, $prefix, $content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $prefix seems to be never defined.
Loading history...
927
928
			// does our extending class registered table-plugins AND document contains table tags
929
			if ($this->table_plugins && preg_match_all('/\\$\\$table\\/([A-Za-z0-9_]+)\\$\\$(.*?)\\$\\$endtable\\$\\$/s',$content,$matches,PREG_SET_ORDER))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->table_plugins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
930
			{
931
				// process each table
932
				foreach($matches as $match)
933
				{
934
					$plugin   = $match[1];	// plugin name
935
					$callback = $this->table_plugins[$plugin];
936
					$repeat   = $match[2];	// line to repeat
937
					$repeats = '';
938
					if (isset($callback))
939
					{
940
						for($n = 0; ($row_replacements = $this->$callback($plugin,$id,$n,$repeat)); ++$n)
941
						{
942
							$_repeat = $this->process_commands($repeat, $row_replacements);
943
							$repeats .= $this->replace($_repeat,$row_replacements,$mimetype,$mso_application_progid);
944
						}
945
					}
946
					$content = str_replace($match[0],$repeats,$content);
947
				}
948
			}
949
			$content = $this->process_commands($this->replace($content,$replacements,$mimetype,$mso_application_progid,$charset), $replacements);
950
951
			// remove not existing replacements (eg. from calendar array)
952
			if (strpos($content,'$$') !== null)
953
			{
954
				$content = preg_replace('/\$\$[a-z0-9_\/]+\$\$/i','',$content);
955
			}
956
			if ($contentrepeat)
957
			{
958
				fwrite($content_stream, ($n == 0 ? '' : $joiner) . $content);
959
			}
960
			if($lableprint)
961
			{
962
				$contentrep[is_array($id) ? implode(':',$id) : $id] = $content;
963
			}
964
		}
965
		if ($Labelrepeat)
966
		{
967
			$countpage=0;
968
			$count=0;
969
			$contentrepeatpages[$countpage] = $Labelstart.$Labeltend;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$contentrepeatpages was never initialized. Although not strictly required by PHP, it is generally a good practice to add $contentrepeatpages = array(); before regardless.
Loading history...
970
971
			foreach ($contentrep as $Label)
972
			{
973
				$contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/',$Label,$contentrepeatpages[$countpage],1);
974
				$count=$count+1;
975
				if (($count % $countlables) == 0 && count($contentrep)>$count)  //new page
976
				{
977
					$countpage = $countpage+1;
978
					$contentrepeatpages[$countpage] = $Labelstart.$Labeltend;
979
				}
980
			}
981
			$contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/','',$contentrepeatpages[$countpage],-1);  //clean empty fields
982
983
			switch($mimetype)
984
			{
985
				case 'application/rtf':
986
				case 'text/rtf':
987
					return $contentstart.implode('\\par \\page\\pard\\plain',$contentrepeatpages).$contentend;
988
				case 'application/vnd.oasis.opendocument.text':
989
				case 'application/vnd.oasis.opendocument.presentation':
990
				case 'application/vnd.oasis.opendocument.text-template':
991
				case 'application/vnd.oasis.opendocument.presentation-template':
992
					return $contentstart.implode('<text:line-break />',$contentrepeatpages).$contentend;
993
				case 'application/vnd.oasis.opendocument.spreadsheet':
994
				case 'application/vnd.oasis.opendocument.spreadsheet-template':
995
					return $contentstart.implode('</text:p><text:p>',$contentrepeatpages).$contentend;
996
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
997
				case 'application/vnd.ms-word.document.macroenabled.12':
998
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
999
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1000
					return $contentstart.implode('<w:br w:type="page" />',$contentrepeatpages).$contentend;
1001
				case 'text/plain':
1002
					return $contentstart.implode("\r\n",$contentrep).$contentend;
1003
			}
1004
			$err = lang('%1 not implemented for %2!','$$labelplacement$$',$mimetype);
1005
			return false;
1006
		}
1007
1008
		if ($contentrepeat)
1009
		{
1010
			fwrite($content_stream, $contentend);
1011
			rewind($content_stream);
1012
			return stream_get_contents($content_stream);
1013
		}
1014
		if ($this->report_memory_usage) error_log(__METHOD__."() returning ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1015
1016
		return $content;
1017
	}
1018
1019
	/**
1020
	 * Replace placeholders in $content of $mimetype with $replacements
1021
	 *
1022
	 * @param string $content
1023
	 * @param array $replacements name => replacement pairs
1024
	 * @param string $mimetype mimetype of content
1025
	 * @param string $mso_application_progid ='' MS Office 2003: 'Excel.Sheet' or 'Word.Document'
1026
	 * @param string $charset =null charset to override default set by mimetype or export charset
1027
	 * @return string
1028
	 */
1029
	protected function replace($content,array $replacements,$mimetype,$mso_application_progid='',$charset=null)
1030
	{
1031
		switch($mimetype)
1032
		{
1033
			case 'application/vnd.oasis.opendocument.text':		// open office
1034
			case 'application/vnd.oasis.opendocument.spreadsheet':
1035
			case 'application/vnd.oasis.opendocument.presentation':
1036
			case 'application/vnd.oasis.opendocument.text-template':
1037
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
1038
			case 'application/vnd.oasis.opendocument.presentation-template':
1039
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1040
			case 'application/vnd.ms-word.document.macroenabled.12':
1041
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1042
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1043
			case 'application/xml':
1044
			case 'text/xml':
1045
				$is_xml = true;
1046
				$charset = 'utf-8';	// xml files --> always use utf-8
1047
				break;
1048
1049
			case 'application/rtf':
1050
			case 'text/rtf':
1051
				$charset = 'iso-8859-1';	// rtf seems to user iso-8859-1 or equivalent windows charset, not utf-8
1052
				break;
1053
1054
			case 'text/html':
1055
				$is_xml = true;
1056
				$matches = null;
1057
				if (preg_match('/<meta http-equiv="content-type".*charset=([^;"]+)/i',$content,$matches))
1058
				{
1059
					$charset = $matches[1];
1060
				}
1061
				elseif (empty($charset))
1062
				{
1063
					$charset = 'utf-8';
1064
				}
1065
				break;
1066
1067
			default:	// div. text files --> use our export-charset, defined in addressbook prefs
1068
				if (empty($charset)) $charset = $this->contacts->prefs['csv_charset'];
1069
				break;
1070
		}
1071
		//error_log(__METHOD__."('$document', ... ,$mimetype) --> $charset (egw=".Api\Translation::charset().', export='.$this->contacts->prefs['csv_charset'].')');
1072
1073
		// do we need to convert charset
1074
		if ($charset && $charset != Api\Translation::charset())
1075
		{
1076
			$replacements = Api\Translation::convert($replacements,Api\Translation::charset(),$charset);
1077
		}
1078
1079
		// Date only placeholders for timestamps
1080
		if(is_array($this->date_fields))
0 ignored issues
show
introduced by
The condition is_array($this->date_fields) is always true.
Loading history...
1081
		{
1082
			foreach($this->date_fields as $field)
1083
			{
1084
				if(($value = $replacements['$$'.$field.'$$']))
1085
				{
1086
					$time = Api\DateTime::createFromFormat('+'.Api\DateTime::$user_dateformat.' '.Api\DateTime::$user_timeformat.'*', $value);
1087
					$replacements['$$'.$field.'/date$$'] = $time ? $time->format(Api\DateTime::$user_dateformat)  : '';
1088
				}
1089
			}
1090
		}
1091
		if ($is_xml)	// zip'ed xml document (eg. OO)
1092
		{
1093
			// Numeric fields
1094
			$names = array();
1095
1096
			// Tags we can replace with the target document's version
1097
			$replace_tags = array();
1098
			// only keep tags, if we have xsl extension available
1099
			if (class_exists('XSLTProcessor') && class_exists('DOMDocument') && $this->parse_html_styles)
1100
			{
1101
				switch($mimetype.$mso_application_progid)
1102
				{
1103
					case 'text/html':
1104
						$replace_tags = array(
1105
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1106
							'<table>','<tr>','<td>','<a>','<style>','<img>',
1107
						);
1108
						break;
1109
					case 'application/vnd.oasis.opendocument.text':		// open office
1110
					case 'application/vnd.oasis.opendocument.spreadsheet':
1111
					case 'application/vnd.oasis.opendocument.presentation':
1112
					case 'application/vnd.oasis.opendocument.text-template':
1113
					case 'application/vnd.oasis.opendocument.spreadsheet-template':
1114
					case 'application/vnd.oasis.opendocument.presentation-template':
1115
						$replace_tags = array(
1116
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1117
							'<table>','<tr>','<td>','<a>',
1118
						);
1119
						break;
1120
					case 'application/xmlWord.Document':	// Word 2003*/
1121
					case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1122
					case 'application/vnd.ms-word.document.macroenabled.12':
1123
					case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1124
					case 'application/vnd.ms-excel.sheet.macroenabled.12':
1125
						$replace_tags = array(
1126
							'<b>','<strong>','<i>','<em>','<u>','<span>','<ol>','<ul>','<li>',
1127
							'<table>','<tr>','<td>',
1128
						);
1129
						break;
1130
				}
1131
			}
1132
			// clean replacements from array values and html or html-entities, which mess up xml
1133
			foreach($replacements as $name => &$value)
1134
			{
1135
				// set unresolved array values to empty string
1136
				if(is_array($value))
1137
				{
1138
					$value = '';
1139
					continue;
1140
				}
1141
				// decode html entities back to utf-8
1142
1143
				if (is_string($value) && (strpos($value,'&') !== false) && $this->parse_html_styles)
1144
				{
1145
					$value = html_entity_decode($value,ENT_QUOTES,$charset);
1146
1147
					// remove all non-decodable entities
1148
					if (strpos($value,'&') !== false)
1149
					{
1150
						$value = preg_replace('/&[^; ]+;/','',$value);
1151
					}
1152
				}
1153
				if(!$this->parse_html_styles || (
1154
					strpos($value, "\n") !== FALSE &&
1155
						strpos($value,'<br') === FALSE && strpos($value, '<span') === FALSE && strpos($value, '<p') === FALSE && strpos($value, '<div') === FALSE
1156
				))
1157
				{
1158
					// Encode special chars so they don't break the file
1159
					$value = htmlspecialchars($value,ENT_NOQUOTES);
1160
				}
1161
				else if (is_string($value) && (strpos($value,'<') !== false))
1162
				{
1163
					// Clean HTML, if it's being kept
1164
					if($replace_tags && extension_loaded('tidy')) {
1165
						$tidy = new tidy();
1166
						$cleaned = $tidy->repairString($value, self::$tidy_config);
1167
						// Found errors. Strip it all so there's some output
1168
						if($tidy->getStatus() == 2)
1169
						{
1170
							error_log($tidy->errorBuffer);
1171
							$value = strip_tags($value);
1172
						}
1173
						else
1174
						{
1175
							$value = $cleaned;
1176
						}
1177
					}
1178
					// replace </p> and <br /> with CRLF (remove <p> and CRLF)
1179
					$value = strip_tags(str_replace(array("\r","\n",'<p>','</p>','<div>','</div>','<br />'),
1180
						array('','','',"\r\n",'',"\r\n","\r\n"), $value),
1181
						implode('', $replace_tags));
1182
1183
					// Change <tag>...\r\n</tag> to <tag>...</tag>\r\n or simplistic line break below will mangle it
1184
					// Loop to catch things like <b><span>Break:\r\n</span></b>
1185
					if($mso_application_progid)
1186
					{
1187
						$count = $i = 0;
1188
						do
1189
						{
1190
							$value = preg_replace('/<(b|strong|i|em|u|span)\b([^>]*?)>(.*?)'."\r\n".'<\/\1>/u', '<$1$2>$3</$1>'."\r\n",$value,-1,$count);
1191
							$i++;
1192
						} while($count > 0 && $i < 10); // Limit of 10 chosen arbitrarily just in case
1193
					}
1194
				}
1195
				// replace all control chars (C0+C1) but CR (\015), LF (\012) and TAB (\011) (eg. vertical tabulators) with space
1196
				// as they are not allowed in xml
1197
				$value = preg_replace('/[\000-\010\013\014\016-\037\177-\237]/u',' ',$value);
1198
				if(is_numeric($value) && $name != '$$user/account_id$$') // account_id causes problems with the preg_replace below
1199
				{
1200
					$names[] = preg_quote($name,'/');
1201
				}
1202
			}
1203
1204
			// Look for numbers, set their value if needed
1205
			if($this->numeric_fields || count($names))
1206
			{
1207
				foreach((array)$this->numeric_fields as $fieldname) {
1208
					$names[] = preg_quote($fieldname,'/');
1209
				}
1210
				$this->format_spreadsheet_numbers($content, $names, $mimetype.$mso_application_progid);
1211
			}
1212
1213
			// Look for dates, set their value if needed
1214
			if($this->date_fields || count($names))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->date_fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1215
			{
1216
				$names = array();
1217
				foreach((array)$this->date_fields as $fieldname) {
1218
					$names[] = $fieldname;
1219
				}
1220
				$this->format_spreadsheet_dates($content, $names, $replacements, $mimetype.$mso_application_progid);
1221
			}
1222
1223
			// replace CRLF with linebreak tag of given type
1224
			switch($mimetype.$mso_application_progid)
1225
			{
1226
				case 'application/vnd.oasis.opendocument.text':		// open office writer
1227
				case 'application/vnd.oasis.opendocument.text-template':
1228
				case 'application/vnd.oasis.opendocument.presentation':
1229
				case 'application/vnd.oasis.opendocument.presentation-template':
1230
					$break = '<text:line-break/>';
1231
					break;
1232
				case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1233
				case 'application/vnd.oasis.opendocument.spreadsheet-template':
1234
					$break = '</text:p><text:p>';
1235
					break;
1236
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms word 2007
1237
				case 'application/vnd.ms-word.document.macroenabled.12':
1238
					$break = '</w:t><w:br/><w:t>';
1239
					break;
1240
				case 'application/xmlExcel.Sheet':	// Excel 2003
1241
					$break = '&#10;';
1242
					break;
1243
				case 'application/xmlWord.Document':	// Word 2003*/
1244
					$break = '</w:t><w:br/><w:t>';
1245
					break;
1246
				case 'text/html':
1247
					$break = '<br/>';
1248
					break;
1249
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':	// ms excel 2007
1250
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1251
				default:
1252
					$break = "\r\n";
1253
					break;
1254
			}
1255
			// now decode &, < and >, which need to be encoded as entities in xml
1256
			// Check for encoded >< getting double-encoded
1257
			if($this->parse_html_styles)
1258
			{
1259
				$replacements = str_replace(array('&',"\r","\n",'&amp;lt;','&amp;gt;'),array('&amp;','',$break,'&lt;','&gt;'),$replacements);
1260
			}
1261
			else
1262
			{
1263
				// Need to at least handle new lines, or it'll be run together on one line
1264
				$replacements = str_replace(array("\r","\n"),array('',$break),$replacements);
1265
			}
1266
		}
1267
		if ($mimetype == 'application/x-yaml')
1268
		{
1269
			$content = preg_replace_callback('/^( +)([^$\n]*)(\$\$.+?\$\$)/m', function($matches) use ($replacements)
1270
			{
1271
				// allow with {{name/replace/with}} syntax to replace eg. commas with linebreaks: "{{name/, */\n}}"
1272
				$parts = null;
1273
				if (preg_match('|^\$\$([^/]+)/([^/]+)/([^$]*)\$\$$|', $matches[3], $parts) && isset($replacements['$$'.$parts[1].'$$']))
1274
				{
1275
					$replacement =& $replacements['$$'.$parts[1].'$$'];
1276
					$replacement = preg_replace('/'.$parts[2].'/', strtr($parts[3], array(
1277
						'\\n' => "\n", '\\r' => "\r", '\\t' => "\t", '\\v' => "\v", '\\\\' => '\\', '\\f' => "\f",
1278
					)), $replacement);
1279
				}
1280
				else
1281
				{
1282
					$replacement =& $replacements[$matches[3]];
1283
				}
1284
				// replacement with multiple lines --> add same number of space as before placeholder
1285
				if (isset($replacement))
1286
				{
1287
					return $matches[1].$matches[2].implode("\n".$matches[1], preg_split("/\r?\n/", $replacement));
1288
				}
1289
				return $matches[0];	// regular replacement below
1290
			}, $content);
1291
		}
1292
		return str_replace(array_keys($replacements),array_values($replacements),$content);
1293
	}
1294
1295
	/**
1296
	 * Convert numeric values in spreadsheets into actual numeric values
1297
	 */
1298
	protected function format_spreadsheet_numbers(&$content, $names, $mimetype)
1299
	{
1300
		foreach((array)$this->numeric_fields as $fieldname) {
1301
			$names[] = preg_quote($fieldname,'/');
1302
		}
1303
		switch($mimetype)
1304
		{
1305
			case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1306
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
1307
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)(?:calcext:value-type="[^"]+")?>.?<([a-z].*?)[^>]*>('.implode('|',$names).')<\/\3>.?<\/table:table-cell>/s';
1308
				$replacement = '<table:table-cell$1office:value-type="float" office:value="$4"$2><$3>$4</$3></table:table-cell>';
1309
				break;
1310
			case 'application/vnd.oasis.opendocument.text':		// tables in open office writer
1311
			case 'application/vnd.oasis.opendocument.presentation':
1312
			case 'application/vnd.oasis.opendocument.text-template':
1313
			case 'application/vnd.oasis.opendocument.presentation-template':
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="float" office:value="$4"$2><text:p text:style-name="Standard">$4</text:p></table:table-cell>';
1316
				break;
1317
			case 'application/vnd.oasis.opendocument.text':		// open office writer
1318
			case 'application/xmlExcel.Sheet':	// Excel 2003
1319
				$format = '/'.preg_quote('<Data ss:Type="String">','/').'('.implode('|',$names).')'.preg_quote('</Data>','/').'/';
1320
				$replacement = '<Data ss:Type="Number">$1</Data>';
1321
1322
				break;
1323
		}
1324
		if($format && $names)
1325
		{
1326
			// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
1327
			do {
1328
				$result = preg_replace($format, $replacement, $content, -1);
1329
			}
1330
			// try to increase/double pcre.backtrack_limit failure
1331
			while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit());
1332
1333
			if ($result) $content = $result;  // On failure $result would be NULL
1334
		}
1335
	}
1336
1337
	/**
1338
	 * Increase/double prce.backtrack_limit up to 1/4 of memory_limit
1339
	 *
1340
	 * @return boolean true: backtrack_limit increased, may try again, false limit already to high
1341
	 */
1342
	protected static function increase_backtrack_limit()
1343
	{
1344
		static $backtrack_limit=null,$memory_limit=null;
1345
		if (!isset($backtrack_limit))
1346
		{
1347
			$backtrack_limit = ini_get('pcre.backtrack_limit');
1348
		}
1349
		if (!isset($memory_limit))
1350
		{
1351
			$memory_limit = ini_get('memory_limit');
1352
			switch(strtoupper(substr($memory_limit, -1)))
1353
			{
1354
				case 'G': $memory_limit *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
1355
				case 'M': $memory_limit *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
1356
				case 'K': $memory_limit *= 1024;
1357
			}
1358
		}
1359
		if ($backtrack_limit < $memory_limit/8)
1360
		{
1361
			ini_set( 'pcre.backtrack_limit', $backtrack_limit*=2);
1362
			return true;
1363
		}
1364
		error_log("pcre.backtrack_limit exceeded @ $backtrack_limit, some cells left as text.");
1365
		return false;
1366
	}
1367
1368
	/**
1369
	 * Convert date / timestamp values in spreadsheets into actual date / timestamp values
1370
	 */
1371
	protected function format_spreadsheet_dates(&$content, $names, &$values, $mimetype)
1372
	{
1373
		if(!in_array($mimetype, array(
1374
			'application/vnd.oasis.opendocument.spreadsheet',		// open office calc
1375
			'application/xmlExcel.Sheet',					// Excel 2003
1376
			//'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'//Excel WTF
1377
		))) return;
1378
1379
		// Some different formats dates could be in, depending what they've been through
1380
		$formats = array(
0 ignored issues
show
Unused Code introduced by
The assignment to $formats is dead and can be removed.
Loading history...
1381
			'!'.Api\DateTime::$user_dateformat . ' ' .Api\DateTime::$user_timeformat.':s',
1382
			'!'.Api\DateTime::$user_dateformat . '*' .Api\DateTime::$user_timeformat.':s',
1383
			'!'.Api\DateTime::$user_dateformat . '* ' .Api\DateTime::$user_timeformat,
1384
			'!'.Api\DateTime::$user_dateformat . '*',
1385
			'!'.Api\DateTime::$user_dateformat,
1386
			'!Y-m-d\TH:i:s'
1387
		);
1388
1389
		// Properly format values for spreadsheet
1390
		foreach($names as $idx => &$field)
1391
		{
1392
			$key = '$$'.$field.'$$';
1393
			$field = preg_quote($field, '/');
1394
			if($values[$key])
1395
			{
1396
				$date = Api\DateTime::createFromUserFormat($values[$key]);
1397
				if($mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
1398
					$mimetype == 'application/vnd.ms-excel.sheet.macroenabled.12')//Excel WTF
1399
				{
1400
					$interval = $date->diff(new Api\DateTime('1900-01-00 0:00'));
1401
					$values[$key] = $interval->format('%a')+1;// 1900-02-29 did not exist
1402
					// 1440 minutes in a day - fractional part
1403
					$values[$key] += ($date->format('H') * 60 + $date->format('i'))/1440;
1404
				}
1405
				else
1406
				{
1407
					$values[$key] = date('Y-m-d\TH:i:s',Api\DateTime::to($date,'ts'));
1408
				}
1409
			}
1410
			else
1411
			{
1412
				unset($names[$idx]);
1413
			}
1414
		}
1415
1416
		switch($mimetype)
1417
		{
1418
			case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1419
				// Removing these forces calc to respect our set value-type
1420
				$content = str_ireplace('calcext:value-type="string"','',$content);
1421
1422
				$format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>\$\$('.implode('|',$names).')\$\$<\/\3>.?<\/table:table-cell>/s';
1423
				$replacement = '<table:table-cell$1office:value-type="date" office:date-value="\$\$$4\$\$"$2><$3>\$\$$4\$\$</$3></table:table-cell>';
1424
				break;
1425
			case 'application/xmlExcel.Sheet':	// Excel 2003
1426
				$format = '/'.preg_quote('<Data ss:Type="String">','/').'..('.implode('|',$names).')..'.preg_quote('</Data>','/').'/';
1427
				$replacement = '<Data ss:Type="DateTime">\$\$$1\$\$</Data>';
1428
1429
				break;
1430
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1431
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1432
				break;
1433
		}
1434
		if($format && $names)
1435
		{
1436
			// Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs
1437
			do {
1438
				$result = preg_replace($format, $replacement, $content, -1);
1439
			}
1440
			// try to increase/double pcre.backtrack_limit failure
1441
			while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit());
1442
1443
			if ($result) $content = $result;  // On failure $result would be NULL
1444
		}
1445
	}
1446
1447
	/**
1448
	 * Expand link_to custom fields with the merge replacements from the app
1449
	 * but only if the template uses them.
1450
	 */
1451
	public function cf_link_to_expand($values, $content, &$replacements, $app = null)
1452
	{
1453
		if($app == null)
1454
		{
1455
			$app = str_replace('_merge','',get_class($this));
1456
		}
1457
		$cfs = Api\Storage\Customfields::get($app);
1458
1459
		// Cache, in case more than one sub-placeholder is used
1460
		$app_replacements = array();
1461
1462
		// Custom field placeholders look like {{#name}}
1463
		// Placeholders that need expanded will look like {{#name/placeholder}}
1464
		$matches = null;
1465
		preg_match_all('/\${2}(([^\/#]*?\/)?)#([^$\/]+)\/(.*?)[$}]{2}/', $content, $matches);
1466
		list($placeholders, , , $cf, $sub) = $matches;
1467
1468
		// Collect any used custom fields from entries so you can do
1469
		// {{#other_app/#other_app_cf/n_fn}}
1470
		$expand_sub_cfs = [];
1471
		foreach($sub as $index => $cf_sub)
1472
		{
1473
			if(strpos($cf_sub, '#') === 0)
1474
			{
1475
				$expand_sub_cfs[$cf[$index]] .= '$$'.$cf_sub . '$$ ';
1476
			}
1477
		}
1478
1479
		foreach($cf as $index => $field)
1480
		{
1481
			if($cfs[$field])
1482
			{
1483
				if(in_array($cfs[$field]['type'],array_keys($GLOBALS['egw_info']['apps'])))
1484
				{
1485
					$field_app = $cfs[$field]['type'];
1486
				}
1487
				else if ($cfs[$field]['type'] == 'api-accounts' || $cfs[$field]['type'] == 'select-account')
1488
				{
1489
					// Special case for api-accounts -> contact
1490
					$field_app = 'addressbook';
1491
					$account = $GLOBALS['egw']->accounts->read($values['#'.$field]);
1492
					$app_replacements[$field] = $this->contact_replacements($account['person_id']);
1493
				}
1494
				else if (($list = explode('-',$cfs[$field]['type']) && in_array($list[0], array_keys($GLOBALS['egw_info']['apps']))))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $list seems to be defined later in this foreach loop on line 1494. Are you sure it is defined here?
Loading history...
1495
				{
1496
					// Sub-type - use app
1497
					$field_app = $list[0];
1498
				}
1499
				else
1500
				{
1501
					continue;
1502
				}
1503
1504
				// Get replacements for that application
1505
				if(!$app_replacements[$field])
1506
				{
1507
					// If we send the real content it can result in infinite loop of lookups
1508
					// so we send only the used fields
1509
					$content = $expand_sub_cfs[$field] ? $expand_sub_cfs[$field] : '';
1510
					$app_replacements[$field] = $this->get_app_replacements($field_app, $values['#'.$field], $content);
1511
				}
1512
				$replacements[$placeholders[$index]] = $app_replacements[$field]['$$'.$sub[$index].'$$'];
1513
			}
1514
			else
1515
			{
1516
				if ($cfs[$field]['type'] == 'date' || $cfs[$field]['type'] == 'date-time') $this->date_fields[] = '#'.$field;
1517
			}
1518
		}
1519
	}
1520
1521
	/**
1522
	 * Figure out which app we're running as
1523
	 *
1524
	 * @return string
1525
	 */
1526
	protected function get_app()
1527
	{
1528
		switch (get_class($this))
1529
		{
1530
			case 'EGroupware\Api\Contacts\Merge':
1531
				$app = 'addressbook';
1532
				break;
1533
			default:
1534
				$app = str_replace('_merge','',get_class($this));
1535
				if(!in_array($app, array_keys($GLOBALS['egw_info']['apps'])))
1536
				{
1537
					$app = false;
1538
				}
1539
				break;
1540
1541
		}
1542
1543
		return $app;
1544
	}
1545
1546
	/**
1547
	 * Get the replacements for any entry specified by app & id
1548
	 *
1549
	 * @param stribg $app
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\stribg was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1550
	 * @param string $id
1551
	 * @param string $content
1552
	 * @return array
1553
	 */
1554
	public function get_app_replacements($app, $id, $content, $prefix='')
1555
	{
1556
		$replacements = array();
1557
		if($app == 'addressbook')
0 ignored issues
show
introduced by
The condition $app == 'addressbook' is always false.
Loading history...
1558
		{
1559
			return $this->contact_replacements($id, $prefix, false, $content);
1560
		}
1561
1562
		try
1563
		{
1564
			$classname = "{$app}_merge";
1565
			$class = new $classname();
1566
			$method = $app.'_replacements';
1567
			if(method_exists($class,$method))
1568
			{
1569
				$replacements = $class->$method($id, $prefix, $content);
1570
			}
1571
			else
1572
			{
1573
				$replacements = $class->get_replacements($id, $content);
1574
			}
1575
		}
1576
		catch (\Exception $e)
1577
		{
1578
			// Don't break merge, just log it
1579
			error_log($e->getMessage());
1580
		}
1581
		return $replacements;
1582
	}
1583
1584
	/**
1585
	 * Process special flags, such as IF or NELF
1586
	 *
1587
	 * @param content Text to be examined and changed
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\Text was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1588
	 * @param replacements array of markers => replacement
1589
	 *
1590
	 * @return changed content
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Storage\changed was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1591
	 */
1592
	private function process_commands($content, $replacements)
1593
	{
1594
		if (strpos($content,'$$IF') !== false)
1595
		{	//Example use to use: $$IF n_prefix~Herr~Sehr geehrter~Sehr geehrte$$
1596
			$this->replacements =& $replacements;
0 ignored issues
show
Bug Best Practice introduced by
The property replacements does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1597
			$content = preg_replace_callback('/\$\$IF ([#0-9a-z_\/-]+)~(.*)~(.*)~(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1598
			unset($this->replacements);
1599
		}
1600
		if (strpos($content,'$$NELF') !== false)
1601
		{	//Example: $$NEPBR org_unit$$ sets a LF and value of org_unit, only if there is a value
1602
			$this->replacements =& $replacements;
1603
			$content = preg_replace_callback('/\$\$NELF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1604
			unset($this->replacements);
1605
		}
1606
		if (strpos($content,'$$NENVLF') !== false)
1607
		{	//Example: $$NEPBRNV org_unit$$ sets only a LF if there is a value for org_units, but did not add any value
1608
			$this->replacements =& $replacements;
1609
			$content = preg_replace_callback('/\$\$NENVLF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content);
1610
			unset($this->replacements);
1611
		}
1612
		if (strpos($content,'$$LETTERPREFIX$$') !== false)
1613
		{	//Example use to use: $$LETTERPREFIX$$
1614
			$LETTERPREFIXCUSTOM = '$$LETTERPREFIXCUSTOM n_prefix title n_family$$';
1615
			$content = str_replace('$$LETTERPREFIX$$',$LETTERPREFIXCUSTOM,$content);
1616
		}
1617
		if (strpos($content,'$$LETTERPREFIXCUSTOM') !== false)
1618
		{	//Example use to use for a custom Letter Prefix: $$LETTERPREFIX n_prefix title n_family$$
1619
			$this->replacements =& $replacements;
1620
			$content = preg_replace_callback('/\$\$LETTERPREFIXCUSTOM ([#0-9a-z_-]+)(.*)\$\$/imU',Array($this,'replace_callback'),$content);
1621
			unset($this->replacements);
1622
		}
1623
		return $content;
1624
	}
1625
1626
	/**
1627
	 * Callback for preg_replace to process $$IF
1628
	 *
1629
	 * @param array $param
1630
	 * @return string
1631
	 */
1632
	private function replace_callback($param)
1633
	{
1634
		if (array_key_exists('$$'.$param[4].'$$',$this->replacements)) $param[4] = $this->replacements['$$'.$param[4].'$$'];
1635
		if (array_key_exists('$$'.$param[3].'$$',$this->replacements)) $param[3] = $this->replacements['$$'.$param[3].'$$'];
1636
1637
		$pattern = '/'.preg_quote($param[2], '/').'/';
1638
		if (strpos($param[0],'$$IF') === 0 && (trim($param[2]) == "EMPTY" || $param[2] === ''))
1639
		{
1640
			$pattern = '/^$/';
1641
		}
1642
		$replace = preg_match($pattern,$this->replacements['$$'.$param[1].'$$']) ? $param[3] : $param[4];
1643
		switch($this->mimetype)
1644
		{
1645
			case 'application/vnd.oasis.opendocument.text':		// open office
1646
			case 'application/vnd.oasis.opendocument.spreadsheet':
1647
			case 'application/vnd.oasis.opendocument.presentation':
1648
			case 'application/vnd.oasis.opendocument.text-template':
1649
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
1650
			case 'application/vnd.oasis.opendocument.presentation-template':
1651
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':	// ms office 2007
1652
			case 'application/vnd.ms-word.document.macroenabled.12':
1653
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1654
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1655
			case 'application/xml':
1656
			case 'text/xml':
1657
			case 'text/html':
1658
				$is_xml = true;
1659
				break;
1660
		}
1661
1662
		switch($this->mimetype)
1663
			{
1664
				case 'application/rtf':
1665
				case 'text/rtf':
1666
					$LF = '}\par \pard\plain{';
1667
					break;
1668
				case 'application/vnd.oasis.opendocument.text':
1669
				case 'application/vnd.oasis.opendocument.presentation':
1670
				case 'application/vnd.oasis.opendocument.text-template':
1671
				case 'application/vnd.oasis.opendocument.presentation-template':
1672
					$LF ='<text:line-break/>';
1673
					break;
1674
				case 'application/vnd.oasis.opendocument.spreadsheet':		// open office calc
1675
				case 'application/vnd.oasis.opendocument.spreadsheet-template':
1676
					$LF = '</text:p><text:p>';
1677
					break;
1678
				case 'application/xmlExcel.Sheet':	// Excel 2003
1679
					$LF = '&#10;';
1680
					break;
1681
				case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1682
				case 'application/vnd.ms-word.document.macroenabled.12':
1683
				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1684
				case 'application/vnd.ms-excel.sheet.macroenabled.12':
1685
					$LF ='</w:t></w:r></w:p><w:p><w:r><w:t>';
1686
					break;
1687
				case 'application/xml';
1688
					$LF ='</w:t></w:r><w:r><w:br w:type="text-wrapping" w:clear="all"/></w:r><w:r><w:t>';
1689
					break;
1690
				case 'text/html':
1691
					$LF = "<br/>";
1692
					break;
1693
				default:
1694
					$LF = "\n";
1695
			}
1696
		if($is_xml) {
1697
			$this->replacements = str_replace(array('&','&amp;amp;','<','>',"\r","\n"),array('&amp;','&amp;','&lt;','&gt;','',$LF),$this->replacements);
0 ignored issues
show
Bug Best Practice introduced by
The property replacements does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1698
		}
1699
		if (strpos($param[0],'$$NELF') === 0)
1700
		{	//sets a Pagebreak and value, only if the field has a value
1701
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF.$this->replacements['$$'.$param[1].'$$'];
1702
		}
1703
		if (strpos($param[0],'$$NENVLF') === 0)
1704
		{	//sets a Pagebreak without any value, only if the field has a value
1705
			if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF;
1706
		}
1707
		if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0)
1708
		{	//sets a Letterprefix
1709
			$replaceprefixsort = array();
1710
			// ToDo Stefan: $contentstart is NOT defined here!!!
1711
			$replaceprefix = explode(' ',substr($param[0],21,-2));
1712
			foreach ($replaceprefix as $nameprefix)
1713
			{
1714
				if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$'];
1715
			}
1716
			$replace = implode($replaceprefixsort,' ');
0 ignored issues
show
Unused Code introduced by
The call to implode() has too many arguments starting with ' '. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1716
			$replace = /** @scrutinizer ignore-call */ implode($replaceprefixsort,' ');

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. Please note the @ignore annotation hint above.

Loading history...
1717
		}
1718
		return $replace;
1719
	}
1720
1721
	/**
1722
	 * Download document merged with contact(s)
1723
	 *
1724
	 * @param string $document vfs-path of document
1725
	 * @param array $ids array with contact id(s)
1726
	 * @param string $name ='' name to use for downloaded document
1727
	 * @param string $dirs comma or whitespace separated directories, used if $document is a relative path
1728
	 * @return string with error-message on error, otherwise it does NOT return
1729
	 */
1730
	public function download($document, $ids, $name='', $dirs='')
1731
	{
1732
		$result = $this->merge_file($document, $ids, $name, $dirs, $header);
1733
1734
		if(is_file($result) && is_readable($result))
1735
		{
1736
			Api\Header\Content::type($header['name'],$header['mime'],$header['filesize']);
1737
			readfile($result,'r');
1738
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1739
		}
1740
1741
		return $result;
1742
	}
1743
1744
	/**
1745
	 * Merge the IDs into the document, puts the document into the output buffer
1746
	 *
1747
	 * @param string $document vfs-path of document
1748
	 * @param array $ids array with contact id(s)
1749
	 * @param string $name ='' name to use for downloaded document
1750
	 * @param string $dirs comma or whitespace separated directories, used if $document is a relative path
1751
	 * @param Array $header File name, mime & filesize if you want to send a header
1752
	 *
1753
	 * @return string with error-message on error
1754
	 * @throws Api\Exception
1755
	 */
1756
	public function merge_file($document, $ids, &$name='', $dirs='', &$header)
0 ignored issues
show
Coding Style introduced by
Parameters which have default values should be placed at the end.

If you place a parameter with a default value before a parameter with a default value, the default value of the first parameter will never be used as it will always need to be passed anyway:

// $a must always be passed; it's default value is never used.
function someFunction($a = 5, $b) { }
Loading history...
1757
	{
1758
		//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs') ->".function_backtrace());
1759
		if (($error = $this->check_document($document, $dirs)))
1760
		{
1761
			return $error;
1762
		}
1763
		$content_url = Api\Vfs::PREFIX.$document;
1764
		switch (($mimetype = Api\Vfs::mime_content_type($document)))
1765
		{
1766
			case 'message/rfc822':
1767
				//error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs')=>$content_url ->".function_backtrace());
1768
				$mail_bo = Api\Mail::getInstance();
1769
				$mail_bo->openConnection();
1770
				try
1771
				{
1772
					$msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, $_folder=($this->keep_emails ? '' : FALSE));
0 ignored issues
show
Bug introduced by
$_folder = $this->keep_emails ? '' : FALSE cannot be passed to EGroupware\Api\Mail::importMessageToMergeAndSend() as the parameter $_folder expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1772
					$msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, /** @scrutinizer ignore-type */ $_folder=($this->keep_emails ? '' : FALSE));
Loading history...
1773
				}
1774
				catch (Api\Exception\WrongUserinput $e)
1775
				{
1776
					// if this returns with an exeption, something failed big time
1777
					return $e->getMessage();
1778
				}
1779
				//error_log(__METHOD__.__LINE__.' Message after importMessageToMergeAndSend:'.array2string($msgs));
1780
				$retString = '';
1781
				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']);
1782
				//if (strlen($retString)>0) $retString .= '<br />';
1783
				foreach($msgs['failed'] as $c =>$e)
1784
				{
1785
					$errorString .= lang('contact').' '.lang('id').':'.$c.'->'.$e.'.';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $errorString does not exist. Did you maybe mean $error?
Loading history...
1786
				}
1787
				if (count($msgs['failed'])>0) $retString .= count($msgs['failed']).' '.lang('Message(s) send failed!').'=>'.$errorString;
1788
				return $retString;
1789
			case 'application/vnd.oasis.opendocument.text':
1790
			case 'application/vnd.oasis.opendocument.spreadsheet':
1791
			case 'application/vnd.oasis.opendocument.presentation':
1792
			case 'application/vnd.oasis.opendocument.text-template':
1793
			case 'application/vnd.oasis.opendocument.spreadsheet-template':
1794
			case 'application/vnd.oasis.opendocument.presentation-template':
1795
				switch($mimetype)
1796
				{
1797
					case 'application/vnd.oasis.opendocument.text':	$ext = '.odt'; break;
1798
					case 'application/vnd.oasis.opendocument.spreadsheet': $ext = '.ods'; break;
1799
					case 'application/vnd.oasis.opendocument.presentation': $ext = '.odp'; break;
1800
					case 'application/vnd.oasis.opendocument.text-template': $ext = '.ott'; break;
1801
					case 'application/vnd.oasis.opendocument.spreadsheet-template': $ext = '.ots'; break;
1802
					case 'application/vnd.oasis.opendocument.presentation-template': $ext = '.otp'; break;
1803
				}
1804
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,$ext).'-').$ext;
1805
				copy($content_url,$archive);
1806
				$content_url = 'zip://'.$archive.'#'.($content_file = 'content.xml');
1807
				$this->parse_html_styles = true;
1808
				break;
1809
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d':	// mimetypes in vfs are limited to 64 chars
1810
				$mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
1811
			case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
1812
			case 'application/vnd.ms-word.document.macroenabled.12':
1813
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.docx').'-').'.docx';
1814
				copy($content_url,$archive);
1815
				$content_url = 'zip://'.$archive.'#'.($content_file = 'word/document.xml');
1816
				$fix = array(		// regular expression to fix garbled placeholders
1817
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1818
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$$\\1$$',
1819
					'/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="','/').
1820
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>','/').'([a-z0-9_]+)'.
1821
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="','/').
1822
						'([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>$$','/').'/i' => '$$\\2$$',
1823
					'/'.preg_quote('$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1824
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$\\1$',
1825
					'/'.preg_quote('$ $</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'.
1826
						preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$ $\\1$ $',
1827
				);
1828
				break;
1829
			case 'application/xml':
1830
				$fix = array(	// hack to get Excel 2003 to display additional rows in tables
1831
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1832
				);
1833
				break;
1834
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee':
1835
				$mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
1836
			case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
1837
			case 'application/vnd.ms-excel.sheet.macroenabled.12':
1838
				$fix = array(	// hack to get Excel 2007 to display additional rows in tables
1839
					'/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"',
1840
				);
1841
				$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.xlsx').'-').'.xlsx';
1842
				copy($content_url,$archive);
1843
				$content_url = 'zip://'.$archive.'#'.($content_file = 'xl/sharedStrings.xml');
1844
				break;
1845
		}
1846
		$err = null;
1847
		if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1848
		{
1849
			//error_log(__METHOD__."() !this->merge() err=$err");
1850
			return $err;
1851
		}
1852
		// Apply HTML formatting to target document, if possible
1853
		// check if we can use the XSL extension, to not give a fatal error and rendering whole merge-print non-functional
1854
		if (class_exists('XSLTProcessor') && class_exists('DOMDocument') && $this->parse_html_styles)
1855
		{
1856
			try
1857
			{
1858
				$this->apply_styles($merged, $mimetype);
1859
			}
1860
			catch (\Exception $e)
1861
			{
1862
				// Error converting HTML styles over
1863
				error_log($e->getMessage());
1864
				error_log("Target document: $content_url, IDs: ". array2string($ids));
1865
1866
				// Try again, but strip HTML so user gets something
1867
				$this->parse_html_styles = false;
1868
				if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix)))
1869
				{
1870
					return $err;
1871
				}
1872
			}
1873
			if ($this->report_memory_usage) error_log(__METHOD__."() after HTML processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1874
		}
1875
		if(!empty($name))
1876
		{
1877
			if(empty($ext))
1878
			{
1879
				$ext = '.'.pathinfo($document,PATHINFO_EXTENSION);
1880
			}
1881
			$name .= $ext;
1882
		}
1883
		else
1884
		{
1885
			$name = basename($document);
1886
		}
1887
		$header = array('name' => $name, 'mime' => $mimetype);
1888
		if (isset($archive))
1889
		{
1890
			$zip = new ZipArchive;
1891
			if ($zip->open($archive, ZipArchive::CHECKCONS) !== true)
1892
			{
1893
				error_log(__METHOD__.__LINE__." !ZipArchive::open('$archive',ZIPARCHIVE"."::CHECKCONS) failed. Trying open without validating");
1894
				if ($zip->open($archive) !== true) throw new Api\Exception("!ZipArchive::open('$archive',|ZIPARCHIVE::CHECKCONS)");
1895
			}
1896
			if ($zip->addFromString($content_file,$merged) !== true) throw new Api\Exception("!ZipArchive::addFromString('$content_file',\$merged)");
1897
			if ($zip->close() !== true) throw new Api\Exception("!ZipArchive::close()");
1898
			unset($zip);
1899
			unset($merged);
1900
			if ($this->report_memory_usage) error_log(__METHOD__."() after ZIP processing ".Api\Vfs::hsize(memory_get_peak_usage(true)));
1901
			$header['filesize'] = filesize($archive);
1902
		}
1903
		else
1904
		{
1905
			$archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.'.$ext).'-').'.'.$ext;
1906
			if ($mimetype == 'application/xml')
1907
			{
1908
				if (strpos($merged,'<?mso-application progid="Word.Document"?>') !== false)
1909
				{
1910
					$header['mimetype'] = 'application/msword';	// to open it automatically in word or oowriter
1911
				}
1912
				elseif (strpos($merged,'<?mso-application progid="Excel.Sheet"?>') !== false)
1913
				{
1914
					$header['mimetype'] = 'application/vnd.ms-excel';	// to open it automatically in excel or oocalc
1915
				}
1916
			}
1917
			$handle = fopen($archive, 'w');
1918
			fwrite($handle, $merged);
1919
			fclose($handle);
1920
		}
1921
		return $archive;
1922
	}
1923
1924
	/**
1925
	 * Download document merged with contact(s)
1926
	 * frontend for HTTP POST requests
1927
	 * accepts POST vars and calls internal function download()
1928
	 *   string data_document_name: the document name
1929
	 *   string data_document_dir: the document vfs directory
1930
	 *   string data_checked: contact id(s) to merge with (can be comma separated)
1931
	 *
1932
	 * @return string with error-message on error, otherwise it does NOT return
1933
	 */
1934
	public function download_by_request()
1935
	{
1936
		if(empty($_POST['data_document_name'])) return false;
1937
		if(empty($_POST['data_document_dir'])) return false;
1938
		if(empty($_POST['data_checked'])) return false;
1939
1940
		return $this->download(
1941
			$_POST['data_document_name'],
1942
			explode(',',$_POST['data_checked']),
1943
			'',
1944
			$_POST['data_document_dir']
1945
		);
1946
	}
1947
1948
	/**
1949
	 * Get a list of document actions / files from the given directory
1950
	 *
1951
	 * @param string $dirs Directory(s comma or space separated) to search
1952
	 * @param string $prefix='document_' prefix for array keys
1953
	 * @param array|string $mime_filter=null allowed mime type(s), default all, negative filter if $mime_filter[0] === '!'
1954
	 * @return array List of documents, suitable for a selectbox.  The key is document_<filename>.
1955
	 */
1956
	public static function get_documents($dirs, $prefix='document_', $mime_filter=null, $app='')
1957
	{
1958
		$export_limit=self::getExportLimit($app);
1959
		if (!$dirs || (!self::hasExportLimit($export_limit,'ISALLOWED') && !self::is_export_limit_excepted())) return array();
1960
1961
		// split multiple comma or whitespace separated directories
1962
		// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
1963
		if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
1964
		{
1965
			foreach($dirs as $n => &$d)
1966
			{
1967
				if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
1968
			}
1969
		}
1970
		if ($mime_filter && ($negativ_filter = $mime_filter[0] === '!'))
1971
		{
1972
			if (is_array($mime_filter))
1973
			{
1974
				unset($mime_filter[0]);
1975
			}
1976
			else
1977
			{
1978
				$mime_filter = substr($mime_filter, 1);
1979
			}
1980
		}
1981
		$list = array();
1982
		foreach($dirs as $dir)
1983
		{
1984
			if (($files = Api\Vfs::find($dir,array('need_mime'=>true),true)))
1985
			{
1986
				foreach($files as $file)
1987
				{
1988
					// return only the mime-types we support
1989
					$parts = explode('.',$file['name']);
1990
					if (!self::is_implemented($file['mime'],'.'.array_pop($parts))) continue;
1991
					if ($mime_filter && $negativ_filter === in_array($file['mime'], (array)$mime_filter)) continue;
1992
					$list[$prefix.$file['name']] = Api\Vfs::decodePath($file['name']);
1993
				}
1994
			}
1995
		}
1996
		return $list;
1997
	}
1998
1999
	/**
2000
	 * From this number of documents, show them in submenus by mime type
2001
	 */
2002
	const SHOW_DOCS_BY_MIME_LIMIT = 10;
2003
2004
	/**
2005
	 * Get insert-in-document action with optional default document on top
2006
	 *
2007
	 * If more than SHOW_DOCS_BY_MIME_LIMIT=10 documents found, they are displayed in submenus by mime type.
2008
	 *
2009
	 * @param string $dirs Directory(s comma or space separated) to search
2010
	 * @param int $group see nextmatch_widget::egw_actions
2011
	 * @param string $caption ='Insert in document'
2012
	 * @param string $prefix ='document_'
2013
	 * @param string $default_doc ='' full path to default document to show on top with action == 'document'!
2014
	 * @param int|string $export_limit =null export-limit, default $GLOBALS['egw_info']['server']['export_limit']
2015
	 * @return array see nextmatch_widget::egw_actions
2016
	 */
2017
	public static function document_action($dirs, $group=0, $caption='Insert in document', $prefix='document_', $default_doc='',
2018
		$export_limit=null)
2019
	{
2020
		$documents = array();
2021
		$editable_mimes = array();
2022
		if ($export_limit == null) $export_limit = self::getExportLimit(); // check if there is a globalsetting
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $export_limit of type integer|null|string against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
2023
2024
		try {
2025
			if (class_exists('EGroupware\\collabora\\Bo') &&
2026
					$GLOBALS['egw_info']['user']['apps']['collabora'] &&
2027
					($discovery = \EGroupware\collabora\Bo::discover()) &&
0 ignored issues
show
Bug introduced by
The type EGroupware\collabora\Bo was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2028
					$GLOBALS['egw_info']['user']['preferences']['filemanager']['merge_open_handler'] != 'download'
2029
			)
2030
			{
2031
				$editable_mimes = $discovery;
2032
			}
2033
		}
2034
		catch (\Exception $e)
2035
		{
2036
			// ignore failed discovery
2037
			unset($e);
2038
		}
2039
		if ($default_doc && ($file = Api\Vfs::stat($default_doc)))	// put default document on top
2040
		{
2041
			if(!$file['mime'])
2042
			{
2043
				$file['mime'] = Api\Vfs::mime_content_type($default_doc);
2044
				$file['path'] = $default_doc;
2045
			}
2046
			$documents['document'] = array(
2047
				'icon' => Api\Vfs::mime_icon($file['mime']),
2048
				'caption' => Api\Vfs::decodePath(Api\Vfs::basename($default_doc)),
2049
				'group' => 1,
2050
				'postSubmit' => true,	// download needs post submit (not Ajax) to work
2051
			);
2052
			if ($file['mime'] == 'message/rfc822')
2053
			{
2054
				self::document_mail_action($documents['document'], $file);
2055
			}
2056
			else if ($editable_mimes[$file['mime']])
2057
			{
2058
				self::document_editable_action($documents['document'], $file);
2059
			}
2060
		}
2061
2062
		$files = array();
2063
		if ($dirs)
2064
		{
2065
			// split multiple comma or whitespace separated directories
2066
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
2067
			if (count($dirs = preg_split('/[,\s]+\//', $dirs)) > 1)
2068
			{
2069
				foreach($dirs as $n => &$d)
2070
				{
2071
					if ($n) $d = '/'.$d;	// re-adding trailing slash removed by split
2072
				}
2073
			}
2074
			foreach($dirs as $dir)
2075
			{
2076
				$files += Api\Vfs::find($dir,array(
2077
					'need_mime' => true,
2078
					'order' => 'fs_name',
2079
					'sort' => 'ASC',
2080
				),true);
2081
			}
2082
		}
2083
2084
		$dircount = array();
2085
		foreach($files as $key => $file)
2086
		{
2087
			// use only the mime-types we support
2088
			$parts = explode('.',$file['name']);
2089
			if (!self::is_implemented($file['mime'],'.'.array_pop($parts)) ||
2090
				!Api\Vfs::check_access($file['path'], Api\Vfs::READABLE, $file) ||	// remove files not readable by user
2091
				$file['path'] === $default_doc)	// default doc already added
2092
			{
2093
				unset($files[$key]);
2094
			}
2095
			else
2096
			{
2097
				$dirname = Api\Vfs::dirname($file['path']);
2098
				if(!isset($dircount[$dirname]))
2099
				{
2100
					$dircount[$dirname] = 1;
2101
				}
2102
				else
2103
				{
2104
					$dircount[$dirname] ++;
2105
				}
2106
			}
2107
		}
2108
		foreach($files as $file)
2109
		{
2110
			if (count($dircount) > 1)
2111
			{
2112
				$name_arr = explode('/', $file['name']);
2113
				$current_level = &$documents;
2114
				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...
2115
				{
2116
					if($count == 0)
2117
					{
2118
						$current_level = &$documents;
2119
					}
2120
					else
2121
					{
2122
						$current_level = &$current_level[$prefix.$name_arr[($count-1)]]['children'];
2123
					}
2124
					switch($count)
2125
					{
2126
						case (count($name_arr)-1):
2127
							$current_level[$prefix.$file['name']] = array(
2128
								'icon'		=> Api\Vfs::mime_icon($file['mime']),
2129
								'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
2130
								'group'		=> 2,
2131
								'postSubmit' => true,	// download needs post submit (not Ajax) to work
2132
							);
2133
							if ($file['mime'] == 'message/rfc822')
2134
							{
2135
								self::document_mail_action($current_level[$prefix.$file['name']], $file);
2136
							}
2137
							else if ($editable_mimes[$file['mime']])
2138
							{
2139
								self::document_editable_action($current_level[$prefix.$file['name']], $file);
2140
							}
2141
							break;
2142
2143
						default:
2144
							if(!is_array($current_level[$prefix.$name_arr[$count]]))
2145
							{
2146
								// create parent folder
2147
								$current_level[$prefix.$name_arr[$count]] = array(
2148
									'icon'		=> 'phpgwapi/foldertree_folder',
2149
									'caption'	=> Api\Vfs::decodePath($name_arr[$count]),
2150
									'group'		=> 2,
2151
									'children'	=> array(),
2152
								);
2153
							}
2154
							break;
2155
					}
2156
				}
2157
			}
2158
			else if (count($files) >= self::SHOW_DOCS_BY_MIME_LIMIT)
2159
			{
2160
				if (!isset($documents[$file['mime']]))
2161
				{
2162
					$documents[$file['mime']] = array(
2163
						'icon' => Api\Vfs::mime_icon($file['mime']),
2164
						'caption' => Api\MimeMagic::mime2label($file['mime']),
2165
						'group' => 2,
2166
						'children' => array(),
2167
					);
2168
				}
2169
				$documents[$file['mime']]['children'][$prefix.$file['name']] = array(
2170
					'caption' => Api\Vfs::decodePath($file['name']),
2171
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
2172
				);
2173
				if ($file['mime'] == 'message/rfc822')
2174
				{
2175
					self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
2176
				}
2177
				else if ($editable_mimes[$file['mime']])
2178
				{
2179
					self::document_editable_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
2180
				}
2181
			}
2182
			else
2183
			{
2184
				$documents[$prefix.$file['name']] = array(
2185
					'icon' => Api\Vfs::mime_icon($file['mime']),
2186
					'caption' => Api\Vfs::decodePath($file['name']),
2187
					'group' => 2,
2188
					'postSubmit' => true,	// download needs post submit (not Ajax) to work
2189
				);
2190
				if ($file['mime'] == 'message/rfc822')
2191
				{
2192
					self::document_mail_action($documents[$prefix.$file['name']], $file);
2193
				}
2194
				else if ($editable_mimes[$file['mime']])
2195
				{
2196
					self::document_editable_action($documents[$prefix.$file['name']], $file);
2197
				}
2198
			}
2199
		}
2200
2201
		return array(
2202
			'icon' => 'etemplate/merge',
2203
			'caption' => $caption,
2204
			'children' => $documents,
2205
			// disable action if no document or export completly forbidden for non-admins
2206
			'enabled' => (boolean)$documents && (self::hasExportLimit($export_limit,'ISALLOWED') || self::is_export_limit_excepted()),
2207
			'hideOnDisabled' => true,	// do not show 'Insert in document', if no documents defined or no export allowed
2208
			'group' => $group,
2209
		);
2210
	}
2211
2212
	/**
2213
	 * Set up a document action for an eml (email) document
2214
	 *
2215
	 * Email (.eml) documents get special action handling.  They don't send a file
2216
	 * back to the client like the other documents.  Merging for a single selected
2217
	 * contact opens a compose window, multiple contacts just sends.
2218
	 *
2219
	 * @param Array &$action Action to be modified for mail
2220
	 * @param Array $file Array of information about the document from Api\Vfs::find
2221
	 * @return void
2222
	 */
2223
	private static function document_mail_action(Array &$action, $file)
2224
	{
2225
		unset($action['postSubmit']);
2226
2227
		// Lots takes a while, confirm
2228
		$action['confirm_multiple'] = lang('Do you want to send the message to all selected entries, WITHOUT further editing?');
2229
2230
		// These parameters trigger compose + merge - only if 1 row
2231
		$extra = array(
2232
			'from=merge',
2233
			'document='.$file['path'],
2234
			'merge='.get_called_class()
2235
		);
2236
2237
		// egw.open() used if only 1 row selected
2238
		$action['egw_open'] = 'edit-mail--'.implode('&',$extra);
2239
		$action['target'] = 'compose_' .$file['path'];
2240
2241
		// long_task runs menuaction once for each selected row
2242
		$action['nm_action'] = 'long_task';
2243
		$action['popup'] = Api\Link::get_registry('mail', 'edit_popup');
2244
		$action['message'] = lang('insert in %1',Api\Vfs::decodePath($file['name']));
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with EGroupware\Api\Vfs::decodePath($file['name']). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2244
		$action['message'] = /** @scrutinizer ignore-call */ lang('insert in %1',Api\Vfs::decodePath($file['name']));

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. Please note the @ignore annotation hint above.

Loading history...
2245
		$action['menuaction'] = 'mail.mail_compose.ajax_merge&document='.$file['path'].'&merge='. get_called_class();
2246
	}
2247
2248
	/**
2249
	 * Set up a document action so the generated file is saved and opened in
2250
	 * the collabora editor (if collabora is available)
2251
	 *
2252
	 * @param Array &$action Action to be modified for editor
2253
	 * @param Array $file Array of information about the document from Api\Vfs::find
2254
	 * @return void
2255
	 */
2256
	private static function document_editable_action(Array &$action, $file)
2257
	{
2258
		unset($action['postSubmit']);
2259
		$action['nm_action'] = 'location';
2260
		$action['url'] = urldecode(http_build_query(array(
2261
				'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit',
2262
				'document'   => $file['path'],
2263
				'merge'      => get_called_class(),
2264
				'id'         => '$id',
2265
				'select_all' => '$select_all'
2266
		)));
2267
		$action['target'] = '_blank';
2268
	}
2269
2270
	/**
2271
	 * Check if given document (relative path from document_actions()) exists in one of the given dirs
2272
	 *
2273
	 * @param string &$document maybe relative path of document, on return true absolute path to existing document
2274
	 * @param string $dirs comma or whitespace separated directories
2275
	 * @return string|boolean false if document exists, otherwise string with error-message
2276
	 */
2277
	public static function check_document(&$document, $dirs)
2278
	{
2279
		if($document[0] !== '/')
2280
		{
2281
			// split multiple comma or whitespace separated directories
2282
			// to still allow space or comma in dirnames, we also use the trailing slash of all pathes to split
2283
			if ($dirs && ($dirs = preg_split('/[,\s]+\//', $dirs)))
2284
			{
2285
				foreach($dirs as $n => $dir)
2286
				{
2287
					if ($n) $dir = '/'.$dir;	// re-adding trailing slash removed by split
2288
					if (Api\Vfs::stat($dir.'/'.$document) && Api\Vfs::is_readable($dir.'/'.$document))
2289
					{
2290
						$document = $dir.'/'.$document;
2291
						return false;
2292
					}
2293
				}
2294
			}
2295
		}
2296
		elseif (Api\Vfs::stat($document) && Api\Vfs::is_readable($document))
2297
		{
2298
			return false;
2299
		}
2300
		//error_log(__METHOD__."('$document', dirs='$dirs') returning 'Document '$document' does not exist or is not readable for you!'");
2301
		return lang("Document '%1' does not exist or is not readable for you!",$document);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $document. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2301
		return /** @scrutinizer ignore-call */ lang("Document '%1' does not exist or is not readable for you!",$document);

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. Please note the @ignore annotation hint above.

Loading history...
2302
	}
2303
2304
	/**
2305
	 * Get a list of supported extentions
2306
	 */
2307
	public static function get_file_extensions()
2308
	{
2309
		return array('txt', 'rtf', 'odt', 'ods', 'docx', 'xml', 'eml');
2310
	}
2311
2312
	/**
2313
	 * Format a number according to user prefs with decimal and thousands separator
2314
	 *
2315
	 * Reimplemented from etemplate to NOT use user prefs for Excel 2003, which gives an xml error
2316
	 *
2317
	 * @param int|float|string $number
2318
	 * @param int $num_decimal_places =2
2319
	 * @param string $_mimetype =''
2320
	 * @return string
2321
	 */
2322
	static public function number_format($number,$num_decimal_places=2,$_mimetype='')
2323
	{
2324
		if ((string)$number === '') return '';
2325
		//error_log(__METHOD__.$_mimetype);
2326
		switch($_mimetype)
2327
		{
2328
			case 'application/xml':	// Excel 2003
2329
			case 'application/vnd.oasis.opendocument.spreadsheet': // OO.o spreadsheet
2330
				return number_format(str_replace(' ','',$number),$num_decimal_places,'.','');
2331
		}
2332
		return Api\Etemplate::number_format($number,$num_decimal_places);
2333
	}
2334
}
2335