diff --git a/plugins/xtxpatch/php/classes/plgntls_class.php b/plugins/xtxpatch/php/classes/plgntls_class.php new file mode 100644 index 0000000..1e20660 --- /dev/null +++ b/plugins/xtxpatch/php/classes/plgntls_class.php @@ -0,0 +1,973 @@ + 'path/to/style2.css', // -> depends on style1.css +* 'http://my_url1.com', // +* 'script1_js' => 'path/to/script2.js', // -> depends on script1.js +* 'path/to/file1.html', // | will be returned +* 'path/to/file2.html', // -> | as expanded html +* 'path/to/file3.html', // | in the order included +* 'handle_name' => 'path/to/script3.js', // -> depends on the script with handle 'handle_name' +* array( // +* 'path/to/script4.js', // | add a script with attributes +* 'attribute_1' => 'value_1', // -> | first element of array must be +* 'attribute_2' => 'value_2', // | the script, without explicit key +* ), // +* array( 'js' => 'var_js' ), // -> add inline js, only first element will be used +* array( 'css' => 'var_css' ), // -> add inline css, only first element will be used +* ), +* compact( // these variables are added to html and js files +* 'var_1', // - in html files you can access them directly +* 'var_2', // - in js files they are properties of object PLGNTLS_data +* 'var_3', // like PLGNTLS_data.var_1; +* ) +* ); +* +*/ + +class Plgntls_xtx { + /* + * 0 : dont output + * 1 : output only level 1 - not always on top of function, output default, only prevent if specifically level 2 + * typical for early hooks like 'init' or 'wp', where there is a first check to see if you should 'enter' in this function, level 1 will be present after thoses checks + * 2 : output everything + * + */ + private static $_DEBUG_INFOS = 0; + + private static $_instance_count = 0; + private static $_adding_count = 0; + + private $_first_script = null; + private $_first_style = null; + private $_prefix = 'PLGNTLS_XTX'; + private $_js_dependencies = array(); + private $_css_dependencies = array(); + private $_scripts_attributes = array(); + + /* + */ + public function __construct() { + ++self::$_instance_count; + } + + + + + + public static function debug_infos($level = null) { + self::_debug_infos($level); + } + + + public static function add_to_front($srcs_arr = array(), $vars = array()) { + $instance = new self(); + return $instance->_add_to_front($srcs_arr, $vars); + } + + + + + + + + + + + + +/* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* ( +* PRIVATE FUNCTIONS +* +* 1. set paths and urls +* 2. debug logs +* 3. add to front +* 4. add menu +* +* END{ 4{ 3{ 2{ 1{ +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +*/ + + + /* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * 1 * + * * * * * * * + *})( set paths and urls * + * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + private static $_plugin_dir_path; + private static $_plugin_name; + private static $_file_dir_path; + private static $_file_name; + private static $_root_path; + private static $_root_url; + + /* + * ex: + * /home/www-data/cipf_plugin/php/test/test2/test3/test.php + * _plugin_dir_path /home/www-data + * _plugin_name cipf_plugin + * _file_dir_path php/test/test2/test3 + * _file_name test.php + * _root_path /home/www-data/cipf_plugin/ + * + * /home/www-data/cipf_plugin/test.php + * _plugin_dir_path /home/www-data + * _plugin_name cipf_plugin + * _file_dir_path '' + * _file_name test.php + * _root_path /home/www-data/cipf_plugin/ + * + */ + private static function set_root_dir() { + if (isset(self::$_plugin_name, self::$_file_name, self::$_file_dir_path, self::$_plugin_dir_path)) + return ; + /* + * it uses exploded_path_path by removing data from the array + * so order is important ! + * plugin_name / path / to / file.php + * exploded [plugin_name, path, to, file.php] + * plugin_name plugin_name [path, to, file.php] + * file_name [path, to] file.php + * file_dir_name path / to + */ + $exploded_plugin_path = explode('/', plugin_basename( __FILE__ )); + + self::$_plugin_name = array_shift($exploded_plugin_path); + self::$_file_name = array_pop($exploded_plugin_path); + self::$_file_dir_path = implode('/', $exploded_plugin_path); + + self::$_plugin_dir_path = str_replace('/'.plugin_basename(__DIR__).'/', '', plugin_dir_path(__FILE__)); + self::$_root_path = self::$_plugin_dir_path.'/'.self::$_plugin_name.'/'; + self::$_root_url = plugins_url(self::$_plugin_name.'/'); + } + public static function root_path() { + if (!isset(self::$_plugin_name, self::$_file_name, self::$_file_dir_path, self::$_plugin_dir_path)) + self::set_root_dir(); + return(self::$_root_path); + } + public static function root_url() { + if (!isset(self::$_plugin_name, self::$_file_name, self::$_file_dir_path, self::$_plugin_dir_path)) + self::set_root_dir(); + return(self::$_root_url); + } + + + + + + + + + + + + + /* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * 2 * + * * * * * * * + *})( debugs logs * + * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + private static function _debug_infos($level) { + if (self::$_DEBUG_INFOS === 0) { + return; + } + else if (self::$_DEBUG_INFOS === 1 && $level === 2) { + return; + } + $trace = debug_backtrace(); + $function = $trace[1]['function']; + $file = $trace[0]['file']; + $line = $trace[0]['line']; + error_log("-debug: function '".$function."' (in ".$file.", line ".$line .')'); + } + + + + + + + + + + + + + + /* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * 3 * + * * * * * * * + *})( add to front * + * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + + + private function _add_to_front($srcs_arr, $vars) { + /* + * even if the function is called with no srcs + * add fetch, because it can be used just for that + */ + if (self::$_adding_count === 0) { + $this->_add_fetch($srcs_arr, $vars); + } + + $srcs = array(); + foreach($srcs_arr as $src_key => $src_value) { + $init = $this->_init_src($src_key, $src_value); + if ($init !== null) + $srcs[] = $init; + } + if (!is_null($srcs_arr)) { + $this->_add_srcs_to_front($srcs); + } + + if (!is_null($vars)) { + $this->_add_vars_to_front($vars); + } + $add_html = $this->_create_html($srcs, $vars); + self::$_adding_count += 1; + return $add_html; + } + + + /* + * for fetch, we add the script that contains the fetch function + * it's an inline script, but is made globally available by adding it to window + * + */ + private function _add_fetch(&$srcs_arr, &$vars) { + // add fetch script at end of scripts list + // it will try to be added at the first script anyway, + // but for that, the scripts must be already enqueued, + // hence adding it at the end of the array + $srcs_arr[] = array('js'=>$this->_fetch_script()); + // for the custom endpoints in rest api to work + // they need to have a nonce named 'wp_rest' + // see : https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/ + $fetch_nonce = array("fetch_nonce" => wp_create_nonce('wp_rest')); + $fetch_url = array("fetch_url" => get_site_url() . "/wp-json"); + $vars = array_merge($vars, $fetch_nonce); + $vars = array_merge($vars, $fetch_url); + } + private function _fetch_script() { + return " + if (typeof window !== 'undefined') { + window.PLGNTLS_fetch = function PLGNTLS_fetch(url, options = {}) { + url = PLGNTLS_data.fetch_url + url; + + options.headers = options.headers || {}; + options.headers['X-WP-Nonce'] = PLGNTLS_data.fetch_nonce; + + return fetch(url, options); + } + } + "; + } + + + /* + * @param two arguments : + * 1. html files to include in front + * - can be a string of 1 filename + * - or an array of strings of filenames + * ( https://stackoverflow.com/q/4747876/9497573 ) + * - it's probably better to only add 1 file, and let it include other files + * 2. list of variables to make available to this files + * - in the form of key => val + * - recommanded to do it with compact() + * ex: _create_html( "file.html", compact("var1","var2",) ); + * ex: _create_html( array("file1.html", "file2.html"), array("var1"=>"value") ); + * @return a string of html code + * + */ + private function _create_html($files = null, $vars = null) { + if (is_null($files)) + return null; + $plgn_dir = $this->root_path(); + if (!is_null($vars)) + extract($vars); + + // using ob_start() and ob_get_clean() + // allows to have php expansion inside the html loaded + // in opposition to the method file_get_contents() + // https://stackoverflow.com/a/4402045/9497573 + ob_start(); + foreach($files as $file) { + if ($file->ext === 'html') + include($file->path); + } + $html = ob_get_clean(); + + return $html; + } + + /* + * pass variables to js front as global variables + * @param array : list of key => value + * with the key being name of the variable, like this : + * 'my_var' => 'value', + * simpler way to do it is to use compact when calling the function : + * add_var_to_front(compact("var1", "var2", "var3")); + * @param string (optionnal) : name of first embended script that need these variables + * (it will be available to this script and all followings) + * this name is the filename + "_" + extension : + * init.js -> init_js + */ + private function _add_vars_to_front($vars_arr) { + if (is_null($vars_arr)) + return ; + + $object_name = $this->_prefix . "_data"; + $vars_json = json_encode($vars_arr); + // note : we need to use 'var' instead of 'let' or 'const', + // because their scope is restricted to the if statement + $front = " + if (typeof $object_name === 'undefined') { + var $object_name = $vars_json; + } + else { + Object.assign($object_name, $vars_json); + } + "; + + $handle = $this->_check_inline_handles('js'); + if (!is_null($handle)) { + wp_add_inline_script($handle, $front, 'before'); + } + else { + // in last ressort, but bad + echo ''; + } + } + + /* + * @param array : list of files : + * - with their path from root of their type of file (ex: from js/ to .js files) + * - and with their extension + * @param boolean + * - to add ajax script and variables + * - default to true + */ + private function _add_srcs_to_front($srcs_arr) { + //wp_enqueue_script(, /url/to/script, [depends on], version, defer? ); + //wp_enqueue_style( , /url/to/script, [depends on], version, media ); + + $previous_css_basename = ''; + $previous_js_basename = ''; + foreach ($srcs_arr as $src) { + if ($src->inline === "js") { + $this->_add_inline_script($src); + } + else if ($src->inline === "css") { + $this->_add_inline_style($src); + } + else if (in_array($src->ext, array("js", "url"))) { + $this->_add_script($src, $previous_js_basename); + $previous_js_basename = $src->handle; + } + else if ($src->ext === "css") { + $this->_add_style($src, $previous_css_basename); + $previous_css_basename = $src->handle; + } + } + + // https://developer.wordpress.org/reference/hooks/wp_script_attributes/ + // https://wordpress.stackexchange.com/questions/66843/attach-a-private-function-at-a-hook + add_filter( 'wp_script_attributes', fn($attr)=>$this->_add_attributes_to_script($attr), 10, 1 ); + + //uncomment to print all enqueued files, can be usefull + /* + global $wp_scripts; + error_log("wp_scripts->queue:"); + error_log(json_encode($wp_scripts->queue)); + global $wp_styles; + error_log("wp_styles->queue:"); + error_log(json_encode($wp_styles->queue)); + */ + } + private function _add_script($script, $previous_js_basename) { + if (is_null($this->_first_script)) { + $this->_first_script = $script->handle; + } + $depends_on = $this->_check_dependencies($script, $previous_js_basename); + if ($depends_on !== null) { + wp_enqueue_script( $script->handle, $script->url, $depends_on, $script->version, true); + } + } + private function _add_style($style, $previous_css_basename) { + if (is_null($this->_first_style)) { + $this->_first_style = $style->handle; + } + $depends_on = $this->_check_dependencies($style, $previous_css_basename); + if ($depends_on !== null) { + wp_enqueue_style( $style->handle, $style->url, $depends_on, $style->version, ''); + } + } + private function _add_inline_script($src) { + $handle = $this->_check_inline_handles('js', $src); + if (!is_null($handle)) { + wp_add_inline_script($handle, $src->src, 'before'); + } + else { + // in last ressort, only add the script inline, it's not ideal, + // but the only situation where it should not work is if another script is loaded before + // and it should not be the case otherwise the handle would not have returned true + echo ''; + } + } + private function _add_inline_style($src) { + $handle = $this->_check_inline_handles('css', $src); + if (!is_null($handle)) { + wp_add_inline_style($handle, $src->src); + } + else { + // in last ressort, cf script notes above + echo ''; + } + } + + private function _add_attributes_to_script($attr) { + if (empty($attr['id'])) { + return $attr; + } + $handle = $attr['id']; + if (isset($this->_scripts_attributes[$handle])) { + $script = $this->_scripts_attributes[$handle]; + } + else { + return $attr; + } + + foreach($script as $key => $value){ + $attr[$key] = $value; + } + unset($this->_scripts_attributes[$handle]); + + return $attr; + } + + private function _check_dependencies(&$src, $previous_basename) { + if (in_array($src->ext, array("js", "url"))) { + global $wp_scripts; + $already_enqueued = array_search($src->handle, $wp_scripts->queue); + if ($already_enqueued !== false) { + return null; + } + } + else if ($src->ext === "css") { + global $wp_styles; + $already_enqueued = array_search($src->handle, $wp_styles->queue); + if ($already_enqueued !== false) { + return null; + } + } + $depends_on = array(); + if (isset($src->depends) && $src->depends !== '') { + $depends_on[] = $src->depends; + } + if (isset($previous_basename) && $previous_basename !== '') { + $depends_on[] = $previous_basename; + } + + return $depends_on; + } + + private function _check_inline_handles($type = null, $src = null) { + /* + * first, try to simply get the explicit dependency + * + */ + $handle = null;; + if (!is_null($src)){ + $handle = $src->depends; + } + if (!empty($handle) && !is_null($handle)) { + return $handle; + } + + /* + * second, try to get the first enqueued src of this call to 'add_to_front()' + * and make it the dependency + * default to js + * + */ + if ($type === "js" || is_null($type)) { + $handle = $this->_first_script; + } + else if ($type === "css") { + $handle = $this->_first_style; + } + if (!empty($handle) && !is_null($handle)) { + return $handle; + } + + /* + * third, try to get the last enqueued src of the page + * https://www.php.net/manual/en/function.array-key-last.php + * + */ + if ($type === "js" || is_null($type)) { + global $wp_scripts; + $array = $wp_scripts->queue; + } + else if ($type === "css") { + global $wp_styles; + $array = $wp_styles->queue; + } + $last_key = array_key_last($array); + if (!is_null($last_key)) { + return $array[$last_key]; + } + + /* + * didn't find any handle to add the src to it + * + */ + return null; + } + + /* + * @param string : name of the file, with its path from its extension directory + * - from js/ root for .js files + * - from css/ root for .css files + * @return object / null : + * - null if file is not valid + * - or an object with all the necessary infos : + * 0. src : array("name.js", ...) -> "name.js" + * "name.js" -> "name.js" + * 1. ext : name.js -> "js" + * 2. basename : name.js -> "name" + * 3. handle : name.js -> "name_js" + * 4. url : url to file in wordpress + * 5. path : path to file in server + * 6. version : used to avoid browser caching + * 7. depends : string if depends on a handle, or '' + * 8. attr : associative array of html attribute like 'type'=>'module' + * 9. inline : array("js" => "name.js") -> js + * array("css" => "name.css") -> css + */ + private function _init_src($key, $value) { + if (empty($value)) + return null; + $src = (object)[ + 'src' => null, + 'ext' => null, + 'basename' => null, + 'handle' => null, + 'url' => null, + 'path' => null, + 'version' => null, + 'depends' => null, + 'attr' => null, + 'inline' => null, + ]; + + // 7. depends + if (is_string($key)) + $src->depends = $key; + + // 0. src + // 8. attr + // 9. inline + // first element of array is used, so must not be empty + // value => ['path/to/file', 'key1'=>'value1', 'key2'=>'value2'] + // src => 'path/to/file' + // attr => ['key1'=>'value1', 'key2'=>'value2'] + if (is_array($value)){ + $first_key = array_keys($value)[0]; + if (empty($value[$first_key])) + return null; + if ($first_key === 0) { // is a script file or url with attributes + $src->src = array_shift($value); + $src->attr = $value; + } + else if ($first_key === "js" || $first_key === "css") { // is an inline code + $src->src = $value[$first_key]; + $src->inline = $first_key; + return $src; // inline only needs 'depends', 'src' and 'inline' + } + } + else { + $src->src = $value; + $src->attr = null; + } + + // 1. ext + if(filter_var($src->src, FILTER_VALIDATE_URL)) + $src->ext = "url"; + else + $src->ext = pathinfo($src->src, PATHINFO_EXTENSION); + if (! in_array($src->ext, array("js", "css", "html", "url"))) + return null; + + // 2. basename + // 3. handle + // basename handle + // https://www.url.com/route -> www.url.com/route -> www_url_com_route + // path/to/script.js -> script.js -> script_js + if ($src->ext === "url") { + $url = parse_url($src->src); + $src->basename = $url['host'] . $url['path']; + } + else + $src->basename = pathinfo($src->src, PATHINFO_BASENAME); + $src->handle = "PLGNTLS_" . str_replace(array('.', '/'), "_", $src->basename); + + // 4. url + // 5. path + // 6. version + if ($src->ext === "url") { + $src->url = $src->src; + $src->path = null; + $src->version = null; + } + else { + $src->url = $this->root_url().$src->src; + $src->path = $this->root_path().$src->src; + $src->version = date("ymd-Gis", filemtime($src->path)); + } + + // if ext is 'js' or 'url' and attr is not empty + // also add to global variable to access in 'wp_script_attributes' filter + if ($src->ext === 'js' || $src->ext === 'url') { + if ($src->attr !== null) { + $this->_scripts_attributes[$src->handle.'-js'] = $src->attr; + } + } + + return $src; + } + + + + + + + + + + + + + + /* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * 4 * + * * * * * * * + *})( add menu * + * * * * * * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ + + const SLUG_TOOGLE_ADMIN_MENU = ['_name'=>'toogle_admin_menu_url_xtxpatch', 'toggle'=>'toggle', 'show'=>'show', 'hide'=>'hide']; + const OPTION_TOGGLE_MENU = ['_name'=>'toggle_admin_menu_option_xtxpatch', 'show'=>'show', 'hide'=>'hide']; + + public static function add_menu($options) { + if (empty($options)) { + return; + } + else if (is_string($options)) { + $options = array('callback'=>$options); + } + add_action('admin_menu', function() use ($options) { + return self::_create_menu($options); + }); + add_filter('plugin_action_links_xtxpatch/xtxpatch.php', array(__CLASS__, '_add_link_to_plugin')); + add_action('template_redirect', array(__CLASS__, '_toggle_plugin_menu')); + } + + /* + * + * page_title -> (optional, default 'name') + * name -> (optional, default _plugin_name) + * capability -> (optional, default 'manage_options') + * slug -> (optional, default 'name') + * callback -> required + * toggle -> (optionale, default true) + * + */ + private static function _create_menu($options) { + if (!isset($options['name'])) { + $options['name'] = self::$_plugin_name; + } + $default = array( + 'page_title'=> $options['name'], + 'name' => $options['name'], + 'capability'=> 'manage_options', + 'slug' => $options['name'], + 'callback' => $options['callback'], + 'toggle' => true, + ); + foreach ($default as $key => $value) { + if (!isset($options[$key])) { + $options[$key] = $value; + } + } + + if (false === $options['toggle']) { + add_menu_page($options['page_title'], $options['name'], $options['capability'], $options['slug'], $options['callback']); + } + else { + self::_toggle_menu($options['page_title'], $options['name'], $options['capability'], $options['slug'], $options['callback']); + } + } + private static function _toggle_menu($menu_page_title, $menu_title, $menu_capability, $menu_slug, $menu_callback) { + $toggle_menu = self::OPTION_TOGGLE_MENU; + + if (false === get_option($toggle_menu['_name'])) { + add_option($toggle_menu['_name'], $toggle_menu['hide'], '', 'no'); + } + + $toggle = get_option($toggle_menu['_name']); + + if ($toggle === $toggle_menu['hide']) { + remove_menu_page($menu_slug); + } + else if ($toggle === $toggle_menu['show']) { + add_menu_page($menu_page_title, $menu_title, $menu_capability, $menu_slug, $menu_callback); + } + } + + /* + * add link under the plugin in the plugins admin page + * + */ + public static function _add_link_to_plugin($links) { + $slug_toggle = self::SLUG_TOOGLE_ADMIN_MENU; + $toggle_menu = self::OPTION_TOGGLE_MENU; + + $toggle = get_option($toggle_menu['_name']); + + if ($toggle === $toggle_menu['hide']) { + $links[] = 'show menu'; + } + else if ($toggle === $toggle_menu['show']) { + $links[] = 'hide menu'; + } + return $links; + } + + /* + * handle the toggle menu when url is reached + * + */ + public static function _toggle_plugin_menu() { + $slug_toggle = self::SLUG_TOOGLE_ADMIN_MENU; + $toggle_menu = self::OPTION_TOGGLE_MENU; + + global $wp; + $current_slug = $wp->request; + if ($current_slug !== $slug_toggle['_name']) { + return; + } + + $show = null; + if (!isset($_GET)) { + $show = null; + } + else if (empty($_GET)) { + $show = null; + } + if (!isset($_GET[$slug_toggle['toggle']])) { + $show = null; + } + else if ($_GET[$slug_toggle['toggle']] === $slug_toggle['show']) { + $show = true; + } + else if ($_GET[$slug_toggle['toggle']] === $slug_toggle['hide']) { + $show = false; + } + + if ($show === true) { + update_option($toggle_menu['_name'], $toggle_menu['show']); + } + else if ($show === false) { + update_option($toggle_menu['_name'], $toggle_menu['hide']); + } + + $plugins_menu_url = admin_url('plugins.php'); + wp_redirect($plugins_menu_url, 301); + exit; + } + + + + + + + /* + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + *}) end + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + */ +} + + +?> diff --git a/plugins/xtxpatch/php/classes/xtxpatch_class.php b/plugins/xtxpatch/php/classes/xtxpatch_class.php new file mode 100644 index 0000000..72aa58c --- /dev/null +++ b/plugins/xtxpatch/php/classes/xtxpatch_class.php @@ -0,0 +1,24 @@ +'toogle_admin_menu_url_xtxpatch', 'toggle'=>'toggle', 'show'=>'show', 'hide'=>'hide']; + const OPTION_TOGGLE_MENU = ['_name'=>'toggle_admin_menu_option_xtxpatch', 'show'=>'show', 'hide'=>'hide']; + +} + +?> diff --git a/plugins/xtxpatch/php/menu/admin_menu.php b/plugins/xtxpatch/php/menu/admin_menu.php new file mode 100644 index 0000000..cdc0bf0 --- /dev/null +++ b/plugins/xtxpatch/php/menu/admin_menu.php @@ -0,0 +1,42 @@ +hello

'; +} + + + + + + + +?> diff --git a/plugins/xtxpatch/xtxpatch.php b/plugins/xtxpatch/xtxpatch.php new file mode 100644 index 0000000..298382c --- /dev/null +++ b/plugins/xtxpatch/xtxpatch.php @@ -0,0 +1,29 @@ +