2 /************************************************************************/
4 /************************************************************************/
5 /* Copyright (c) 2010 */
6 /* Inclusive Design Institute */
8 /* This program is free software. You can redistribute it and/or */
9 /* modify it under the terms of the GNU General Public License */
10 /* as published by the Free Software Foundation. */
11 /************************************************************************/
15 * Class for patch installation
21 define('TR_INCLUDE_PATH', '../../');
23 require_once(TR_INCLUDE_PATH. "classes/DAO/PatchesDAO.class.php");
24 require_once(TR_INCLUDE_PATH. "classes/DAO/PatchesFilesDAO.class.php");
25 require_once(TR_INCLUDE_PATH. "classes/DAO/PatchesFilesActionsDAO.class.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_SERVER (defined in include/constants.inc.php)
42 var $backup_suffix; // suffix appended for backup files
43 var $patch_suffix; // suffix appended for patch files copied from UPDATE_SERVER (defined in include/constants.inc.php)
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
46 var $svn_server_connected; // flag indicating if can connect to svn server, if not, consider all files manipulated by patch as modified
48 // constant, URL of user's ATutor release version in SVN
49 var $sql_file = 'patch.sql';
50 var $relative_to_root = '../'; // relative path from updater/ to root
52 var $patchesDAO; // DAO for table "patches"
53 var $patchesFilesDAO; // DAO for table "patches_files"
54 var $patchesFilesActionsDAO; // DAO for table "patches_files_actions"
57 * Constructor: Initialize object members
59 * @param $patch_array The name of the file to find charset definition
60 * $patch_summary_array
64 function Patch($patch_array, $patch_summary_array, $skipFilesModified, $patch_folder)
66 // add relative path to move to AContent root folder
67 for ($i = 0; $i < count($patch_array[files]); $i++)
69 $patch_array[files][$i]['location'] = $this->relative_to_root . $patch_array[files][$i]['location'];
72 $this->patch_array = $patch_array;
73 $this->patch_summary_array = $patch_summary_array;
75 $this->baseURL = $patch_folder;
76 $this ->backup_suffix = $patch_array['system_patch_id'] . ".old";
77 $this ->patch_suffix = $patch_array['system_patch_id'];
78 $this->skipFilesModified = $skipFilesModified;
80 $this->module_content_dir = TR_CONTENT_DIR . "updater";
81 $this->patchesDAO = new PatchesDAO();
82 $this->patchesFilesDAO = new PatchesFilesDAO();
83 $this->patchesFilesActionsDAO = new PatchesFilesActionsDAO();
87 if (!is_array($_SESSION['remove_permission'])) $_SESSION['remove_permission']=array();
92 * Main process to apply patch.
94 * @return true if patch is successfully applied
103 // 1. if svn server is up. If not, consider all files manipulated by patch as modified
104 // 2. if the local file is customized by user
105 // 3. if script has write priviledge on local file/folder
106 // 4. if dependent patches have been installed
107 if (!$this->pingDomain(SVN_TAG_FOLDER))
109 $msg->addInfo('CANNOT_CONNECT_SVN_SERVER');
111 $this->svn_server_connected = false;
114 $this->svn_server_connected = true;
116 if (!$this->checkDependentPatches()) return false;
118 if (!$this->checkAppliedVersion()) return false;
120 if (!$this->skipFilesModified && $this->hasFilesModified()) return false;
122 if (!$this->checkPriviledge()) return false;
125 if (strlen(trim($this->patch_array['sql'])) > 0) $this->runSQL();
127 // Start applying patch
128 $this->createPatchesRecord($this->patch_summary_array);
130 // if no file action defined, update database and return true
131 if (!is_array($this->patch_array[files]))
133 $updateInfo = array("status"=>"Installed");
134 $this->patchesDAO->UpdateByArray($this->patch_id, $updateInfo);
139 foreach ($this->patch_array[files] as $row_num => $patch_file)
141 $this->createPatchesFilesRecord($this->patch_array['files'][$row_num]);
143 if ($patch_file['action'] == 'alter')
145 $this->alterFile($row_num);
147 else if ($patch_file['action'] == 'add')
149 $this->addFile($row_num);
151 else if ($patch_file['action'] == 'delete')
153 $this->deleteFile($row_num);
155 else if ($patch_file['action'] == 'overwrite')
157 $this->overwriteFile($row_num);
161 // if only has backup files info, patch is considered successfully installed
162 // if has permission to remove, considered partly installed
163 $updateInfo = array();
165 if (count($this->backup_files) > 0)
167 foreach($this->backup_files as $backup_file)
168 $backup_files .= $backup_file. '|';
170 $updateInfo = array("backup_files"=>mysql_real_escape_string($backup_files));
173 if (count($this->patch_files) > 0)
175 foreach($this->patch_files as $patch_file)
176 $patch_files .= $patch_file. '|';
178 $updateInfo = array_merge($updateInfo, array("patch_files"=>mysql_real_escape_string($patch_files)));
181 if (is_array($_SESSION['remove_permission']) && count($_SESSION['remove_permission']))
183 foreach($_SESSION['remove_permission'] as $remove_permission_file)
184 $remove_permission_files .= $remove_permission_file. '|';
186 $updateInfo = array_merge($updateInfo, array("remove_permission_files"=>mysql_real_escape_string($remove_permission_files), "status"=>"Partly Installed"));
190 $updateInfo = array_merge($updateInfo, array("status"=>"Installed"));
193 $this->patchesDAO->UpdateByArray($this->patch_id, $updateInfo);
195 unset($_SESSION['remove_permission']);
203 * @return patch array
204 * @author Cindy Qi Li
206 function getPatchArray()
208 return $this->patch_array;
212 * return patch id processed by this object
215 * @author Cindy Qi Li
217 function getPatchID()
219 return $this->patch_id;
223 * Check if script has write permission to the files and folders that need to be written
224 * if no permission, warn user to give permission
226 * @return true if there are files or folders that script has no permission
227 * false if permissions are in place
228 * @author Cindy Qi Li
230 function checkPriviledge()
234 // no file action is defined, return true;
235 if (!is_array($this->patch_array[files])) return true;
237 foreach ($this->patch_array[files] as $row_num => $patch_file)
239 $real_location = realpath($patch_file['location']);
241 if (!is_writable($patch_file['location']) && !in_array($real_location, $this->need_access_to_folders))
243 $this->need_access_to_folders[] = $real_location;
245 if (!in_array($real_location, $_SESSION['remove_permission']))
246 $_SESSION['remove_permission'][] = $real_location;
249 if ($patch_file['action'] == 'alter' || $patch_file['action'] == 'delete' || $patch_file['action'] == 'overwrite')
251 $file = $patch_file['location'] . "/" . $patch_file['name'];
253 $real_file = realpath($file);
254 if (file_exists($file) && !is_writable($file) && !in_array($real_file, $this->need_access_to_files))
256 $this->need_access_to_files[] = $real_file;
258 if (!in_array($real_file, $_SESSION['remove_permission']) && $patch_file['action'] <> 'delete')
259 $_SESSION['remove_permission'][] = $real_file;
264 if (count($this->need_access_to_folders) > 0 || count($this->need_access_to_files) > 0)
266 $this->errors[] = _AT('grant_write_permission');
268 foreach($this->need_access_to_folders as $folder)
270 $this->errors[0] .= '<strong>'. $folder . "</strong><br />";
273 foreach($this->need_access_to_files as $file)
275 $this->errors[0] .= '<strong>'. $file . "</strong><br />";
278 $notes = '<form action="'. $_SERVER['PHP_SELF'].'?id='.$id.'&who='. $who .'" method="post" name="skip_files_modified">
279 <div class="row buttons">
280 <input type="submit" name="yes" value="'._AT('continue').'" accesskey="y" />
281 <input type="submit" name="no" value="'. _AT('cancel'). '" />
282 <input type="hidden" name="install" value="' . $_POST['install'] . '" />
283 <input type="hidden" name="install_upload" value="' . $_POST['install_upload'] . '" />
284 <input type="hidden" name="ignore_version" value="' . $_POST['ignore_version'] . '" />
288 print_errors($this->errors, $notes);
290 unset($this->errors);
298 * Check if AContent version is same as "applied version" defined in the patch.
300 * @return true if versions match
301 * false if versions don't match
302 * @author Cindy Qi Li
304 function checkAppliedVersion()
308 if ($this->patch_summary_array["applied_version"] <> VERSION)
310 $this->errors[] = _AT("version_not_match", $this->patch_summary_array["applied_version"]);
313 <form action="'. $_SERVER['PHP_SELF'].'?id='.$_POST['id'].'&who='. $_POST['who'] .'" method="post" name="skip_files_modified">
314 <div class="row buttons">
315 <input type="submit" name="ignore_version" value="'._AT('yes').'" accesskey="y" />
316 <input type="submit" name="not_ignore_version" value="'. _AT('no'). '" />
317 <input type="hidden" name="install" value="' . $_POST['install'] . '" />
318 <input type="hidden" name="install_upload" value="' . $_POST['install_upload'] . '" />
322 print_errors($this->errors, $notes);
324 unset($this->errors);
333 * Check if all the dependent patches have been installed.
335 * @return true if all the dependent patches have been installed
336 * false if any dependent patch has not been installed.
337 * @author Cindy Qi Li
339 function checkDependentPatches()
343 $dependent_patches_installed = true;
345 // if no dependent patch defined, return true
346 if (!is_array($this->patch_summary_array["dependent_patches"])) return true;
348 foreach($this->patch_summary_array["dependent_patches"] as $num => $dependent_patch)
350 if (!is_patch_installed($dependent_patch))
352 $dependent_patches_installed = false;
353 $dependent_patches .= $dependent_patch. ", ";
357 if (!$dependent_patches_installed)
359 $errors = array('UPDATE_DEPENDENCY', substr($dependent_patches, 0, -2));
360 $msg->addError($errors);
368 * Loop thru all the patch files that will be overwitten or altered,
369 * to find out if they are modified by user. If it's modified, warn user.
371 * @return true if there are files being modified
372 * false if no file is modified
373 * @author Cindy Qi Li
375 function hasFilesModified()
377 $overwrite_modified_files = false;
378 $alter_modified_files = false;
379 $has_not_exist_files = false;
381 // no file action is defined, return nothing is modified (false)
382 if (!is_array($this->patch_array[files])) return false;
384 foreach ($this->patch_array[files] as $row_num => $patch_file)
386 if ($patch_file["action"]=='alter' || $patch_file["action"]=='overwrite')
388 if (!file_exists($patch_file['location'] . $patch_file['name']))
390 $not_exist_files .= $patch_file['location'] . $patch_file['name'] . '<br />';
391 $has_not_exist_files = true;
393 else if ($this->isFileModified($patch_file['location'], $patch_file['name']))
395 if ($patch_file['action']=='overwrite')
397 $overwrite_files .= realpath($patch_file['location'] . $patch_file['name']) . '<br />';
398 $overwrite_modified_files = true;
400 if ($patch_file['action']=='alter')
402 $alter_files .= realpath($patch_file['location'] . $patch_file['name']) . '<br />';
403 $alter_modified_files = true;
409 if ($has_not_exist_files) $this->errors[] = _AT('update_local_file_not_exist'). $not_exist_files;
410 if ($overwrite_modified_files) $this->errors[] = _AT('updater_overwrite_modified_files') . $overwrite_files;
411 if ($alter_modified_files) $this->errors[] = _AT('updater_alter_modified_files') . $alter_files;
412 if (count($this->errors) > 0)
414 if ($has_not_exist_files)
418 <form action="'. $_SERVER['PHP_SELF'].'?id='.$_POST['id'].'&who='. $_POST['who'] .'" method="post" name="skip_files_modified">
419 <div class="row buttons">
420 <input type="submit" name="yes" value="'._AT('yes').'" accesskey="y" />
421 <input type="submit" name="no" value="'. _AT('no'). '" />
422 <input type="hidden" name="install" value="' . $_POST['install'] . '" />
423 <input type="hidden" name="install_upload" value="' . $_POST['install_upload'] . '" />
424 <input type="hidden" name="ignore_version" value="' . $_POST['ignore_version'] . '" />
428 print_errors($this->errors, $notes);
430 unset($this->errors);
438 * Compare user's local file with SVN backup for user's AContent version,
439 * if different, check table TR_patches_files to see if user's local file
440 * was altered by previous patch installation. If it is, return false
441 * (not modified), otherwise, return true (modified).
443 * @param $folder folder of the file to be compared
444 * $file name of the file to be compared
445 * @return true if the file is modified
446 * false if the file is not modified
447 * @author Cindy Qi Li
449 function isFileModified($folder, $file)
453 if (!$this->svn_server_connected) return true;
455 $svn_file = SVN_TAG_FOLDER . 'acontent_' . str_replace('.', '_', VERSION) .
456 str_replace(substr($this->relative_to_root, 0, -1), '' , $folder) .$file;
457 $local_file = $folder.$file;
459 // if svn script does not exist, consider the script is modified
460 if (!@file_get_contents($svn_file)) return true;
462 // check if the local file has been modified by user. if it is, don't overwrite
463 if ($this->compareFiles($svn_file, $local_file) <> 0 && $this->patchesFilesDAO->getNumOfUpdatesOnFile($file) == 0)
465 // check if the file was changed by previous installed patches
472 * Run SQL defined in patch.xml
474 * @author Cindy Qi Li
479 // As sqlutility.class.php reads sql from a file, write sql to module content folder
480 $patch_sql_file = $this->module_content_dir . '/' . $this->sql_file;
482 $fp = fopen($patch_sql_file, 'w');
483 fwrite($fp, trim($this->patch_array['sql']));
486 require(TR_INCLUDE_PATH . 'classes/sqlutility.class.php');
487 $sqlUtility = new SqlUtility();
489 $sqlUtility->queryFromFile($patch_sql_file, TABLE_PREFIX);
491 @unlink($patch_sql_file);
497 * Copy file from UPDATE_SERVER (defined in include/constants.inc.php) to user's computer
499 * @param $row_num row number of patch record to be processed
500 * @author Cindy Qi Li
502 function addFile($row_num)
504 $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']);
510 * Delete file, backup before deletion
512 * @param $row_num row number of patch record to be processed
513 * @author Cindy Qi Li
515 function deleteFile($row_num)
517 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
518 $backup_file = $local_file . "." . $this->backup_suffix;
520 if (file_exists($local_file))
522 // move file to backup
523 $this->copyFile($local_file, $backup_file);
524 $this->backup_files[] = realpath($backup_file);
525 @unlink($local_file);
533 * Alter file based on <action_detail>
534 * If user's local file is modified and user agrees to proceed with applying patch,
535 * alter user's local file.
537 * @param $row_num row number of patch record to be processed
538 * @author Cindy Qi Li
540 function alterFile($row_num)
542 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
544 // backup user's file
545 $backup_file = $local_file . "." . $this->backup_suffix;
547 // Checking existence of $backup_file is to fix the bug when there are multiple alter/delete actions
548 // on the same file, the following backups overwrite the first backup which results in the loss of the
550 if (!file_exists($backup_file))
552 $this->copyFile($local_file, $backup_file);
553 $this->backup_files[] = realpath($backup_file);
556 $local_file_content = file_get_contents($local_file);
558 // Modify user's file
559 foreach ($this->patch_array['files'][$row_num]['action_detail'] as $garbage => $alter_file_action)
561 if ($alter_file_action['type'] == 'delete')
562 $modified_local_file_content = $this->strReplace($alter_file_action['code_from'], '', $local_file_content);
564 if ($alter_file_action['type'] == 'replace')
565 $modified_local_file_content = $this->strReplace($alter_file_action['code_from'], $alter_file_action['code_to'], $local_file_content);
567 // when code_from is not found, add in warning
568 if ($modified_local_file_content == $local_file_content)
570 for ($i = 0; $i < count($this->backup_files); $i++)
571 if ($this->backup_files[$i] == realpath($backup_file))
572 $this->backup_files[$i] .= ' '._AT("chunks_not_found");
575 $local_file_content = $modified_local_file_content;
577 $this->createPatchesFilesActionsRecord($alter_file_action);
580 $fp = fopen($local_file, 'w');
581 fwrite($fp, $local_file_content);
588 * Fetch file from UPDATE_SERVER (defined in include/constants.inc.php) and overwrite
589 * user's local file if the local file is not modified
590 * If user's local file is modified and user agrees to proceed with applying patch,
591 * copy the new file to user's local for them to merge manually.
593 * @param $row_num row number of patch record to be processed
594 * @author Cindy Qi Li
596 function overwriteFile($row_num)
598 $local_file = $this->patch_array['files'][$row_num]['location'].$this->patch_array['files'][$row_num]['name'];
599 $patch_file = $this->baseURL . preg_replace('/.php$/', '.new', $this->patch_array['files'][$row_num]['name']);
601 // if local file is modified and user agrees to proceed with applying patch,
602 // copy the new file to user's local for them to merge manually
603 if ($this->skipFilesModified && $this->isFileModified($this->patch_array['files'][$row_num]['location'], $this->patch_array['files'][$row_num]['name']))
605 $local_patch_file = $local_file . "." . $this->patch_suffix;
607 $this->copyFile($patch_file, $local_patch_file);
609 $this->patch_files[] = realpath($local_patch_file);
613 $backup_file = $local_file . "." . $this->backup_suffix;
615 // backup user's file
616 $this->copyFile($local_file, $backup_file);
617 $this->backup_files[] = realpath($backup_file);
619 // overwrite user's file
620 $this->copyFile($patch_file, $local_file);
627 * Copy file $src to $dest. $src can be a local file or a remote file
629 * @param $src location of the source file
630 * $dest location of the destination file
631 * @author Cindy Qi Li
633 function copyFile($src, $dest)
635 $content = file_get_contents($src);
636 $fp = fopen($dest, 'w');
637 fwrite($fp, $content);
644 * Compare files $src against $dest
646 * @param $src location of the source file
647 * $dest location of the destination file
648 * @return Returns < 0 if $src is less than $dest ; > 0 if $src is greater than $dest, and 0 if they are equal.
649 * @author Cindy Qi Li
651 function compareFiles($src, $dest)
653 // use preg_replace to delete
654 // 1. the line starting with // $Id:
655 // 2. the line starting with $lm = '$LastChangedDate, ending with ;
656 // These lines are created by SVN. It could be different in different copies of the same file.
657 $pattern = '/\/\/ \$Id.*\$|\$lm = \'\$LastChangedDate.*;/';
659 $src_content = preg_replace($pattern, '', file_get_contents($src));
660 $dest_content = preg_replace($pattern, '', file_get_contents($dest));
662 return strcasecmp($src_content, $dest_content);
666 * Replace single/multiple lines of string.
667 * This function handles different new line character at windows/unix platform
669 * @param $search String to replace from
670 * $replace String to replace to
671 * $subject Subject to be handled
672 * @return return replaced string, if nothing is replaced, return original subject
673 * @author Cindy Qi Li
675 function strReplace($search, $replace, $subject)
677 // Note: DO NOT change the order of the array elements.
678 // "\n\r", "\r\n" must come before "\n", "\r" in the array,
679 // otherwise, the new line replace underneath would wrongly replace "\n\r" to "\r\r" or "\n\n"
680 $new_line_array = array("\n\r", "\r\n", "\r", "\n");
682 foreach ($new_line_array as $new_line)
684 if (preg_match('/'.preg_quote($new_line).'/', $search) > 0) $search_new_lines[] = $new_line;
685 if (preg_match('/'.preg_quote($new_line).'/', $replace) > 0) $replace_new_lines[] = $new_line;
686 if (preg_match('/'.preg_quote($new_line).'/', $subject) > 0) $subject_new_lines[] = $new_line;
689 // replace new line chars in $search, $replace, $subject to the last new line in $subject
690 if (is_array($subject_new_lines)) $new_line_replace_to = array_pop($subject_new_lines);
692 if ($new_line_replace_to <> '')
694 if (count($search_new_lines) > 0)
695 foreach ($search_new_lines as $new_line)
696 if ($new_line <> $new_line_replace_to)
697 $search = preg_replace('/'.preg_quote($new_line).'/', $new_line_replace_to, $search);
699 if (count($replace_new_lines) > 0)
700 foreach ($replace_new_lines as $new_line)
701 if ($new_line <> $new_line_replace_to)
702 $replace = preg_replace('/'.preg_quote($new_line).'/', $new_line_replace_to, $replace);
704 if (count($subject_new_lines) > 0)
705 foreach ($subject_new_lines as $new_line)
706 $subject = preg_replace('/'.preg_quote($new_line).'/', $new_line_replace_to, $subject);
709 return preg_replace('/'. preg_quote($search, '/') .'/', $replace, $subject);
713 * Check if the server is down
715 * @param $domain Server Domain
716 * @return return false if server is down, otherwise, return true
717 * @author Cindy Qi Li
719 function pingDomain($domain)
721 $file = @fopen ($domain, 'r');
730 * Insert record into table patches
732 * @param $patch_summary_array Patch summary information
733 * @author Cindy Qi Li
735 function createPatchesRecord($patch_summary_array)
737 $this->patch_id = $this->patchesDAO->Create($patch_summary_array["system_patch_id"],
738 $patch_summary_array["applied_version"],
739 $patch_summary_array["patch_folder"],
740 $patch_summary_array["description"],
741 $patch_summary_array["available_to"],
742 $patch_summary_array["sql"],
743 $patch_summary_array["status"],
747 $patch_summary_array["author"]);
753 * Insert record into table patches_files
755 * @param $patch_files_array Patch information
756 * @author Cindy Qi Li
758 function createPatchesFilesRecord($patch_files_array)
760 $this->patch_file_id = $this->patchesFilesDAO->Create($this->patch_id,
761 $patch_files_array['action'],
762 $patch_files_array['name'],
763 $patch_files_array['location']);
769 * Insert record into table patches_files_actions
771 * @param $patch_files_actions_array alter file actions and contents
772 * @author Cindy Qi Li
774 function createPatchesFilesActionsRecord($patch_files_actions_array)
776 $this->patchesFilesActionsDAO->Create($this->patch_file_id,
777 $patch_files_actions_array['type'],
778 $patch_files_actions_array['code_from'],
779 $patch_files_actions_array['code_to']);