2 /************************************************************************/
4 /************************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay, Joel Kronenberg & Heidi Hazelton*/
6 /* Adaptive Technology Resource Centre / University of Toronto */
9 /* This program is free software. You can redistribute it and/or */
10 /* modify it under the terms of the GNU General Public License */
11 /* as published by the Free Software Foundation. */
12 /************************************************************************/
13 // $Id: Patch.class.php 7208 2008-02-08 16:07:24Z cindy $
17 * Class for patch installation
23 define('AT_INCLUDE_PATH', '../../../include/');
25 require_once(AT_INCLUDE_PATH. "../mods/patcher/include/common.inc.php");
30 var $patch_array = array(); // the patch data
31 var $patch_summary_array = array(); // patch summary information
32 var $patch_id; // current patches.patches_id
33 var $patch_file_id; // current patches_files.patches_files_id
35 var $need_access_to_folders = array();// folders that need to have write permission
36 var $need_access_to_files = array(); // files that need to have write permission
37 var $backup_files = array(); // backup files
38 var $patch_files = array(); // patch files
40 var $errors = array(); // error messages
41 var $baseURL; // patch folder at update.atutor.ca
42 var $backup_suffix; // suffix appended for backup files
43 var $patch_suffix; // suffix appended for patch files copied from update.atutor.ca
44 var $skipFilesModified = false; // if set to true, report error for files that have been modified by user
45 var $module_content_dir; // content folder used to create patch.sql
47 // constant, URL of user's ATutor release version in SVN
48 var $svn_tag_folder = 'http://atutorsvn.atrc.utoronto.ca/repos/atutor/tags/';
49 var $sql_file = 'patch.sql';
50 var $relative_to_atutor_root = '../../'; // relative path from mods/patcher to root
53 * Constructor: Initialize object members
55 * @param $patch_array The name of the file to find charset definition
56 * $patch_summary_array
60 function Patch($patch_array, $patch_summary_array, $skipFilesModified, $patch_folder)
62 // add relative path to move to ATutor root folder
63 for ($i = 0; $i < count($patch_array[files]); $i++)
65 $patch_array[files][$i]['location'] = $this->relative_to_atutor_root . $patch_array[files][$i]['location'];
68 $this->patch_array = $patch_array;
69 $this->patch_summary_array = $patch_summary_array;
71 $this->baseURL = $patch_folder;
72 $this ->backup_suffix = $patch_array['atutor_patch_id'] . ".old";
73 $this ->patch_suffix = $patch_array['atutor_patch_id'];
74 $this->skipFilesModified = $skipFilesModified;
76 $this->module_content_dir = AT_CONTENT_DIR . "patcher";
80 if (!is_array($_SESSION['remove_permission'])) $_SESSION['remove_permission']=array();
85 * Main process to apply patch.
87 * @return true if patch is successfully applied
94 // 1. if svn server is up
95 // 2. if the local file is customized by user
96 // 3. if script has write priviledge on local file/folder
97 // 4. if dependent patches have been installed
98 if (!$this->pingDomain($this->svn_tag_folder)) return false;
100 if (!$this->checkDependentPatches()) return false;
102 if (!$this->skipFilesModified && $this->hasFilesModified()) return false;
104 if (!$this->checkPriviledge()) return false;
107 if (strlen(trim($this->patch_array['sql'])) > 0) $this->runSQL();
109 // Start applying patch
110 $this->createPatchesRecord($this->patch_summary_array);
112 // if no file action defined, return true
113 if (!is_array($this->patch_array[files])) return true;
115 foreach ($this->patch_array[files] as $row_num => $patch_file)
117 $this->createPatchesFilesRecord($this->patch_array['files'][$row_num]);
119 if ($patch_file['action'] == 'alter')
121 $this->alterFile($row_num);
123 else if ($patch_file['action'] == 'add')
125 $this->addFile($row_num);
127 else if ($patch_file['action'] == 'delete')
129 $this->deleteFile($row_num);
131 else if ($patch_file['action'] == 'overwrite')
133 $this->overwriteFile($row_num);
137 // if only has backup files info, patch is considered successfully installed
138 // if has permission to remove, considered partly installed
139 $updateInfo = array();
141 if (count($this->backup_files) > 0)
143 foreach($this->backup_files as $backup_file)
144 $backup_files .= $backup_file. '|';
146 $updateInfo = array("backup_files"=>preg_quote($backup_files));
149 if (count($this->patch_files) > 0)
151 foreach($this->patch_files as $patch_file)
152 $patch_files .= $patch_file. '|';
154 $updateInfo = array_merge($updateInfo, array("patch_files"=>preg_quote($patch_files)));
157 if (is_array($_SESSION['remove_permission']) && count($_SESSION['remove_permission']))
159 foreach($_SESSION['remove_permission'] as $remove_permission_file)
160 $remove_permission_files .= $remove_permission_file. '|';
162 $updateInfo = array_merge($updateInfo, array("remove_permission_files"=>preg_quote($remove_permission_files), "status"=>"Partly Installed"));
166 $updateInfo = array_merge($updateInfo, array("status"=>"Installed"));
169 updatePatchesRecord($this->patch_id, $updateInfo);
171 unset($_SESSION['remove_permission']);
179 * @return patch array
180 * @author Cindy Qi Li
182 function getPatchArray()
184 return $this->patch_array;
188 * return patch id processed by this object
191 * @author Cindy Qi Li
193 function getPatchID()
195 return $this->patch_id;
199 * Check if script has write permission to the files and folders that need to be written
200 * if no permission, warn user to give permission
202 * @return true if there are files or folders that script has no permission
203 * false if permissions are in place
204 * @author Cindy Qi Li
206 function checkPriviledge()
210 // no file action is defined, return true;
211 if (!is_array($this->patch_array[files])) return true;
213 foreach ($this->patch_array[files] as $row_num => $patch_file)
215 $real_location = realpath($patch_file['location']);
216 if (!is_writable($patch_file['location']) && !in_array($real_location, $this->need_access_to_folders))
218 $this->need_access_to_folders[] = $real_location;
220 if (!in_array($real_location, $_SESSION['remove_permission']))
221 $_SESSION['remove_permission'][] = $real_location;
224 if ($patch_file['action'] == 'alter' || $patch_file['action'] == 'delete' || $patch_file['action'] == 'overwrite')
226 $file = $patch_file['location'] . "/" . $patch_file['name'];
228 $real_file = realpath($file);
229 if (file_exists($file) && !is_writable($file) && !in_array($real_file, $this->need_access_to_files))
231 $this->need_access_to_files[] = $real_file;
233 if (!in_array($real_file, $_SESSION['remove_permission']) && $patch_file['action'] <> 'delete')
234 $_SESSION['remove_permission'][] = $real_file;
239 if (count($this->need_access_to_folders) > 0 || count($this->need_access_to_files) > 0)
241 $this->errors[] = _AT('grant_write_permission');
243 foreach($this->need_access_to_folders as $folder)
245 $this->errors[0] .= '<strong>'. $folder . "</strong><br>";
248 foreach($this->need_access_to_files as $file)
250 $this->errors[0] .= '<strong>'. $file . "</strong><br>";
253 $notes = '<form action="'. $_SERVER['PHP_SELF'].'?id='.$id.'&who='. $who .'" method="post" name="skip_files_modified">
254 <div class="row buttons">
255 <input type="submit" name="yes" value="'._AT('continue').'" accesskey="y" />
256 <input type="submit" name="no" value="'. _AT('cancel'). '" />
257 <input type="hidden" name="install" value="' . $_POST['install'] . '" />
258 <input type="hidden" name="install_upload" value="' . $_POST['install_upload'] . '" />
262 print_errors($this->errors, $notes);
264 unset($this->errors);
272 * Check if all the dependent patches have been installed.
274 * @return true if all the dependent patches have been installed
275 * false if any dependent patch has not been installed.
276 * @author Cindy Qi Li
278 function checkDependentPatches()
282 $dependent_patches_installed = true;
284 // if no dependent patch defined, return true
285 if (!is_array($this->patch_summary_array["dependent_patches"])) return true;
287 foreach($this->patch_summary_array["dependent_patches"] as $num => $dependent_patch)
289 if (!is_patch_installed($dependent_patch))
291 $dependent_patches_installed = false;
292 $dependent_patches .= $dependent_patch. ", ";
296 if (!$dependent_patches_installed)
298 $errors = array('PATCH_DEPENDENCY', substr($dependent_patches, 0, -2));
299 $msg->addError($errors);
307 * Loop thru all the patch files that will be overwitten or altered,
308 * to find out if they are modified by user. If it's modified, warn user.
310 * @return true if there are files being modified
311 * false if no file is modified
312 * @author Cindy Qi Li
314 function hasFilesModified()
316 $overwrite_modified_files = false;
317 $alter_modified_files = false;
318 $has_not_exist_files = false;
320 // no file action is defined, return nothing is modified (false)
321 if (!is_array($this->patch_array[files])) return false;
323 foreach ($this->patch_array[files] as $row_num => $patch_file)
325 if ($patch_file["action"]=='alter' || $patch_file["action"]=='overwrite')
327 if (!file_exists($patch_file['location'] . $patch_file['name']))
329 $not_exist_files .= $patch_file['location'] . $patch_file['name'] . '<br>';
330 $has_not_exist_files = true;
332 else if ($this->isFileModified($patch_file['location'], $patch_file['name']))
334 if ($patch_file['action']=='overwrite')
336 $overwrite_files .= realpath($patch_file['location'] . $patch_file['name']) . '<br>';
337 $overwrite_modified_files = true;
339 if ($patch_file['action']=='alter')
341 $alter_files .= realpath($patch_file['location'] . $patch_file['name']) . '<br>';
342 $alter_modified_files = true;
348 if ($has_not_exist_files) $this->errors[] = _AT('patch_local_file_not_exist'). $not_exist_files;
349 if ($overwrite_modified_files) $this->errors[] = _AT('patcher_overwrite_modified_files') . $overwrite_files;
350 if ($alter_modified_files) $this->errors[] = _AT('patcher_alter_modified_files') . $alter_files;
351 if (count($this->errors) > 0)
353 if ($has_not_exist_files)
357 <form action="'. $_SERVER['PHP_SELF'].'?id='.$_POST['id'].'&who='. $_POST['who'] .'" method="post" name="skip_files_modified">
358 <div class="row buttons">
359 <input type="submit" name="yes" value="'._AT('yes').'" accesskey="y" />
360 <input type="submit" name="no" value="'. _AT('no'). '" />
361 <input type="hidden" name="install" value="' . $_POST['install'] . '" />
362 <input type="hidden" name="install_upload" value="' . $_POST['install_upload'] . '" />
366 print_errors($this->errors, $notes);
368 unset($this->errors);
376 * Compare user's local file with SVN backup for user's ATutor version,
377 * if different, check table at_patches_files to see if user's local file
378 * was altered by previous patch installation. If it is, return false
379 * (not modified), otherwise, return true (modified).
381 * @param $folder folder of the file to be compared
382 * $file name of the file to be compared
383 * @return true if the file is modified
384 * false if the file is not modified
385 * @author Cindy Qi Li
387 function isFileModified($folder, $file)
391 $svn_file = $this->svn_tag_folder . 'atutor_' . str_replace('.', '_', VERSION) .
392 str_replace('../..', '', $folder) .$file;
393 $local_file = $folder.$file;
395 // check if the local file has been modified by user. if it is, don't overwrite
396 if ($this->compareFiles($svn_file, $local_file) <> 0)
398 // check if the file was changed by previous installed patches
399 $sql = "SELECT count(*) num_of_updates FROM " . TABLE_PREFIX. "patches patches, " . TABLE_PREFIX."patches_files patches_files " .
400 "WHERE patches.applied_version = '" . VERSION . "' ".
401 " AND patches.status = 'Installed' " .
402 " AND patches.patches_id = patches_files.patches_id " .
403 " AND patches_files.name = '" . $file . "'";
405 $result = mysql_query($sql, $db) or die(mysql_error());
406 $row = mysql_fetch_assoc($result);
408 if ($row["num_of_updates"] == 0) return true;
414 * Run SQL defined in patch.xml
416 * @author Cindy Qi Li
421 // As sqlutility.class.php reads sql from a file, write sql to module content folder
422 $patch_sql_file = $this->module_content_dir . '/' . $this->sql_file;
424 $fp = fopen($patch_sql_file, 'w');
425 fwrite($fp, trim($this->patch_array['sql']));
428 require(AT_INCLUDE_PATH . 'classes/sqlutility.class.php');
429 $sqlUtility =& new SqlUtility();
431 $sqlUtility->queryFromFile($patch_sql_file, TABLE_PREFIX);
433 @unlink($patch_sql_file);
439 * Copy file from update.atutor.ca to user's computer
441 * @param $row_num row number of patch record to be processed
442 * @author Cindy Qi Li
444 function addFile($row_num)
446 $this->copyFile($this->baseURL . preg_replace('/.php$/', '.new', $this->patch_array['files'][$row_num]['name']), $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name']);
452 * Delete file, backup before deletion
454 * @param $row_num row number of patch record to be processed
455 * @author Cindy Qi Li
457 function deleteFile($row_num)
459 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
460 $backup_file = $local_file . "." . $this->backup_suffix;
462 if (file_exists($local_file))
464 // move file to backup
465 $this->copyFile($local_file, $backup_file);
466 $this->backup_files[] = realpath($backup_file);
467 @unlink($local_file);
475 * Alter file based on <action_detail>
476 * If user's local file is modified and user agrees to proceed with applying patch,
477 * alter user's local file.
479 * @param $row_num row number of patch record to be processed
480 * @author Cindy Qi Li
482 function alterFile($row_num)
484 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
486 // backup user's file
487 $backup_file = $local_file . "." . $this->backup_suffix;
488 $this->copyFile($local_file, $backup_file);
489 $this->backup_files[] = realpath($backup_file);
491 $local_file_content = file_get_contents($local_file);
493 // Modify user's file
494 foreach ($this->patch_array['files'][$row_num]['action_detail'] as $garbage => $alter_file_action)
496 if ($alter_file_action['type'] == 'delete')
498 $local_file_content = $this->strReplace($alter_file_action['code_from'], '', $local_file_content);
501 if ($alter_file_action['type'] == 'replace')
502 $local_file_content = $this->strReplace($alter_file_action['code_from'], $alter_file_action['code_to'], $local_file_content);
504 $this->createPatchesFilesActionsRecord($alter_file_action);
507 $fp = fopen($local_file, 'w');
508 fwrite($fp, $local_file_content);
515 * Fetch file from update.atutor.ca and overwrite user's local file if the local file is not modified
516 * If user's local file is modified and user agrees to proceed with applying patch,
517 * copy the new file to user's local for them to merge manually.
519 * @param $row_num row number of patch record to be processed
520 * @author Cindy Qi Li
522 function overwriteFile($row_num)
524 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
525 $patch_file = $this->baseURL . preg_replace('/.php$/', '.new', $this->patch_array['files'][$row_num]['name']);
527 // if local file is modified and user agrees to proceed with applying patch,
528 // copy the new file to user's local for them to merge manually
529 if ($this->skipFilesModified && $this->isFileModified($this->patch_array['files'][$row_num]['location'], $this->patch_array['files'][$row_num]['name']))
531 $local_patch_file = $local_file . "." . $this->patch_suffix;
533 $this->copyFile($patch_file, $local_patch_file);
535 $this->patch_files[] = realpath($local_patch_file);
539 $backup_file = $local_file . "." . $this->backup_suffix;
541 // backup user's file
542 $this->copyFile($local_file, $backup_file);
543 $this->backup_files[] = realpath($backup_file);
545 // overwrite user's file
546 $this->copyFile($patch_file, $local_file);
553 * Copy file $src to $dest. $src can be a local file or a remote file
555 * @param $src location of the source file
556 * $dest location of the destination file
557 * @author Cindy Qi Li
559 function copyFile($src, $dest)
561 $content = file_get_contents($src);
562 $fp = fopen($dest, 'w');
563 fwrite($fp, $content);
570 * Compare files $src against $dest
572 * @param $src location of the source file
573 * $dest location of the destination file
574 * @return Returns < 0 if $src is less than $dest ; > 0 if $src is greater than $dest, and 0 if they are equal.
575 * @author Cindy Qi Li
577 function compareFiles($src, $dest)
579 // use preg_replace to delete
580 // 1. the line starting with // $Id:
581 // 2. the line starting with $lm = '$LastChangedDate, ending with ;
582 // These lines are created by SVN. It could be different in different copies of the same file.
583 $pattern = '/\/\/ \$Id.*\$|\$lm = \'\$LastChangedDate.*;/';
585 $src_content = preg_replace($pattern, '', file_get_contents($src));
586 $dest_content = preg_replace($pattern, '', file_get_contents($dest));
588 return strcasecmp($src_content, $dest_content);
592 * Replace single/multiple lines of string.
593 * This function handles different new line character at windows/unix platform
595 * @param $search String to replace from
596 * $replace String to replace to
597 * $subject Subject to be handled
598 * @return return replaced string, if nothing is replaced, return original subject
599 * @author Cindy Qi Li
601 function strReplace($search, $replace, $subject)
603 $new_line_array = array("\n", "\r", "\n\r", "\r\n");
605 foreach ($new_line_array as $new_line)
607 if (preg_match('/'.preg_quote($new_line).'/', $search) > 0) $search_new_line = $new_line;
608 if (preg_match('/'.preg_quote($new_line).'/', $replace) > 0) $replace_new_line = $new_line;
609 if (preg_match('/'.preg_quote($new_line).'/', $subject) > 0) $subject_new_line = $new_line;
612 // replace new line chars in $search & $replace to new line in $subject
613 if ($search_new_line <> "" && $search_new_line <> $subject_new_line)
614 $search = preg_replace('/'.preg_quote($search_new_line).'/', $subject_new_line, $search);
616 if ($replace_new_line <> "" && $replace_new_line <> $subject_new_line)
617 $replace = preg_replace('/'.preg_quote($replace_new_line).'/', $subject_new_line, $replace);
619 return preg_replace('/'. preg_quote($search, '/') .'/', $replace, $subject);
623 * Check if the server is down
625 * @param $domain Server Domain
626 * @return return false if server is down, otherwise, return true
627 * @author Cindy Qi Li
629 function pingDomain($domain)
631 $file = @fopen ($domain, 'r');
641 * Insert record into table patches
643 * @param $patch_summary_array Patch summary information
644 * @author Cindy Qi Li
646 function createPatchesRecord($patch_summary_array)
650 $sql = "INSERT INTO " . TABLE_PREFIX. "patches " .
659 ('".$patch_summary_array["atutor_patch_id"]."',
660 '".$patch_summary_array["applied_version"]."',
661 '".preg_quote($patch_summary_array["patch_folder"])."',
662 '".preg_quote($patch_summary_array["description"])."',
663 '".$patch_summary_array["available_to"]."',
665 '".$patch_summary_array["status"]."'
668 $result = mysql_query($sql, $db) or die(mysql_error());
670 $this->patch_id = mysql_insert_id();
676 * Insert record into table patches_files
678 * @param $patch_files_array Patch information
679 * @author Cindy Qi Li
681 function createPatchesFilesRecord($patch_files_array)
685 $sql = "INSERT INTO " . TABLE_PREFIX. "patches_files " .
691 (".$this->patch_id.",
692 '".$patch_files_array['action']."',
693 '".preg_quote($patch_files_array['name'])."',
694 '".preg_quote($patch_files_array['location'])."'
697 $result = mysql_query($sql, $db) or die(mysql_error());
699 $this->patch_file_id = mysql_insert_id();
705 * Insert record into table patches_files_actions
707 * @param $patch_files_actions_array alter file actions and contents
708 * @author Cindy Qi Li
710 function createPatchesFilesActionsRecord($patch_files_actions_array)
714 $sql = "INSERT INTO " . TABLE_PREFIX. "patches_files_actions " .
720 (".$this->patch_file_id.",
721 '".$patch_files_actions_array['type']."',
722 '".preg_replace('/\'/', '\\\'', $patch_files_actions_array['code_from'])."',
723 '".preg_replace('/\'/', '\\\'', $patch_files_actions_array['code_to'])."'
726 $result = mysql_query($sql, $db) or die(mysql_error());