* @package api * @subpackage storage * @copyright (c) 2007-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ namespace EGroupware\Api\Storage; use EGroupware\Api; use EGroupware\Stylite; use DOMDocument; use XSLTProcessor; use tidy; use ZipArchive; // explicit import old, non-namespaced phpgwapi classes use uiaccountsel; /** * Document merge print * * @todo move apply_styles call into merge_string to run for each entry merged and not all together to lower memory requirements */ abstract class Merge { /** * Instance of the addressbook_bo class * * @var addressbook_bo */ var $contacts; /** * Datetime format according to user preferences * * @var string */ var $datetime_format = 'Y-m-d H:i'; /** * Fields that are to be treated as datetimes, when merged into spreadsheets */ var $date_fields = array(); /** * Mimetype of document processed by merge * * @var string */ var $mimetype; /** * Plugins registered by extending class to create a table with multiple rows * * $$table/$plugin$$ ... $$endtable$$ * * Callback returns replacements for row $n (stringing with 0) or null if no further rows * * @var array $plugin => array callback($plugin,$id,$n) */ var $table_plugins = array(); /** * Export limit in number of entries or some non-numerical value, if no export allowed at all, empty means no limit * * Set by constructor to $GLOBALS[egw_info][server][export_limit] * * @var int|string */ public $export_limit; /** * Configuration for HTML Tidy to clean up any HTML content that is kept */ public static $tidy_config = array( 'output-xml' => true, // Entity encoding 'show-body-only' => true, 'output-encoding' => 'utf-8', 'input-encoding' => 'utf-8', 'quote-ampersand' => false, // Prevent double encoding 'quote-nbsp' => true, // XSLT can handle spaces easier 'preserve-entities' => true, 'wrap' => 0, // Wrapping can break output ); /** * Parse HTML styles into target document style, if possible * * Apps not using html in there own data should set this with Customfields::use_html($app) * to avoid memory and time consuming html processing. */ protected $parse_html_styles = true; /** * Enable this to report memory_usage to error_log * * @var boolean */ public $report_memory_usage = false; /** * Constructor */ function __construct() { // Common messages are in preferences Api\Translation::add_app('preferences'); // All contact fields are in addressbook Api\Translation::add_app('addressbook'); $this->contacts = new Api\Contacts(); $this->datetime_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'].' '. ($GLOBALS['egw_info']['user']['preferences']['common']['timeformat']==12 ? 'h:i a' : 'H:i'); $this->export_limit = self::getExportLimit(); } /** * Hook returning options for export_limit_excepted groups * * @param array $config */ public static function hook_export_limit_excepted($config) { $accountsel = new uiaccountsel(); return ''. $accountsel->selection('newsettings[export_limit_excepted]','export_limit_excepted',$config['export_limit_excepted'],'both',4); } /** * Get all replacements, must be implemented in extending class * * Can use eg. the following high level methods: * - contact_replacements($contact_id,$prefix='') * - format_datetime($time,$format=null) * * @param int $id id of entry * @param string &$content=null content to create some replacements only if they are use * @return array|boolean array with replacements or false if entry not found */ abstract protected function get_replacements($id,&$content=null); /** * Return if merge-print is implemented for given mime-type (and/or extension) * * @param string $mimetype eg. text/plain * @param string $extension only checked for applications/msword and .rtf */ static public function is_implemented($mimetype,$extension=null) { static $zip_available=null; if (is_null($zip_available)) { $zip_available = check_load_extension('zip') && class_exists('ZipArchive'); // some PHP has zip extension, but no ZipArchive (eg. RHEL5!) } switch ($mimetype) { case 'application/msword': if (strtolower($extension) != '.rtf') break; case 'application/rtf': case 'text/rtf': return true; // rtf files case 'application/vnd.oasis.opendocument.text': // oo text case 'application/vnd.oasis.opendocument.spreadsheet': // oo spreadsheet if (!$zip_available) break; return true; // open office write xml files case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': // ms word 2007 xml format case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d': // mimetypes in vfs are limited to 64 chars case 'application/vnd.ms-word.document.macroenabled.12': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': // ms excel 2007 xml format case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee': case 'application/vnd.ms-excel.sheet.macroenabled.12': if (!$zip_available) break; return true; // ms word xml format case 'application/xml': return true; // alias for text/xml, eg. ms office 2003 word format case 'message/rfc822': return true; // ToDo: check if you are theoretical able to send mail case 'application/x-yaml': return true; // yaml file, plain text with marginal syntax support for multiline replacements default: if (substr($mimetype,0,5) == 'text/') { return true; // text files } break; } return false; // As browsers not always return correct mime types, one could use a negative list instead //return !($mimetype == Api\Vfs::DIR_MIME_TYPE || substr($mimetype,0,6) == 'image/'); } /** * Return replacements for a contact * * @param int|string|array $contact contact-array or id * @param string $prefix ='' prefix like eg. 'user' * @param boolean $ignore_acl =false true: no acl check * @return array */ public function contact_replacements($contact,$prefix='',$ignore_acl=false) { if (!is_array($contact)) { $contact = $this->contacts->read($contact, $ignore_acl); } if (!is_array($contact)) return array(); $replacements = array(); foreach(array_keys($this->contacts->contact_fields) as $name) { $value = $contact[$name]; switch($name) { case 'created': case 'modified': if($value) $value = Api\DateTime::to($value); break; case 'bday': if ($value) { try { $value = Api\DateTime::to($value, true); } catch (\Exception $e) { unset($e); // ignore exception caused by wrongly formatted date } } break; case 'owner': case 'creator': case 'modifier': $value = Api\Accounts::username($value); break; case 'cat_id': if ($value) { // if cat-tree is displayed, we return a full category path not just the name of the cat $use = $GLOBALS['egw_info']['server']['cat_tab'] == 'Tree' ? 'path' : 'name'; $cats = array(); foreach(is_array($value) ? $value : explode(',',$value) as $cat_id) { $cats[] = $GLOBALS['egw']->categories->id2name($cat_id,$use); } $value = implode(', ',$cats); } break; case 'jpegphoto': // returning a link might make more sense then the binary photo if ($contact['photo']) { $value = ($GLOBALS['egw_info']['server']['webserver_url'][0] == '/' ? ($_SERVER['HTTPS'] ? 'https://' : 'http://').$_SERVER['HTTP_HOST'] : ''). $GLOBALS['egw']->link('/index.php',$contact['photo']); } break; case 'tel_prefer': if ($value && $contact[$value]) { $value = $contact[$value]; } break; case 'account_id': if ($value) { $replacements['$$'.($prefix ? $prefix.'/':'').'account_lid$$'] = $GLOBALS['egw']->accounts->id2name($value); } break; } if ($name != 'photo') $replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = $value; } // set custom fields, should probably go to a general method all apps can use // need to load all cfs for $ignore_acl=true foreach($ignore_acl ? Customfields::get('addressbook', true) : $this->contacts->customfields as $name => $field) { $name = '#'.$name; $replacements['$$'.($prefix ? $prefix.'/':'').$name.'$$'] = // use raw data for yaml, no user-preference specific formatting $this->mimetype == 'application/x-yaml' ? (string)$contact[$name] : Customfields::format($field, (string)$contact[$name]); } // Add in extra cat field $cats = array(); foreach(is_array($contact['cat_id']) ? $contact['cat_id'] : explode(',',$contact['cat_id']) as $cat_id) { if(!$cat_id) continue; if($GLOBALS['egw']->categories->id2name($cat_id,'main') != $cat_id) { $path = explode(' / ', $GLOBALS['egw']->categories->id2name($cat_id, 'path')); unset($path[0]); // Drop main $cats[$GLOBALS['egw']->categories->id2name($cat_id,'main')][] = implode(' / ', $path); } elseif($cat_id) { $cats[$cat_id] = array(); } } foreach($cats as $main => $cat) { $replacements['$$'.($prefix ? $prefix.'/':'').'categories$$'] .= $GLOBALS['egw']->categories->id2name($main,'name') . (count($cat) > 0 ? ': ' : '') . implode(', ', $cats[$main]) . "\n"; } return $replacements; } /** * Get links for the given record * * Uses egw_link system to get link titles * * @param app Name of current app * @param id ID of current entry * @param only_app Restrict links to only given application * @param exclude Exclude links to these applications * @param style String One of: * 'title' - plain text, just the title of the link * 'link' - URL to the entry * 'href' - HREF tag wrapped around the title */ protected function get_links($app, $id, $only_app='', $exclude = array(), $style = 'title') { $links = Api\Link::get_links($app, $id, $only_app); $link_titles = array(); foreach($links as $link_info) { // Using only_app only returns the ID if(!is_array($link_info) && $only_app && $only_app[0] !== '!') { $link_info = array( 'app' => $only_app, 'id' => $link_info ); } if($exclude && in_array($link_info['id'], $exclude)) continue; $title = Api\Link::title($link_info['app'], $link_info['id']); if(class_exists('EGroupware\Stylite\Vfs\Links\StreamWrapper') && $link_info['app'] != Api\Link::VFS_APPNAME) { $title = Stylite\Vfs\Links\StreamWrapper::entry2name($link_info['app'], $link_info['id'], $title); } if($style == 'href' || $style == 'link') { $link = Api\Link::view($link_info['app'], $link_info['id'], $link_info); if($link_info['app'] != Api\Link::VFS_APPNAME) { // Set app to false so we always get an external link $link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false)); } else { $link = Api\Framework::link($link, array()); } // Prepend site if ($link{0} == '/') { $link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://'). ($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link; } $title = $style == 'href' ? Api\Html::a_href(Api\Html::htmlspecialchars($title), $link) : $link; } $link_titles[] = $title; } return implode("\n",$link_titles); } /** * Get all link placeholders * * Calls get_links() repeatedly to get all the combinations for the content. * * @param $app String appname * @param $id String ID of record * @param $prefix * @param $content String document content */ protected function get_all_links($app, $id, $prefix, &$content) { $array = array(); $pattern = '@\$(links_attachments|links|attachments|link)\/?(title|href|link)?\/?([a-z]*)\$@'; static $link_cache=null; $matches = null; if(preg_match_all($pattern, $content, $matches)) { foreach($matches[0] as $i => $placeholder) { $placeholder = substr($placeholder, 1, -1); if($link_cache[$id][$placeholder]) { $array[$placeholder] = $link_cache[$id][$placeholder]; continue; } switch($matches[1][$i]) { case 'link': // Link to current record $title = Api\Link::title($app, $id); if(class_exists('EGroupware\Stylite\Vfs\Links\StreamWrapper') && $app != Api\Link::VFS_APPNAME) { $title = Stylite\Vfs\Links\StreamWrapper::entry2name($app, $id, $title); } $link = Api\Link::view($app, $id); if($app != Api\Link::VFS_APPNAME) { // Set app to false so we always get an external link $link = str_replace(',', '%2C', $GLOBALS['egw']->framework->link('/index.php',$link, false)); } else { $link = Api\Framework::link($link, array()); } // Prepend site if ($link{0} == '/') { $link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://'). ($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link; } $array[($prefix?$prefix.'/':'').$placeholder] = Api\Html::a_href(Api\Html::htmlspecialchars($title), $link); break; case 'links': $link_app = $matches[3][$i] ? $matches[3][$i] : '!'.Api\Link::VFS_APPNAME; $array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, $link_app, array(),$matches[2][$i]); break; case 'attachments': $array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, Api\Link::VFS_APPNAME,array(),$matches[2][$i]); break; default: $array[($prefix?$prefix.'/':'').$placeholder] = $this->get_links($app, $id, $matches[3][$i], array(), $matches[2][$i]); break; } $link_cache[$id][$placeholder] = $array[$placeholder]; } } // Need to set each app, to make sure placeholders are removed foreach(array_keys($GLOBALS['egw_info']['user']['apps']) as $_app) { $array[($prefix?$prefix.'/':'')."links/$app"] = $this->get_links($app,$id,$_app); } return $array; } /** * Format a datetime * * @param int|string|DateTime $time unix timestamp or Y-m-d H:i:s string (in user time!) * @param string $format =null format string, default $this->datetime_format * @deprecated use Api\DateTime::to($time='now',$format='') * @return string */ protected function format_datetime($time,$format=null) { trigger_error(__METHOD__ . ' is deprecated, use Api\DateTime::to($time, $format)', E_USER_DEPRECATED); if (is_null($format)) $format = $this->datetime_format; return Api\DateTime::to($time,$format); } /** * Checks if current user is excepted from the export-limit: * a) access to admin application * b) he or one of his memberships is named in export_limit_excepted config var * * @return boolean */ public static function is_export_limit_excepted() { static $is_excepted=null; if (is_null($is_excepted)) { $is_excepted = isset($GLOBALS['egw_info']['user']['apps']['admin']); // check export-limit and fail if user tries to export more entries then allowed if (!$is_excepted && (is_array($export_limit_excepted = $GLOBALS['egw_info']['server']['export_limit_excepted']) || is_array($export_limit_excepted = unserialize($export_limit_excepted)))) { $id_and_memberships = $GLOBALS['egw']->accounts->memberships($GLOBALS['egw_info']['user']['account_id'],true); $id_and_memberships[] = $GLOBALS['egw_info']['user']['account_id']; $is_excepted = (bool) array_intersect($id_and_memberships, $export_limit_excepted); } } return $is_excepted; } /** * Checks if there is an exportlimit set, and returns * * @param string $app ='common' checks and validates app_limit, if not set returns the global limit * @return mixed - no if no export is allowed, false if there is no restriction and int as there is a valid restriction * you may have to cast the returned value to int, if you want to use it as number */ public static function getExportLimit($app='common') { static $exportLimitStore=array(); if (empty($app)) $app='common'; //error_log(__METHOD__.__LINE__.' called with app:'.$app); if (!array_key_exists($app,$exportLimitStore)) { //error_log(__METHOD__.__LINE__.' -> '.$app_limit.' '.function_backtrace()); $exportLimitStore[$app] = $GLOBALS['egw_info']['server']['export_limit']; if ($app !='common') { $app_limit = Api\Hooks::single('export_limit',$app); if ($app_limit) $exportLimitStore[$app] = $app_limit; } //error_log(__METHOD__.__LINE__.' building cache for app:'.$app.' -> '.$exportLimitStore[$app]); if (empty($exportLimitStore[$app])) { $exportLimitStore[$app] = false; return false; } if (is_numeric($exportLimitStore[$app])) { $exportLimitStore[$app] = (int)$exportLimitStore[$app]; } else { $exportLimitStore[$app] = 'no'; } //error_log(__METHOD__.__LINE__.' -> '.$exportLimit); } //error_log(__METHOD__.__LINE__.' app:'.$app.' -> '.$exportLimitStore[$app]); return $exportLimitStore[$app]; } /** * hasExportLimit * checks wether there is an exportlimit set, and returns true or false * @param mixed $app_limit app_limit, if not set checks the global limit * @param string $checkas [AND|ISALLOWED], AND default; if set to ISALLOWED it is checked if Export is allowed * * @return bool - true if no export is allowed or a limit is set, false if there is no restriction */ public static function hasExportLimit($app_limit,$checkas='AND') { if (strtoupper($checkas) == 'ISALLOWED') return (empty($app_limit) || ($app_limit !='no' && $app_limit > 0) ); if (empty($app_limit)) return false; if ($app_limit == 'no') return true; if ($app_limit > 0) return true; } /** * Merges a given document with contact data * * @param string $document path/url of document * @param array $ids array with contact id(s) * @param string &$err error-message on error * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders * @return string|boolean merged document or false on error */ public function &merge($document,$ids,&$err,$mimetype,array $fix=null) { if (!($content = file_get_contents($document))) { $err = lang("Document '%1' does not exist or is not readable for you!",$document); return false; } if (self::hasExportLimit($this->export_limit) && !self::is_export_limit_excepted() && count($ids) > (int)$this->export_limit) { $err = lang('No rights to export more than %1 entries!',(int)$this->export_limit); return false; } // fix application/msword mimetype for rtf files if ($mimetype == 'application/msword' && strtolower(substr($document,-4)) == '.rtf') { $mimetype = 'application/rtf'; } try { $content = $this->merge_string($content,$ids,$err,$mimetype,$fix); } catch (\Exception $e) { _egw_log_exception($e); $err = $e->getMessage(); return false; } return $content; } protected function apply_styles (&$content, $mimetype, $mso_application_progid=null) { if (!isset($mso_application_progid)) { $matches = null; $mso_application_progid = $mimetype == 'application/xml' && preg_match('/'.preg_quote('').'/',substr($content,0,200),$matches) ? $matches[1] : ''; } // Tags we can replace with the target document's version $replace_tags = array(); switch($mimetype.$mso_application_progid) { case 'application/vnd.oasis.opendocument.text': // open office case 'application/vnd.oasis.opendocument.spreadsheet': // It seems easier to split the parent tags here $replace_tags = array( '/<(ol|ul|table)( [^>]*)?>/' => '<$1$2>', '/<\/(ol|ul|table)>/' => '', //'/<(li)(.*?)>(.*?)<\/\1>/' => '<$1 $2>$3', ); $content = preg_replace(array_keys($replace_tags),array_values($replace_tags),$content); $doc = new DOMDocument(); $xslt = new XSLTProcessor(); $doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/openoffice.xslt'); $xslt->importStyleSheet($doc); //echo $content;die(); break; case 'application/xmlWord.Document': // Word 2003*/ case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': // ms office 2007 case 'application/vnd.ms-word.document.macroenabled.12': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': case 'application/vnd.ms-excel.sheet.macroenabled.12': // It seems easier to split the parent tags here $replace_tags = array( // Tables, lists don't go inside '/<(ol|ul|table)( [^>]*)?>/' => '<$1$2>', '/<\/(ol|ul|table)>/' => '', // Fix for things other than text (newlines) inside table row '/<(td)( [^>]*)?>((?!))(.*?)<\/td>[\s]*?/' => '<$1$2>$4', // Remove extra whitespace '/]*?)>[^:print:]*?(.*?)<\/li>/' => '$2', // This doesn't get it all '/[\s]+(.*?)<\/w:t>/' => '$1', // Remove spans with no attributes, linebreaks inside them cause problems '/(.*?)<\/span>/' => '$1' ); $content = preg_replace(array_keys($replace_tags),array_values($replace_tags),$content); /* In the case where you have something like (invalid - mismatched tags), it takes multiple runs to get rid of both spans. So, loop. OO.o files have not yet been shown to have this problem. */ $count = $i = 0; do { $content = preg_replace('/(.*?)<\/span>/','$1',$content, -1, $count); $i++; } while($count > 0 && $i < 10); $doc = new DOMDocument(); $xslt = new XSLTProcessor(); $xslt_file = $mimetype == 'application/xml' ? 'wordml.xslt' : 'msoffice.xslt'; $doc->load(EGW_INCLUDE_ROOT.'/api/templates/default/Merge/'.$xslt_file); $xslt->importStyleSheet($doc); break; } // XSLT transform known tags if($xslt) { // does NOT work with php 5.2.6: Catchable fatal error: argument 1 to transformToXml() must be of type DOMDocument //$element = new SimpleXMLelement($content); $element = new DOMDocument('1.0', 'utf-8'); $result = $element->loadXML($content); if(!$result) { throw new Api\Exception('Unable to parse merged document for styles. Check warnings in log for details.'); } $content = $xslt->transformToXml($element); // Word 2003 needs two declarations, add extra declaration back in if($mimetype == 'application/xml' && $mso_application_progid == 'Word.Document' && strpos($content, ''.$content; } // Validate /* $doc = new DOMDocument(); $doc->loadXML($content); $doc->schemaValidate(*Schema (xsd) file*); */ } } /** * Merges a given document with contact data * * @param string $_content * @param array $ids array with contact id(s) * @param string &$err error-message on error * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders * @param string $charset =null charset to override default set by mimetype or export charset * @return string|boolean merged document or false on error */ public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null) { $matches = null; if ($mimetype == 'application/xml' && preg_match('/'.preg_quote('').'/',substr($_content,0,200),$matches)) { $mso_application_progid = $matches[1]; } else { $mso_application_progid = ''; } // alternative syntax using double curly brackets (eg. {{cat_id}} instead $$cat_id$$), // agressivly removing all xml-tags eg. Word adds within placeholders $content = preg_replace_callback('/{{[^}]+}}/i', function($matches) { return '$$'.strip_tags(substr($matches[0], 2, -2)).'$$'; }, $_content); // Handle escaped placeholder markers in RTF, they won't match when escaped if($mimetype == 'application/rtf') { $content = preg_replace('/\\\{\\\{([^\\}]+)\\\}\\\}/i','$$\1$$',$content); } // make currently processed mimetype available to class methods; $this->mimetype = $mimetype; // fix garbled placeholders if ($fix && is_array($fix)) { $content = preg_replace(array_keys($fix),array_values($fix),$content); //die("
".htmlspecialchars($content)."
\n"); } list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY); //get differt parts of document, seperatet by Pagerepeat if ($mimetype == 'text/plain' && count($ids) > 1) { // textdocuments are simple, they do not hold start and end, but they may have content before and after the $$pagerepeat$$ tag // header and footer should not hold any $$ tags; if we find $$ tags with the header, we assume it is the pagerepeatcontent $nohead = false; if (stripos($contentstart,'$$') !== false) $nohead = true; if ($nohead) { $contentend = $contentrepeat; $contentrepeat = $contentstart; $contentstart = ''; } } if ($mimetype == 'application/vnd.oasis.opendocument.text' && count($ids) > 1) { if(strpos($content, '$$pagerepeat') === false) { //for odt files we have to split the content and add a style for page break to the style area list($contentstart,$contentrepeat,$contentend) = preg_split('/office:body>/',$content,-1, PREG_SPLIT_NO_EMPTY); //get differt parts of document, seperatet by Pagerepeat $contentstart = substr($contentstart,0,strlen($contentstart)-1); //remove "<" $contentrepeat = substr($contentrepeat,0,strlen($contentrepeat)-2); //remove "/',$content,-1, PREG_SPLIT_NO_EMPTY); //get differt parts of document style sheets $contentstart = $stylestart.''; $contentstart .= ''; $contentend = ''; } else { // Template specifies where to repeat list($contentstart,$contentrepeat,$contentend) = preg_split('/\$\$pagerepeat\$\$/',$content,-1, PREG_SPLIT_NO_EMPTY); //get different parts of document, seperated by pagerepeat } } if (in_array($mimetype, array('application/vnd.ms-word.document.macroenabled.12', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) && count($ids) > 1) { //for Word 2007 XML files we have to split the content and add a style for page break to the style area list($contentstart,$contentrepeat,$contentend) = preg_split('/w:body>/',$content,-1, PREG_SPLIT_NO_EMPTY); //get differt parts of document, seperatet by Pagerepeat $contentstart = substr($contentstart,0,strlen($contentstart)-1); //remove "'; $contentend = ''; } list($Labelstart,$Labelrepeat,$Labeltend) = preg_split('/\$\$label\$\$/',$contentrepeat,-1, PREG_SPLIT_NO_EMPTY); //get the Lable content preg_match_all('/\$\$labelplacement\$\$/',$contentrepeat,$countlables, PREG_SPLIT_NO_EMPTY); $countlables = count($countlables[0]); preg_replace('/\$\$labelplacement\$\$/','',$Labelrepeat,1); if ($countlables > 1) $lableprint = true; if (count($ids) > 1 && !$contentrepeat) { $err = lang('for more than one contact in a document use the tag pagerepeat!'); return false; } if ($this->report_memory_usage) error_log(__METHOD__."(count(ids)=".count($ids).") strlen(contentrepeat)=".strlen($contentrepeat).', strlen(labelrepeat)='.strlen($Labelrepeat)); if ($contentrepeat) { $content_stream = fopen('php://temp','r+'); fwrite($content_stream, $contentstart); $joiner = ''; switch($mimetype) { case 'application/rtf': case 'text/rtf': $joiner = '\\par \\page\\pard\\plain'; break; case 'application/vnd.oasis.opendocument.text': case 'application/vnd.oasis.opendocument.spreadsheet': case 'application/xml': case 'text/html': case 'text/csv': $joiner = ''; break; case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': case 'application/vnd.ms-word.document.macroenabled.12': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': case 'application/vnd.ms-excel.sheet.macroenabled.12': $joiner = ''; break; case 'text/plain': $joiner = "\r\n"; break; default: $err = lang('%1 not implemented for %2!','$$pagerepeat$$',$mimetype); return false; } } foreach ((array)$ids as $n => $id) { if ($contentrepeat) $content = $contentrepeat; //content to repeat if ($lableprint) $content = $Labelrepeat; // generate replacements; if exeption is thrown, catch it set error message and return false try { if(!($replacements = $this->get_replacements($id,$content))) { $err = lang('Entry not found!'); return false; } } catch (Api\Exception\WrongUserinput $e) { // if this returns with an exeption, something failed big time $err = $e->getMessage(); return false; } if ($this->report_memory_usage) error_log(__METHOD__."() $n: $id ".Api\Vfs::hsize(memory_get_usage(true))); // some general replacements: current user, date and time if (strpos($content,'$$user/') !== null && ($user = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id'))) { $replacements += $this->contact_replacements($user,'user'); $replacements['$$user/primary_group$$'] = $GLOBALS['egw']->accounts->id2name($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'account_primary_group')); } $replacements['$$date$$'] = Api\DateTime::to('now',true); $replacements['$$datetime$$'] = Api\DateTime::to('now'); $replacements['$$time$$'] = Api\DateTime::to('now',false); // does our extending class registered table-plugins AND document contains table tags if ($this->table_plugins && preg_match_all('/\\$\\$table\\/([A-Za-z0-9_]+)\\$\\$(.*?)\\$\\$endtable\\$\\$/s',$content,$matches,PREG_SET_ORDER)) { // process each table foreach($matches as $match) { $plugin = $match[1]; // plugin name $callback = $this->table_plugins[$plugin]; $repeat = $match[2]; // line to repeat $repeats = ''; if (isset($callback)) { for($n = 0; ($row_replacements = $this->$callback($plugin,$id,$n,$repeat)); ++$n) { $_repeat = $this->process_commands($repeat, $row_replacements); $repeats .= $this->replace($_repeat,$row_replacements,$mimetype,$mso_application_progid); } } $content = str_replace($match[0],$repeats,$content); } } $content = $this->process_commands($this->replace($content,$replacements,$mimetype,$mso_application_progid,$charset), $replacements); // remove not existing replacements (eg. from calendar array) if (strpos($content,'$$') !== null) { $content = preg_replace('/\$\$[a-z0-9_\/]+\$\$/i','',$content); } if ($contentrepeat) { fwrite($content_stream, ($n == 0 ? '' : $joiner) . $content); } if($lableprint) { $contentrep[is_array($id) ? implode(':',$id) : $id] = $content; } } if ($Labelrepeat) { $countpage=0; $count=0; $contentrepeatpages[$countpage] = $Labelstart.$Labeltend; foreach ($contentrep as $Label) { $contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/',$Label,$contentrepeatpages[$countpage],1); $count=$count+1; if (($count % $countlables) == 0 && count($contentrep)>$count) //new page { $countpage = $countpage+1; $contentrepeatpages[$countpage] = $Labelstart.$Labeltend; } } $contentrepeatpages[$countpage] = preg_replace('/\$\$labelplacement\$\$/','',$contentrepeatpages[$countpage],-1); //clean empty fields switch($mimetype) { case 'application/rtf': case 'text/rtf': return $contentstart.implode('\\par \\page\\pard\\plain',$contentrepeatpages).$contentend; case 'application/vnd.oasis.opendocument.text': return $contentstart.implode('',$contentrepeatpages).$contentend; case 'application/vnd.oasis.opendocument.spreadsheet': return $contentstart.implode('
',$contentrepeatpages).$contentend; case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': case 'application/vnd.ms-word.document.macroenabled.12': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': case 'application/vnd.ms-excel.sheet.macroenabled.12': return $contentstart.implode('',$contentrepeatpages).$contentend; case 'text/plain': return $contentstart.implode("\r\n",$contentrep).$contentend; } $err = lang('%1 not implemented for %2!','$$labelplacement$$',$mimetype); return false; } if ($contentrepeat) { fwrite($content_stream, $contentend); rewind($content_stream); return stream_get_contents($content_stream); } if ($this->report_memory_usage) error_log(__METHOD__."() returning ".Api\Vfs::hsize(memory_get_peak_usage(true))); return $content; } /** * Replace placeholders in $content of $mimetype with $replacements * * @param string $content * @param array $replacements name => replacement pairs * @param string $mimetype mimetype of content * @param string $mso_application_progid ='' MS Office 2003: 'Excel.Sheet' or 'Word.Document' * @param string $charset =null charset to override default set by mimetype or export charset * @return string */ protected function replace($content,array $replacements,$mimetype,$mso_application_progid='',$charset=null) { switch($mimetype) { case 'application/vnd.oasis.opendocument.text': // open office case 'application/vnd.oasis.opendocument.spreadsheet': case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': // ms office 2007 case 'application/vnd.ms-word.document.macroenabled.12': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': case 'application/vnd.ms-excel.sheet.macroenabled.12': case 'application/xml': case 'text/xml': $is_xml = true; $charset = 'utf-8'; // xml files --> always use utf-8 break; case 'text/html': $is_xml = true; $matches = null; if (preg_match('/ use our export-charset, defined in addressbook prefs if (empty($charset)) $charset = $this->contacts->prefs['csv_charset']; break; } //error_log(__METHOD__."('$document', ... ,$mimetype) --> $charset (egw=".Api\Translation::charset().', export='.$this->contacts->prefs['csv_charset'].')'); // do we need to convert charset if ($charset && $charset != Api\Translation::charset()) { $replacements = Api\Translation::convert($replacements,Api\Translation::charset(),$charset); } // Date only placeholders for timestamps if(is_array($this->date_fields)) { foreach($this->date_fields as $field) { if(($value = $replacements['$$'.$field.'$$'])) { $time = Api\DateTime::createFromFormat('+'.Api\DateTime::$user_dateformat.' '.Api\DateTime::$user_timeformat.'*', $value); $replacements['$$'.$field.'/date$$'] = $time ? $time->format(Api\DateTime::$user_dateformat) : ''; } } } if ($is_xml) // zip'ed xml document (eg. OO) { // Numeric fields $names = array(); // Tags we can replace with the target document's version $replace_tags = array(); // only keep tags, if we have xsl extension available if (class_exists(XSLTProcessor) && class_exists(DOMDocument) && $this->parse_html_styles) { switch($mimetype.$mso_application_progid) { case 'text/html': $replace_tags = array( '','','','','','','
    ','