I have got hundereds of HTML files that need to be conveted in XML. We are using these HTML to serve contents for applications but now we have to serve these contents as XML.
HTML files are contains, tables, div's, image's, p's, b or strong tags, etc..
I googled and found some applications but i couldn't achive yet.
Could you suggest a way to convert these file contents to XML?
I was successful using tidy command line utility. On linux I installed it quickly with apt-get install tidy. Then the command:
tidy -q -asxml --numeric-entities yes source.html >file.xml
gave an xml file, which I was able to process with xslt processor. However I needed to set up xhtml1 dtds correctly.
This is their homepage: html-tidy.org (and the legacy one: HTML Tidy)
I did found a way to convert (even bad) html into well formed XML. I started to base this on the DOM loadHTML function. However during time several issues occurred and I optimized and added patches to correct side effects.
function tryToXml($dom,$content) {
if(!$content) return false;
// xml well formed content can be loaded as xml node tree
$fragment = $dom->createDocumentFragment();
// wonderfull appendXML to add an XML string directly into the node tree!
// aappendxml will fail on a xml declaration so manually skip this when occurred
if( substr( $content,0, 5) == '<?xml' ) {
$content = substr($content,strpos($content,'>')+1);
if( strpos($content,'<') ) {
$content = substr($content,strpos($content,'<'));
}
}
// if appendXML is not working then use below htmlToXml() for nasty html correction
if(!#$fragment->appendXML( $content )) {
return $this->htmlToXml($dom,$content);
}
return $fragment;
}
// convert content into xml
// dom is only needed to prepare the xml which will be returned
function htmlToXml($dom, $content, $needEncoding=false, $bodyOnly=true) {
// no xml when html is empty
if(!$content) return false;
// real content and possibly it needs encoding
if( $needEncoding ) {
// no need to convert character encoding as loadHTML will respect the content-type (only)
$content = '<meta http-equiv="Content-Type" content="text/html;charset='.$this->encoding.'">' . $content;
}
// return a dom from the content
$domInject = new DOMDocument("1.0", "UTF-8");
$domInject->preserveWhiteSpace = false;
$domInject->formatOutput = true;
// html type
try {
#$domInject->loadHTML( $content );
} catch(Exception $e){
// do nothing and continue as it's normal that warnings will occur on nasty HTML content
}
// to check encoding: echo $dom->encoding
$this->reworkDom( $domInject );
if( $bodyOnly ) {
$fragment = $dom->createDocumentFragment();
// retrieve nodes within /html/body
foreach( $domInject->documentElement->childNodes as $elementLevel1 ) {
if( $elementLevel1->nodeName == 'body' and $elementLevel1->nodeType == XML_ELEMENT_NODE ) {
foreach( $elementLevel1->childNodes as $elementInject ) {
$fragment->insertBefore( $dom->importNode($elementInject, true) );
}
}
}
} else {
$fragment = $dom->importNode($domInject->documentElement, true);
}
return $fragment;
}
protected function reworkDom( $node, $level = 0 ) {
// start with the first child node to iterate
$nodeChild = $node->firstChild;
while ( $nodeChild ) {
$nodeNextChild = $nodeChild->nextSibling;
switch ( $nodeChild->nodeType ) {
case XML_ELEMENT_NODE:
// iterate through children element nodes
$this->reworkDom( $nodeChild, $level + 1);
break;
case XML_TEXT_NODE:
case XML_CDATA_SECTION_NODE:
// do nothing with text, cdata
break;
case XML_COMMENT_NODE:
// ensure comments to remove - sign also follows the w3c guideline
$nodeChild->nodeValue = str_replace("-","_",$nodeChild->nodeValue);
break;
case XML_DOCUMENT_TYPE_NODE: // 10: needs to be removed
case XML_PI_NODE: // 7: remove PI
$node->removeChild( $nodeChild );
$nodeChild = null; // make null to test later
break;
case XML_DOCUMENT_NODE:
// should not appear as it's always the root, just to be complete
// however generate exception!
case XML_HTML_DOCUMENT_NODE:
// should not appear as it's always the root, just to be complete
// however generate exception!
default:
throw new exception("Engine: reworkDom type not declared [".$nodeChild->nodeType. "]");
}
$nodeChild = $nodeNextChild;
} ;
}
Now this also allows to add more html pieces into one XML which I needed to use myself. In general it can be used like this:
$c='<p>test<font>two</p>';
$dom=new DOMDocument('1.0', 'UTF-8');
$n=$dom->appendChild($dom->createElement('info')); // make a root element
if( $valueXml=tryToXml($dom,$c) ) {
$n->appendChild($valueXml);
}
echo '<pre/>'. htmlentities($dom->saveXml($n)). '</pre>';
In this example '<p>test<font>two</p>' will nicely be outputed in well formed XML as '<info><p>test<font>two</font></p></info>'. The info root tag is added as it will also allow to convert '<p>one</p><p>two</p>' which is not XML as it has not one root element. However if you html does for sure have one root element then the extra root <info> tag can be skipped.
With this I'm getting real nice XML out of unstructured and even corrupted HTML!
I hope it's a bit clear and might contribute to other people to use it.
Related
I am not fan of DOMDocument because I believe it is not very good for real world usages. Yet in current project I need to replace all texts in a page (which I don't have access to source code) with other strings (some sort of translation); so I need to use it.
I tried doing this with DOMDocument and I didn't received the expected result. Here is the code I use:
function Translate_DoHTML($body, $replaceArray){
if ($replaceArray && is_array($replaceArray) && count($replaceArray) > 0){
$body2 = mb_convert_encoding($body, 'HTML-ENTITIES', "UTF-8");
$doc = new DOMDocument();
$doc->resolveExternals = false;
$doc->substituteEntities = false;
$doc->strictErrorChecking = false;
if (#$doc->loadHTML($body2)){
Translate_DoHTML_Process($doc, $replaceArray);
$body = $doc->saveHTML();
}
}
return $body;
}
function Translate_DoHTML_Process($node, $replaceRules){
if($node->hasChildNodes()) {
$nodes = array();
foreach ($node->childNodes as $childNode)
$nodes[] = $childNode;
foreach ($nodes as $childNode)
if ($childNode instanceof DOMText) {
if (trim($childNode->wholeText)){
$text = str_ireplace(array_keys($replaceRules), array_values($replaceRules), $childNode->wholeText);
$node->replaceChild(new DOMText($text),$childNode);
}
}else
Translate_DoHTML_Process($childNode, $replaceRules);
}
}
And here are the problems:
Escaping attributes: There are data-X attributes in file that become escaped. This is not a major problem but it would be great if I could disable this behavior.
Before DOM:
data-link-content=" <a class="submenuitem" href="
After DOM:
data-link-content=' <a class="submenuitem" href="
Removing of closing tags in javascript:
This is actually the main problem for me here. I don't know for what reason in the world DOMDocument may see any need to remove these tags. But it do. As you can clearly see in below example it remove closing tags in java-script string. It also removed last part of script. It seems like DOMDocument parse the java-script inside. Maybe because there is no CDATA tag? But any way it is HTML and we don't need CDDATA in HTML. I thought CDATA is for xHTML. Also I have no way to add CDDATA here. So can I ask it to not parse script tags?
Before DOM:
<script type="text/javascript"> document.write('<video src="http://x.webm"><p>You will need to Install the latest Flash plugin to view this page properly.</p></video>'); </script>
After DOM:
<script type="text/javascript"> document.write('<video src="http://x.webm"><p>You will need to <a href="http://www.adobe.com/go/getflashplayer" target="_blank">Install the latest Flash plugin to view this page properly.</script>
If there is no way for me to prevent these things, is there any way that I can port this code to SimpleHTMLDOM?
Thanks you very much.
Try this , and replace line content ;
$body2 = mb_convert_encoding($body, 'HTML-ENTITIES', "UTF-8");
to ;
$body2 = convertor($body);
and insert in your code ;
function convertor($ToConvert)
{
$FromConvert = html_entity_decode($ToConvert,ENT_QUOTES,'ISO-8859-1');
$Convert = mb_convert_encoding($FromConvert, "ISO-8859-1", "UTF-8");
return ltrim($Convert);
}
But use the right encoding in the context.
Have a nice day.
Based on my search, reason of the second problem is actually what "Alex" told us in this question: DOM parser that allows HTML5-style </ in <script> tag
But based on their research there is no good parser out there capable of understanding today's HTML. Also, html5lib's last update was 2 years ago and it failed to work in real world situations based on my tests.
So I had only one way to solve the second problem. RegEx. Here is the code I use:
function Translate_DoHTML_GetScripts($body){
$res = array();
if (preg_match_all('/<script\b[^>]*>([\s\S]*?)<\/script>/m', $body, $matches) && is_array($matches) && isset($matches[0])){
foreach ($matches[0] as $key => $match)
$res["<!-- __SCRIPTBUGFIXER_PLACEHOLDER".$key."__ -->"] = $match;
$body = str_ireplace(array_values($res), array_keys($res), $body);
}
return array('Body' => $body, 'Scripts' => $res);
}
function Translate_DoHTML_SetScripts($body, $scripts){
return str_ireplace(array_keys($scripts), array_values($scripts), $body);
}
Using above two functions I will remove any script from HTML so I can use DomDocument to do my works. Then again at the end, I will add them back exactly where they were.
Yet I am not sure if regex is fast enough for this.
And don't tell me to not use RegEx for HTML. I know that HTML is not a regular language and so on; but if you read the problem your self, you will suggest the same approach.
Is it possible to have a wiki page with two tables reflecting the data from two different 3rd party sites?
If so, how to get it done? Will page templates be of any help here?
Short answer is no, there's no easy, built-in way to pull external content into a MediaWiki site. Allowing a third party to inject arbitrary content would be massive security risk.
Long answer is that anything is possible with extensions, either existing ones or ones you write yourself. The MediaWiki site has an entire category of listings for "Remote content extensions" that do this kind of thing in one form or another, with External Data looking particularly useful. You will need admin rights to install any of these, and you'll need to trust both the extension code and the data you pull in.
I already wrote exactly what you describe. Might be helpful for you.
# Define a setup function
$wgHooks['ParserFirstCallInit'][] = 'efStackOverflow_Setup';
# Add a hook to initialise the magic word
$wgHooks['LanguageGetMagic'][] = 'efStackOverflow_Magic';
function efStackOverflow_Setup( &$parser ) {
# Set a function hook associating the "example" magic word with our function
$parser->setFunctionHook( 'stag', 'efStackOverflow_Render' );
return true;
}
function efStackOverflow_Magic( &$magicWords, $langCode ) {
# Add the magic word
# The first array element is whether to be case sensitive, in this case (0) it is not case sensitive, 1 would be sensitive
# All remaining elements are synonyms for our parser function
$magicWords['stag'] = array(1, 'stag');
# unless we return true, other parser functions extensions won't get loaded.
return true;
}
function efStackOverflow_Render( $parser, $param1 = '', $param2 = '' ) {
// there was filtering
$modif = 0;
$cache_file_path = "cache/".$param1."_".$param2;
if (file_exists($cache_file_path))
$modif = time() - #filemtime ($cache_file_path);
if (file_exists($cache_file_path) and $modif < 60*60*24) {
return file_get_contents($cache_file_path);
}
$page = file_get_contents("http://www.google.com/rss/".$param1);
$xml = new SimpleXMLElement($page);
foreach ($xml as $key => $value) {
// do some
}
if (!empty($output))
file_put_contents($cache_file_path, $output);
return $output;
}
Mediawiki version was 1.16.
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 5 years ago.
Improve this question
i need to speed up my Wordpress blog. I searched around the web, but no success.
I want to minify or compress my output html code on one(single) line, like Matt Cutt's blog.
I tried W3TC, WP Minify and many others, but without result.
I need script, plugin, function or something that works.
Thanks in advance.
Here is one solution what I give in WordPress.StackExchange.com
https://wordpress.stackexchange.com/a/227896/82023
Generaly, in index.php you can place one code what will compress your HTML using regex. i made and use this on many places and work fine. Is not complete inline but do it's job.
<?php
/**
* Front to the WordPress application. This file doesn't do anything, but loads
* wp-blog-header.php which does and tells WordPress to load the theme.
*
* #package WordPress
*/
/**
* Tells WordPress to load the WordPress theme and output it.
*
* #var bool
*/
define('WP_USE_THEMES', true);
/** Manualy compress WP by Ivijan-Stefan Stipic **/
function compressorCF($str)
{
// clear HEAD
$str = preg_replace_callback('/(?=<head(.*?)>)(.*?)(?<=<\/head>)/s',
function($matches) {
return preg_replace(array(
/* Fix HTML */
'/\>[^\S ]+/s', // strip whitespaces after tags, except space
'/[^\S ]+\</s', // strip whitespaces before tags, except space
'/\>\s+\</', // strip whitespaces between tags
), array(
/* Fix HTML */
'>', // strip whitespaces after tags, except space
'<', // strip whitespaces before tags, except space
'><', // strip whitespaces between tags
), $matches[2]);
}, $str);
// clear BODY
$str = preg_replace_callback('/(?=<body(.*?)>)(.*?)(?<=<\/body>)/s',
function($matches) {
return preg_replace(array(
'/<!--(.*?)-->/s', // delete HTML comments
'#\/\*(.*?)\*\/#s', // delete JavaScript comments
/* Fix HTML */
'/\>[^\S ]+/s', // strip whitespaces after tags, except space
'/[^\S ]+\</s', // strip whitespaces before tags, except space
'/\>\s+\</', // strip whitespaces between tags
), array(
'', // delete HTML comments
'', // delete JavaScript comments
/* Fix HTML */
'>', // strip whitespaces after tags, except space
'<', // strip whitespaces before tags, except space
'><', // strip whitespaces between tags
), $matches[2]);
}, $str);
return $str;
}
/** Loads the WordPress Environment and Template */
ob_start();
require_once( dirname( __FILE__ ) . '/wp-blog-header.php' );
$content=ob_get_clean();
//echo $content;
echo compressorCF($content);
This is really not the best way to speedup your site. If you do it in the template it make the files unreadable and hard to maintain for less than 1% speedup. If you do it with a plugin that process the output, it will slow down the render.
Make sure :
You use as few plugins as possible, for example it's much faster to copy tracking code (google analytics or such) in footer.php than using a plugin
You have compiled, cleaned, minifyied CSS and JS that is on your server and properly compressed files.
You use CDN for all files that are on CDN like JQuery on https://developers.google.com/speed/libraries/devguide
Put mod_expire on your server and set expire date for media files far in future with .htaccess . This will prevent browsers from checking if files have changed (all the 200 status code you see in network traffic analysis)
Cache content using WP supercache or similar plugin
Install APC cache with enough memory (at least 32M for a single WP installation)
If your plugin loads CSS and JS files properly there is no point to say that they will make your website slow.
1. Another issue is how many database call these plugin make to render a particular task.
2. Whether that plugin check data from a remote server to update something like akismat and jetpack, thus makes these two pluin the most resource hungry.
Proper coded theme can help you to load your website properly. like my own site ( http://www.binarynote.com ) score loads score is 99/100 in getmetrix.com
We have Just developed a code in C++ using that we can compress any code. The main function is given here for your ready reference.
//function to compress text files
void compress(char source[], char dest[])
{
ifstream fin(source);
ofstream fout(dest);
char ch;
int sp=0;
while(fin.get(ch))
{
if(ch==' '||ch=='\t')
sp++;
else
sp=0;
if(ch=='\n' || ch=='\r')
fout<<' ';
else
if(sp>=1)
fout<<' ';
else
fout<<ch;
}
fin.close();
fout.close();
}
Put this code to function.php:
class WP_HTML_Compression
{
// Settings
protected $compress_css = true;
protected $compress_js = true;
protected $info_comment = true;
protected $remove_comments = true;
// Variables
protected $html;
public function __construct($html)
{
if (!empty($html))
{
$this->parseHTML($html);
}
}
public function __toString()
{
return $this->html;
}
protected function bottomComment($raw, $compressed)
{
$raw = strlen($raw);
$compressed = strlen($compressed);
$savings = ($raw-$compressed) / $raw * 100;
$savings = round($savings, 2);
return '<!--HTML compressed, size saved '.$savings.'%. From '.$raw.' bytes, now '.$compressed.' bytes-->';
}
protected function minifyHTML($html)
{
$pattern = '/<(?<script>script).*?<\/script\s*>|<(?<style>style).*?<\/style\s*>|<!(?<comment>--).*?-->|<(?<tag>[\/\w.:-]*)(?:".*?"|\'.*?\'|[^\'">]+)*>|(?<text>((<[^!\/\w.:-])?[^<]*)+)|/si';
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
$overriding = false;
$raw_tag = false;
// Variable reused for output
$html = '';
foreach ($matches as $token)
{
$tag = (isset($token['tag'])) ? strtolower($token['tag']) : null;
$content = $token[0];
if (is_null($tag))
{
if ( !empty($token['script']) )
{
$strip = $this->compress_js;
}
else if ( !empty($token['style']) )
{
$strip = $this->compress_css;
}
else if ($content == '<!--wp-html-compression no compression-->')
{
$overriding = !$overriding;
// Don't print the comment
continue;
}
else if ($this->remove_comments)
{
if (!$overriding && $raw_tag != 'textarea')
{
// Remove any HTML comments, except MSIE conditional comments
$content = preg_replace('/<!--(?!\s*(?:\[if [^\]]+]|<!|>))(?:(?!-->).)*-->/s', '', $content);
}
}
}
else
{
if ($tag == 'pre' || $tag == 'textarea')
{
$raw_tag = $tag;
}
else if ($tag == '/pre' || $tag == '/textarea')
{
$raw_tag = false;
}
else
{
if ($raw_tag || $overriding)
{
$strip = false;
}
else
{
$strip = true;
// Remove any empty attributes, except:
// action, alt, content, src
$content = preg_replace('/(\s+)(\w++(?<!\baction|\balt|\bcontent|\bsrc)="")/', '$1', $content);
// Remove any space before the end of self-closing XHTML tags
// JavaScript excluded
$content = str_replace(' />', '/>', $content);
}
}
}
if ($strip)
{
$content = $this->removeWhiteSpace($content);
}
$html .= $content;
}
return $html;
}
public function parseHTML($html)
{
$this->html = $this->minifyHTML($html);
if ($this->info_comment)
{
$this->html .= "\n" . $this->bottomComment($html, $this->html);
}
}
protected function removeWhiteSpace($str)
{
$str = str_replace("\t", ' ', $str);
$str = str_replace("\n", '', $str);
$str = str_replace("\r", '', $str);
while (stristr($str, ' '))
{
$str = str_replace(' ', ' ', $str);
}
return $str;
}
}
function wp_html_compression_finish($html)
{
return new WP_HTML_Compression($html);
}
function wp_html_compression_start()
{
ob_start('wp_html_compression_finish');
}
add_action('get_header', 'wp_html_compression_start');
on https://photogrist.com it's works fine :)
I am developing a magazine WordPress site that will have a json feed for a Mobile App. I set the backend up using Advanced Custom Fields with a Repeater Field for Multiple Articles and Multiple Pages within each article. http://www.advancedcustomfields.com/add-ons/repeater-field/
I am using the JSON API but this does not include any of my custom fields. Is there currently a plugin that can do this?
#Myke: you helped me tremendously. Here's my humble addition:
add_filter('json_api_encode', 'json_api_encode_acf');
function json_api_encode_acf($response)
{
if (isset($response['posts'])) {
foreach ($response['posts'] as $post) {
json_api_add_acf($post); // Add specs to each post
}
}
else if (isset($response['post'])) {
json_api_add_acf($response['post']); // Add a specs property
}
return $response;
}
function json_api_add_acf(&$post)
{
$post->acf = get_fields($post->id);
}
Update for Wordpress 4.7
With the release of Wordpress 4.7 the REST functionality is no longer provided as a distinct plugin, rather its rolled in (no plugin required).
The previous filters don't appear to work. However the following snippet does (can be in your functions.php):
>= PHP 5.3
add_filter('rest_prepare_post', function($response) {
$response->data['acf'] = get_fields($response->data['id']);
return $response;
});
< PHP 5.3
add_filter('rest_prepare_post', 'append_acf');
function append_acf($response) {
$response->data['acf'] = get_fields($response->data['id']);
return $response;
};
Note the filter is a wild card filter, applied like
apply_filters("rest_prepare_$type", ...
so if you have multiple content types (custom), you will need to do:
add_filter('rest_prepare_multiple_choice', 'append_acf');
add_filter('rest_prepare_vocabularies', 'append_acf');
function append_acf($response) {
$response->data['acf'] = get_fields($response->data['id']);
return $response;
};
Note It appears that rest_prepare_x is called per record. So if you are pinging the index endpoint, it will be called multiple times (so you don't need to check if its posts or post)
Came here by searching with the same question. This isn't totally vetted yet but I think this is getting on the right path. Check it out.
I have one less nested level than you do so this might need altered a bit. But the JSON API plugin has a filter called json_api_encode. I have a repeater called specifications that looks like this.
http://d.pr/i/YMvv
In my functions file I have this.
add_filter('json_api_encode', 'my_encode_specs');
function my_encode_specs($response) {
if (isset($response['posts'])) {
foreach ($response['posts'] as $post) {
my_add_specs($post); // Add specs to each post
}
} else if (isset($response['post'])) {
my_add_specs($response['post']); // Add a specs property
}
return $response;
}
function my_add_specs(&$post) {
$post->specs = get_field('specifications', $post->id);
}
Which appends a custom value to the JSON API output. Notice the get_field function from ACF works perfectly here for bringing back the array of the repeater values.
Hope this helps!
There is now a small plugin which adds the filter for you.
https://github.com/PanManAms/WP-JSON-API-ACF
I'm not sure if you're still interested in a solution, but I was able to modify the json-api plugin models/post.php file to display the repeater data as an array. This is a modification of a modification made by http://wordpress-problem.com/marioario-on-plugin-json-api-fixed-get-all-custom-fields-the-right-way/
replace the set_custom_fields_value() function with the following:
function set_custom_fields_value() {
global $json_api;
if ($json_api->include_value('custom_fields') && $json_api->query->custom_fields) {
// Query string params for this query var
$params = trim($json_api->query->custom_fields);
// Get all custom fields if true|all|* is passed
if ($params === "*" || $params === "true" || $params === "all") {
$wp_custom_fields = get_post_custom($this->id);
$this->custom_fields = new stdClass();
// Loop through our custom fields and place on property
foreach($wp_custom_fields as $key => $val) {
if (get_field($key)) {
$this->custom_fields->$key = get_field($key);
} else if ($val) {
// Some fields are stored as serialized arrays.
// This method does not support multidimensionals...
// but didn't see anything wrong with this approach
$current_custom_field = #unserialize($wp_custom_fields[$key][0]);
if (is_array($current_custom_field)) {
// Loop through the unserialized array
foreach($current_custom_field as $sub_key => $sub_val) {
// Lets append these for correct JSON output
$this->custom_fields->$key->$sub_key = $sub_val;
}
} else {
// Break this value of this custom field out of its array
// and place it on the stack like usual
$this->custom_fields->$key = $wp_custom_fields[$key][0];
}
}
}
} else {
// Well this is the old way but with the unserialized array fix
$params = explode(',', $params);
$wp_custom_fields = get_post_custom($this->id);
$this->custom_fields = new stdClass();
foreach ($params as $key) {
if (isset($wp_custom_fields[$key]) && $wp_custom_fields[$key][0] ) {
$current_custom_field = #unserialize($wp_custom_fields[$key][0]);
if (is_array($current_custom_field)) {
foreach($current_custom_field as $sub_key => $sub_val) {
$this->custom_fields->$key->$sub_key = $sub_val;
}
} else {
$this->custom_fields->$key = $wp_custom_fields[$key][0];
}
}
}
}
} else {
unset($this->custom_fields);
}
}
Current version of ACF prints out a custom_fields object on a call to the JSON API, containing all the fields relative to the Post or Page. I edited #Myke version to add specific custom fields from the ACF Option page to each Post or Page. Unfortunately there is not get_fields() function for the whole Option Page so you'll have to edit it depending on your fields structure.
add_filter('json_api_encode', 'json_api_encode_acf');
function json_api_encode_acf($response) {
if (isset($response['posts'])) {
foreach ($response['posts'] as $post) {
json_api_add_acf($post); // Add specs to each post
}
}
else if (isset($response['post'])) {
json_api_add_acf($response['post']); // Add a specs property
}
else if (isset($response['page'])) {
json_api_add_acf($response['page']); // Add a specs to a page
}
return $response;
}
function json_api_add_acf(&$post) {
$post->custom_fields->NAME_OF_YOUR_CUSTOM_FIELD = get_field( 'NAME_OF_YOUR_CUSTOM_FIELD', 'option' );
}
Suppose, I have 3 stores.
I want to disable a module in Store 2. I only want it to be enabled in Store 1 and Store 3.
I see that I can do it by:-
Going to System -> Configuration -> Advanced
Selecting desired store from Current Configuration Scope dropdown list.
But this does not work fully.
And, I also don't want to check store in the module code itself or create system configuration field for the module to check/uncheck store to enable/disable.
What I am expecting is by adding some code in app/etc/modules/MyNamespace_MyModule.xml. Can we do it this way?
To disable a module on the store scope, I've found it's possible to do it like this:
Move app/code/core/Mage/Core/Model/Config.php to app/code/local/Mage/Core/Model/Config.php
Inside Config.php find the method "loadModulesConfiguration" Don't change anything, but add the following code to make the method look like this.
public function loadModulesConfiguration($fileName, $mergeToObject = null, $mergeModel=null)
{
$disableLocalModules = !$this->_canUseLocalModules();
if ($mergeToObject === null) {
$mergeToObject = clone $this->_prototype;
$mergeToObject->loadString('<config/>');
}
if ($mergeModel === null) {
$mergeModel = clone $this->_prototype;
}
$modules = $this->getNode('modules')->children();
foreach ($modules as $modName=>$module) {
if ($module->is('active')) {
// Begin additional code
if((bool)$module->restricted) {
$restricted = explode(',', (string)$module->restricted);
$runCode = (isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : 'default');
if(in_array($runCode, $restricted)) {
continue;
}
}
// End additional code
if ($disableLocalModules && ('local' === (string)$module->codePool)) {
continue;
}
if (!is_array($fileName)) {
$fileName = array($fileName);
}
foreach ($fileName as $configFile) {
$configFile = $this->getModuleDir('etc', $modName).DS.$configFile;
if ($mergeModel->loadFile($configFile)) {
$mergeToObject->extend($mergeModel, true);
}
}
}
}
return $mergeToObject;
}
The new code will cause the method to also check for a new node in the module xml file, <restricted>. If the node exists, the value would be a comma separated list of store codes that you do NOT want the module to load on. If you have multiple stores, the $_SERVER variable "MAGE_RUN_CODE" should be set with the current store code. If it's not set, the script will fallback to assuming the store code is "default" which is what it is by default unless for some bizarre reason you decide to change that in the backend.
A modules xml file could then look like this:
<?xml version="1.0"?>
<config>
<modules>
<MyPackage_MyModule>
<active>false</active>
<restricted>mystore1,mystore4,mystore5</restricted>
<codePool>local</codePool>
</MyPackage_MyModule>
</modules>
</config>
With this, the module will not even load while on the stores with a store code of mystore1, mystore4, or mystore5. The <restricted> tag is entirely optional, if you omit it the module will load as it normally would.
This configuration just disables module output in layout for frontend, but module controllers, event observers, admin pages, etc still working.
Also don't forget to specify your module name in layout files definition, otherwise all the layout file content will be loaded for a particular store:
<config>
<layout>
<module_alias module="Module_Name">
<file>yourlayoutfile.xml</file>
</module_alias>
</layout>
</config>
If you are developing a module and want to disable full its functionality on the frontent for a particular store, then you should create a configuration field of "Yes/No" type and check its value via Mage::getStoreConfigFlag('config/field/path') in your module code.
I was using Eric solution for a while. In my case I disabled certain module responsible for Layered Navigation in one of my shops - thus returning to default Layered Navigation behaviour.
And it looked like its working, but after a while I've noticed that layered navigation options stopped to appear where they should. Soon I've noticed that in fact the module that should not work on this shop continued to work. Then I realized that when I disable configuration cache Eric's solution works, but after enabling it again it stops.
After a while I realized it had to work that way, with configuration cache enabled, because Eric's solution includes (or not) specified config files in global xml only while this xml is being generated. Then its cached and called from cache only. So when it was generated from site which should use some module it was included, and then used also on site which wasn't suppose to use it.
Anyway I worked out another solution, based on Eric's code (using restricted in modules config). I thought Magento should decide what to load when class is being requested. Then it could check what is current MAGE_RUN_CODE and use it dynamically.
There is a method in Mage_Core_Model_Config which is responsible for getting class name: getGroupedClassName.
Here is the code I used there:
if (strpos($className, 'Pneumatig_') !== false) {
$var = substr($className, 0, strpos($className, '_', strpos($className, '_') + 1));
if (isset($this->_xml->modules->$var)) {
if ((bool)$this->_xml->modules->$var->restricted === true) {
$code = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : 'default';
if (strpos((string)$this->_xml->modules->$var->restricted, $code) !== false) {
$className = '';
}
}
}
}
This Pneumatig condition is because all my modules start from Company name, so i wanted to avoid not necessary processing, but its optional, code should work without it, or you can change it to anything else.
Then I get actual module name [Company]_[Module], and then check if its enabled in _xml (which is current configuration object). If it is restricted I clear $className so it force Magento to load the default in next line.
And this code is added just before is empty condition:
// Second - if entity is not rewritten then use class prefix to form class name
if (empty($className)) {
if (!empty($config)) {
$className = $config->getClassName();
}
if (empty($className)) {
$className = 'mage_'.$group.'_'.$groupType;
}
if (!empty($class)) {
$className .= '_'.$class;
}
$className = uc_words($className);
}
$this->_classNameCache[$groupRootNode][$group][$class] = $className;
return $className;
And for your convenience i paste whole getGroupedClassName code:
public function getGroupedClassName($groupType, $classId, $groupRootNode=null)
{
if (empty($groupRootNode)) {
$groupRootNode = 'global/'.$groupType.'s';
}
$classArr = explode('/', trim($classId));
$group = $classArr[0];
$class = !empty($classArr[1]) ? $classArr[1] : null;
if (isset($this->_classNameCache[$groupRootNode][$group][$class])) {
return $this->_classNameCache[$groupRootNode][$group][$class];
}
$config = $this->_xml->global->{$groupType.'s'}->{$group};
// First - check maybe the entity class was rewritten
$className = null;
if (isset($config->rewrite->$class)) {
$className = (string)$config->rewrite->$class;
} else {
/**
* Backwards compatibility for pre-MMDB extensions.
* In MMDB release resource nodes <..._mysql4> were renamed to <..._resource>. So <deprecatedNode> is left
* to keep name of previously used nodes, that still may be used by non-updated extensions.
*/
if (isset($config->deprecatedNode)) {
$deprecatedNode = $config->deprecatedNode;
$configOld = $this->_xml->global->{$groupType.'s'}->$deprecatedNode;
if (isset($configOld->rewrite->$class)) {
$className = (string) $configOld->rewrite->$class;
}
}
}
//START CHECKING IF CLASS MODULE IS ENABLED
if (strpos($className, 'Pneumatig_') !== false) {
$var = substr($className, 0, strpos($className, '_', strpos($className, '_') + 1));
if (isset($this->_xml->modules->$var)) {
if ((bool)$this->_xml->modules->$var->restricted === true) {
$code = isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : 'default';
if (strpos((string)$this->_xml->modules->$var->restricted, $code) !== false) {
$className = '';
}
}
}
}
//END CHECKING IF CLASS MODULE IS ENABLED
// Second - if entity is not rewritten then use class prefix to form class name
if (empty($className)) {
if (!empty($config)) {
$className = $config->getClassName();
}
if (empty($className)) {
$className = 'mage_'.$group.'_'.$groupType;
}
if (!empty($class)) {
$className .= '_'.$class;
}
$className = uc_words($className);
}
$this->_classNameCache[$groupRootNode][$group][$class] = $className;
return $className;
}
My clients install of Magento 1.8.1.0 has a problematic module that breaks another site's menu on a multi-store setup. The solution above posted by Eric Hainer didn't work for this install, so I altered it slightly:
Instead of using $_SERVER['MAGE_RUN_CODE'], I used $_SERVER['SERVER_NAME']. Worked like a charm. :)
So instead of:
$runCode = (isset($_SERVER['MAGE_RUN_CODE']) ? $_SERVER['MAGE_RUN_CODE'] : 'default');
use:
$runCode = (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'www.site1.com');
and instead of:
<restricted>mystore1,mystore4,mystore5</restricted>
use:
<restricted>www.site2.com,www.site3.com</restricted>
obviously changing "www.site1.com", "www.site2.com", and "www.site3.com" with your own locations.
Thanks for the idea Eric :)
Also interesting solution ,
http://inchoo.net/ecommerce/magento/how-to-activatedeactivate-magento-module-per-a-website-level/