* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * */ // Disallow direct access to this file for security reasons if(!defined("IN_MYBB")) { die("Direct initialization of this file is not allowed.

Please make sure IN_MYBB is defined."); } /* --- Plugin API: --- */ function pluginlibrary_info() { return array( "name" => "PluginLibrary", "description" => "A collection of useful functions for other plugins.", "website" => "http://mods.mybb.com/view/pluginlibrary", "author" => "Andreas Klauer", "authorsite" => "mailto:Andreas.Klauer@metamorpher.de", "version" => "13", "guid" => "839e9d72e2875a51fccbc0257dfeda03", "compatibility" => "18*", "codename" => "pluginlibrary", ); } function pluginlibrary_is_installed() { // Don't try this at home. return false; } function pluginlibrary_install() { // Avoid unnecessary activation as a plugin with a friendly success message. flash_message("You have successfully installed PluginLibrary 13.", 'success'); admin_redirect("index.php?module=config-plugins"); } function pluginlibrary_uninstall() { } function pluginlibrary_activate() { } function pluginlibrary_deactivate() { } /* --- PluginLibrary class: --- */ class PluginLibrary { /** * Version number. */ public $version = 13; /** * Cache handler. */ public $cachehandler; /* --- Setting groups and settings: --- */ /** * Create and/or update setting group and settings. * * @param string Internal unique group name and setting prefix. * @param string Group title that will be shown to the admin. * @param string Group description that will show up in the group overview. * @param array The list of settings to be added to that group. * @param bool Generate language file. (Developer option, default false) */ function settings($name, $title, $description, $list, $makelang=false) { global $db; /* Setting group: */ if($makelang) { header("Content-Type: text/plain; charset=UTF-8"); echo " $db->escape_string($name), 'title' => $db->escape_string($title), 'description' => $db->escape_string($description)); // Check if the group already exists. $query = $db->simple_select("settinggroups", "gid", "name='${group['name']}'"); if($row = $db->fetch_array($query)) { // We already have a group. Update title and description. $gid = $row['gid']; $db->update_query("settinggroups", $group, "gid='{$gid}'"); } else { // We don't have a group. Create one with proper disporder. $query = $db->simple_select("settinggroups", "MAX(disporder) AS disporder"); $row = $db->fetch_array($query); $group['disporder'] = $row['disporder'] + 1; $gid = $db->insert_query("settinggroups", $group); } /* Settings: */ // Deprecate all the old entries. $db->update_query("settings", array("description" => "PLUGINLIBRARYDELETEMARKER"), "gid='$gid'"); // Create and/or update settings. foreach($list as $key => $setting) { // Prefix all keys with group name. $key = "{$name}_{$key}"; if($makelang) { echo "\$l['setting_{$key}'] = \"".addcslashes($setting['title'], '\\"$')."\";\n"; echo "\$l['setting_{$key}_desc'] = \"".addcslashes($setting['description'], '\\"$')."\";\n"; } // Filter valid entries. $setting = array_intersect_key($setting, array( 'title' => 0, 'description' => 0, 'optionscode' => 0, 'value' => 0, )); // Escape input values. $setting = array_map(array($db, 'escape_string'), $setting); // Add missing default values. $disporder += 1; $setting = array_merge( array('description' => '', 'optionscode' => 'yesno', 'value' => '0', 'disporder' => $disporder), $setting); $setting['name'] = $db->escape_string($key); $setting['gid'] = $gid; // Check if the setting already exists. $query = $db->simple_select('settings', 'sid', "gid='$gid' AND name='{$setting['name']}'"); if($row = $db->fetch_array($query)) { // It exists, update it, but keep value intact. unset($setting['value']); $db->update_query("settings", $setting, "sid='{$row['sid']}'"); } else { // It doesn't exist, create it. $db->insert_query("settings", $setting); } } if($makelang) { echo "\n?>\n"; exit; } // Delete deprecated entries. $db->delete_query("settings", "gid='$gid' AND description='PLUGINLIBRARYDELETEMARKER'"); // Rebuild the settings file. rebuild_settings(); } /** * Delete setting groups and settings. * * @param string Internal unique group name. * @param bool Also delete groups starting with name_. */ function settings_delete($name, $greedy=false) { global $db; $name = $db->escape_string($name); $where = "name='{$name}'"; if($greedy) { $lname = strtr($name, array('=' => '==', '_' => '=_', '%' => '=%')); $where .= " OR name LIKE '{$lname}=_%' ESCAPE '='"; } // Query the setting groups. $query = $db->simple_select('settinggroups', 'gid', $where); // Delete the group and all its settings. while($gid = $db->fetch_field($query, 'gid')) { $db->delete_query('settinggroups', "gid='{$gid}'"); $db->delete_query('settings', "gid='{$gid}'"); } // Rebuild the settings file. rebuild_settings(); } /* --- Template groups and templates: --- */ /** * Create and update template group and templates. * * @param string Prefix for the template group * @param string Title for the template group * @param array List of templates to be added to this group. */ function templates($prefix, $title, $list) { global $db; // Template prefix must not be empty, and must not contain _ if(!strlen($prefix) || strpos($prefix, '_') !== false) { trigger_error("Invalid template prefix", E_USER_ERROR); } $group = array('prefix' => $db->escape_string($prefix), 'title' => $db->escape_string($title)); // Update or create template group: $query = $db->simple_select('templategroups', 'prefix', "prefix='{$group['prefix']}'"); if($db->fetch_array($query)) { $db->update_query('templategroups', $group, "prefix='{$group['prefix']}'"); } else { $db->insert_query('templategroups', $group); } // Query already existing templates. $query = $db->simple_select('templates', 'tid,title,template', "sid=-2 AND (title='{$group['prefix']}' OR title LIKE '{$group['prefix']}=_%' ESCAPE '=')"); $templates = array(); $duplicates = array(); while($row = $db->fetch_array($query)) { $title = $row['title']; if(isset($templates[$title])) { // PluginLibrary had a bug that caused duplicated templates. $duplicates[] = $row['tid']; $templates[$title]['template'] = false; // force update later } else { $templates[$title] = $row; } } // Delete duplicated master templates, if they exist. if($duplicates) { $db->delete_query('templates', 'tid IN ('.implode(",", $duplicates).')'); } // Update or create templates. foreach($list as $name => $code) { if(strlen($name)) { $name = "{$prefix}_{$name}"; } else { $name = "{$prefix}"; } $template = array('title' => $db->escape_string($name), 'template' => $db->escape_string($code), 'version' => 1, 'sid' => -2, 'dateline' => TIME_NOW); // Update if(isset($templates[$name])) { if($templates[$name]['template'] !== $code) { // Update version for custom templates if present $db->update_query('templates', array('version' => 0), "title='{$template['title']}'"); // Update master template $db->update_query('templates', $template, "tid={$templates[$name]['tid']}"); } } // Create else { $db->insert_query('templates', $template); } // Remove this template from the earlier queried list. unset($templates[$name]); } // Remove no longer used templates. foreach($templates as $name => $row) { $name = $db->escape_string($name); $db->delete_query('templates', "title='{$name}'"); } } /** * Delete template group(s) and templates. * * @param string Prefix of the template group. * @param bool Also delete other groups starting with the prefix. */ function templates_delete($prefix, $greedy=false) { global $db; $prefix = $db->escape_string($prefix); $where = "prefix='{$prefix}'"; if($greedy) { $where .= " OR prefix LIKE '{$prefix}%'"; } // Query the template groups $query = $db->simple_select('templategroups', 'prefix', $where); // Build where string for templates $twhere = array(); while($row = $db->fetch_array($query)) { $tprefix = $db->escape_string($row['prefix']); $twhere[] = "title='{$tprefix}' OR title LIKE '{$tprefix}=_%' ESCAPE '='"; } if($twhere) // else there are no groups to delete { // Delete template groups. $db->delete_query('templategroups', $where); // Delete templates belonging to template groups. $db->delete_query('templates', implode(' OR ', $twhere)); } } /* --- Stylesheets: --- */ /** * build CSS string out of an [selector => [property => value]] array */ function _build_css($styles) { if(is_array($styles)) { $css = ""; foreach($styles as $selector => $properties) { $rule = "{$selector} {\n"; if(is_array($properties)) { foreach($properties as $property => $value) { $rule .= "\t{$property}: {$value};\n"; } } else { $rule .= "\t{$properties}\n"; } $rule .= "}\n\n"; $css .= $rule; } $styles = $css; } return $styles; } /** * build attachedto string out of an [file => [action, ]] array */ function _build_attachedto($attachedto) { if(is_array($attachedto)) { $result = array(); foreach($attachedto as $file => $actions) { if(is_array($actions)) { $actions = implode(",", $actions); } if($actions) { $file = "{$file}?{$actions}"; } $result[] = $file; } $attachedto = implode("|", $result); } return $attachedto; } /** * Update stylesheet metadata. * */ function _update_themes_stylesheets($stylesheet=false) { global $mybb; $tid = 1; // MyBB Master Style require_once MYBB_ROOT.$mybb->config['admin_dir'].'/inc/functions_themes.php'; if($stylesheet) { cache_stylesheet($stylesheet['tid'], $stylesheet['cachefile'], $stylesheet['stylesheet']); } update_theme_stylesheet_list($tid, false, true); // includes all children } /** * Add, update or activate a stylesheet * @param string Name of the stylesheet - lowercase version used for cache file. * @param string Stylesheet content. * @param string The files/actions the stylesheet is attached to. For global attachment, don't include this parameter. */ function stylesheet($name, $styles, $attachedto="") { global $db; // Build stylesheet data. $tid = 1; // MyBB Master Style if(substr($name, -4) != ".css") { $name .= '.css'; } $styles = $this->_build_css($styles); $attachedto = $this->_build_attachedto($attachedto); $stylesheet = array( 'name' => $name, 'tid' => $tid, 'attachedto' => $attachedto, 'stylesheet' => $styles, 'cachefile' => $name, 'lastmodified' => TIME_NOW, ); $dbstylesheet = array_map(array($db, 'escape_string'), $stylesheet); // Activate children, if present. $db->update_query('themestylesheets', array('attachedto' => $dbstylesheet['attachedto']), "name='{$dbstylesheet['name']}'"); // Update or insert parent stylesheet. $query = $db->simple_select('themestylesheets', 'sid', "tid='{$tid}' AND cachefile='{$name}'"); $sid = intval($db->fetch_field($query, 'sid')); if($sid) { $db->update_query('themestylesheets', $dbstylesheet, "sid='$sid'"); } else { $sid = $db->insert_query('themestylesheets', $dbstylesheet); $stylesheet['sid'] = intval($sid); } $this->_update_themes_stylesheets($stylesheet); } /** * Remove a stylesheet * @param string Stylesheet name */ function stylesheet_delete($name, $greedy=false, $delete=true) { global $db; // Check $name ends in .css and if not append it $tid = 1; // MyBB Master Style if(substr($name, -4) == ".css") { $name = substr($name, 0, -4); } // Query all stylesheets matching $name $dbname = $db->escape_string($name); $where = "name='{$dbname}.css'"; if($greedy) { $ldbname = strtr($dbname, array('=' => '==', '_' => '=_', '%' => '=%')); $where .= " OR name LIKE '{$ldbname}=_%.css' ESCAPE '='"; } // Delete stylesheets. if($delete) { $query = $db->simple_select('themestylesheets', 'tid,name', $where); while($stylesheet = $db->fetch_array($query)) { @unlink(MYBB_ROOT."cache/themes/{$stylesheet['tid']}_{$stylesheet['name']}"); @unlink(MYBB_ROOT."cache/themes/theme{$stylesheet['tid']}/{$stylesheet['name']}"); } $db->delete_query('themestylesheets', $where); } else { // Deactivate stylesheets. $db->update_query('themestylesheets', array('attachedto' => '-'), $where); } $this->_update_themes_stylesheets(); } /** * Deactivate stylesheets without deleting them. * */ function stylesheet_deactivate($name, $greedy=false) { $this->stylesheet_delete($name, $greedy, false); } /* --- Cache: --- */ /** * Obtain a non-database cache handler. */ function _cache_handler() { global $cache; if(is_object($cache->handler)) { return $cache->handler; } if(is_object($this->cachehandler)) { return $this->cachehandler; } // Fall back to disk handler. require_once MYBB_ROOT.'/inc/cachehandlers/disk.php'; $this->cachehandler = new diskCacheHandler(); return $this->cachehandler; } /** * Read on-demand cache. */ function cache_read($name) { global $cache; if(isset($cache->cache[$name])) { return $cache->cache[$name]; } $handler = $this->_cache_handler(); $contents = $handler->fetch($name); $cache->cache[$name] = $contents; return $contents; } /** * Write on-demand cache. */ function cache_update($name, $contents) { global $cache; $handler = $this->_cache_handler(); $cache->cache[$name] = $contents; return $handler->put($name, $contents); } /** * Delete cache. * * @param string Cache name or title. * @param bool Also delete caches starting with name_. */ function cache_delete($name, $greedy=false) { global $db, $cache; // Prepare for database query. $dbname = $db->escape_string($name); $where = "title='{$dbname}'"; // Delete on-demand or handler cache. $handler = $this->_cache_handler(); $handler->delete($name); // Greedy? if($greedy) { // Collect possible additional names... $names = array(); $name .= '_'; // ...from the currently loaded cache... $keys = array_keys($cache->cache); foreach($keys as $key) { if(strpos($key, $name) === 0) { $names[$key] = 0; } } // ...from the database... $ldbname = strtr($dbname, array('%' => '=%', '=' => '==', '_' => '=_')); $where .= " OR title LIKE '{$ldbname}=_%' ESCAPE '='"; $query = $db->simple_select('datacache', 'title', $where); while($row = $db->fetch_array($query)) { $names[$row['title']] = 0; } // ...from the filesystem... $start = strlen(MYBB_ROOT."cache/"); foreach((array)@glob(MYBB_ROOT."cache/{$name}*.php") as $filename) { if($filename) { $filename = substr($filename, $start, strlen($filename)-4-$start); $names[$filename] = 0; } } // ...and delete them all. foreach($names as $key=>$val) { $handler->delete($key); } } // Delete database caches too. $db->delete_query('datacache', $where); } /* --- Corefile edits: --- */ /** * insert comment at the beginning of each line */ function _comment($comment, $code) { if(is_array($code)) { $code = implode("\n", $code); } if(!is_string($code) || !strlen($code)) { return ""; } if(substr($code, -1) == "\n") { $code = substr($code, 0, -1); } $code = str_replace("\n", "\n{$comment}", "\n{$code}"); return substr($code, 1)."\n"; } /** * remove comment at the beginning of each line */ function _uncomment($comment, $code) { if(!strlen($code)) { return ""; } $code = "\n{$code}"; $code = str_replace("\n{$comment}", "\n", $code); return substr($code, 1); } /** * remove lines with comment at the beginning entirely */ function _zapcomment($comment, $code) { return preg_replace("#^".preg_quote($comment, "#").".*\n?#m", "", $code); } /** * align start and stop to newline characters in text */ function _align($text, &$start, &$stop) { // Align start to line boundary. $nl = strrpos($text, "\n", -strlen($text)+$start); $start = ($nl === false ? 0 : $nl + 1); // Align stop to line boundary. $nl = strpos($text, "\n", $stop); $stop = ($nl === false ? strlen($text) : $nl + 1); } /** * in text find the smallest first match for a series of search strings */ function _match($text, $search, &$start) { $stop = $start; // forward search (determine smallest stop) foreach($search as $needle) { $stop = strpos($text, $needle, $stop); if($stop === false) { // we did not find out needle, so this does not match return false; } $stop += strlen($needle); } // backward search (determine largest start) $start = $stop; foreach(array_reverse($search) as $needle) { $start = strrpos($text, $needle, -strlen($text)+$start-strlen($needle)); } return $stop; } /** * dissect text based on a series of edits */ function _dissect($text, &$edits) { $matches = array(); foreach($edits as &$edit) { $search = (array)$edit['search']; $start = 0; $edit['matches'] = array(); while(($stop = $this->_match($text, $search, $start)) !== false) { $pos = $stop; $this->_align($text, $start, $stop); // to count the matches, and help debugging $edit['matches'][] = array($start, $stop, substr($text, $start, $stop-$start)); if(isset($matches[$start])) { $matches[$start][1]['error'] = 'match collides with another edit'; $edit['error'] = 'match collides with another edit'; return false; } else if(count($edit['matches']) > 1 && !$edit['multi']) { $edit['error'] = 'multiple matches not allowed for this edit'; return false; } $matches[$start] = array($stop, &$edit); $start = $pos; } if(!count($edit['matches']) && !$edit['none']) { $edit['error'] = 'zero matches not allowed for this edit'; return false; } } ksort($matches); return $matches; } /** * edit text (perform the actual string modification) */ function _edit($text, &$edits, $ins='/**/', $del='/*/*') { $matches = $this->_dissect($text, $edits); if($matches === false) { return false; } $result = array(); $pos = 0; foreach($matches as $start => $val) { $stop = $val[0]; $edit = &$val[1]; if($start < $pos) { $edit['error'] = 'match overlaps with another edit'; $previous_edit['error'] = 'match overlaps with another edit'; return false; } // Keep previous edit for overlapping detection $previous_edit = &$edit; // unmodified text before match $result[] = substr($text, $pos, $start-$pos); // insert before $result[] = $this->_comment($ins, $edit['before']); // original matched text $match = substr($text, $start, $stop-$start); $pos = $stop; $dirty = 0; if($edit['replace'] || is_string($edit['replace']) || is_array($edit['replace'])) { // insert match (commented out) $result[] = $this->_comment($del, $match); $result[] = $this->_comment($ins, $edit['replace']); if(!strlen($result[count($result)-1])) { $dirty = 1; // still a comment open } } else { // insert match unmodified $result[] = $match; } // insert after $result[] = $this->_comment($ins, $edit['after']); if($dirty && !strlen($result[count($result)-1])) { // close open comment $result[] = "{$ins}\n"; } } // insert rest $result[] = substr($text, $pos); return implode("", $result); } /** * edit core */ function edit_core($name, $file, $edits=array(), $apply=false, &$debug=null) { $ins = "/* + PL:{$name} + */ "; $del = "/* - PL:{$name} - /* "; $text = file_get_contents(MYBB_ROOT.$file); $result = $text; if($text === false) { return false; } // Convert single edit into array of edits. if(array_key_exists('search', $edits)) { $edits = array($edits); } // Step 1: remove old comments, if present. $result = $this->_zapcomment($ins, $result); $result = $this->_uncomment($del, $result); // Step 2: prevent colliding edits by adding conditions. $edits[] = array('search' => array('/* + PL:'), 'multi' => true, 'none' => true); $edits[] = array('search' => array('/* - PL:'), 'multi' => true, 'none' => true); // Step 3: perform edits. $result = $this->_edit($result, $edits, $ins, $del); // call_time_pass_reference :-( $debug = $edits; if($result === false) { // edits couldn't be performed return false; } if($result == $text) { // edit made no changes return true; } // try to write the file if($apply && @file_put_contents(MYBB_ROOT.$file, $result) !== false) { // changes successfully applied return true; } // return the string return $result; } /* --- Group memberships: --- */ /** * is_member */ function is_member($groups, $user=false) { global $mybb; // Default to current user. if($user === false) { $user = $mybb->user; } else if(is_array($user)) { // do nothing } else { // assume it's a UID $user = get_user($user); } // Collect the groups the user is in. $memberships = explode(',', $user['additionalgroups']); $memberships[] = $user['usergroup']; // Convert search to an array of group ids if(is_array($groups)) { // already an array, do nothing } else if(is_string($groups)) { $groups = explode(',', $groups); } else { // probably a single number $groups = (array)$groups; } // Make sure we're comparing numbers. $groups = array_map('intval', $groups); $memberships = array_map('intval', $memberships); // Remove 0 if present. $groups = array_filter($groups); // Return the group intersection. return array_intersect($groups, $memberships); } /* --- String functions: --- */ /** * url_append */ function url_append($url, $params, $sep="&", $encode=true) { if(strpos($url, '?') === false) { $separator = '?'; } else { $separator = $sep; } $append = ''; foreach($params as $key => $value) { if($encode) { $value = urlencode($value); } $append .= "{$separator}{$key}={$value}"; $separator = $sep; } $pos = strpos($url, '#'); if($pos === false) { $pos = strlen($url); } return substr_replace($url, $append, $pos, 0); } /** * _xml_element */ function _xml_tag($tag, $content, $indent=0) { $nl = "\n" . str_repeat(' ', $indent); $result = ''; if(is_string($content)) { // We can either htmlspecialchars, $a = htmlspecialchars($content); // or cdata (properly escaped), $b = '', ']]]]>', $content) .']]>'; // just pick whatever is shorter $content = (strlen($a) < strlen($b) ? $a : $b); $result .= "{$nl}<{$tag}>{$content}"; } else if(is_bool($content)) { $result .= "{$nl}<{$tag} type=\"BOOL\">{$content}"; } else if(is_int($content)) { $result .= "{$nl}<{$tag} type=\"INT\">{$content}"; } else if(is_float($content)) { $result .= "{$nl}<{$tag} type=\"FLOAT\">{$content}"; } else if(is_array($content)) { $result .= "{$nl}<{$tag}>".$this->_xml_array($content, $indent+2)."{$nl}"; } return $result; } /** * _xml_array */ function _xml_array($array, $indent=0) { $nl = "\n".str_repeat(' ', $indent); $nl2 = $nl.' '; $result = ''; foreach($array as $key => $value) { $key = $this->_xml_tag('key', $key, $indent+4); if($key) { $value = $this->_xml_tag('value', $value, $indent+4); if($value) { $result .= "{$nl2}{$key}{$value}{$nl2}"; } } } return "{$nl}{$result}{$nl}"; } /** * xml_export */ function xml_export($data, $filename=false, $comment='MyBB PluginLibrary XML-Export :: {time}', $endcomment='End of file.') { $result = ''; if(is_array($data)) { $xml = $this->_xml_array($data); } else { $xml = $this->_xml_tag('value', $data); } if($xml) { $result = "\n"; $time = date('c', TIME_NOW); if($comment) { $comment = str_replace('{time}', $time, $comment); $result .= ""; } $result .= $xml."\n"; if($endcomment) { $endcomment = str_replace('{time}', $time, $endcomment); $result .= "\n"; } if($filename) { // Filename encoding sucks. $filename = trim(basename('/'.$filename)); // Output the XML directly. @header('Content-Type: application/xml; charset=UTF-8'); @header('Expires: Sun, 20 Feb 2011 13:47:47 GMT'); // past @header('Last-Modified: '.gmdate('D, d M Y H:i:s T')); @header('Pragma: no-cache'); @header('Content-Disposition: attachment; filename="'.$filename.'"'); @header('Content-Length: '.strlen($result)); echo $result; exit; } } return $result; } /** * xml_import */ function xml_import($xml, &$error=null) { $parser = xml_parser_create(); $stack = array(); if(xml_parse_into_struct($parser, $xml, $values)) { foreach($values as $value) { // Convert data switch(strtoupper($value['attributes']['TYPE'])) { case 'BOOL': $value['value'] = $value['value'] && true; break; case 'INT': $value['value'] = intval($value['value']); break; case 'FLOAT': $value['value'] = floatval($value['value']); break; default: // Assume string. Mainly for NULL => ''. $value['value'] = strval($value['value']); break; } $input = strtolower("{$value['tag']}-{$value['type']}"); // Parse XML element (sloppy) switch($input) { case 'array-complete': case 'array-open': // Put array on stack array_unshift($stack, array()); break; case 'element-close': // Put key, value in array // Remove value, key from stack $stack[2][$stack[1]] = $stack[0]; array_shift($stack); array_shift($stack); break; case 'element-open': // Put key, value on stack array_unshift($stack, null, null); break; case 'key-complete': // Set key $stack[1] = $value['value']; break; case 'value-complete': // Set value $stack[0] = $value['value']; break; case 'value-open': // Remove value from stack (new array should follow) array_shift($stack); break; default: // Ignore others. break; } // If there is something wrong, quit early. if(!sizeof($stack)) { break; } } // The stack should contain a single value now if(sizeof($stack) == 1) { $result = $stack[0]; } else { $error = array('line' => -1, 'code' => -1, 'error' => -1, 'message' => 'XML is valid, but there is no data to import.'); } } else { // collect error information for debugging purposes $lines = explode("\n", $xml); $error = array('line' => xml_get_current_line_number($parser), 'code' => $lines[xml_get_current_line_number($parser)-1], 'error' => xml_get_error_code($parser), 'message' => xml_error_string(xml_get_error_code($parser))); } xml_parser_free($parser); return $result; } } global $PL; $PL = new PluginLibrary(); /* --- End of file. --- */