Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Merge often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Merge, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 32 | abstract class Merge |
||
| 33 | { |
||
| 34 | /** |
||
| 35 | * Instance of the addressbook_bo class |
||
| 36 | * |
||
| 37 | * @var addressbook_bo |
||
| 38 | */ |
||
| 39 | var $contacts; |
||
| 40 | |||
| 41 | /** |
||
| 42 | * Datetime format according to user preferences |
||
| 43 | * |
||
| 44 | * @var string |
||
| 45 | */ |
||
| 46 | var $datetime_format = 'Y-m-d H:i'; |
||
| 47 | |||
| 48 | /** |
||
| 49 | * Fields that are to be treated as datetimes, when merged into spreadsheets |
||
| 50 | */ |
||
| 51 | var $date_fields = array(); |
||
| 52 | |||
| 53 | /** |
||
| 54 | * Mimetype of document processed by merge |
||
| 55 | * |
||
| 56 | * @var string |
||
| 57 | */ |
||
| 58 | var $mimetype; |
||
| 59 | |||
| 60 | /** |
||
| 61 | * Plugins registered by extending class to create a table with multiple rows |
||
| 62 | * |
||
| 63 | * $$table/$plugin$$ ... $$endtable$$ |
||
| 64 | * |
||
| 65 | * Callback returns replacements for row $n (stringing with 0) or null if no further rows |
||
| 66 | * |
||
| 67 | * @var array $plugin => array callback($plugin,$id,$n) |
||
| 68 | */ |
||
| 69 | var $table_plugins = array(); |
||
| 70 | |||
| 71 | /** |
||
| 72 | * Export limit in number of entries or some non-numerical value, if no export allowed at all, empty means no limit |
||
| 73 | * |
||
| 74 | * Set by constructor to $GLOBALS[egw_info][server][export_limit] |
||
| 75 | * |
||
| 76 | * @var int|string |
||
| 77 | */ |
||
| 78 | public $export_limit; |
||
| 79 | |||
| 80 | |||
| 81 | /** |
||
| 82 | * Configuration for HTML Tidy to clean up any HTML content that is kept |
||
| 83 | */ |
||
| 84 | public static $tidy_config = array( |
||
| 85 | 'output-xml' => true, // Entity encoding |
||
| 86 | 'show-body-only' => true, |
||
| 87 | 'output-encoding' => 'utf-8', |
||
| 88 | 'input-encoding' => 'utf-8', |
||
| 89 | 'quote-ampersand' => false, // Prevent double encoding |
||
| 90 | 'quote-nbsp' => true, // XSLT can handle spaces easier |
||
| 91 | 'preserve-entities' => true, |
||
| 92 | 'wrap' => 0, // Wrapping can break output |
||
| 93 | ); |
||
| 94 | |||
| 95 | /** |
||
| 96 | * Parse HTML styles into target document style, if possible |
||
| 97 | * |
||
| 98 | * Apps not using html in there own data should set this with Customfields::use_html($app) |
||
| 99 | * to avoid memory and time consuming html processing. |
||
| 100 | */ |
||
| 101 | protected $parse_html_styles = true; |
||
| 102 | |||
| 103 | /** |
||
| 104 | * Enable this to report memory_usage to error_log |
||
| 105 | * |
||
| 106 | * @var boolean |
||
| 107 | */ |
||
| 108 | public $report_memory_usage = false; |
||
| 109 | |||
| 110 | /** |
||
| 111 | * Constructor |
||
| 112 | */ |
||
| 113 | function __construct() |
||
| 127 | |||
| 128 | /** |
||
| 129 | * Hook returning options for export_limit_excepted groups |
||
| 130 | * |
||
| 131 | * @param array $config |
||
| 132 | */ |
||
| 133 | public static function hook_export_limit_excepted($config) |
||
| 140 | |||
| 141 | /** |
||
| 142 | * Get all replacements, must be implemented in extending class |
||
| 143 | * |
||
| 144 | * Can use eg. the following high level methods: |
||
| 145 | * - contact_replacements($contact_id,$prefix='') |
||
| 146 | * - format_datetime($time,$format=null) |
||
| 147 | * |
||
| 148 | * @param int $id id of entry |
||
| 149 | * @param string &$content=null content to create some replacements only if they are use |
||
| 150 | * @return array|boolean array with replacements or false if entry not found |
||
| 151 | */ |
||
| 152 | abstract protected function get_replacements($id,&$content=null); |
||
| 153 | |||
| 154 | /** |
||
| 155 | * Return if merge-print is implemented for given mime-type (and/or extension) |
||
| 156 | * |
||
| 157 | * @param string $mimetype eg. text/plain |
||
| 158 | * @param string $extension only checked for applications/msword and .rtf |
||
| 159 | */ |
||
| 160 | static public function is_implemented($mimetype,$extension=null) |
||
| 205 | |||
| 206 | /** |
||
| 207 | * Return replacements for a contact |
||
| 208 | * |
||
| 209 | * @param int|string|array $contact contact-array or id |
||
| 210 | * @param string $prefix ='' prefix like eg. 'user' |
||
| 211 | * @param boolean $ignore_acl =false true: no acl check |
||
| 212 | * @return array |
||
| 213 | */ |
||
| 214 | public function contact_replacements($contact,$prefix='',$ignore_acl=false) |
||
| 312 | |||
| 313 | /** |
||
| 314 | * Get links for the given record |
||
| 315 | * |
||
| 316 | * Uses egw_link system to get link titles |
||
| 317 | * |
||
| 318 | * @param app Name of current app |
||
| 319 | * @param id ID of current entry |
||
| 320 | * @param only_app Restrict links to only given application |
||
| 321 | * @param exclude Exclude links to these applications |
||
| 322 | * @param style String One of: |
||
| 323 | * 'title' - plain text, just the title of the link |
||
| 324 | * 'link' - URL to the entry |
||
| 325 | * 'href' - HREF tag wrapped around the title |
||
| 326 | */ |
||
| 327 | protected function get_links($app, $id, $only_app='', $exclude = array(), $style = 'title') |
||
| 372 | |||
| 373 | /** |
||
| 374 | * Get all link placeholders |
||
| 375 | * |
||
| 376 | * Calls get_links() repeatedly to get all the combinations for the content. |
||
| 377 | * |
||
| 378 | * @param $app String appname |
||
| 379 | * @param $id String ID of record |
||
| 380 | * @param $prefix |
||
| 381 | * @param $content String document content |
||
| 382 | */ |
||
| 383 | protected function get_all_links($app, $id, $prefix, &$content) |
||
| 448 | |||
| 449 | /** |
||
| 450 | * Format a datetime |
||
| 451 | * |
||
| 452 | * @param int|string|DateTime $time unix timestamp or Y-m-d H:i:s string (in user time!) |
||
| 453 | * @param string $format =null format string, default $this->datetime_format |
||
| 454 | * @deprecated use Api\DateTime::to($time='now',$format='') |
||
| 455 | * @return string |
||
| 456 | */ |
||
| 457 | protected function format_datetime($time,$format=null) |
||
| 464 | |||
| 465 | /** |
||
| 466 | * Checks if current user is excepted from the export-limit: |
||
| 467 | * a) access to admin application |
||
| 468 | * b) he or one of his memberships is named in export_limit_excepted config var |
||
| 469 | * |
||
| 470 | * @return boolean |
||
| 471 | */ |
||
| 472 | public static function is_export_limit_excepted() |
||
| 491 | |||
| 492 | /** |
||
| 493 | * Checks if there is an exportlimit set, and returns |
||
| 494 | * |
||
| 495 | * @param string $app ='common' checks and validates app_limit, if not set returns the global limit |
||
| 496 | * @return mixed - no if no export is allowed, false if there is no restriction and int as there is a valid restriction |
||
| 497 | * you may have to cast the returned value to int, if you want to use it as number |
||
| 498 | */ |
||
| 499 | public static function getExportLimit($app='common') |
||
| 533 | |||
| 534 | /** |
||
| 535 | * hasExportLimit |
||
| 536 | * checks wether there is an exportlimit set, and returns true or false |
||
| 537 | * @param mixed $app_limit app_limit, if not set checks the global limit |
||
| 538 | * @param string $checkas [AND|ISALLOWED], AND default; if set to ISALLOWED it is checked if Export is allowed |
||
| 539 | * |
||
| 540 | * @return bool - true if no export is allowed or a limit is set, false if there is no restriction |
||
| 541 | */ |
||
| 542 | public static function hasExportLimit($app_limit,$checkas='AND') |
||
| 549 | |||
| 550 | /** |
||
| 551 | * Merges a given document with contact data |
||
| 552 | * |
||
| 553 | * @param string $document path/url of document |
||
| 554 | * @param array $ids array with contact id(s) |
||
| 555 | * @param string &$err error-message on error |
||
| 556 | * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf |
||
| 557 | * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders |
||
| 558 | * @return string|boolean merged document or false on error |
||
| 559 | */ |
||
| 560 | public function &merge($document,$ids,&$err,$mimetype,array $fix=null) |
||
| 589 | |||
| 590 | protected function apply_styles (&$content, $mimetype, $mso_application_progid=null) |
||
| 685 | |||
| 686 | /** |
||
| 687 | * Merges a given document with contact data |
||
| 688 | * |
||
| 689 | * @param string $_content |
||
| 690 | * @param array $ids array with contact id(s) |
||
| 691 | * @param string &$err error-message on error |
||
| 692 | * @param string $mimetype mimetype of complete document, eg. text/*, application/vnd.oasis.opendocument.text, application/rtf |
||
| 693 | * @param array $fix =null regular expression => replacement pairs eg. to fix garbled placeholders |
||
| 694 | * @param string $charset =null charset to override default set by mimetype or export charset |
||
| 695 | * @return string|boolean merged document or false on error |
||
| 696 | */ |
||
| 697 | public function &merge_string($_content,$ids,&$err,$mimetype,array $fix=null,$charset=null) |
||
| 932 | |||
| 933 | /** |
||
| 934 | * Replace placeholders in $content of $mimetype with $replacements |
||
| 935 | * |
||
| 936 | * @param string $content |
||
| 937 | * @param array $replacements name => replacement pairs |
||
| 938 | * @param string $mimetype mimetype of content |
||
| 939 | * @param string $mso_application_progid ='' MS Office 2003: 'Excel.Sheet' or 'Word.Document' |
||
| 940 | * @param string $charset =null charset to override default set by mimetype or export charset |
||
| 941 | * @return string |
||
| 942 | */ |
||
| 943 | protected function replace($content,array $replacements,$mimetype,$mso_application_progid='',$charset=null) |
||
| 1190 | |||
| 1191 | /** |
||
| 1192 | * Convert numeric values in spreadsheets into actual numeric values |
||
| 1193 | */ |
||
| 1194 | protected function format_spreadsheet_numbers(&$content, $names, $mimetype) |
||
| 1228 | |||
| 1229 | /** |
||
| 1230 | * Increase/double prce.backtrack_limit up to 1/4 of memory_limit |
||
| 1231 | * |
||
| 1232 | * @return boolean true: backtrack_limit increased, may try again, false limit already to high |
||
| 1233 | */ |
||
| 1234 | protected static function increase_backtrack_limit() |
||
| 1259 | |||
| 1260 | /** |
||
| 1261 | * Convert date / timestamp values in spreadsheets into actual date / timestamp values |
||
| 1262 | */ |
||
| 1263 | protected function format_spreadsheet_dates(&$content, $names, &$values, $mimetype) |
||
| 1264 | { |
||
| 1265 | if(!in_array($mimetype, array( |
||
| 1266 | 'application/vnd.oasis.opendocument.spreadsheet', // open office calc |
||
| 1267 | 'application/xmlExcel.Sheet', // Excel 2003 |
||
| 1268 | //'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'//Excel WTF |
||
| 1269 | ))) return; |
||
| 1270 | |||
| 1271 | // Some different formats dates could be in, depending what they've been through |
||
| 1272 | $formats = array( |
||
| 1273 | '!'.Api\DateTime::$user_dateformat . ' ' .Api\DateTime::$user_timeformat.':s', |
||
| 1274 | '!'.Api\DateTime::$user_dateformat . '*' .Api\DateTime::$user_timeformat.':s', |
||
| 1275 | '!'.Api\DateTime::$user_dateformat . '* ' .Api\DateTime::$user_timeformat, |
||
| 1276 | '!'.Api\DateTime::$user_dateformat . '*', |
||
| 1277 | '!'.Api\DateTime::$user_dateformat, |
||
| 1278 | '!Y-m-d\TH:i:s' |
||
| 1279 | ); |
||
| 1280 | |||
| 1281 | // Properly format values for spreadsheet |
||
| 1282 | foreach($names as $idx => &$field) |
||
| 1283 | { |
||
| 1284 | $key = '$$'.$field.'$$'; |
||
| 1285 | $field = preg_quote($field, '/'); |
||
| 1286 | if($values[$key]) |
||
| 1287 | { |
||
| 1288 | $date = Api\DateTime::createFromUserFormat($values[$key]); |
||
| 1289 | if($mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || |
||
| 1290 | $mimetype == 'application/vnd.ms-excel.sheet.macroenabled.12')//Excel WTF |
||
| 1291 | { |
||
| 1292 | $interval = $date->diff(new Api\DateTime('1900-01-00 0:00')); |
||
| 1293 | $values[$key] = $interval->format('%a')+1;// 1900-02-29 did not exist |
||
| 1294 | // 1440 minutes in a day - fractional part |
||
| 1295 | $values[$key] += ($date->format('H') * 60 + $date->format('i'))/1440; |
||
| 1296 | } |
||
| 1297 | else |
||
| 1298 | { |
||
| 1299 | $values[$key] = date('Y-m-d\TH:i:s',Api\DateTime::to($date,'ts')); |
||
| 1300 | } |
||
| 1301 | } |
||
| 1302 | else |
||
| 1303 | { |
||
| 1304 | unset($names[$idx]); |
||
| 1305 | } |
||
| 1306 | } |
||
| 1307 | |||
| 1308 | switch($mimetype) |
||
| 1309 | { |
||
| 1310 | case 'application/vnd.oasis.opendocument.spreadsheet': // open office calc |
||
| 1311 | // Removing these forces calc to respect our set value-type |
||
| 1312 | $content = str_ireplace('calcext:value-type="string"','',$content); |
||
| 1313 | |||
| 1314 | $format = '/<table:table-cell([^>]+?)office:value-type="[^"]+"([^>]*?)>.?<([a-z].*?)[^>]*>\$\$('.implode('|',$names).')\$\$<\/\3>.?<\/table:table-cell>/s'; |
||
| 1315 | $replacement = '<table:table-cell$1office:value-type="date" office:date-value="\$\$$4\$\$"$2><$3>\$\$$4\$\$</$3></table:table-cell>'; |
||
| 1316 | break; |
||
| 1317 | View Code Duplication | case 'application/xmlExcel.Sheet': // Excel 2003 |
|
| 1318 | $format = '/'.preg_quote('<Data ss:Type="String">','/').'..('.implode('|',$names).')..'.preg_quote('</Data>','/').'/'; |
||
| 1319 | $replacement = '<Data ss:Type="DateTime">\$\$$1\$\$</Data>'; |
||
| 1320 | |||
| 1321 | break; |
||
| 1322 | case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': |
||
| 1323 | case 'application/vnd.ms-excel.sheet.macroenabled.12': |
||
| 1324 | break; |
||
| 1325 | } |
||
| 1326 | View Code Duplication | if($format && $names) |
|
| 1327 | { |
||
| 1328 | // Dealing with backtrack limit per AmigoJack 10-Jul-2010 comment on php.net preg-replace docs |
||
| 1329 | do { |
||
| 1330 | $result = preg_replace($format, $replacement, $content, -1); |
||
| 1331 | } |
||
| 1332 | // try to increase/double pcre.backtrack_limit failure |
||
| 1333 | while(preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR && self::increase_backtrack_limit()); |
||
| 1334 | |||
| 1335 | if ($result) $content = $result; // On failure $result would be NULL |
||
| 1336 | } |
||
| 1337 | } |
||
| 1338 | |||
| 1339 | /** |
||
| 1340 | * Expand link_to custom fields with the merge replacements from the app |
||
| 1341 | * but only if the template uses them. |
||
| 1342 | */ |
||
| 1343 | public function cf_link_to_expand($values, $content, &$replacements, $app = null) |
||
| 1344 | { |
||
| 1345 | if($app == null) |
||
| 1346 | { |
||
| 1347 | $app = str_replace('_merge','',get_class($this)); |
||
| 1348 | } |
||
| 1349 | $cfs = Api\Storage\Customfields::get($app); |
||
| 1350 | |||
| 1351 | // Cache, in case more than one sub-placeholder is used |
||
| 1352 | $app_replacements = array(); |
||
| 1353 | |||
| 1354 | // Custom field placeholders look like {{#name}} |
||
| 1355 | // Placeholders that need expanded will look like {{#name/placeholder}} |
||
| 1356 | $matches = null; |
||
| 1357 | preg_match_all('/\${2}(([^\/#]*?\/)?)#([^$\/]+)\/(.*?)[$}]{2}/', $content, $matches); |
||
| 1358 | list($placeholders, , , $cf, $sub) = $matches; |
||
| 1359 | |||
| 1360 | // Collect any used custom fields from entries so you can do |
||
| 1361 | // {{#other_app/#other_app_cf/n_fn}} |
||
| 1362 | $expand_sub_cfs = []; |
||
| 1363 | foreach($sub as $index => $cf_sub) |
||
| 1364 | { |
||
| 1365 | if(strpos($cf_sub, '#') === 0) |
||
| 1366 | { |
||
| 1367 | $expand_sub_cfs[$cf[$index]] .= '$$'.$cf_sub . '$$ '; |
||
| 1368 | } |
||
| 1369 | } |
||
| 1370 | $expand_sub_cfs = array_unique($expand_sub_cfs); |
||
| 1371 | |||
| 1372 | foreach($cf as $index => $field) |
||
| 1373 | { |
||
| 1374 | if($cfs[$field]) |
||
| 1375 | { |
||
| 1376 | if(in_array($cfs[$field]['type'],array_keys($GLOBALS['egw_info']['apps']))) |
||
| 1377 | { |
||
| 1378 | $field_app = $cfs[$field]['type']; |
||
| 1379 | } |
||
| 1380 | else if ($cfs[$field]['type'] == 'api-accounts' || $cfs[$field]['type'] == 'select-account') |
||
| 1381 | { |
||
| 1382 | // Special case for api-accounts -> contact |
||
| 1383 | $field_app = 'addressbook'; |
||
| 1384 | $account = $GLOBALS['egw']->accounts->read($values['#'.$field]); |
||
| 1385 | $app_replacements[$field] = $this->contact_replacements($account['person_id']); |
||
| 1386 | } |
||
| 1387 | else if (($list = explode('-',$cfs[$field]['type']) && in_array($list[0], array_keys($GLOBALS['egw_info']['apps'])))) |
||
| 1388 | { |
||
| 1389 | // Sub-type - use app |
||
| 1390 | $field_app = $list[0]; |
||
| 1391 | } |
||
| 1392 | else |
||
| 1393 | { |
||
| 1394 | continue; |
||
| 1395 | } |
||
| 1396 | |||
| 1397 | // Get replacements for that application |
||
| 1398 | if(!$app_replacements[$field]) |
||
| 1399 | { |
||
| 1400 | $app_replacements[$field] = $this->get_app_replacements($field_app, $values['#'.$field], $content); |
||
| 1401 | } |
||
| 1402 | $replacements[$placeholders[$index]] = $app_replacements[$field]['$$'.$sub[$index].'$$']; |
||
| 1403 | } |
||
| 1404 | else |
||
| 1405 | { |
||
| 1406 | if ($cfs[$field]['type'] == 'date' || $cfs[$field]['type'] == 'date-time') $this->date_fields[] = '#'.$field; |
||
| 1407 | } |
||
| 1408 | } |
||
| 1409 | } |
||
| 1410 | |||
| 1411 | /** |
||
| 1412 | * Get the replacements for any entry specified by app & id |
||
| 1413 | * |
||
| 1414 | * @param stribg $app |
||
| 1415 | * @param string $id |
||
| 1416 | * @param string $content |
||
| 1417 | * @return array |
||
| 1418 | */ |
||
| 1419 | protected function get_app_replacements($app, $id, $content, $prefix) |
||
| 1420 | { |
||
| 1421 | $replacements = array(); |
||
| 1422 | if($app == 'addressbook') |
||
| 1423 | { |
||
| 1424 | return $this->contact_replacements($id, $prefix); |
||
| 1425 | } |
||
| 1426 | |||
| 1427 | try |
||
| 1428 | { |
||
| 1429 | $classname = "{$app}_merge"; |
||
| 1430 | $class = new $classname(); |
||
| 1431 | $method = $app.'_replacements'; |
||
| 1432 | if(method_exists($class,$method)) |
||
| 1433 | { |
||
| 1434 | $replacements = $class->$method($id, $prefix, $content); |
||
| 1435 | } |
||
| 1436 | else |
||
| 1437 | { |
||
| 1438 | $replacements = $class->get_replacements($id, $content); |
||
| 1439 | } |
||
| 1440 | } |
||
| 1441 | catch (\Exception $e) |
||
| 1442 | { |
||
| 1443 | // Don't break merge, just log it |
||
| 1444 | error_log($e->getMessage()); |
||
| 1445 | } |
||
| 1446 | return $replacements; |
||
| 1447 | } |
||
| 1448 | |||
| 1449 | /** |
||
| 1450 | * Process special flags, such as IF or NELF |
||
| 1451 | * |
||
| 1452 | * @param content Text to be examined and changed |
||
| 1453 | * @param replacements array of markers => replacement |
||
| 1454 | * |
||
| 1455 | * @return changed content |
||
| 1456 | */ |
||
| 1457 | private function process_commands($content, $replacements) |
||
| 1458 | { |
||
| 1459 | View Code Duplication | if (strpos($content,'$$IF') !== false) |
|
| 1460 | { //Example use to use: $$IF n_prefix~Herr~Sehr geehrter~Sehr geehrte$$ |
||
| 1461 | $this->replacements =& $replacements; |
||
| 1462 | $content = preg_replace_callback('/\$\$IF ([#0-9a-z_\/-]+)~(.*)~(.*)~(.*)\$\$/imU',Array($this,'replace_callback'),$content); |
||
| 1463 | unset($this->replacements); |
||
| 1464 | } |
||
| 1465 | View Code Duplication | if (strpos($content,'$$NELF') !== false) |
|
| 1466 | { //Example: $$NEPBR org_unit$$ sets a LF and value of org_unit, only if there is a value |
||
| 1467 | $this->replacements =& $replacements; |
||
| 1468 | $content = preg_replace_callback('/\$\$NELF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content); |
||
| 1469 | unset($this->replacements); |
||
| 1470 | } |
||
| 1471 | View Code Duplication | if (strpos($content,'$$NENVLF') !== false) |
|
| 1472 | { //Example: $$NEPBRNV org_unit$$ sets only a LF if there is a value for org_units, but did not add any value |
||
| 1473 | $this->replacements =& $replacements; |
||
| 1474 | $content = preg_replace_callback('/\$\$NENVLF ([#0-9a-z_\/-]+)\$\$/imU',Array($this,'replace_callback'),$content); |
||
| 1475 | unset($this->replacements); |
||
| 1476 | } |
||
| 1477 | if (strpos($content,'$$LETTERPREFIX$$') !== false) |
||
| 1478 | { //Example use to use: $$LETTERPREFIX$$ |
||
| 1479 | $LETTERPREFIXCUSTOM = '$$LETTERPREFIXCUSTOM n_prefix title n_family$$'; |
||
| 1480 | $content = str_replace('$$LETTERPREFIX$$',$LETTERPREFIXCUSTOM,$content); |
||
| 1481 | } |
||
| 1482 | View Code Duplication | if (strpos($content,'$$LETTERPREFIXCUSTOM') !== false) |
|
| 1483 | { //Example use to use for a custom Letter Prefix: $$LETTERPREFIX n_prefix title n_family$$ |
||
| 1484 | $this->replacements =& $replacements; |
||
| 1485 | $content = preg_replace_callback('/\$\$LETTERPREFIXCUSTOM ([#0-9a-z_-]+)(.*)\$\$/imU',Array($this,'replace_callback'),$content); |
||
| 1486 | unset($this->replacements); |
||
| 1487 | } |
||
| 1488 | return $content; |
||
| 1489 | } |
||
| 1490 | |||
| 1491 | /** |
||
| 1492 | * Callback for preg_replace to process $$IF |
||
| 1493 | * |
||
| 1494 | * @param array $param |
||
| 1495 | * @return string |
||
| 1496 | */ |
||
| 1497 | private function replace_callback($param) |
||
| 1498 | { |
||
| 1499 | View Code Duplication | if (array_key_exists('$$'.$param[4].'$$',$this->replacements)) $param[4] = $this->replacements['$$'.$param[4].'$$']; |
|
| 1500 | View Code Duplication | if (array_key_exists('$$'.$param[3].'$$',$this->replacements)) $param[3] = $this->replacements['$$'.$param[3].'$$']; |
|
| 1501 | |||
| 1502 | $pattern = '/'.preg_quote($param[2], '/').'/'; |
||
| 1503 | if (strpos($param[0],'$$IF') === 0 && (trim($param[2]) == "EMPTY" || $param[2] === '')) |
||
| 1504 | { |
||
| 1505 | $pattern = '/^$/'; |
||
| 1506 | } |
||
| 1507 | $replace = preg_match($pattern,$this->replacements['$$'.$param[1].'$$']) ? $param[3] : $param[4]; |
||
| 1508 | switch($this->mimetype) |
||
| 1509 | { |
||
| 1510 | case 'application/vnd.oasis.opendocument.text': // open office |
||
| 1511 | case 'application/vnd.oasis.opendocument.spreadsheet': |
||
| 1512 | case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': // ms office 2007 |
||
| 1513 | case 'application/vnd.ms-word.document.macroenabled.12': |
||
| 1514 | case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': |
||
| 1515 | case 'application/vnd.ms-excel.sheet.macroenabled.12': |
||
| 1516 | case 'application/xml': |
||
| 1517 | case 'text/xml': |
||
| 1518 | case 'text/html': |
||
| 1519 | $is_xml = true; |
||
| 1520 | break; |
||
| 1521 | } |
||
| 1522 | |||
| 1523 | switch($this->mimetype) |
||
| 1524 | { |
||
| 1525 | case 'application/rtf': |
||
| 1526 | case 'text/rtf': |
||
| 1527 | $LF = '}\par \pard\plain{'; |
||
| 1528 | break; |
||
| 1529 | case 'application/vnd.oasis.opendocument.text': |
||
| 1530 | $LF ='<text:line-break/>'; |
||
| 1531 | break; |
||
| 1532 | case 'application/vnd.oasis.opendocument.spreadsheet': // open office calc |
||
| 1533 | $LF = '</text:p><text:p>'; |
||
| 1534 | break; |
||
| 1535 | case 'application/xmlExcel.Sheet': // Excel 2003 |
||
| 1536 | $LF = ' '; |
||
| 1537 | break; |
||
| 1538 | case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': |
||
| 1539 | case 'application/vnd.ms-word.document.macroenabled.12': |
||
| 1540 | case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': |
||
| 1541 | case 'application/vnd.ms-excel.sheet.macroenabled.12': |
||
| 1542 | $LF ='</w:t></w:r></w:p><w:p><w:r><w:t>'; |
||
| 1543 | break; |
||
| 1544 | case 'application/xml'; |
||
| 1545 | $LF ='</w:t></w:r><w:r><w:br w:type="text-wrapping" w:clear="all"/></w:r><w:r><w:t>'; |
||
| 1546 | break; |
||
| 1547 | case 'text/html': |
||
| 1548 | $LF = "<br/>"; |
||
| 1549 | break; |
||
| 1550 | default: |
||
| 1551 | $LF = "\n"; |
||
| 1552 | } |
||
| 1553 | if($is_xml) { |
||
| 1554 | $this->replacements = str_replace(array('&','&amp;','<','>',"\r","\n"),array('&','&','<','>','',$LF),$this->replacements); |
||
| 1555 | } |
||
| 1556 | if (strpos($param[0],'$$NELF') === 0) |
||
| 1557 | { //sets a Pagebreak and value, only if the field has a value |
||
| 1558 | if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF.$this->replacements['$$'.$param[1].'$$']; |
||
| 1559 | } |
||
| 1560 | if (strpos($param[0],'$$NENVLF') === 0) |
||
| 1561 | { //sets a Pagebreak without any value, only if the field has a value |
||
| 1562 | if ($this->replacements['$$'.$param[1].'$$'] !='') $replace = $LF; |
||
| 1563 | } |
||
| 1564 | if (strpos($param[0],'$$LETTERPREFIXCUSTOM') === 0) |
||
| 1565 | { //sets a Letterprefix |
||
| 1566 | $replaceprefixsort = array(); |
||
| 1567 | // ToDo Stefan: $contentstart is NOT defined here!!! |
||
| 1568 | $replaceprefix = explode(' ',substr($param[0],21,-2)); |
||
| 1569 | foreach ($replaceprefix as $nameprefix) |
||
| 1570 | { |
||
| 1571 | if ($this->replacements['$$'.$nameprefix.'$$'] !='') $replaceprefixsort[] = $this->replacements['$$'.$nameprefix.'$$']; |
||
| 1572 | } |
||
| 1573 | $replace = implode($replaceprefixsort,' '); |
||
| 1574 | } |
||
| 1575 | return $replace; |
||
| 1576 | } |
||
| 1577 | |||
| 1578 | /** |
||
| 1579 | * Download document merged with contact(s) |
||
| 1580 | * |
||
| 1581 | * @param string $document vfs-path of document |
||
| 1582 | * @param array $ids array with contact id(s) |
||
| 1583 | * @param string $name ='' name to use for downloaded document |
||
| 1584 | * @param string $dirs comma or whitespace separated directories, used if $document is a relative path |
||
| 1585 | * @return string with error-message on error, otherwise it does NOT return |
||
| 1586 | */ |
||
| 1587 | public function download($document, $ids, $name='', $dirs='') |
||
| 1588 | { |
||
| 1589 | //error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs') ->".function_backtrace()); |
||
| 1590 | if (($error = $this->check_document($document, $dirs))) |
||
| 1591 | { |
||
| 1592 | return $error; |
||
| 1593 | } |
||
| 1594 | $content_url = Api\Vfs::PREFIX.$document; |
||
| 1595 | switch (($mimetype = Api\Vfs::mime_content_type($document))) |
||
| 1596 | { |
||
| 1597 | case 'message/rfc822': |
||
| 1598 | //error_log(__METHOD__."('$document', ".array2string($ids).", '$name', dirs='$dirs')=>$content_url ->".function_backtrace()); |
||
| 1599 | $mail_bo = Api\Mail::getInstance(); |
||
| 1600 | $mail_bo->openConnection(); |
||
| 1601 | try |
||
| 1602 | { |
||
| 1603 | $msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, $_folder=''); |
||
| 1604 | } |
||
| 1605 | catch (Api\Exception\WrongUserinput $e) |
||
| 1606 | { |
||
| 1607 | // if this returns with an exeption, something failed big time |
||
| 1608 | return $e->getMessage(); |
||
| 1609 | } |
||
| 1610 | //error_log(__METHOD__.__LINE__.' Message after importMessageToMergeAndSend:'.array2string($msgs)); |
||
| 1611 | $retString = ''; |
||
| 1612 | if (count($msgs['success'])>0) $retString .= count($msgs['success']).' '.(count($msgs['success'])+count($msgs['failed'])==1?lang('Message prepared for sending.'):lang('Message(s) send ok.'));//implode('<br />',$msgs['success']); |
||
| 1613 | //if (strlen($retString)>0) $retString .= '<br />'; |
||
| 1614 | foreach($msgs['failed'] as $c =>$e) |
||
| 1615 | { |
||
| 1616 | $errorString .= lang('contact').' '.lang('id').':'.$c.'->'.$e.'.'; |
||
| 1617 | } |
||
| 1618 | if (count($msgs['failed'])>0) $retString .= count($msgs['failed']).' '.lang('Message(s) send failed!').'=>'.$errorString; |
||
| 1619 | return $retString; |
||
| 1620 | case 'application/vnd.oasis.opendocument.text': |
||
| 1621 | case 'application/vnd.oasis.opendocument.spreadsheet': |
||
| 1622 | $ext = $mimetype == 'application/vnd.oasis.opendocument.text' ? '.odt' : '.ods'; |
||
| 1623 | $archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,$ext).'-').$ext; |
||
| 1624 | copy($content_url,$archive); |
||
| 1625 | $content_url = 'zip://'.$archive.'#'.($content_file = 'content.xml'); |
||
| 1626 | $this->parse_html_styles = true; |
||
| 1627 | break; |
||
| 1628 | case 'application/vnd.openxmlformats-officedocument.wordprocessingml.d': // mimetypes in vfs are limited to 64 chars |
||
| 1629 | $mimetype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; |
||
| 1630 | case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': |
||
| 1631 | case 'application/vnd.ms-word.document.macroenabled.12': |
||
| 1632 | $archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.docx').'-').'.docx'; |
||
| 1633 | copy($content_url,$archive); |
||
| 1634 | $content_url = 'zip://'.$archive.'#'.($content_file = 'word/document.xml'); |
||
| 1635 | $fix = array( // regular expression to fix garbled placeholders |
||
| 1636 | '/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'. |
||
| 1637 | preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$$\\1$$', |
||
| 1638 | '/'.preg_quote('$$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:rPr><w:lang w:val="','/'). |
||
| 1639 | '([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>','/').'([a-z0-9_]+)'. |
||
| 1640 | preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:rPr><w:lang w:val="','/'). |
||
| 1641 | '([a-z]{2}-[A-Z]{2})'.preg_quote('"/></w:rPr><w:t>$$','/').'/i' => '$$\\2$$', |
||
| 1642 | '/'.preg_quote('$</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'. |
||
| 1643 | preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$\\1$', |
||
| 1644 | '/'.preg_quote('$ $</w:t></w:r><w:proofErr w:type="spellStart"/><w:r><w:t>','/').'([a-z0-9_]+)'. |
||
| 1645 | preg_quote('</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r><w:t>','/').'/i' => '$ $\\1$ $', |
||
| 1646 | ); |
||
| 1647 | break; |
||
| 1648 | case 'application/xml': |
||
| 1649 | $fix = array( // hack to get Excel 2003 to display additional rows in tables |
||
| 1650 | '/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"', |
||
| 1651 | ); |
||
| 1652 | break; |
||
| 1653 | case 'application/vnd.openxmlformats-officedocument.spreadsheetml.shee': |
||
| 1654 | $mimetype = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; |
||
| 1655 | case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': |
||
| 1656 | case 'application/vnd.ms-excel.sheet.macroenabled.12': |
||
| 1657 | $fix = array( // hack to get Excel 2007 to display additional rows in tables |
||
| 1658 | '/ss:ExpandedRowCount="\d+"/' => 'ss:ExpandedRowCount="9999"', |
||
| 1659 | ); |
||
| 1660 | $archive = tempnam($GLOBALS['egw_info']['server']['temp_dir'], basename($document,'.xlsx').'-').'.xlsx'; |
||
| 1661 | copy($content_url,$archive); |
||
| 1662 | $content_url = 'zip://'.$archive.'#'.($content_file = 'xl/sharedStrings.xml'); |
||
| 1663 | break; |
||
| 1664 | } |
||
| 1665 | $err = null; |
||
| 1666 | if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix))) |
||
| 1667 | { |
||
| 1668 | //error_log(__METHOD__."() !this->merge() err=$err"); |
||
| 1669 | return $err; |
||
| 1670 | } |
||
| 1671 | // Apply HTML formatting to target document, if possible |
||
| 1672 | // check if we can use the XSL extension, to not give a fatal error and rendering whole merge-print non-functional |
||
| 1673 | if (class_exists(XSLTProcessor) && class_exists(DOMDocument) && $this->parse_html_styles) |
||
| 1674 | { |
||
| 1675 | try |
||
| 1676 | { |
||
| 1677 | $this->apply_styles($merged, $mimetype); |
||
| 1678 | } |
||
| 1679 | catch (\Exception $e) |
||
| 1680 | { |
||
| 1681 | // Error converting HTML styles over |
||
| 1682 | error_log($e->getMessage()); |
||
| 1683 | error_log("Target document: $content_url, IDs: ". array2string($ids)); |
||
| 1684 | |||
| 1685 | // Try again, but strip HTML so user gets something |
||
| 1686 | $this->parse_html_styles = false; |
||
| 1687 | if (!($merged =& $this->merge($content_url,$ids,$err,$mimetype,$fix))) |
||
| 1688 | { |
||
| 1689 | return $err; |
||
| 1690 | } |
||
| 1691 | } |
||
| 1692 | if ($this->report_memory_usage) error_log(__METHOD__."() after HTML processing ".Api\Vfs::hsize(memory_get_peak_usage(true))); |
||
| 1693 | } |
||
| 1694 | if(!empty($name)) |
||
| 1695 | { |
||
| 1696 | if(empty($ext)) |
||
| 1697 | { |
||
| 1698 | $ext = '.'.pathinfo($document,PATHINFO_EXTENSION); |
||
| 1699 | } |
||
| 1700 | $name .= $ext; |
||
| 1701 | } |
||
| 1702 | else |
||
| 1703 | { |
||
| 1704 | $name = basename($document); |
||
| 1705 | } |
||
| 1706 | if (isset($archive)) |
||
| 1707 | { |
||
| 1708 | $zip = new ZipArchive; |
||
| 1709 | if ($zip->open($archive, ZipArchive::CHECKCONS) !== true) |
||
| 1710 | { |
||
| 1711 | error_log(__METHOD__.__LINE__." !ZipArchive::open('$archive',ZIPARCHIVE"."::CHECKCONS) failed. Trying open without validating"); |
||
| 1712 | if ($zip->open($archive) !== true) throw new Api\Exception("!ZipArchive::open('$archive',|ZIPARCHIVE::CHECKCONS)"); |
||
| 1713 | } |
||
| 1714 | if ($zip->addFromString($content_file,$merged) !== true) throw new Api\Exception("!ZipArchive::addFromString('$content_file',\$merged)"); |
||
| 1715 | if ($zip->close() !== true) throw new Api\Exception("!ZipArchive::close()"); |
||
| 1716 | unset($zip); |
||
| 1717 | unset($merged); |
||
| 1718 | if ($this->report_memory_usage) error_log(__METHOD__."() after ZIP processing ".Api\Vfs::hsize(memory_get_peak_usage(true))); |
||
| 1719 | Api\Header\Content::type($name,$mimetype,filesize($archive)); |
||
| 1720 | readfile($archive,'r'); |
||
| 1721 | } |
||
| 1722 | else |
||
| 1723 | { |
||
| 1724 | if ($mimetype == 'application/xml') |
||
| 1725 | { |
||
| 1726 | if (strpos($merged,'<?mso-application progid="Word.Document"?>') !== false) |
||
| 1727 | { |
||
| 1728 | $mimetype = 'application/msword'; // to open it automatically in word or oowriter |
||
| 1729 | } |
||
| 1730 | elseif (strpos($merged,'<?mso-application progid="Excel.Sheet"?>') !== false) |
||
| 1731 | { |
||
| 1732 | $mimetype = 'application/vnd.ms-excel'; // to open it automatically in excel or oocalc |
||
| 1733 | } |
||
| 1734 | } |
||
| 1735 | Api\Header\Content::type($name, $mimetype); |
||
| 1736 | echo $merged; |
||
| 1737 | } |
||
| 1738 | exit; |
||
| 1739 | } |
||
| 1740 | |||
| 1741 | /** |
||
| 1742 | * Download document merged with contact(s) |
||
| 1743 | * frontend for HTTP POST requests |
||
| 1744 | * accepts POST vars and calls internal function download() |
||
| 1745 | * string data_document_name: the document name |
||
| 1746 | * string data_document_dir: the document vfs directory |
||
| 1747 | * string data_checked: contact id(s) to merge with (can be comma separated) |
||
| 1748 | * |
||
| 1749 | * @return string with error-message on error, otherwise it does NOT return |
||
| 1750 | */ |
||
| 1751 | public function download_by_request() |
||
| 1764 | |||
| 1765 | /** |
||
| 1766 | * Get a list of document actions / files from the given directory |
||
| 1767 | * |
||
| 1768 | * @param string $dirs Directory(s comma or space separated) to search |
||
| 1769 | * @param string $prefix='document_' prefix for array keys |
||
| 1770 | * @param array|string $mime_filter=null allowed mime type(s), default all, negative filter if $mime_filter[0] === '!' |
||
| 1771 | * @return array List of documents, suitable for a selectbox. The key is document_<filename>. |
||
| 1772 | */ |
||
| 1773 | public static function get_documents($dirs, $prefix='document_', $mime_filter=null, $app='') |
||
| 1815 | |||
| 1816 | /** |
||
| 1817 | * From this number of documents, show them in submenus by mime type |
||
| 1818 | */ |
||
| 1819 | const SHOW_DOCS_BY_MIME_LIMIT = 10; |
||
| 1820 | |||
| 1821 | /** |
||
| 1822 | * Get insert-in-document action with optional default document on top |
||
| 1823 | * |
||
| 1824 | * If more than SHOW_DOCS_BY_MIME_LIMIT=10 documents found, they are displayed in submenus by mime type. |
||
| 1825 | * |
||
| 1826 | * @param string $dirs Directory(s comma or space separated) to search |
||
| 1827 | * @param int $group see nextmatch_widget::egw_actions |
||
| 1828 | * @param string $caption ='Insert in document' |
||
| 1829 | * @param string $prefix ='document_' |
||
| 1830 | * @param string $default_doc ='' full path to default document to show on top with action == 'document'! |
||
| 1831 | * @param int|string $export_limit =null export-limit, default $GLOBALS['egw_info']['server']['export_limit'] |
||
| 1832 | * @return array see nextmatch_widget::egw_actions |
||
| 1833 | */ |
||
| 1834 | public static function document_action($dirs, $group=0, $caption='Insert in document', $prefix='document_', $default_doc='', |
||
| 1995 | |||
| 1996 | /** |
||
| 1997 | * Set up a document action for an eml (email) document |
||
| 1998 | * |
||
| 1999 | * Email (.eml) documents get special action handling. They don't send a file |
||
| 2000 | * back to the client like the other documents. Merging for a single selected |
||
| 2001 | * contact opens a compose window, multiple contacts just sends. |
||
| 2002 | * |
||
| 2003 | * @param Array &$action Action to be modified for mail |
||
| 2004 | * @param Array $file Array of information about the document from Api\Vfs::find |
||
| 2005 | * @return void |
||
| 2006 | */ |
||
| 2007 | private static function document_mail_action(Array &$action, $file) |
||
| 2031 | |||
| 2032 | /** |
||
| 2033 | * Check if given document (relative path from document_actions()) exists in one of the given dirs |
||
| 2034 | * |
||
| 2035 | * @param string &$document maybe relative path of document, on return true absolute path to existing document |
||
| 2036 | * @param string $dirs comma or whitespace separated directories |
||
| 2037 | * @return string|boolean false if document exists, otherwise string with error-message |
||
| 2038 | */ |
||
| 2039 | public static function check_document(&$document, $dirs) |
||
| 2065 | |||
| 2066 | /** |
||
| 2067 | * Get a list of supported extentions |
||
| 2068 | */ |
||
| 2069 | public static function get_file_extensions() |
||
| 2073 | |||
| 2074 | /** |
||
| 2075 | * Format a number according to user prefs with decimal and thousands separator |
||
| 2076 | * |
||
| 2077 | * Reimplemented from etemplate to NOT use user prefs for Excel 2003, which gives an xml error |
||
| 2078 | * |
||
| 2079 | * @param int|float|string $number |
||
| 2080 | * @param int $num_decimal_places =2 |
||
| 2081 | * @param string $_mimetype ='' |
||
| 2082 | * @return string |
||
| 2083 | */ |
||
| 2084 | static public function number_format($number,$num_decimal_places=2,$_mimetype='') |
||
| 2096 | } |
||
| 2097 |
This checks looks for assignemnts to variables using the
list(...)function, where not all assigned variables are subsequently used.Consider the following code example.
Only the variables
$aand$care used. There was no need to assign$b.Instead, the list call could have been.