Index: language/en_us/wiki.php =================================================================== --- language/en_us/wiki.php (revision 0) +++ language/en_us/wiki.php (revision 0) @@ -0,0 +1,83 @@ + 'Revert', + 'compare' => 'Compare', + 'wiki page revision no' => 'Revision %s', + 'wiki page revision' => 'Revision', + 'viewing revision of' => 'Viewing Revision %s of \'%s\'', + 'view page history' => 'View Page History', + + 'confirm delete wiki page' => 'Are you sure you want to delete it?', + + 'log add wiki' => 'Page \'%s\' added to the wiki', + 'log edit wiki' => 'Wiki page \'%s\' edited', + 'log delete wiki' => 'Wiki page \'%s\' deleted', + + 'success add wiki page' => 'Wiki page successfully added', + 'success edit wiki page' => 'Wiki page successfully edited', + 'success delete wiki page' => 'Wiki page successfully deleted', + 'success restore wiki page revision' => 'Wiki page revision successfully restored', + + 'failure delete wiki page' => 'Failed to delete wiki page error was %s', + 'failure restore wiki page revision' => 'Failed to restore revision. Error was(%s)', + + 'add wiki page' => 'Add page', + 'delete wiki page' => 'Delete page', + 'edit wiki page' => 'Edit page', + 'wiki page dnx' => 'Wiki page not found', + + 'wiki page content' => 'Page content', + + 'wiki log message' => 'Log message', + + 'wiki' => 'Wiki', + 'wiki lock page' => 'Lock wiki page?', + 'wiki page' => 'Wiki page', + 'wiki page created on' => 'Created on', + + 'wiki page id required' => 'Page Id is required', + 'wiki page name required' => 'Page Name is required', + 'wiki page content required' => 'Page Content is required', + + 'wiki page history' => 'Page history for \'%s\'', + 'wiki page revisions dnx' => 'Wiki page has no revisions', + + 'wiki default sidebar name' => 'Default Sidebar', + 'wiki default sidebar content' => 'This is some default content for your sidebar + + To get rid of this, make a new page and assign it as project sidebar', + + 'wiki default page name' => 'Default Wiki Page', + 'wiki default page content' => 'Welcome to your new wiki!!', + + 'wiki page revision dnx' => 'Wiki page revision does not exist', + + 'wiki page revision restored from' => 'Restored revision %s', + + 'wiki page diff' => 'Revision comparission', + + 'wiki no visible changes' => 'There are no visible changes between the revisions', + + 'wiki no compare identical' => 'Cannot compare two instances of the same version', + + 'wiki page revision title' => 'Revision Title', + 'wiki page revision author' => 'Revision Author', + 'wiki page revision created' => 'Revision Created', + 'wiki page revision log message' => 'Revision Log Message', + 'wiki set page as index' => 'Set as Project Wiki Index Page?', + 'wiki set page as sidebar' => 'Set as Project Wiki Sidebar?', + + 'wiki page locked' => 'Page is locked', + 'wiki page not locked' => 'Page is not locked', + 'wiki page locked by' => 'Page locked by %s', + 'wiki page locked by on' => 'Page locked by %s on %s', + ) + +?> \ No newline at end of file Index: application/helpers/wiki.php =================================================================== --- application/helpers/wiki.php (revision 0) +++ application/helpers/wiki.php (revision 0) @@ -0,0 +1,36 @@ +getTableName(true) . ' WHERE page_id = ' . $matches[1] . ' AND project_id = ' . active_project()->getId(); + + $row = DB::executeOne($sql); + if(!count($row)){ + return null; + } + + return '' . $row['name'] . ''; +} + +?> \ No newline at end of file Index: application/helpers/project_website.php =================================================================== --- application/helpers/project_website.php (revision 154) +++ application/helpers/project_website.php (working copy) @@ -28,6 +28,7 @@ define('PROJECT_TAB_TAGS', 'tags'); define('PROJECT_TAB_FORMS', 'forms'); define('PROJECT_TAB_PEOPLE', 'people'); + define('PROJECT_TAB_WIKI', 'wiki'); /** * Prepare dashboard tabbed navigation @@ -66,6 +67,11 @@ lang('tags'), get_url('project', 'tags') )); + add_tabbed_navigation_item(new TabbedNavigationItem( + PROJECT_TAB_WIKI, + lang('wiki'), + get_url('wiki') + )); if (logged_user()->isAdministrator()) { add_tabbed_navigation_item(new TabbedNavigationItem( PROJECT_TAB_FORMS, Index: application/models/wiki/Revision.class.php =================================================================== --- application/models/wiki/Revision.class.php (revision 0) +++ application/models/wiki/Revision.class.php (revision 0) @@ -0,0 +1,62 @@ +validatePresenceOf('name')) { + $errors[] = lang('wiki page name required'); + } + if (!$this->validatePresenceOf('content')) { + $errors[] = lang('wiki page content required'); + } + if(!$this->validatePresenceOf('project_id')){ + $errors[] = lang('wiki project id required'); + } + if(!$this->validatePresenceOf('page_id')){ + $errors[] = lang('wiki page id required'); + } + + } // validate + + + /** + * Get url to revert to this revision + * + * @return + */ + function getRevertUrl() + { + return get_url('wiki', 'revert', array('id' => $this->getPageId(), 'revision' => $this->getRevision())); + } + + /** + * Get url to view this revision + * + * @return + */ + function getViewUrl() + { + return get_url('wiki', 'view', array('id' => $this->getPageId(), 'revision' => $this->getRevision())); + } + +} + +?> \ No newline at end of file Index: application/models/wiki/base/BaseRevision.class.php =================================================================== --- application/models/wiki/base/BaseRevision.class.php (revision 0) +++ application/models/wiki/base/BaseRevision.class.php (revision 0) @@ -0,0 +1,50 @@ +getColumnValue($col); + + } elseif($type[1] == 'set' && count($args)) { + //Else if we want to set a value, and we have an arguement + return $this->setColumnValue($col, $args[0]); + + }// if + } //if + // me no understand! + return false; + }// _calls + + /** + * Return instance of manager + * + * @return + */ + function manager() + { + return Revisions::instance(); + }//manager + +} + +?> \ No newline at end of file Index: application/models/wiki/base/BaseWikiPage.class.php =================================================================== --- application/models/wiki/base/BaseWikiPage.class.php (revision 0) +++ application/models/wiki/base/BaseWikiPage.class.php (revision 0) @@ -0,0 +1,132 @@ +getColumnValue($col); + + } elseif($type[1] == 'set' && count($args)) { + //Else if we want to set a value, and we have an arguement + return $this->setColumnValue($col, $args[0]); + + }// if + } //if + + // me no understand! + return false; + } //__call + + + /** + * Return manager instance + * + * @access protected + * @param void + * @return ProjectLinks + */ + function manager() + { + if (!instance_of($this->manager, 'Wiki')) { + $this->manager = Wiki::instance(); + } + return $this->manager; + } // manager + + /** + * This function saves the wiki page + * + * @return void + */ + function save() + { + if(instance_of($this->new_revision, 'Revision')){ + + //Increase the page revision number + $this->setColumnValue('revision', $this->getColumnValue('revision') + 1); + + //Remove any other pages in this project which have the default page status + if(($this->isColumnModified('project_index') && $this->getColumnValue('project_index') == 1)){ + $sql = 'UPDATE ' . $this->getTableName() . ' SET `project_index` = 0 WHERE `project_id` = ' . $this->getProjectId(); + Db::execute($sql); + } + + //Remove any other pages in this project which have sidebar status + if(($this->isColumnModified('project_sidebar') && $this->getColumnValue('project_sidebar') == 1)){ + $sql = 'UPDATE ' . $this->getTableName() . ' SET `project_sidebar` = 0 WHERE `project_id` = ' . $this->getProjectId(); + Db::execute($sql); + } + + //Save this page with the new revision id + parent::save(); + + //Set the revisions's page Id + $this->new_revision->setPageId($this->getId()); + + //Set the project Id + $this->new_revision->setProjectId($this->getProjectId()); + + //Set the revision number in the revision object + $this->new_revision->setRevision($this->getColumnValue('revision')); + + //If we have made a new revision of this page, then save the revision + $this->new_revision->save(); + + } else { + //We haven't made a new revision, so we shouldn't update this page + return false; + } + }//save + + /** + * Override the getSearchableColumnContent function + * The Wikipage doesn't store any content(The revisions table does), + * but we access the revisions through the wiki object. + * + * To be able to search a wiki revision, we need to pretend that the wiki object + * stores the content, while actually getting it from the revision + * + * If you find this really confusing, then just look at the code =P + * + * @param mixed $column + * @return + */ + function getSearchableColumnContent($column) + { + if($this->new_revision instanceof Revision){ + if($this->new_revision->columnExists($column)){ + return $this->new_revision->getColumnValue($column); + }// if + }// if + return null; + }// getSearchableColumnContent +} + +?> \ No newline at end of file Index: application/models/wiki/base/BaseRevisions.class.php =================================================================== --- application/models/wiki/base/BaseRevisions.class.php (revision 0) +++ application/models/wiki/base/BaseRevisions.class.php (revision 0) @@ -0,0 +1,227 @@ + DATA_TYPE_INTEGER, + 'page_id' => DATA_TYPE_INTEGER, + 'created_on' => DATA_TYPE_DATETIME, + 'created_by_id' => DATA_TYPE_INTEGER, + 'name' => DATA_TYPE_STRING, + 'project_id' => DATA_TYPE_INTEGER, + 'revision' => DATA_TYPE_INTEGER, + 'content' => DATA_TYPE_STRING, + 'log_message' => DATA_TYPE_STRING, + ); + + + /** + * Construct + * + * @return void + */ + function __construct() + { + parent::__construct('Revision', 'wiki_revisions'); + }// __construct + + /** + * Return array of object columns + * + * @access public + * @param void + * @return array + */ + function getColumns() + { + return array_keys(self::$columns); + } // getColumns + +/** + * Return column type + * + * @access public + * @param string $column_name + * @return string + */ + function getColumnType($column_name) { + if (isset(self::$columns[$column_name])) { + return self::$columns[$column_name]; + } else { + return DATA_TYPE_STRING; + } // if + } // getColumnType + + /** + * Return array of PK columns. If only one column is PK returns its name as string + * + * @access public + * @param void + * @return array or string + */ + function getPkColumns() { + return 'id'; + } // getPkColumns + + /** + * Return name of first auto_incremenent column if it exists + * + * @access public + * @param void + * @return string + */ + function getAutoIncrementColumn() { + return 'id'; + } // getAutoIncrementColumn + +/** + * Returns an instance of the Wiki class + * + * @return + */ + function instance() + { + static $instance; + if(!instance_of($instance, 'Revisions')){ + $instance = new Revisions; + } + return $instance; + } + + /** + * Return number of rows in this table + * + * @access public + * @param string $conditions Query conditions + * @return integer + */ + function count($condition = null) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::count($condition); + } else { + return Revisions::instance()->count($condition); + } // if + } // count + + /** + * Delete rows that match specific conditions. If $conditions is NULL all rows from table will be deleted + * + * @access public + * @param string $conditions Query conditions + * @return boolean + */ + function delete($condition = null) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::delete($condition); + } else { + return Revisions::instance()->delete($condition); + } // if + } // delete + + // ------------------------------------------------------- + // Finders + // ------------------------------------------------------- + + /** + * Do a SELECT query over database with specified arguments + * + * @access public + * @param array $arguments Array of query arguments. Fields: + * + * - one - select first row + * - conditions - additional conditions + * - order - order by string + * - offset - limit offset, valid only if limit is present + * - limit + * + * @return one or Users objects + * @throws DBQueryError + */ + function find($arguments = null) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::find($arguments); + } else { + return Revisions::instance()->find($arguments); + } // if + } // find + + /** + * Find all records + * + * @access public + * @param array $arguments + * @return one or Users objects + */ + function findAll($arguments = null) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::findAll($arguments); + } else { + return Revisions::instance()->findAll($arguments); + } // if + } // findAll + + /** + * Find one specific record + * + * @access public + * @param array $arguments + * @return Wiki Page + */ + function findOne($arguments = null) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::findOne($arguments); + } else { + return Revisions::instance()->findOne($arguments); + } // if + } // findOne + + /** + * Return object by its PK value + * + * @access public + * @param mixed $id + * @param boolean $force_reload If true cache will be skipped and data will be loaded from database + * @return User + */ + function findById($id, $force_reload = false) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::findById($id, $force_reload); + } else { + return Revisions::instance()->findById($id, $force_reload); + } // if + } // findById + + /** + * This function will return paginated result. Result is an array where first element is + * array of returned object and second populated pagination object that can be used for + * obtaining and rendering pagination data using various helpers. + * + * Items and pagination array vars are indexed with 0 for items and 1 for pagination + * because you can't use associative indexing with list() construct + * + * @access public + * @param array $arguments Query argumens (@see find()) Limit and offset are ignored! + * @param integer $items_per_page Number of items per page + * @param integer $current_page Current page number + * @return array + */ + function paginate($arguments = null, $items_per_page = 10, $current_page = 1) { + if (isset($this) && instance_of($this, 'Revisions')) { + return parent::paginate($arguments, $items_per_page, $current_page); + } else { + return Revisions::instance()->paginate($arguments, $items_per_page, $current_page); + } // if + } // paginate + +} + +?> \ No newline at end of file Index: application/models/wiki/base/BaseWiki.class.php =================================================================== --- application/models/wiki/base/BaseWiki.class.php (revision 0) +++ application/models/wiki/base/BaseWiki.class.php (revision 0) @@ -0,0 +1,225 @@ + DATA_TYPE_INTEGER, + 'project_id' => DATA_TYPE_INTEGER, + 'revision' => DATA_TYPE_INTEGER, + 'project_index' => DATA_TYPE_BOOLEAN, + 'project_sidebar' => DATA_TYPE_BOOLEAN, + 'locked' => DATA_TYPE_BOOLEAN, + 'locked_by_id' => DATA_TYPE_INTEGER, + 'locked_on' => DATA_TYPE_DATETIME, + ); + + /** + * Construct + * + * @return void + */ + function __construct() + { + parent::__construct('WikiPage', 'wiki_pages'); + }// __construct + + /** + * Return array of object columns + * + * @access public + * @param void + * @return array + */ + function getColumns() + { + return array_keys(self::$columns); + } // getColumns + + /** + * Return column type + * + * @access public + * @param string $column_name + * @return string + */ + function getColumnType($column_name) { + if (isset(self::$columns[$column_name])) { + return self::$columns[$column_name]; + } else { + return DATA_TYPE_STRING; + } // if + } // getColumnType + + /** + * Return array of PK columns. If only one column is PK returns its name as string + * + * @access public + * @param void + * @return array or string + */ + function getPkColumns() { + return 'id'; + } // getPkColumns + + /** + * Return name of first auto_incremenent column if it exists + * + * @access public + * @param void + * @return string + */ + function getAutoIncrementColumn() { + return 'id'; + } // getAutoIncrementColumn + + /** + * Returns an instance of the Wiki class + * + * @return + */ + function instance() + { + static $instance; + if(!instance_of($instance, 'Wiki')){ + $instance = new Wiki; + } + return $instance; + } // instance + + /** + * Return number of rows in this table + * + * @access public + * @param string $conditions Query conditions + * @return integer + */ + function count($condition = null) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::count($condition); + } else { + return Wiki::instance()->count($condition); + } // if + } // count + + /** + * Delete rows that match specific conditions. If $conditions is NULL all rows from table will be deleted + * + * @access public + * @param string $conditions Query conditions + * @return boolean + */ + function delete($condition = null) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::delete($condition); + } else { + return Wiki::instance()->delete($condition); + } // if + } // delete + + // ------------------------------------------------------- + // Finders + // ------------------------------------------------------- + + /** + * Do a SELECT query over database with specified arguments + * + * @access public + * @param array $arguments Array of query arguments. Fields: + * + * - one - select first row + * - conditions - additional conditions + * - order - order by string + * - offset - limit offset, valid only if limit is present + * - limit + * + * @return one or Users objects + * @throws DBQueryError + */ + function find($arguments = null) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::find($arguments); + } else { + return Wiki::instance()->find($arguments); + } // if + } // find + + /** + * Find all records + * + * @access public + * @param array $arguments + * @return one or Users objects + */ + function findAll($arguments = null) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::findAll($arguments); + } else { + return Wiki::instance()->findAll($arguments); + } // if + } // findAll + + /** + * Find one specific record + * + * @access public + * @param array $arguments + * @return Wiki Page + */ + function findOne($arguments = null) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::findOne($arguments); + } else { + return Wiki::instance()->findOne($arguments); + } // if + } // findOne + + /** + * Return object by its PK value + * + * @access public + * @param mixed $id + * @param boolean $force_reload If true cache will be skipped and data will be loaded from database + * @return User + */ + function findById($id, $force_reload = false) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::findById($id, $force_reload); + } else { + return Wiki::instance()->findById($id, $force_reload); + } // if + } // findById + + /** + * This function will return paginated result. Result is an array where first element is + * array of returned object and second populated pagination object that can be used for + * obtaining and rendering pagination data using various helpers. + * + * Items and pagination array vars are indexed with 0 for items and 1 for pagination + * because you can't use associative indexing with list() construct + * + * @access public + * @param array $arguments Query argumens (@see find()) Limit and offset are ignored! + * @param integer $items_per_page Number of items per page + * @param integer $current_page Current page number + * @return array + */ + function paginate($arguments = null, $items_per_page = 10, $current_page = 1) { + if (isset($this) && instance_of($this, 'Wiki')) { + return parent::paginate($arguments, $items_per_page, $current_page); + } else { + return Wiki::instance()->paginate($arguments, $items_per_page, $current_page); + } // if + } // paginate +} + +?> \ No newline at end of file Index: application/models/wiki/WikiPage.class.php =================================================================== --- application/models/wiki/WikiPage.class.php (revision 0) +++ application/models/wiki/WikiPage.class.php (revision 0) @@ -0,0 +1,400 @@ +isAdministrator() || $user->isMemberOfOwnerCompany(); + }// canAdd + + /** + * Can the user edit this page + * + * @param mixed User object + * @return (bool) + */ + function canEdit(User $user) + { + //Is the user a member of the owner company, or an admin? + return $user->isAdministrator() || $user->isMemberOfOwnerCompany(); + }// canEdit + + /** + * Can the user delete this page + * + * @param mixed User object + * @return (bool) + */ + function canDelete(User $user) + { + //Only admins can delete a page + return $user->isAdministrator(); + }// canDelete + + /** + * Can the user lock this page + * + * @param mixed User Object + * @return (bool) + */ + function canLock(User $user) + { + //Only admins can lock a page + return $user->isAdministrator(); + }// canLock + + /** + * Can the user unlock this page + * + * @param mixed User Object + * @return (bool) + */ + function canUnlock(User $user) + { + //Only admins can unlock a page + return $user->isAdministrator(); + } + + /** + * Can the user view this page + * + * @param mixed $user + * @return + */ + function canView(User $user) + { + return $user->isProjectUser($this->getProject()); + }// canView + + ////////////////////////////////////////// + // Urls + ////////////////////////////////////////// + + /** + * Get url to the add wiki page + * + * @return string + */ + function getAddUrl() + { + return $this->makeUrl('add', array('active_project' => active_project()->getId()), false); + }// getAddUrl + + /** + * Get url to edit this wiki page + * + * @return string + */ + function getEditUrl() + { + return $this->makeUrl('edit'); + }//getEditUrl + + /** + * Get url to delete this wiki page + * + * @return string + */ + function getDeleteUrl() + { + return $this->makeUrl('delete'); + }//getDeleteUrl + + /** + * Get url to view page's revision history + * + * @return string + */ + function getViewHistoryUrl() + { + return $this->makeUrl('history'); + }// getViewHistoryUrl + + /** + * Get url to view this wiki page + * + * @return string + */ + function getViewUrl() + { + return $this->makeUrl('view'); + }// getViewUrl + + /** + * Generic function to make a url to a wiki page + * + * + * @param string The action of the target page(e.g. view, delete etc.) + * @param mixed Optional array of params + * @param bool Include the page id? Defaults true + * @return + */ + function makeUrl($action = 'index', $params = array(), $include_page_id = true) + { + //Merge params with the wiki page id + $params = $include_page_id ? + array_merge(array('id' => $this->getId(), 'active_project' => $this->getProjectId()), $params) : + array_merge(array('active_project' => active_project()->getId()), $params); + //: $params; + + return get_url('wiki', $action, $params); + + // ----- DEPRECTED ------ /// + //Decide if this link is for the wiki controller, or the dashboard + return $this->getProjectId() == 0 ? + get_url('dashboard', 'wiki', array_merge(array('s' => $action), $params), null, false) : + get_url('wiki', $action, $params); + }//makeUrl + + ////////////////////////////////////////// + // Revisions + ////////////////////////////////////////// + + /** + * Get a specific revision + * + * @param mixed $revision + * @return mixed + */ + function getRevision($revision = null) + { + if($revision == null && instance_of($this->cur_revision, 'Revision')){ + + //If we want the latest revision, and we have it cached, return cache + return $this->cur_revision; + + } else if (isset($this->revisions[$revision])){ + + //If we have the revision cached, return it + return $this->revisions[$revision]; + + } else if($revision === null){ + + //Update and return cache of latest revision + $this->cur_revision = Revisions::getRevision($this->getId(), $revision); + + //Make another cache of it + $this->revisions[$this->cur_revision->getId()] = $this->cur_revision; + + return $this->cur_revision; + + } else { + + //Cache and return the revision + $revision = (int) $revision; + return $this->revisions[$revision] = Revisions::getRevision($this->getId(), $revision); + + }// if + + + }// getRevision + + /** + * Get the latest revision of this page + * + * @return mixed + */ + function getLatestRevision() + { + return $this->getRevision(null); + }// getLatestRevision + + /** + * Makes a new revision of this page + * + * @return Revision object + */ + function makeRevision() + { + //Make a new revision + $this->new_revision = new Revision; + //Set the project ID + $this->new_revision->setProjectId($this->getProjectId()); + + //Return a reference to the revision + return $this->new_revision; + }//makeRevision + + ////////////////////////////////////////// + // System + ////////////////////////////////////////// + + /** + * Delete page & its revisions + * + * @return + */ + function delete() + { + $revisions = (array) Revisions::buildPageHistory($this->getId(), $this->getProject()); + foreach($revisions as $revision){ + $revision->delete(); + } + return parent::delete(); + }// delete + + /** + * Return object type name + * + * @param void + * @return string + */ + function getObjectTypeName() { + return lang('wiki'); + } // getObjectTypeName + + /** + * Get page name + * + * @return + */ + function getObjectName() + { + return instance_of($this->new_revision, 'Revision') ? $this->new_revision->getName() : $this->getLatestRevision()->getName(); + }// getObjectName + + /** + * Return object URl + * + * @access public + * @param void + * @return string + */ + function getObjectUrl() { + return $this->getViewUrl(); + }// getObjectUrl + + /** + * This function will return paginated result. Result is an array where first element is + * array of returned object and second populated pagination object that can be used for + * obtaining and rendering pagination data using various helpers. + * + * Items and pagination array vars are indexed with 0 for items and 1 for pagination + * because you can't use associative indexing with list() construct + * + * @access public + * @param array $arguments Query argumens (@see find()) Limit and offset are ignored! + * @param integer $items_per_page Number of items per page + * @param integer $current_page Current page number + * @return array + */ + function paginateRevisions($arguments = array(), $items_per_page = 10, $current_page = 1) { + + if(is_array($arguments) && !isset($arguments['conditions'])){ + $arguments['conditions'] = array('`project_id` = ? AND `page_id` = ?', $this->getProjectId(), $this->getId()); + } + if(is_array($arguments) && !isset($arguments['order'])){ + $arguments['order'] = '`revision` DESC'; + } + + return Revisions::instance()->paginate($arguments, $items_per_page, $current_page); + }// paginate + + + ////////////////////////////////////////// + // Lockage + ////////////////////////////////////////// + /** + * Get the user object for the user which locked this page + * + * Returns null if user DNX or page is not locked + * + * @return + */ + function getLockedByUser() + { + //Cache the user object + static $user = null; + return $this->getLocked() ? + //If the page is locked + (($user instanceof User) ? + //If we have cached the user's object + $user : + //Else find it and cache it + ($user = Users::findById($this->getLockedById()))) : + //If the page is not locked, return null + null; + }// if + + function isLocked() + { + return (bool) $this->getColumnValue('locked'); + } + + +} + + +?> \ No newline at end of file Index: application/models/wiki/Revisions.class.php =================================================================== --- application/models/wiki/Revisions.class.php (revision 0) +++ application/models/wiki/Revisions.class.php (revision 0) @@ -0,0 +1,57 @@ + array('`page_id` = ? AND `project_id` = ?', $id, $project->getId()), 'order' => '`revision` DESC')); + } + + /** + * Get a specific revision + * + * @param mixed $page_id + * @param mixed $revision + * @return + */ + function getRevision($page_id, $revision = null) + { + if($revision === null){ + //If the user wants the latest page + $params = array( + 'conditions' => array('page_id = ?', $page_id), + 'order' => 'revision DESC'); + + } else { + $revision = (int) $revision; + //They want a specific revision + $params = array( + 'conditions' => array('page_id = ? AND revision = ?', $page_id, $revision)); + + } + //Return the revision object + return parent::findone($params); + } + +} + +?> \ No newline at end of file Index: application/models/wiki/Wiki.class.php =================================================================== --- application/models/wiki/Wiki.class.php (revision 0) +++ application/models/wiki/Wiki.class.php (revision 0) @@ -0,0 +1,92 @@ + array( + '`id` = ? AND `project_id` = ?', + $wiki_page_id, + $project->getId() + + ), + + ); + return parent::findOne($params); + } + + /** + * Get the index page of a project + * + * @param mixed Instance of project + * @return + */ + function getProjectIndex(Project $project) + { + $params = array( + 'conditions' => array( + 'project_id = ? AND project_index = 1', + $project->getId() + ) + ); + return parent::findone($params); + } + + /** + * Get the sidebar for a project + * + * @param mixed $project + * @return + */ + function getProjectSidebar($project = null) + { + $params = array( + 'conditions' => array( + 'project_id = ? AND project_sidebar = 1', + (instance_of($project, 'Project') ? $project->getId() : 0) + ) + ); + + return parent::findone($params); + } + + /** + * Get a list of pages for a project + * + * @param mixed $project + * @return + */ + function getPagesList(Project $project) + { + + $sql = 'SELECT p.id, r.name FROM ' . Wiki::instance()->getTableName() . ' AS p, ' . Revisions::instance()->getTableName() . ' AS r WHERE p.project_id = ' . $project->getId() . ' AND p.id = r.page_id AND r.revision = p.revision'; + $return = array(); + + foreach(((array) Db::executeAll($sql)) as $page){ + $return[] = array('name' => $page['name'], + 'view_url' => get_url('wiki', 'view', array('id' => $page['id'])) + ); + } + return $return; + } + +} + +?> \ No newline at end of file Index: application/controllers/WikiController.class.php =================================================================== --- application/controllers/WikiController.class.php (revision 0) +++ application/controllers/WikiController.class.php (revision 0) @@ -0,0 +1,527 @@ +addHelper('textile'); + $this->addHelper('wiki'); + } // __controller + + /** + * Wiki index + * + * @return void + */ + function index() + { + //Here we show them the default wiki page + $page = Wiki::getProjectIndex(active_project()); + + if(!($page instanceof WikiPage)){ + //There isn't a wiki page at the moment + //to prevent nasty errors, make a temp page + $page = new WikiPage; + //Make a revision for the page + $revision = $page->makeRevision(); + //Fill in the default content + $revision->setContent(lang('wiki default page content')); + //Set the name of the page + $revision->setName(lang('wiki default page name')); + } else { + //Fetch the latest revision of the page + $revision = $page->getLatestRevision(); + + if(!($revision instanceof Revision)){ + //If for some screwy reason there isn't a revision + flash_error(lang('wiki revision dnx')); + //Go to the dashboard + $this->redirectTo(); + }// if + } + + + tpl_assign('page', $page); + tpl_assign('revision', $revision); + $this->_load_sidebar(); + + }// index + + /** + * Delete a wiki page + * + * @todo Add password confirmation + * @return void + */ + function delete() + { + + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + //If the page doesn't exist + flash_error(lang('wiki page dnx')); + //redirect to wiki index + $this->redirectTo('wiki'); + }// if + + if(!$page->canDelete(logged_user())){ + //We will only let authorised users delete pages + flash_error(lang('no access permissions')); + $this->redirectToReferer(get_url('wiki')); + }// if + + //Check that the page isn't locked + //Yes, I know this could be handled by the canEdit(), but + //I'm sure most users would like to know a page has been locked + if($page->isLocked() && !$page->canUnlock(logged_user())){ + flash_error(lang('wiki page locked by', $page->getLockedByUser()->getUsername())); + $this->redirectToUrl($page->getViewUrl()); + }// if + + //We fetch the latest revision so that we can show the user page title & use it in app logs + $revision = $page->getLatestRevision(); + + if(array_var($_POST, 'deleteWikiPage')){ + try { + //Start the transaction + DB::beginWork(); + + //Delete the page and all of its revisions + $page->delete(); + + //Make a log of the deletion + ApplicationLogs::createLog($page, $page->getProject(), ApplicationLogs::ACTION_DELETE); + + //Commit changes + DB::commit(); + + //Tell the user they did a good job + flash_success(lang('success delete wiki page')); + + } catch (Exception $e){ + //Something went wrong, so delete any changes made to the DB + DB::rollback(); + //Show the user the error + flash_error(lang('failure delete wiki page', '(' . $e->getMessage() . ')')); + }// try + + //Redirect to the wiki index either way + $this->redirectTo('wiki'); + }// if + + tpl_assign('page', $page); + tpl_assign('revision', $revision); + + }// delete + + /** + * Loads the sidebar + * + * @return void + */ + function _load_sidebar() + { + //Quick error / XSS preventor + if(request_action() == '_load_sidebar'){ + flash_error('no access permissions'); + $this->redirectTo(); + }// if + + //Get Sidebar stuff + $sidebar_page = Wiki::getProjectSidebar(active_project()); + if($sidebar_page instanceof WikiPage){ + $sidebar_revision = $sidebar_page->getLatestRevision(); + } else { + //Make some default content which should help the user set stuff up + $sidebar_page = new WikiPage; + $sidebar_revision = new Revision; + $sidebar_revision->setName(lang('wiki default sidebar name')); + $sidebar_revision->setContent(lang('wiki default sidebar content')); + $all_pages = Wiki::getPagesList(active_project()); + tpl_assign('sidebar_links', $all_pages); + }// if + + tpl_assign('sidebar_page', $sidebar_page); + tpl_assign('sidebar_revision', $sidebar_revision); + + $this->setSidebar(get_template_path('view_sidebar', 'wiki')); + }// _load_sidebar + + /** + * View a wiki page + * + * @return void + */ + function view() + { + //Get the page that they want + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + //If page isn't an instanceof WikiPage then the page does not exist + flash_error(lang('wiki page dnx')); + //Redirect to wiki controller + $this->redirectTo('wiki'); + }// if + + if(!$page->canView(logged_user())){ + //If User can't view page + flash_error(lang('no access permissions')); + $this->redirectTo(get_url('wiki')); + } // if + + //Get the revision the user wants. defaults to latest + $revision = $page->getRevision(array_var($_GET, 'revision')); + + if(!($revision instanceof Revision)){ + //If for some screwy reason there isn't a revision + flash_error(lang('wiki revision dnx')); + //Go to the dashboard + $this->redirectTo('wiki'); + }// if + + tpl_assign('iscurrev', (!(bool) array_var($_GET, 'revision', false))); + tpl_assign('page', $page); + tpl_assign('revision', $revision); + $this->setTemplate('view'); + + //Get Sidebar stuff + $this->_load_sidebar(); + + } + + /** + * Add a wiki page + * + * @return void + */ + function add() + { + if(!WikiPage::canAdd(logged_user(), active_project())){ + flash_error(lang('no access permissions')); + $this->redirectTo('wiki'); + } //if + + if(false !== ($data = array_var($_POST, 'wiki', false))){ + //Make a new wiki page + $page = new WikiPage; + //Set the Id for this project + $page->setProjectId(active_project()->getId()); + $page->setProjectIndex((logged_user()->isMemberOfOwnerCompany() ? $data['project_index'] : 0)); + $page->setProjectSidebar((logged_user()->isMemberOfOwnerCompany() ? $data['project_sidebar'] : 0)); + + //Make a new revision of this page + $revision = $page->makeRevision(); + + //Check to see if we want to lock this page + if(isset($data['locked'])){ + + if($data['locked'] == 1 && $page->canLock(logged_user())){ + //If we want to lock this page and the user has permissions to lock it, and the page is not already locked + $page->setLocked(true); + $page->setLockedById(logged_user()->getId()); + $page->setLockedOn(DateTimeValueLib::now()); + }// if + + }// if + + //Set attributes from form + $revision->setFromAttributes($data); + + //Set user ID and project ID + $revision->setCreatedbyId(logged_user()->getId()); + + + try{ + //Start the db transaction + DB::beginWork(); + + //Save the page + $page->save(); + + //Make a log entry + ApplicationLogs::createLog($page, active_project(), ApplicationLogs::ACTION_ADD); + + //Add page tags + $page->setTagsFromCSV(); + + //Commit changed + DB::commit(); + + //Tell the user they made a page + flash_success(lang('success add wiki page')); + + //Redirect + $this->redirectToUrl($page->getViewUrl()); + + } catch (Exception $e){ + DB::rollback(); + tpl_assign('error', $e); + }//try + }// if + + if(!isset($page) || !($page instanceof WikiPage)){ + $page = new WikiPage; + $page->setProjectId(active_project()->getId()); + }// if + + tpl_assign('page', $page); + tpl_assign('revision', $page->makeRevision()); + $this->setTemplate('edit'); + + } // add + + /** + * Edit a wiki page + * + * @return void + */ + function edit() + { + + //Get the page from the url params + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + //If the page doesn't exist, redirect to wiki index + flash_error(lang('wiki page dnx')); + $this->redirectToReferer(get_url('wiki')); + }// if + + //Check that the page isn't locked + //Yes, I know this could be handled by the canEdit(), but + //I'm sure most users would like to know a page has been locked + if($page->isLocked() && !$page->canUnlock(logged_user())){ + flash_error(lang('wiki page locked by', $page->getLockedByUser()->getUsername())); + $this->redirectToUrl($page->getViewUrl()); + }// if + + //Check that the user can edit this entry + if(!$page->canEdit(logged_user())){ + flash_error(lang('no wiki page edit permissions')); + $this->redirectToUrl(($page->isProjectIndex() ? get_url('wiki') : $page->getViewUrl())); + }// if + + //Here we will edit a wiki page + if(null !== ($data = array_var($_POST, 'wiki'))){ + //If we have received data + + //Make a new revision + $revision = $page->makeRevision(); + $revision->setFromAttributes($data); + + $page->setProjectIndex($data['project_index']); + $page->setProjectSidebar($data['project_sidebar']); + + //Check to see if we want to lock this page + if(isset($data['locked'])){ + + if($data['locked'] == 1 && $page->canLock(logged_user()) && !$page->isLocked()){ + //If we want to lock this page and the user has permissions to lock it, and the page is not already locked + $page->setLocked(true); + $page->setLockedById(logged_user()->getId()); + $page->setLockedOn(DateTimeValueLib::now()); + } else if ($data['locked'] == 0 & $page->canUnlock(logged_user()) && $page->isLocked()){ + //Else if we want to unlock the page, and the user is allowed to, and the page is locked + $page->setLocked(false); + }// if + + }// if + + //Set the users ID + $revision->setCreatedById(logged_user()->getId()); + + try{ + + //Start the transaction + Db::beginWork(); + + //Save the page and create revision + //The page will make sure that the revision's project and page Id are correct + $page->save(); + + //Create a log of the edit + ApplicationLogs::createLog($page, active_project(), ApplicationLogs::ACTION_EDIT); + + //Set the tags + //$page->setTagsFromCSV($data['tags']); + + //Commit changes + Db::commit(); + + flash_success(lang('success edit wiki page')); + + //Redirect to the page we just created + $this->redirectToUrl($page->getViewUrl()); + + } catch (Exception $e){ + //Get rid of any Db changes we've made + Db::rollback(); + //Assign the problem to the template so we can tell the user + tpl_assign('error', $e); + }//try + } else if(array_var($_GET, 'revision')){ + //If we want to make a new revision based off a revision + $revision = $page->getRevision($_GET['revision']); + } else { + $revision = $page->getLatestRevision(); + }//if + + //Assign revision object + tpl_assign('revision', $revision); + //Assign the page object + tpl_assign('page', $page); + //Set the template + $this->setTemplate('edit'); + }// edit + + /** + * View the revision history of a page + * + * @return void + */ + function history() + { + //Here we will view the history of a wiki page + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + //If page DNX (Does not exist) + flash_error('wiki page dnx'); + $this->redirectTo('wiki'); + }// if + + if(!$page->canView(logged_user())){ + //If the user can't view a page, then they have no business looking at it's revisions :p + flash_error('no access permissions'); + //Redirect to dashboard + $this->redirectTo(); + }// if + + //Work out the page we are on + $pnum = (integer) array_var($_GET, 'page', 1); + if ($pnum < 0) { + $pnum = 1; + }// if + + //Get the revisions for this page + list($revisions, $pagination) = $page->paginateRevisions(array(), 30, $pnum); + + //Assign template variables + tpl_assign('page', $page); + tpl_assign('cur_revision', $page->getLatestRevision()); + tpl_assign('revisions', $revisions); + tpl_assign('pagination', $pagination); + + //Load the wiki sidebar + $this->_load_sidebar(); + + }// history + + /** + * Revert to a revision + * + * @return void + */ + function revert() + { + //Here we will rollback to a wiki page + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + flash_error(lang('wiki page dnx')); + $this->redirectTo('wiki'); + }// if + + if(!$page->canEdit(logged_user())){ + flash_error(lang('no access permissions')); + $this->redirectTo('wiki'); + }// if + + $old_revision = $page->getRevision(array_var($_GET, 'revision', -1)); + + if(!($old_revision instanceof Revision)){ + flash_error(lang('wiki page revision dnx')); + $this->redirectTo('wiki'); + }// if + + $new_revision = $page->makeRevision(); + + $new_revision->setContent($old_revision->getContent()); + $new_revision->setName($old_revision->getName()); + $new_revision->setLogMessage(lang('wiki page revision restored from', $old_revision->getRevision())); + + try{ + DB::beginWork(); + $page->save(); + DB::commit(); + flash_success(lang('success restore wiki page revision')); + $this->redirectToUrl($page->getViewUrl()); + } catch (Exception $e){ + DB::rollback(); + flash_error(lang('failure restore wiki page revision', $e->getMessage())); + $this->redirectTo('wiki'); + }// try + }// revert + + /** + * View the diff of two revisions + * + * @return void + */ + function diff() + { + $page = Wiki::getPageById(get_id(), active_project()); + + if(!($page instanceof WikiPage)){ + flash_error('wiki page dnx'); + $this->redirectTo('wiki'); + }// if + + if(!$page->canView(logged_user())){ + flash_error('no access permissions'); + $this->redirectTo('wiki'); + }// if + + $rev1 = $page->getRevision(array_var($_GET, 'rev1', -1)); + + $rev2 = $page->getRevision(array_var($_GET, 'rev2', -1)); + + if(!($rev1 instanceof Revision) || !($rev2 instanceof Revision)){ + flash_error(lang('wiki page revision dnx')); + $this->redirectTo('wiki'); + }// if + + if($rev1->getId() == $rev2->getId()){ + flash_error(lang('wiki no compare identical')); + $this->redirectToReferer($rev1->getViewUrl()); + } + + $this->addHelper('textile'); + + //Load text diff library + Env::useLibrary('diff'); + + $diff = new diff($rev1->getContent(), $rev2->getContent()); + + $output = new diff_renderer_inline(); + $output = trim($output = $output->render($diff)) == '' ? lang('wiki no visible changes') : $output; + + tpl_assign('diff', $output); + tpl_assign('page', $page); + tpl_assign('revision', $page->getLatestRevision()); + tpl_assign('rev1', $rev1); + tpl_assign('rev2', $rev2); + + }// diff +} + +?> \ No newline at end of file Index: application/views/wiki/view.php =================================================================== --- application/views/wiki/view.php (revision 0) +++ application/views/wiki/view.php (revision 0) @@ -0,0 +1,37 @@ +getRevision(), $revision->getName()) : $revision->getName())); + project_tabbed_navigation(PROJECT_TAB_WIKI); + project_crumbs(array( + array(lang('wiki'), get_url('wiki')), + array($revision->getName())) + ); + if ($page->canAdd(logged_user(), active_project())) { + add_page_action(lang('add wiki page'), $page->getAddUrl()); + } // if + if($page->canEdit(logged_user(), active_project()) && !$page->isNew() && (!$page->isLocked() || $page->canUnlock(logged_user()))){ + add_page_action(lang('edit wiki page'), $page->getEditUrl()); + + } // if + if(!$page->isNew()){ + add_page_action(lang('view page history'), $page->getViewHistoryUrl()); + } + + if($page->canDelete(logged_user(), active_project()) && !$page->isNew() && ((isset($iscurrev) && $iscurrev) || !isset($currev))){ + add_page_action(lang('delete wiki page'), $page->getDeleteUrl()); + } + + add_inline_css_to_page('.wikiPageLocked{float:right; font-weight:bolder; border: 2px solid #D15151; padding: 2px; color: #fff; background-color: #ED6E6E}'); +?> +getLocked()): ?> +
getLockedByUser()->getUserName()); ?>
+ + +
+getContent())); ?> +
\ No newline at end of file Index: application/views/wiki/history.php =================================================================== --- application/views/wiki/history.php (revision 0) +++ application/views/wiki/history.php (revision 0) @@ -0,0 +1,62 @@ +getName())); + project_tabbed_navigation(PROJECT_TAB_WIKI); + project_crumbs(array( + array(lang('wiki'), get_url('wiki')), + array($cur_revision->getName(), $page->getViewUrl()), + array(lang('view page history')))); + +$can_edit = $page->canEdit(logged_user()); + +?> + +
+
$page->getId(), 'page' => '#PAGE#'))) ?>
+
+ + 'hidden')) ?> + 'hidden')) ?> + getProjectId(), array('type' => 'hidden')) ?> + getId(), array('type' => 'hidden')) ?> + + + + + + + + + + + + + + + + + + + + + + +
  
getRevision() ?>getLogMessage() ?>getCreatedOn()) ?>getCreatedBy()->getUsername() ?> $revision->getRevision())) ?> $revision->getRevision())) ?> + getRevision() != $revision->getRevision()) : ?> + + +   + +
+ + +
+
$page->getId(), 'page' => '#PAGE#'))) ?>
+
+ +

+ Index: application/views/wiki/delete.php =================================================================== --- application/views/wiki/delete.php (revision 0) +++ application/views/wiki/delete.php (revision 0) @@ -0,0 +1,22 @@ +getName(), $page->getViewUrl()), + array(lang('delete wiki page')))); + +?> +
+ + +
getName()) ?>
+ +
+ + +
+ + +
Index: application/views/wiki/view_sidebar.php =================================================================== --- application/views/wiki/view_sidebar.php (revision 0) +++ application/views/wiki/view_sidebar.php (revision 0) @@ -0,0 +1,16 @@ +
+

getName() ?>

+
+ getContent())) ?> + isNew() && $sidebar_page->canEdit(logged_user())): ?>

+
+
+ +
+ +
+ \ No newline at end of file Index: application/views/wiki/edit.php =================================================================== --- application/views/wiki/edit.php (revision 0) +++ application/views/wiki/edit.php (revision 0) @@ -0,0 +1,66 @@ +isNew()){ + $project_crumbs[] = array($revision->getName(), $page->getViewUrl()); + $project_crumbs[] = array(lang('edit wiki page')); +} else { + $project_crumbs[] = array(lang('add wiki page')); +} + + + set_page_title(lang('wiki')); + project_tabbed_navigation(PROJECT_TAB_WIKI); + project_crumbs($project_crumbs); + +?> + +isNew()): ?> +
+ + + + + +
+ +getName(), array('class' => 'long', 'id' => 'wikiFormName')) ?> +
+
+ +getContent(), array('cols' => '400', 'class' => 'shot', 'id' => 'wikiFormContent')) ?> +
+
+ +isNew() ? 'Page created' : ''), array('class' => 'long', 'id' => 'wikiFormLog')) ?> +
+
+ +getProjectIndex(), lang('yes'), lang('no')) ?> +
+
+ +getProjectSidebar(), lang('yes'), lang('no')) ?> +
+canLock(logged_user())): ?> +
+ +getLocked(), lang('yes'), lang('no')) ?>
+getLocked()): ?> +
+ getLockedByUser()->getUsername(), format_datetime($page->getLockedOn())); ?>
+ +
+ +
+ + +isNew() ? lang('add wiki page') : lang('edit wiki page')) ?> + +
\ No newline at end of file Index: application/views/wiki/index.php =================================================================== --- application/views/wiki/index.php (revision 0) +++ application/views/wiki/index.php (revision 0) @@ -0,0 +1,35 @@ +getName()); + project_tabbed_navigation(PROJECT_TAB_WIKI); + project_crumbs(lang('wiki')); + if ($page->canAdd(logged_user(), active_project())) { + add_page_action(lang('add wiki page'), $page->getAddUrl()); + } // if + if($page->canEdit(logged_user(), active_project()) && !$page->isNew() && (!$page->isLocked() || $page->canUnlock(logged_user()))){ + add_page_action(lang('edit wiki page'), $page->getEditUrl()); + + } // if + if(!$page->isNew()){ + add_page_action(lang('view page history'), $page->getViewHistoryUrl()); + } + + if($page->canDelete(logged_user(), active_project()) && !$page->isNew() && ((isset($iscurrev) && $iscurrev) || !isset($currev))){ + add_page_action(lang('delete wiki page'), $page->getDeleteUrl()); + } + + add_inline_css_to_page('.wikiPageLocked{float:right; font-weight:bolder; border: 2px solid #D15151; padding: 2px; color: #fff; background-color: #ED6E6E}'); + + +?> +getLocked()): ?> +
getLockedByUser()->getUserName()); ?>
+ +
+getContent())); ?> +
\ No newline at end of file Index: application/views/wiki/diff.php =================================================================== --- application/views/wiki/diff.php (revision 0) +++ application/views/wiki/diff.php (revision 0) @@ -0,0 +1,55 @@ +getRevision() .' against revision ' . $rev2->getRevision()); + project_tabbed_navigation(PROJECT_TAB_WIKI); + project_crumbs(array( + array(lang('wiki'), get_url('wiki')), + array($revision->getName(), $page->getViewUrl()), + array(lang('wiki page diff'))) + ); + + + $css = <<< CSSSNP +.ins { + background-color: #dfd; + text-decoration: underline; +} + +.del { + background-color: #fdd; + text-decoration: line-through; +} +CSSSNP; + + add_inline_css_to_page($css); + + +?> + + + + +

 

+ + + + + + +
getRevision()) ?>
:getName() ?>
:getCreatedBy()->getUsername() ?>
:getCreatedOn()) ?>
:getLogMessage() ?>
+

 

+ + + + + + +
getRevision()) ?>
:getName() ?>
:getCreatedBy()->getUsername() ?>
:getCreatedOn()) ?>
:getLogMessage() ?>
+ + + Index: library/diff/engine.class.php =================================================================== --- library/diff/engine.class.php (revision 0) +++ library/diff/engine.class.php (revision 0) @@ -0,0 +1,458 @@ + 2, and some optimizations) are from + * Geoffrey T. Dairiki . The original PHP version of this + * code was written by him, and is used/adapted with his permission. + * + * @author Geoffrey T. Dairiki + * @package diff + * + * @access private + */ +class diff_engine +{ + function diff(&$from_lines, &$to_lines, $preserve_cr = true) + { + // Remove empty lines... + // If preserve_cr is true, we basically only change \r\n and bare \r to \n to get the same carriage returns for both files + // If it is false, we try to only use \n once per line and ommit all empty lines to be able to get a proper data diff + + if (is_array($from_lines)) { + $from_lines = implode("\n", $from_lines); + } + + if (is_array($to_lines)) { + $to_lines = implode("\n", $to_lines); + } + + if ($preserve_cr) { + $from_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $from_lines))); + $to_lines = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $to_lines))); + } else { + $from_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $from_lines)); + $to_lines = explode("\n", preg_replace('#[\n\r]+#', "\n", $to_lines)); + } + + $n_from = sizeof($from_lines); + $n_to = sizeof($to_lines); + + $this->xchanged = $this->ychanged = $this->xv = $this->yv = $this->xind = $this-> + yind = array(); + unset($this->seq, $this->in_seq, $this->lcs); + + // Skip leading common lines. + for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { + if ($from_lines[$skip] !== $to_lines[$skip]) { + break; + } + $this->xchanged[$skip] = $this->ychanged[$skip] = false; + } + + // Skip trailing common lines. + $xi = $n_from; + $yi = $n_to; + + for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { + if ($from_lines[$xi] !== $to_lines[$yi]) { + break; + } + $this->xchanged[$xi] = $this->ychanged[$yi] = false; + } + + // Ignore lines which do not exist in both files. + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $xhash[$from_lines[$xi]] = 1; + } + + for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { + $line = $to_lines[$yi]; + + if (($this->ychanged[$yi] = empty($xhash[$line]))) { + continue; + } + $yhash[$line] = 1; + $this->yv[] = $line; + $this->yind[] = $yi; + } + + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $line = $from_lines[$xi]; + + if (($this->xchanged[$xi] = empty($yhash[$line]))) { + continue; + } + $this->xv[] = $line; + $this->xind[] = $xi; + } + + // Find the LCS. + $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv)); + + // Merge edits when possible. + $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged); + $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged); + + // Compute the edit operations. + $edits = array(); + $xi = $yi = 0; + + while ($xi < $n_from || $yi < $n_to) { + // Skip matching "snake". + $copy = array(); + + while ($xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this-> + ychanged[$yi]) { + $copy[] = $from_lines[$xi++]; + $yi++; + } + + if ($copy) { + $edits[] = &new diff_op_copy($copy); + } + + // Find deletes & adds. + $delete = array(); + while ($xi < $n_from && $this->xchanged[$xi]) { + $delete[] = $from_lines[$xi++]; + } + + $add = array(); + while ($yi < $n_to && $this->ychanged[$yi]) { + $add[] = $to_lines[$yi++]; + } + + if ($delete && $add) { + $edits[] = &new diff_op_change($delete, $add); + } else + if ($delete) { + $edits[] = &new diff_op_delete($delete); + } else + if ($add) { + $edits[] = &new diff_op_add($add); + } + } + + return $edits; + } + + /** + * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF, + * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized segments. + * + * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an array of + * NCHUNKS+1 (X, Y) indexes giving the diving points between sub + * sequences. The first sub-sequence is contained in (X0, X1), (Y0, Y1), + * the second in (X1, X2), (Y1, Y2) and so on. Note that (X0, Y0) == + * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). + * + * This function assumes that the first lines of the specified portions of + * the two files do not match, and likewise that the last lines do not + * match. The caller must trim matching lines from the beginning and end + * of the portions it is going to specify. + */ + function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) + { + $flip = false; + + if ($xlim - $xoff > $ylim - $yoff) { + // Things seems faster (I'm not sure I understand why) when the shortest sequence is in X. + $flip = true; + list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim); + } + + if ($flip) { + for ($i = $ylim - 1; $i >= $yoff; $i--) { + $ymatches[$this->xv[$i]][] = $i; + } + } else { + for ($i = $ylim - 1; $i >= $yoff; $i--) { + $ymatches[$this->yv[$i]][] = $i; + } + } + + $this->lcs = 0; + $this->seq[0] = $yoff - 1; + $this->in_seq = array(); + $ymids[0] = array(); + + $numer = $xlim - $xoff + $nchunks - 1; + $x = $xoff; + + for ($chunk = 0; $chunk < $nchunks; $chunk++) { + if ($chunk > 0) { + for ($i = 0; $i <= $this->lcs; $i++) { + $ymids[$i][$chunk - 1] = $this->seq[$i]; + } + } + + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks); + + for (; $x < $x1; $x++) { + $line = $flip ? $this->yv[$x] : $this->xv[$x]; + if (empty($ymatches[$line])) { + continue; + } + $matches = $ymatches[$line]; + + foreach ($matches as $y) { + if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + $ymids[$k] = $ymids[$k - 1]; + break; + } + } + + // no reset() here + while (list($junk, $y) = each($matches)) { + if ($y > $this->seq[$k - 1]) { + // Optimization: this is a common case: next match is just replacing previous match. + $this->in_seq[$this->seq[$k]] = false; + $this->seq[$k] = $y; + $this->in_seq[$y] = 1; + } else + if (empty($this->in_seq[$y])) { + $k = $this->_lcs_pos($y); + $ymids[$k] = $ymids[$k - 1]; + } + } + } + } + + $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); + $ymid = $ymids[$this->lcs]; + + for ($n = 0; $n < $nchunks - 1; $n++) { + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); + $y1 = $ymid[$n] + 1; + $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); + } + $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); + + return array($this->lcs, $seps); + } + + function _lcs_pos($ypos) + { + $end = $this->lcs; + + if ($end == 0 || $ypos > $this->seq[$end]) { + $this->seq[++$this->lcs] = $ypos; + $this->in_seq[$ypos] = 1; + return $this->lcs; + } + + $beg = 1; + while ($beg < $end) { + $mid = (int)(($beg + $end) / 2); + if ($ypos > $this->seq[$mid]) { + $beg = $mid + 1; + } else { + $end = $mid; + } + } + + $this->in_seq[$this->seq[$end]] = false; + $this->seq[$end] = $ypos; + $this->in_seq[$ypos] = 1; + + return $end; + } + + /** + * Finds LCS of two sequences. + * + * The results are recorded in the vectors $this->{x,y}changed[], by + * storing a 1 in the element for each line that is an insertion or + * deletion (ie. is not in the LCS). + * + * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1. + * + * Note that XLIM, YLIM are exclusive bounds. All line numbers are + * origin-0 and discarded lines are not counted. + */ + function _compareseq($xoff, $xlim, $yoff, $ylim) + { + // Slide down the bottom initial diagonal. + while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) { + ++$xoff; + ++$yoff; + } + + // Slide up the top initial diagonal. + while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - + 1]) { + --$xlim; + --$ylim; + } + + if ($xoff == $xlim || $yoff == $ylim) { + $lcs = 0; + } else { + // This is ad hoc but seems to work well. + // $nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); + // $nchunks = max(2,min(8,(int)$nchunks)); + $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; + list($lcs, $seps) = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks); + } + + if ($lcs == 0) { + // X and Y sequences have no common subsequence: mark all changed. + while ($yoff < $ylim) { + $this->ychanged[$this->yind[$yoff++]] = 1; + } + + while ($xoff < $xlim) { + $this->xchanged[$this->xind[$xoff++]] = 1; + } + } else { + // Use the partitions to split this problem into subproblems. + reset($seps); + $pt1 = $seps[0]; + + while ($pt2 = next($seps)) { + $this->_compareseq($pt1[0], $pt2[0], $pt1[1], $pt2[1]); + $pt1 = $pt2; + } + } + } + + /** + * Adjusts inserts/deletes of identical lines to join changes as much as possible. + * + * We do something when a run of changed lines include a line at one end + * and has an excluded, identical line at the other. We are free to + * choose which identical line is included. 'compareseq' usually chooses + * the one at the beginning, but usually it is cleaner to consider the + * following identical line to be the "change". + * + * This is extracted verbatim from analyze.c (GNU diffutils-2.7). + */ + function _shift_boundaries($lines, &$changed, $other_changed) + { + $i = 0; + $j = 0; + + $len = sizeof($lines); + $other_len = sizeof($other_changed); + + while (1) { + // Scan forward to find the beginning of another run of + // changes. Also keep track of the corresponding point in the other file. + // + // Throughout this code, $i and $j are adjusted together so that + // the first $i elements of $changed and the first $j elements of + // $other_changed both contain the same number of zeros (unchanged lines). + // + // Furthermore, $j is always kept so that $j == $other_len or $other_changed[$j] == false. + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + + while ($i < $len && !$changed[$i]) { + $i++; + $j++; + + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + } + + if ($i == $len) { + break; + } + + $start = $i; + + // Find the end of this run of changes. + while (++$i < $len && $changed[$i]) { + continue; + } + + do { + // Record the length of this run of changes, so that we can later determine whether the run has grown. + $runlength = $i - $start; + + // Move the changed region back, so long as the previous unchanged line matches the last changed one. + // This merges with previous changed regions. + while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { + $changed[--$start] = 1; + $changed[--$i] = false; + + while ($start > 0 && $changed[$start - 1]) { + $start--; + } + + while ($other_changed[--$j]) { + continue; + } + } + + // Set CORRESPONDING to the end of the changed run, at the last point where it corresponds to a changed run in the + // other file. CORRESPONDING == LEN means no such point has been found. + $corresponding = $j < $other_len ? $i : $len; + + // Move the changed region forward, so long as the first changed line matches the following unchanged one. + // This merges with following changed regions. + // Do this second, so that if there are no merges, the changed region is moved forward as far as possible. + while ($i < $len && $lines[$start] == $lines[$i]) { + $changed[$start++] = false; + $changed[$i++] = 1; + + while ($i < $len && $changed[$i]) { + $i++; + } + + $j++; + if ($j < $other_len && $other_changed[$j]) { + $corresponding = $i; + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + } + } + } while ($runlength != $i - $start); + + // If possible, move the fully-merged run of changes back to a corresponding run in the other file. + while ($corresponding < $i) { + $changed[--$start] = 1; + $changed[--$i] = 0; + + while ($other_changed[--$j]) { + continue; + } + } + } + } +} + +?> \ No newline at end of file Index: library/diff/renderer.class.php =================================================================== --- library/diff/renderer.class.php (revision 0) +++ library/diff/renderer.class.php (revision 0) @@ -0,0 +1,791 @@ + $value) { + $v = '_' . $param; + if (isset($this->$v)) { + $this->$v = $value; + } + } + } + + /** + * Get any renderer parameters. + * + * @return array All parameters of this renderer object. + */ + function get_params() + { + $params = array(); + foreach (get_object_vars($this) as $k => $v) { + if ($k[0] == '_') { + $params[substr($k, 1)] = $v; + } + } + + return $params; + } + + /** + * Renders a diff. + * + * @param diff &$diff A diff object. + * + * @return string The formatted output. + */ + function render(&$diff) + { + $xi = $yi = 1; + $block = false; + $context = array(); + + // Create a new diff object if it is a 3-way diff + if (is_a($diff, 'diff3')) { + $diff3 = &$diff; + + $diff_1 = $diff3->get_original(); + $diff_2 = $diff3->merged_output(); + + unset($diff3); + + $diff = &new diff($diff_1, $diff_2); + } + + $nlead = $this->_leading_context_lines; + $ntrail = $this->_trailing_context_lines; + + $output = $this->_start_diff(); + $diffs = $diff->get_diff(); + + foreach ($diffs as $i => $edit) { + if (is_a($edit, 'diff_op_copy')) { + if (is_array($block)) { + $keep = ($i == sizeof($diffs) - 1) ? $ntrail : $nlead + $ntrail; + if (sizeof($edit->orig) <= $keep) { + $block[] = $edit; + } else { + if ($ntrail) { + $context = array_slice($edit->orig, 0, $ntrail); + $block[] = &new diff_op_copy($context); + } + + $output .= $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block); + $block = false; + } + } + $context = $edit->orig; + } else { + if (!is_array($block)) { + $context = array_slice($context, sizeof($context) - $nlead); + $x0 = $xi - sizeof($context); + $y0 = $yi - sizeof($context); + $block = array(); + + if ($context) { + $block[] = &new diff_op_copy($context); + } + } + $block[] = $edit; + } + + $xi += ($edit->orig) ? sizeof($edit->orig) : 0; + $yi += ($edit->final) ? sizeof($edit->final) : 0; + } + + if (is_array($block)) { + $output .= $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block); + } + + return $output . $this->_end_diff(); + } + + function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) + { + $output = $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen)); + + foreach ($edits as $edit) { + switch (get_class($edit)) { + case 'diff_op_copy': + $output .= $this->_context($edit->orig); + break; + + case 'diff_op_add': + $output .= $this->_added($edit->final); + break; + + case 'diff_op_delete': + $output .= $this->_deleted($edit->orig); + break; + + case 'diff_op_change': + $output .= $this->_changed($edit->orig, $edit->final); + break; + } + } + + return $output . $this->_end_block(); + } + + function _start_diff() + { + return ''; + } + + function _end_diff() + { + return ''; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) + { + if ($xlen > 1) { + $xbeg .= ',' . ($xbeg + $xlen - 1); + } + + if ($ylen > 1) { + $ybeg .= ',' . ($ybeg + $ylen - 1); + } + + return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; + } + + function _start_block($header) + { + return $header . "\n"; + } + + function _end_block() + { + return ''; + } + + function _lines($lines, $prefix = ' ') + { + return $prefix . implode("\n$prefix", $lines) . "\n"; + } + + function _context($lines) + { + return $this->_lines($lines, ' '); + } + + function _added($lines) + { + return $this->_lines($lines, '> '); + } + + function _deleted($lines) + { + return $this->_lines($lines, '< '); + } + + function _changed($orig, $final) + { + return $this->_deleted($orig) . "---\n" . $this->_added($final); + } + + /** + * Our function to get the diff + */ + function get_diff_content($diff) + { + return $this->render($diff); + } +} + +/** + * Renders a unified diff + * @package diff + */ +class diff_renderer_unified extends diff_renderer +{ + var $_leading_context_lines = 4; + var $_trailing_context_lines = 4; + + /** + * Our function to get the diff + */ + function get_diff_content($diff) + { + return nl2br($this->render($diff)); + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) + { + if ($xlen != 1) { + $xbeg .= ',' . $xlen; + } + + if ($ylen != 1) { + $ybeg .= ',' . $ylen; + } + return '
@@ -' . $xbeg . ' +' . $ybeg . + ' @@
'; + } + + function _context($lines) + { + return '
' . htmlspecialchars($this->_lines($lines, ' ')) .
+      '
'; + } + + function _added($lines) + { + return '
' . htmlspecialchars($this->_lines($lines, '+')) .
+      '
'; + } + + function _deleted($lines) + { + return '
' . htmlspecialchars($this->_lines($lines, '-')) .
+      '
'; + } + + function _changed($orig, $final) + { + return $this->_deleted($orig) . $this->_added($final); + } + + function _start_diff() + { + $start = '
'; + + return $start; + } + + function _end_diff() + { + return '
'; + } + + function _end_block() + { + return ''; + } +} + +/** + * "Inline" diff renderer. + * + * This class renders diffs in the Wiki-style "inline" format. + * + * @author Ciprian Popovici + * @package diff + */ +class diff_renderer_inline extends diff_renderer +{ + var $_leading_context_lines = 10000; + var $_trailing_context_lines = 10000; + + // Prefix and suffix for inserted text + var $_ins_prefix = ''; + var $_ins_suffix = ''; + + // Prefix and suffix for deleted text + var $_del_prefix = ''; + var $_del_suffix = ''; + + var $_block_head = ''; + + // What are we currently splitting on? Used to recurse to show word-level + var $_split_level = 'lines'; + + /** + * Our function to get the diff + */ + function get_diff_content($diff) + { + return '
' . nl2br($this->render($diff)) . '
'; + } + + function _start_diff() + { + return ''; + } + + function _end_diff() + { + return ''; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) + { + return $this->_block_head; + } + + function _start_block($header) + { + return $header; + } + + function _lines($lines, $prefix = ' ', $encode = true) + { + if ($encode) { + array_walk($lines, array(&$this, '_encode')); + } + + if ($this->_split_level == 'words') { + return implode('', $lines); + } else { + return implode("\n", $lines) . "\n"; + } + } + + function _added($lines) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_ins_prefix . $lines[0]; + $lines[sizeof($lines) - 1] .= $this->_ins_suffix; + return $this->_lines($lines, ' ', false); + } + + function _deleted($lines, $words = false) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_del_prefix . $lines[0]; + $lines[sizeof($lines) - 1] .= $this->_del_suffix; + return $this->_lines($lines, ' ', false); + } + + function _changed($orig, $final) + { + // If we've already split on words, don't try to do so again - just display. + if ($this->_split_level == 'words') { + $prefix = ''; + while ($orig[0] !== false && $final[0] !== false && substr($orig[0], 0, 1) == + ' ' && substr($final[0], 0, 1) == ' ') { + $prefix .= substr($orig[0], 0, 1); + $orig[0] = substr($orig[0], 1); + $final[0] = substr($final[0], 1); + } + + return $prefix . $this->_deleted($orig) . $this->_added($final); + } + + $text1 = implode("\n", $orig); + $text2 = implode("\n", $final); + + // Non-printing newline marker. + $nl = "\0"; + + // We want to split on word boundaries, but we need to preserve whitespace as well. + // Therefore we split on words, but include all blocks of whitespace in the wordlist. + $splitted_text_1 = $this->_split_on_words($text1, $nl); + $splitted_text_2 = $this->_split_on_words($text2, $nl); + + $diff = &new diff($splitted_text_1, $splitted_text_2); + unset($splitted_text_1, $splitted_text_2); + + // Get the diff in inline format. + $renderer = &new diff_renderer_inline(array_merge($this->get_params(), array('split_level' => + 'words'))); + + // Run the diff and get the output. + return str_replace($nl, "\n", $renderer->render($diff)) . "\n"; + } + + function _split_on_words($string, $newline_escape = "\n") + { + // Ignore \0; otherwise the while loop will never finish. + $string = str_replace("\0", '', $string); + + $words = array(); + $length = strlen($string); + $pos = 0; + + $tab_there = true; + while ($pos < $length) { + // Check for tabs... do not include them + if ($tab_there && substr($string, $pos, 1) === "\t") { + $words[] = "\t"; + $pos++; + + continue; + } else { + $tab_there = false; + } + + // Eat a word with any preceding whitespace. + $spaces = strspn(substr($string, $pos), " \n"); + $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); + $words[] = str_replace("\n", $newline_escape, substr($string, $pos, $spaces + $nextpos)); + $pos += $spaces + $nextpos; + } + + return $words; + } + + function _encode(&$string) + { + $string = htmlspecialchars($string); + } +} + +/** + * "raw" diff renderer. + * This class could be used to output a raw unified patch file + * + * @package diff + */ +class diff_renderer_raw extends diff_renderer +{ + var $_leading_context_lines = 4; + var $_trailing_context_lines = 4; + + /** + * Our function to get the diff + */ + function get_diff_content($diff) + { + return ''; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) + { + if ($xlen != 1) { + $xbeg .= ',' . $xlen; + } + + if ($ylen != 1) { + $ybeg .= ',' . $ylen; + } + return '@@ -' . $xbeg . ' +' . $ybeg . ' @@'; + } + + function _context($lines) + { + return $this->_lines($lines, ' '); + } + + function _added($lines) + { + return $this->_lines($lines, '+'); + } + + function _deleted($lines) + { + return $this->_lines($lines, '-'); + } + + function _changed($orig, $final) + { + return $this->_deleted($orig) . $this->_added($final); + } +} + +/** + * "chora (Horde)" diff renderer - similar style. + * This renderer class is a modified human_readable function from the Horde Framework. + * + * @package diff + */ +class diff_renderer_side_by_side extends diff_renderer +{ + var $_leading_context_lines = 3; + var $_trailing_context_lines = 3; + + var $lines = array(); + + // Hold the left and right columns of lines for change blocks. + var $cols; + var $state; + + var $data = false; + + /** + * Our function to get the diff + */ + function get_diff_content($diff) + { + global $user; + + $output = ''; + $output .= ' + + +'; + + $this->render($diff); + + // Is the diff empty? + if (!sizeof($this->lines)) { + $output .= ''; + } else { + // Iterate through every header block of changes + foreach ($this->lines as $header) { + $output .= ''; + + // Each header block consists of a number of changes (add, remove, change). + $current_context = ''; + + foreach ($header['contents'] as $change) { + if (!empty($current_context) && $change['type'] != 'empty') { + $line = $current_context; + $current_context = ''; + + $output .= ' + '; + } + + switch ($change['type']) { + case 'add': + $line = ''; + + foreach ($change['lines'] as $_line) { + $line .= htmlspecialchars($_line) . '
'; + } + + $output .= ''; + break; + + case 'remove': + $line = ''; + + foreach ($change['lines'] as $_line) { + $line .= htmlspecialchars($_line) . '
'; + } + + $output .= ''; + break; + + case 'empty': + $current_context .= htmlspecialchars($change['line']) . '
'; + break; + + case 'change': + // Pop the old/new stacks one by one, until both are empty. + $oldsize = sizeof($change['old']); + $newsize = sizeof($change['new']); + $left = $right = ''; + + for ($row = 0, $row_max = max($oldsize, $newsize); $row < $row_max; ++$row) { + $left .= isset($change['old'][$row]) ? htmlspecialchars($change['old'][$row]) : + ''; + $left .= '
'; + $right .= isset($change['new'][$row]) ? htmlspecialchars($change['new'][$row]) : + ''; + $right .= '
'; + } + + $output .= ''; + + if (!empty($left)) { + $output .= ''; + } else + if ($row < $oldsize) { + $output .= ''; + } else { + $output .= ''; + } + + if (!empty($right)) { + $output .= ''; + } else + if ($row < $newsize) { + $output .= ''; + } else { + $output .= ''; + } + + $output .= ''; + break; + } + } + + if (!empty($current_context)) { + $line = $current_context; + $current_context = ''; + + $output .= ''; + $output .= ''; + } + } + } + + $output .= '
+   ' . $user->lang['LINE_UNMODIFIED'] . ' +   ' . $user->lang['LINE_ADDED'] . ' +   ' . $user->lang['LINE_MODIFIED'] . ' +   ' . $user->lang['LINE_REMOVED'] . ' +
' . $user->lang['NO_VISIBLE_CHANGES'] . + '
' . $user->lang['LINE'] . ' ' . $header['oldline'] . + '' . $user->lang['LINE'] . ' ' . $header['newline'] . '
' . ((strlen($line)) ? $line :
+              ' ') . '
' . ((strlen($line)) ? $line : ' ') .
+              '
 
' . ((strlen
+                ($line)) ? $line : ' ') . '
' . ((strlen($line)) ? $line : ' ') .
+                '
 
' . $left . '
  
' . $right . '
  
' . ((strlen($line)) ? $line :
+            ' ') . '
' . ((strlen($line)) ? $line : ' ') .
+            '
'; + + return $output; + } + + function _start_diff() + { + $this->lines = array(); + + $this->data = false; + $this->cols = array(array(), array()); + $this->state = 'empty'; + + return ''; + } + + function _end_diff() + { + // Just flush any remaining entries in the columns stack. + switch ($this->state) { + case 'add': + $this->data['contents'][] = array('type' => 'add', 'lines' => $this->cols[0]); + break; + + case 'remove': + // We have some removal lines pending in our stack, so flush them. + $this->data['contents'][] = array('type' => 'remove', 'lines' => $this->cols[0]); + break; + + case 'change': + // We have both remove and addition lines, so this is a change block. + $this->data['contents'][] = array('type' => 'change', 'old' => $this->cols[0], + 'new' => $this->cols[1]); + break; + } + + if ($this->data !== false) { + $this->lines[] = $this->data; + } + + return ''; + } + + function _block_header($xbeg, $xlen, $ybeg, $ylen) + { + // Push any previous header information to the return stack. + if ($this->data !== false) { + $this->lines[] = $this->data; + } + + $this->data = array('type' => 'header', 'oldline' => $xbeg, 'newline' => $ybeg, + 'contents' => array()); + $this->state = 'dump'; + } + + function _added($lines) + { + array_walk($lines, array(&$this, '_perform_add')); + } + + function _perform_add($line) + { + if ($this->state == 'empty') { + return ''; + } + + // This is just an addition line. + if ($this->state == 'dump' || $this->state == 'add') { + // Start adding to the addition stack. + $this->cols[0][] = $line; + $this->state = 'add'; + } else { + // This is inside a change block, so start accumulating lines. + $this->state = 'change'; + $this->cols[1][] = $line; + } + } + + function _deleted($lines) + { + array_walk($lines, array(&$this, '_perform_delete')); + } + + function _perform_delete($line) + { + // This is a removal line. + $this->state = 'remove'; + $this->cols[0][] = $line; + } + + function _context($lines) + { + array_walk($lines, array(&$this, '_perform_context')); + } + + function _perform_context($line) + { + // An empty block with no action. + switch ($this->state) { + case 'add': + $this->data['contents'][] = array('type' => 'add', 'lines' => $this->cols[0]); + break; + + case 'remove': + // We have some removal lines pending in our stack, so flush them. + $this->data['contents'][] = array('type' => 'remove', 'lines' => $this->cols[0]); + break; + + case 'change': + // We have both remove and addition lines, so this is a change block. + $this->data['contents'][] = array('type' => 'change', 'old' => $this->cols[0], + 'new' => $this->cols[1]); + break; + } + + $this->cols = array(array(), array()); + $this->data['contents'][] = array('type' => 'empty', 'line' => $line); + $this->state = 'dump'; + } + + function _changed($orig, $final) + { + return $this->_deleted($orig) . $this->_added($final); + } + +} + +?> \ No newline at end of file Index: library/diff/diff.class.php =================================================================== --- library/diff/diff.class.php (revision 0) +++ library/diff/diff.class.php (revision 0) @@ -0,0 +1,701 @@ + + */ +class diff +{ + /** + * Array of changes. + * @var array + */ + var $_edits; + + /** + * Computes diffs between sequences of strings. + * + * @param array $from_lines An array of strings. Typically these are lines from a file. + * @param array $to_lines An array of strings. + */ + function __construct(&$from_content, &$to_content, $preserve_cr = true) + { + $diff_engine = &new diff_engine(); + $this->_edits = $diff_engine->diff($from_content, $to_content, $preserve_cr); + } + + /** + * Returns the array of differences. + */ + function get_diff() + { + return $this->_edits; + } + + /** + * Computes a reversed diff. + * + * Example: + * + * $diff = &new diff($lines1, $lines2); + * $rev = $diff->reverse(); + * + * + * @return diff A Diff object representing the inverse of the original diff. + * Note that we purposely don't return a reference here, since + * this essentially is a clone() method. + */ + function reverse() + { + if (version_compare(zend_version(), '2', '>')) { + $rev = clone ($this); + } else { + $rev = $this; + } + + $rev->_edits = array(); + + foreach ($this->_edits as $edit) { + $rev->_edits[] = $edit->reverse(); + } + + return $rev; + } + + /** + * Checks for an empty diff. + * + * @return boolean True if two sequences were identical. + */ + function is_empty() + { + foreach ($this->_edits as $edit) { + if (!is_a($edit, 'diff_op_copy')) { + return false; + } + } + return true; + } + + /** + * Computes the length of the Longest Common Subsequence (LCS). + * + * This is mostly for diagnostic purposes. + * + * @return integer The length of the LCS. + */ + function lcs() + { + $lcs = 0; + + foreach ($this->_edits as $edit) { + if (is_a($edit, 'diff_op_copy')) { + $lcs += sizeof($edit->orig); + } + } + return $lcs; + } + + /** + * Gets the original set of lines. + * + * This reconstructs the $from_lines parameter passed to the constructor. + * + * @return array The original sequence of strings. + */ + function get_original() + { + $lines = array(); + + foreach ($this->_edits as $edit) { + if ($edit->orig) { + array_splice($lines, sizeof($lines), 0, $edit->orig); + } + } + return $lines; + } + + /** + * Gets the final set of lines. + * + * This reconstructs the $to_lines parameter passed to the constructor. + * + * @return array The sequence of strings. + */ + function get_final() + { + $lines = array(); + + foreach ($this->_edits as $edit) { + if ($edit->final) { + array_splice($lines, sizeof($lines), 0, $edit->final); + } + } + return $lines; + } + + /** + * Removes trailing newlines from a line of text. This is meant to be used with array_walk(). + * + * @param string &$line The line to trim. + * @param integer $key The index of the line in the array. Not used. + */ + function trim_newlines(&$line, $key) + { + $line = str_replace(array("\n", "\r"), '', $line); + } + + /** + * Checks a diff for validity. + * + * This is here only for debugging purposes. + */ + function _check($from_lines, $to_lines) + { + if (serialize($from_lines) != serialize($this->get_original())) { + trigger_error("[diff] Reconstructed original doesn't match", E_USER_ERROR); + } + + if (serialize($to_lines) != serialize($this->get_final())) { + trigger_error("[diff] Reconstructed final doesn't match", E_USER_ERROR); + } + + $rev = $this->reverse(); + + if (serialize($to_lines) != serialize($rev->get_original())) { + trigger_error("[diff] Reversed original doesn't match", E_USER_ERROR); + } + + if (serialize($from_lines) != serialize($rev->get_final())) { + trigger_error("[diff] Reversed final doesn't match", E_USER_ERROR); + } + + $prevtype = null; + + foreach ($this->_edits as $edit) { + if ($prevtype == get_class($edit)) { + trigger_error("[diff] Edit sequence is non-optimal", E_USER_ERROR); + } + $prevtype = get_class($edit); + } + + return true; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + */ +class mapped_diff extends diff +{ + /** + * Computes a diff between sequences of strings. + * + * This can be used to compute things like case-insensitve diffs, or diffs + * which ignore changes in white-space. + * + * @param array $from_lines An array of strings. + * @param array $to_lines An array of strings. + * @param array $mapped_from_lines This array should have the same size number of elements as $from_lines. + * The elements in $mapped_from_lines and $mapped_to_lines are what is actually + * compared when computing the diff. + * @param array $mapped_to_lines This array should have the same number of elements as $to_lines. + */ + function mapped_diff(&$from_lines, &$to_lines, &$mapped_from_lines, &$mapped_to_lines) + { + if (sizeof($from_lines) != sizeof($mapped_from_lines) || sizeof($to_lines) != + sizeof($mapped_to_lines)) { + return false; + } + + parent::diff($mapped_from_lines, $mapped_to_lines); + + $xi = $yi = 0; + for ($i = 0; $i < sizeof($this->_edits); $i++) { + $orig = &$this->_edits[$i]->orig; + if (is_array($orig)) { + $orig = array_slice($from_lines, $xi, sizeof($orig)); + $xi += sizeof($orig); + } + + $final = &$this->_edits[$i]->final; + if (is_array($final)) { + $final = array_slice($to_lines, $yi, sizeof($final)); + $yi += sizeof($final); + } + } + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff_op +{ + var $orig; + var $final; + + function reverse() + { + trigger_error('[diff] Abstract method', E_USER_ERROR); + } + + function norig() + { + return ($this->orig) ? sizeof($this->orig) : 0; + } + + function nfinal() + { + return ($this->final) ? sizeof($this->final) : 0; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff_op_copy extends diff_op +{ + function diff_op_copy($orig, $final = false) + { + if (!is_array($final)) { + $final = $orig; + } + $this->orig = $orig; + $this->final = $final; + } + + function &reverse() + { + $reverse = &new diff_op_copy($this->final, $this->orig); + return $reverse; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff_op_delete extends diff_op +{ + function diff_op_delete($lines) + { + $this->orig = $lines; + $this->final = false; + } + + function &reverse() + { + $reverse = &new diff_op_add($this->orig); + return $reverse; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff_op_add extends diff_op +{ + function diff_op_add($lines) + { + $this->final = $lines; + $this->orig = false; + } + + function &reverse() + { + $reverse = &new diff_op_delete($this->final); + return $reverse; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff_op_change extends diff_op +{ + function diff_op_change($orig, $final) + { + $this->orig = $orig; + $this->final = $final; + } + + function &reverse() + { + $reverse = &new diff_op_change($this->final, $this->orig); + return $reverse; + } +} + + +/** + * A class for computing three way diffs. + * + * @package diff + * @author Geoffrey T. Dairiki + */ +class diff3 extends diff +{ + /** + * Conflict counter. + * @var integer + */ + var $_conflicting_blocks = 0; + + /** + * Computes diff between 3 sequences of strings. + * + * @param array $orig The original lines to use. + * @param array $final1 The first version to compare to. + * @param array $final2 The second version to compare to. + */ + function diff3(&$orig, &$final1, &$final2) + { + $diff_engine = &new diff_engine(); + + $diff_1 = $diff_engine->diff($orig, $final1); + $diff_2 = $diff_engine->diff($orig, $final2); + + unset($engine); + + $this->_edits = $this->_diff3($diff_1, $diff_2); + } + + /** + * Return merged output + * + * @param string $label1 the cvs file version/label from the original set of lines + * @param string $label2 the cvs file version/label from the new set of lines + * @param string $label_sep the explanation between label1 and label2 - more of a helper for the user + * @param bool $get_conflicts if set to true only the number of conflicts is returned + * @param bool $merge_new if set to true the merged output will have the new file contents on a conflicting merge + * + * @return mixed the merged output + */ + function merged_output($label1 = 'CURRENT_FILE', $label2 = 'NEW_FILE', $label_sep = + 'DIFF_SEP_EXPLAIN', $get_conflicts = false, $merge_new = false) + { + global $user; + + if ($get_conflicts) { + foreach ($this->_edits as $edit) { + if ($edit->is_conflict()) { + $this->_conflicting_blocks++; + } + } + + return $this->_conflicting_blocks; + } + + $label1 = (!empty($user->lang[$label1])) ? $user->lang[$label1] : $label1; + $label2 = (!empty($user->lang[$label2])) ? $user->lang[$label2] : $label2; + $label_sep = (!empty($user->lang[$label_sep])) ? $user->lang[$label_sep] : $label_sep; + + $lines = array(); + + foreach ($this->_edits as $edit) { + if ($edit->is_conflict()) { + if (!$merge_new) { + $lines = array_merge($lines, array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), + $edit->final1, array('=======' . ($label_sep ? ' ' . $label_sep : '')), $edit-> + final2, array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); + } else { + $lines = array_merge($lines, $edit->final1); + } + $this->_conflicting_blocks++; + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + * Merge the output and use the new file code for conflicts + */ + function merged_new_output() + { + $lines = array(); + + foreach ($this->_edits as $edit) { + if ($edit->is_conflict()) { + $lines = array_merge($lines, $edit->final2); + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + * Merge the output and use the original file code for conflicts + */ + function merged_orig_output() + { + $lines = array(); + + foreach ($this->_edits as $edit) { + if ($edit->is_conflict()) { + $lines = array_merge($lines, $edit->final1); + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + * Get conflicting block(s) + */ + function get_conflicts() + { + $conflicts = array(); + + foreach ($this->_edits as $edit) { + if ($edit->is_conflict()) { + $conflicts[] = array($edit->final1, $edit->final2); + } + } + + return $conflicts; + } + + /** + * @access private + */ + function _diff3(&$edits1, &$edits2) + { + $edits = array(); + $bb = &new diff3_block_builder(); + + $e1 = current($edits1); + $e2 = current($edits2); + + while ($e1 || $e2) { + if ($e1 && $e2 && is_a($e1, 'diff_op_copy') && is_a($e2, 'diff_op_copy')) { + // We have copy blocks from both diffs. This is the (only) time we want to emit a diff3 copy block. + // Flush current diff3 diff block, if any. + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + $ncopy = min($e1->norig(), $e2->norig()); + $edits[] = &new diff3_op_copy(array_slice($e1->orig, 0, $ncopy)); + + if ($e1->norig() > $ncopy) { + array_splice($e1->orig, 0, $ncopy); + array_splice($e1->final, 0, $ncopy); + } else { + $e1 = next($edits1); + } + + if ($e2->norig() > $ncopy) { + array_splice($e2->orig, 0, $ncopy); + array_splice($e2->final, 0, $ncopy); + } else { + $e2 = next($edits2); + } + } else { + if ($e1 && $e2) { + if ($e1->orig && $e2->orig) { + $norig = min($e1->norig(), $e2->norig()); + $orig = array_splice($e1->orig, 0, $norig); + array_splice($e2->orig, 0, $norig); + $bb->input($orig); + } else { + $norig = 0; + } + + if (is_a($e1, 'diff_op_copy')) { + $bb->out1(array_splice($e1->final, 0, $norig)); + } + + if (is_a($e2, 'diff_op_copy')) { + $bb->out2(array_splice($e2->final, 0, $norig)); + } + } + + if ($e1 && !$e1->orig) { + $bb->out1($e1->final); + $e1 = next($edits1); + } + + if ($e2 && !$e2->orig) { + $bb->out2($e2->final); + $e2 = next($edits2); + } + } + } + + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + return $edits; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff3_op +{ + function __construct($orig = false, $final1 = false, $final2 = false) + { + $this->orig = $orig ? $orig : array(); + $this->final1 = $final1 ? $final1 : array(); + $this->final2 = $final2 ? $final2 : array(); + } + + function merged() + { + if (!isset($this->_merged)) { + if ($this->final1 === $this->final2) { + $this->_merged = &$this->final1; + } else + if ($this->final1 === $this->orig) { + $this->_merged = &$this->final2; + } else + if ($this->final2 === $this->orig) { + $this->_merged = &$this->final1; + } else { + $this->_merged = false; + } + } + + return $this->_merged; + } + + function is_conflict() + { + return ($this->merged() === false) ? true : false; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff3_op_copy extends diff3_op +{ + function diff3_op_copy($lines = false) + { + $this->orig = $lines ? $lines : array(); + $this->final1 = &$this->orig; + $this->final2 = &$this->orig; + } + + function merged() + { + return $this->orig; + } + + function is_conflict() + { + return false; + } +} + +/** + * @package diff + * @author Geoffrey T. Dairiki + * + * @access private + */ +class diff3_block_builder +{ + function __construct() + { + $this->_init(); + } + + function input($lines) + { + if ($lines) { + $this->_append($this->orig, $lines); + } + } + + function out1($lines) + { + if ($lines) { + $this->_append($this->final1, $lines); + } + } + + function out2($lines) + { + if ($lines) { + $this->_append($this->final2, $lines); + } + } + + function is_empty() + { + return !$this->orig && !$this->final1 && !$this->final2; + } + + function finish() + { + if ($this->is_empty()) { + return false; + } else { + $edit = &new diff3_op($this->orig, $this->final1, $this->final2); + $this->_init(); + return $edit; + } + } + + function _init() + { + $this->orig = $this->final1 = $this->final2 = array(); + } + + function _append(&$array, $lines) + { + array_splice($array, sizeof($array), 0, $lines); + } +} + +?> \ No newline at end of file Index: public/install/installation/templates/sql/mysql_schema.php =================================================================== --- public/install/installation/templates/sql/mysql_schema.php (revision 154) +++ public/install/installation/templates/sql/mysql_schema.php (working copy) @@ -396,3 +396,28 @@ KEY `company_id` (`company_id`), KEY `last_login` (`last_login`) ) ENGINE=InnoDB ; + +CREATE TABLE `wiki_pages` ( + `id` int(10) unsigned NOT NULL auto_increment, + `revision` int(10) unsigned default NULL, + `project_id` int(10) unsigned default NULL, + `project_sidebar` tinyint(1) unsigned default '0', + `project_index` tinyint(1) unsigned default '0', + `locked` tinyint(1) default '0', + `locked_by_id` int(10) unsigned default NULL, + `locked_on` datetime default '0000-00-00 00:00:00', + PRIMARY KEY (`id`) +) TYPE=InnoDB ; + +CREATE TABLE `wiki_revisions` ( + `id` int(10) unsigned NOT NULL auto_increment, + `project_id` int(10) unsigned default NULL, + `page_id` int(10) unsigned default NULL, + `revision` tinyint(3) unsigned default NULL, + `name` varchar(50) NOT NULL, + `content` text NOT NULL, + `created_on` datetime default NULL, + `created_by_id` int(10) unsigned default NULL, + `log_message` varchar(255) default NULL, + PRIMARY KEY (`id`) +) TYPE=InnoDB ;