Moodle "SAFE MODE ON" workaround

[Original spanish version]

Brief problem analisys

The problem arises when the "user" executing the script is different from the "user" that OWNS the script. SAFE MODE restrictions made Moodle (and others) fail.

The solution seems obvious: lets make the owner of the script be the Apache UID.

Yeah man, yeah... but... the SAFE MODE restrictions are going to make this difficult (rather impossible?).

The straight line is the shortest route (even when it have loops).

When the Apache process creates a file the file is owned by the Apache UID. So, lets make Apache create the files (and folders) and so we'll avoid the SAFE MODE conflicts.

And again, like a dog chasing its tail, seems we have the same problem, cause the folder is owned by a differend UID... But now there is a way out for this vicious cycle. Using an extra folder will help.

Step by step

We have to create (via FTP) a new folder called (for example) "training", and change the rights to everybody-everything [CHMOD 777].
Download Moodle, unzip it, and upload it (again using FTP) to the server. If your provider control panel let's you do it, upload it zipped and unzip it in the server (is much faster). We'll do the upload in the "trainig" folder (you can do it anywhere, but this is easier and helps keep the server "tidy").

Let's make our little miracle: changing the ownership of the Moddle files (or something equivalent)... We'll upload two scripts and a text file to the server, again to the same folder: "training".

Loops in our straight route

We'll use 3 scripts and a text file (more or less).

The first script "moodle_safe_mode_workaround.php" (I allways use short names for the scripts) creates another script. When this newly created scripts executes will have no "SAFE MODE" conflicts, since have been created by Apache UID. To create this script we have our first loop...

The text file "script_copiador.txt" contains a PHP script, but we made it a ".txt" file, so the server will NOT execute it, but show it "as is", and the first script will "read" it (via HTTP).

WARNING: those low quality messy code lines have been writen (including copy-paste) in a coke-and-chips night. If you want elegant hi quality code consider yourself invited to rewrite it (and share it).

(I still have some translation work to do in the scripts...)

script "moodle_safe_mode_workaround.php"

<?php
//Read script_copiador.txt via HTTP, avoiding SAFE MODE restrictions with fopen...
$uri=$_SERVER["REQUEST_URI"];
$thisdir=substr($uri,0,strrpos($uri, "/"));
$uri="http://".$_SERVER["SERVER_NAME"].$thisdir."/script_copiador.txt";
$script=file_get_contents($uri);

//We save the script as "script_copiador_ejecutable_DOS.php", so Apache UID owns the file
$fp = fopen($_SERVER["DOCUMENT_ROOT"].$thisdir."/script_copiador_ejecutable_DOS.php", 'w');
fwrite($fp,$script);
fclose($fp);

//URL for the second script to be executed
$uri="http://".$_SERVER["SERVER_NAME"].$thisdir."/script_copiador_ejecutable_UNO.php";
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
	<title>moodle safe mode workaround</title>
</head>
 <body>
Go now to <a href="<?php echo $uri ?>"><?php echo $uri ?></a>
 </body>
</html>

script "script_copiador_ejecutable_UNO.php"

Now starts the funny stuff... The Moddle files and folders that we have in the server are owned by our user UID, just like this script, so this is the script that reads the directories tree, (and not the Apache owned script we made before). (Maybe this script and the first ejecuted one could be just one, but I did it in two pieces :-? )

This script goes thru the directoties, registering files and folders names in an Apache owned file, so the last script will have access to that data without conflicts. This script also create the folder where finally Moodle files will be (moodledata also). (And this code lines are so good and elegant as the others... but copy-paste from PHP info site.)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Crear directorio</title>
 </head>
 <body><pre>
<?php
//folder (hanging from htdocs, the webroot folder) where we have put the Moodle files
$origen=$_SERVER["DOCUMENT_ROOT"]."/training/moodleOrigen";

//Destination folder for moodle
//REMEMBER "training" must be world-writable-folder (CHMOD 777)... at least by now.
$destino=$_SERVER["DOCUMENT_ROOT"]."/training/moodle";
$destinodata=$_SERVER["DOCUMENT_ROOT"]."/training/moodledata";

//Make the lists of files and folders
clearstatcache();
$ficheros=listFiles($origen);
$directorios=listDirs($origen);

$fp = fopen('lista_ficheros', 'w');
foreach ($ficheros as $key => $value) {
		fwrite($fp, $value."\n");
	}	
fclose($fp);

$fp = fopen('lista_directorios', 'w');
foreach ($directorios as $key => $value) {
		fwrite($fp, $value."\n");
	}	
fclose($fp);


//Create destination folders
mkdir($destino);
mkdir($destinodata);
chmod($destino,0777);
chmod($destinodata,0777);
//It's not really necesary to make them "777" since the owner is now Apache UID 
//it's done to make easier to delete them from FTP. For safety reasons permisions should be changed to something more restrictive.

echo 'Go now to <a href="script_copiador_ejecutable_DOS.php">script_copiador_ejecutable_DOS.php</a>';

function listDirs( $from = ".")
{
    if(! is_dir($from))
        return false;
   
    $files = array();
    $dirs = array( $from);
    while( NULL !== ($dir = array_pop( $dirs)))
    {
        if( $dh = opendir($dir))
        {
            while( false !== ($file = readdir($dh)))
            {
                if( $file == "." || $file == "..")
                    continue;
                $path = $dir . "/" . $file;
                if( is_dir($path)){
                    $dirs[] = $path;
				   $files[] = $path;
					}
            }
            closedir($dh);
        }
    }
    return $files;
}

function listFiles( $from = ".")
{
    if(! is_dir($from))
        return false;
   
    $files = array();
    $dirs = array( $from);
    while( NULL !== ($dir = array_pop( $dirs)))
    {
        if( $dh = opendir($dir))
        {
            while( false !== ($file = readdir($dh)))
            {
                if( $file == "." || $file == "..")
                    continue;
                $path = $dir . "/" . $file;
                if( is_dir($path))
                    $dirs[] = $path;
                else
                    $files[] = $path;
            }
            closedir($dh);
        }
    }
    return $files;
}
?>  </pre>
 </body>
</html>

script "script_copiador_ejecutable_DOS.php" (but really "script_copiador.txt")

This last script was created in sted one (and this code is originally the text file) so it's owned by Apache UID, and the files it reads also (second step), so we'll have no SAFE MODE conflicts, but...
For reading Moodle files (and so we could write them being Apache UID) we have to do something a little bit stupid: download them via FTP from the same server. It's stupid... but it works.
SAFE MODE restrictions can be a problem and don't let you read those file from the script, but the FTP connection is "user UID", and so it can read (download) them.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Copiar ficheros</title>
 </head>
 <body><pre>
<?php
//folder (hanging from htdocs, the webroot folder) where we have put the Moodle files
$origen=$_SERVER["DOCUMENT_ROOT"]."/training/moodleOrigen";

//Destination folder for moodle
//REMEMBER "training" must be world-writable-folder (CHMOD 777)... at least by now.
$destino=$_SERVER["DOCUMENT_ROOT"]."/training/moodle";
$destinodata=$_SERVER["DOCUMENT_ROOT"]."/training/moodledata";

//FTP access data
$servidorFTP="ftp.ftpserveryourprovidertoldyou.com";
$ftpuname="ftpuseryourprovidertoldyou";
$ftppass="yourftpuserpassword";
$ftporigendir="/training/moodleOrigen"; //FTP FOLDER where Moodle is. Remember FTP folder, could be different of $origen

//open files lists
$ficheros=explode("\n",file_get_contents("lista_ficheros"));
$directorios=explode("\n",file_get_contents("lista_directorios"));

//Now we have a list of files with their real name...
//skip the server path and origin folder
$pos=strlen($origen);
foreach ($ficheros as $key => $value) {$ficheros[$key]=substr($value,$pos);}
foreach ($directorios as $key => $value) {$directorios[$key]=substr($value,$pos);}

//Make tree
foreach ($directorios as $key => $value) {
	echo "Creating directory $destino$value<br />";
	mkdir($destino.$value);
	chmod($destino.$value,0777);
	}

//Read files via FTP and write them in the new place (so now they are Apache UID owned)
	//Open FTP connection
	echo "Conectando via FTP<BR>";
	$conn = ftp_connect($servidorFTP);
	if (!$conn) die('ERR: Unable to connect.');
	if (!ftp_login($conn, $ftpuname, $ftppass)) die('ERR: Error en login.');

foreach ($ficheros as $key => $value) {
	echo ("-->".$origen.$value."<br />");
	if (FALSE==is_dir($origen.$value)){
			if (!file_exists($destino.$value)){
				if (ftp_get($conn,$destino.$value, $ftporigendir.$value, FTP_BINARY)) {
					echo "Copiado $ftporigendir$value<br />";
					} else {
					die("Problema al copiar $ftporigendir$value");
					}
				chmod($destino.$value,0777);
				//it's done to make easier to delete them from FTP. For safety reasons permisions should be changed to something more restrictive.
			}else{
				echo "Fichero ya existe: $value<br />";
				//comprobar tamaño. si !=, copiar
				if(filesize($destino.$value)!=filesize($origen.$value)){
					echo "Diferencia de tamaño, se vuelve a bajar $value<br />";
					if (ftp_get($conn,$destino.$value, $ftporigendir.$value, FTP_BINARY)) {
						echo "Copiado $ftporigendir$value<br />";
						} else {
						die("Problema al copiar $ftporigendir$value (de nuevo)");
						}
					chmod($destino.$value,0777);
					//it's done to make easier to delete them from FTP. For safety reasons permisions should be changed to something more restrictive.
				}
			}
		}

	}

//Close ftp connection
ftp_close($conn);
echo "Try now Moodle setup (and remember to do your homeworks)";
  </pre>
 </body>
</html>

Those scripts works, or at least seems to work, but of course I am not responsible of what happend if you use it, undesirable side effects or whatever. You know it is published "as is" and it's your own responsability what you do with it (This paragraph is a "It have not been me disclaimer").

I've avoid using certain things (file_put_contents, http_request...) cause many PHP hostings don't support them, and I prefer a longer and uglier code that can be used almost everywhere than a shorter and more beautifull one. I have to say that those $#X@(stinky?) little scripts should also solve the "SAFE MODE problem" with Joomla and others.
The function "listFiles" is copy-paste from http://es.php.net/function.opendir published by archipel dot gb at online dot fr. As far as the rest concerns consider it gimme-your-spare-coins-ware given to the public domain.

And remember, do your homeworks:

Well, there is this botched patch, which is the one that works best of all I've seen. (I've seen no other in fact...)

Sevilla, 10 de Julio de 2008
Miguel Pérez
For contributions about the topic, sharing more tidy code or a better translation: [miguel at ytudequetequejas dot com]