commit abb9a61d0ddc151151c4df4f45299a2b0002cb0f
Author: Hayden Smith
Date: Thu Jul 19 10:40:05 2007 +0000
Moved CMS module to open source path
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/cms/trunk@39000 467b73ca-7a2a-4603-9d3b-597d59a354a9
diff --git a/_config.php b/_config.php
new file mode 100644
index 00000000..99e92ca9
--- /dev/null
+++ b/_config.php
@@ -0,0 +1,19 @@
+ 'BatchProcess_Controller',
+ 'silverstripe/' => '->admin/',
+ 'cms/' => '->admin/',
+ 'admin/security/$Action/$ID/$OtherID' => 'SecurityAdmin',
+ 'admin/help/$Action/$ID' => 'CMSHelp',
+ 'admin/newsletter/$Action/$ID' => 'NewsletterAdmin',
+ 'admin/reports/$Action/$ID' => 'ReportAdmin',
+ 'admin/assets/$Action/$ID' => 'AssetAdmin',
+ 'admin/ReportField/$Action/$ID/$Type/$OtherID' => 'ReportField_Controller',
+ 'admin/bulkload/$Action/$ID/$OtherID' => 'BulkLoaderAdmin',
+ 'admin/$Action/$ID/$OtherID' => 'CMSMain',
+ 'unsubscribe/$Email/$MailingList' => 'Unsubscribe_Controller',
+ 'membercontrolpanel/$Email' => 'MemberControlPanel'
+));
+
+?>
diff --git a/code/AssetAdmin.php b/code/AssetAdmin.php
new file mode 100755
index 00000000..7e866aaf
--- /dev/null
+++ b/code/AssetAdmin.php
@@ -0,0 +1,660 @@
+currentPageID();
+ }
+
+ /**
+ * Return fake-ID "root" if no ID is found (needed to upload files into the root-folder)
+ */
+ public function currentPageID() {
+ if(isset($_REQUEST['ID']) && is_numeric($_REQUEST['ID'])) {
+ return $_REQUEST['ID'];
+ } elseif (is_numeric($this->urlParams['ID'])) {
+ return $this->urlParams['ID'];
+ } elseif(is_numeric(Session::get("{$this->class}.currentPage"))) {
+ return Session::get("{$this->class}.currentPage");
+ } else {
+ return "root";
+ }
+ }
+
+ /**
+ * Set up the controller, in particular, re-sync the File database with the assets folder./
+ */
+ function init() {
+ parent::init();
+
+ // needed for MemberTableField (Requirements not determined before Ajax-Call)
+ Requirements::javascript("sapphire/javascript/ComplexTableField.js");
+ Requirements::css("jsparty/greybox/greybox.css");
+ Requirements::css("sapphire/css/ComplexTableField.css");
+
+ Requirements::javascript("cms/javascript/AssetAdmin.js");
+ Requirements::javascript("cms/javascript/AssetAdmin_left.js");
+ Requirements::javascript("cms/javascript/AssetAdmin_right.js");
+
+ // Requirements::javascript('sapphire/javascript/TableListField.js');
+
+ // Include the right JS]
+ // Hayden: This didn't appear to be used at all
+ /*$fileList = new FileList("Form_EditForm_Files", null);
+ $fileList->setClick_AjaxLoad('admin/assets/getfile/', 'Form_SubForm');
+ $fileList->FieldHolder();*/
+
+ Requirements::javascript("jsparty/greybox/AmiJS.js");
+ Requirements::javascript("jsparty/greybox/greybox.js");
+ Requirements::css("jsparty/greybox/greybox.css");
+ }
+
+ /**
+ * Display the upload form. Returns an iframe tag that will show admin/assets/uploadiframe.
+ */
+ function getUploadIframe() {
+ return <<
+
+HTML;
+ }
+
+ function index() {
+ File::sync();
+ return array();
+ }
+
+ /**
+ * Show the content of the upload iframe. The form is specified by a template.
+ */
+ function uploadiframe() {
+ Requirements::clear();
+
+ Requirements::javascript("jsparty/prototype.js");
+ Requirements::javascript("jsparty/loader.js");
+ Requirements::javascript("jsparty/behaviour.js");
+ Requirements::javascript("jsparty/prototype_improvements.js");
+ Requirements::javascript("jsparty/layout_helpers.js");
+ Requirements::javascript("cms/javascript/LeftAndMain.js");
+ Requirements::javascript("jsparty/multifile/multifile.js");
+ Requirements::css("jsparty/multifile/multifile.css");
+ Requirements::css("cms/css/typography.css");
+ Requirements::css("cms/css/layout.css");
+ Requirements::css("cms/css/cms_left.css");
+ Requirements::css("cms/css/cms_right.css");
+
+ if(isset($data['ID']) && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
+ else $folder = singleton('Folder');
+
+ $canUpload = $folder->userCanEdit();
+
+ return array( 'CanUpload' => $canUpload );
+ }
+
+ /**
+ * Return the form object shown in the uploadiframe.
+ */
+ function UploadForm() {
+
+ return new Form($this,'UploadForm', new FieldSet(
+ new HiddenField("ID", "", $this->currentPageID()),
+ // needed because the button-action is triggered outside the iframe
+ new HiddenField("action_doUpload", "", "1"),
+ new FileField("Files[0]" , "Choose file "),
+ new LiteralField('UploadButton',"
+
+ "),
+ new LiteralField('MultifileCode',"
+ Files ready to upload:
+
+
+ ")
+ ), new FieldSet(
+ ));
+
+ }
+
+ /**
+ * This method processes the results of the UploadForm.
+ * It will save the uploaded files to /assets/ and create new File objects as required.
+ */
+ function doUpload($data, $form) {
+ foreach($data['Files'] as $param => $files) {
+ foreach($files as $key => $value) {
+ $processedFiles[$key][$param] = $value;
+ }
+ }
+
+ if($data['ID'] && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
+ else $folder = singleton('Folder');
+
+ $warnFiles = array();
+ $fileSizeWarnings = '';
+
+ foreach($processedFiles as $file) {
+ if($file['tmp_name']) {
+ // check that the file can be uploaded and isn't too large
+
+ $extensionIndex = strripos( $file['name'], '.' );
+ $extension = strtolower( substr( $file['name'], $extensionIndex + 1 ) );
+
+ if( $extensionIndex !== FALSE )
+ list( $maxSize, $warnSize ) = File::getMaxFileSize( $extension );
+ else
+ list( $maxSize, $warnSize ) = File::getMaxFileSize();
+
+ // check that the file is not too large or that the current user is an administrator
+ if( $this->can('AdminCMS') || ( File::allowedFileType( $extension ) && (!isset($maxsize) || $file['size'] < $maxSize)))
+ $newFiles[] = $folder->addUploadToFolder($file);
+ elseif( !File::allowedFileType( $extension ) ) {
+ $fileSizeWarnings .= "alert( 'Only administrators can upload $extension files.' );";
+ } else {
+ if( $file['size'] > 1048576 )
+ $fileSize = "" . ceil( $file['size'] / 1048576 ) . "MB";
+ elseif( $file['size'] > 1024 )
+ $fileSize = "" . ceil( $file['size'] / 1024 ) . "KB";
+ else
+ $fileSize = "" . ceil( $file['size'] ) . "B";
+
+
+ $fileSizeWarnings .= "alert( '\\'" . $file['name'] . "\\' is too large ($fileSize). Files of this type cannot be larger than $warnSize ' );";
+ }
+ }
+ }
+
+ if($newFiles) {
+ $numFiles = sizeof($newFiles);
+ $statusMessage = "Uploaded $numFiles files";
+ $status = "good";
+ } else {
+ $statusMessage = "There was nothing to upload";
+ $status = "";
+ }
+ echo <<
+ var form = parent.document.getElementById('Form_EditForm');
+ form.getPageFromServer(form.elements.ID.value);
+ parent.statusMessage("{$statusMessage}","{$status}");
+ $fileSizeWarnings
+ parent.document.getElementById('sitetree').getTreeNodeByIdx( "{$folder->ID}" ).getElementsByTagName('a')[0].className += ' contents';
+
+HTML;
+ }
+
+ /**
+ * Needs to be overridden to make sure an ID with value "0" is still valid (rootfolder)
+ */
+
+
+ /**
+ * Return the form that displays the details of a folder, including a file list and fields for editing the folder name.
+ */
+ function getEditForm($id) {
+ if($id && $id != "root") {
+ $record = DataObject::get_by_id("File", $id);
+ } else {
+ $record = singleton("Folder");
+ }
+
+ $fileList = new AssetTableField(
+ $this,
+ "Files",
+ "File",
+ array("Title" => "Title", "LinkedURL" => "Filename"),
+ ""
+ );
+ $fileList->setFolder($record);
+ $fileList->setPopupCaption("View/Edit Asset");
+
+ if($record) {
+ $nameField = ($id != "root") ? new TextField("Name") : new HiddenField("Name");
+ $fields = new FieldSet(
+ new HiddenField("Title"),
+ $nameField,
+ new TabSet("Root",
+ new Tab("Files",
+ $fileList
+ ),
+ new Tab("Details",
+ new ReadonlyField("URL"),
+ new ReadonlyField("ClassName", "Type"),
+ new ReadonlyField("Created", "First Uploaded"),
+ new ReadonlyField("LastEdited", "Last Updated")
+ ),
+ new Tab("Upload",
+ new LiteralField("UploadIframe",
+ $this->getUploadIframe()
+ )
+ )
+ ),
+ new HiddenField("ID")
+ );
+
+ $actions = new FieldSet();
+
+ if( $record->userCanEdit() ) {
+ $actions = new FieldSet(
+ new FormAction('deletemarked',"Delete files"),
+ new FormAction('movemarked',"Move files..."),
+ new FormAction('save',"Save")
+ );
+ }
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ if($record->ID) {
+ $form->loadDataFrom($record);
+ } else {
+ $form->loadDataFrom(array(
+ "ID" => "root",
+ "URL" => Director::absoluteBaseURL() . 'assets/',
+ ));
+ }
+
+ // @todo: These workflow features aren't really appropriate for all projects
+ if( Member::currentUser()->_isAdmin() && project() == 'mot' ) {
+ $fields->addFieldsToTab( 'Root.Workflow', new DropdownField("Owner", "Owner", Member::map() ) );
+ $fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanUse", "Content usable by") );
+ $fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanEdit", "Content modifiable by") );
+ }
+
+ if( !$record->userCanEdit() )
+ $form->makeReadonly();
+
+ return $form;
+
+ }
+ }
+
+ /**
+ * Returns the form used to specify options for the "move marked" action.
+ */
+ public function MoveMarkedOptionsForm() {
+ $folderDropdown = new TreeDropdownField("DestFolderID", "Move files to", "Folder");
+ $folderDropdown->setFilterFunction(create_function('$obj', 'return $obj->class == "Folder";'));
+
+ return new CMSActionOptionsForm($this, "MoveMarkedOptionsForm", new FieldSet(
+ new HiddenField("ID"),
+ new HiddenField("FileIDs"),
+ $folderDropdown
+ ),
+ new FieldSet(
+ new FormAction("movemarked", "Move marked files")
+ ));
+ }
+
+ /**
+ * Perform the "move marked" action.
+ * Called by ajax, with a JavaScript return.
+ */
+ public function movemarked() {
+ if($_REQUEST['DestFolderID'] && is_numeric($_REQUEST['DestFolderID'])) {
+ $destFolderID = $_REQUEST['DestFolderID'];
+ $fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
+ $numFiles = 0;
+
+ if($fileList != "''") {
+ $files = DataObject::get("File", "`File`.ID IN ($fileList)");
+ if($files) {
+ foreach($files as $file) {
+ $file->ParentID = $destFolderID;
+ $file->write();
+ $numFiles++;
+ }
+ } else {
+ user_error("No files in $fileList could be found!", E_USER_ERROR);
+ }
+ }
+
+ echo <<ParentID;
+
+ // $deleteList .= "\$('Form_EditForm_Files').removeById($file->ID);\n";
+ $file->delete();
+ $numFiles++;
+ }
+ if($brokenPages = Notifications::getItems("BrokenLink")) {
+ $brokenPageList = " These pages now have broken links:";
+ foreach($brokenPages as $brokenPage) {
+ $brokenPageList .= "" . $brokenPage->Breadcrumbs(3, true) . " ";
+ }
+ $brokenPageList .= "";
+ Notifications::notifyByEmail("BrokenLink", "Page_BrokenLinkEmail");
+ }
+
+ if( $folderID ) {
+ $remaining = DB::query("SELECT COUNT(*) FROM `File` WHERE `ParentID`=$folderID")->value();
+
+ if( !$remaining )
+ $deleteList = "Element.removeClassName(\$('sitetree').getTreeNodeByIdx( '$folderID' ).getElementsByTagName('a')[0],'contents');";
+ }
+
+ } else {
+ user_error("No files in $fileList could be found!", E_USER_ERROR);
+ }
+ }
+
+ echo <<getSubForm($this->urlParams['ID']))) {
+ return $this->getSubForm($this->urlParams['ID'])->formHtmlContent();
+ }
+ else return null;
+ }
+
+ /**
+ * Action handler for the save button on the file subform.
+ * Saves the file
+ */
+ public function savefile($data, $form) {
+ $record = DataObject::get_by_id("File", $data['ID']);
+ $form->saveInto($record);
+ $record->write();
+ $title = Convert::raw2js($record->Title);
+ $name = Convert::raw2js($record->Name);
+ echo <<setMarkingFilter("ClassName", "Folder");
+ $obj->markPartialTree();
+
+ if($p = $this->currentPage()) $obj->markToExpose($p);
+
+ // getChildrenAsUL is a flexible and complex way of traversing the tree
+
+ $siteTree = $obj->getChildrenAsUL("",
+
+ ' "ID\" class=\"$child->class" . $child->markingClasses() . ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
+
+ ' "Link(),0,-1), "show", $child->ID) . "\" class=\"" . ($child->hasChildren() ? " contents" : "") . "\" >" . $child->Title . " " ',
+
+ $this, true);
+
+
+ // Wrap the root if needs be.
+
+ $rootLink = $this->Link() . 'show/root';
+
+ if(!isset($rootID)) $siteTree = "";
+
+
+ return $siteTree;
+
+ }
+
+ /**
+ * Returns a subtree of items underneat the given folder.
+ */
+ public function getsubtree() {
+ $obj = DataObject::get_by_id("Folder", $_REQUEST['ID']);
+ $obj->setMarkingFilter("ClassName", "Folder");
+ $obj->markPartialTree();
+
+ $results = $obj->getChildrenAsUL("",
+
+ ' " ID\" class=\"$child->class" . $child->markingClasses() . ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
+
+ ' "Link(),0,-1), "show", $child->ID) . "\" >" . $child->Title . " " ',
+
+ $this, true);
+
+ return substr(trim($results), 4,-5);
+
+ }
+
+
+ //------------------------------------------------------------------------------------------//
+
+ // Data saving handlers
+
+ /**
+ * Add a new folder and return its details suitable for ajax.
+ */
+ public function addfolder() {
+ $parent = ($_REQUEST['ParentID'] && is_numeric($_REQUEST['ParentID'])) ? $_REQUEST['ParentID'] : 0;
+
+ if($parent) {
+ $parentObj = DataObject::get_by_id("File", $parent);
+ if(!$parentObj || !$parentObj->ID) $parent = 0;
+ }
+
+ $p = new Folder();
+ $p->ParentID = $parent;
+ $p->Title = "NewFolder";
+
+ $p->Name = "NewFolder";
+
+ // Get the folder to be created
+ if(isset($parentObj->ID)) $filename = $parentObj->FullPath . $p->Name;
+ else $filename = '../assets/' . $p->Name;
+
+ // Ensure uniqueness
+ $i = 2;
+ $baseFilename = $filename . '-';
+ while(file_exists($filename)) {
+ $filename = $baseFilename . $i;
+ $p->Name = $p->Title = basename($filename);
+ $i++;
+ }
+
+ // Actually create
+ mkdir($filename);
+ chmod($filename, 02775);
+
+ $p->write();
+
+
+ return $this->returnItemToUser($p);
+
+ }
+
+ /**
+ * Return the given tree item to the client.
+ * If called by ajax, this will be some javascript commands.
+ * Otherwise, it will redirect back.
+ */
+ public function returnItemToUser($p) {
+ if($_REQUEST['ajax']) {
+ $parentID = (int)$p->ParentID;
+ return <<ID, "$p->Title", "$p->class");
+
+ tree.getTreeNodeByIdx($parentID).appendTreeNode(newNode);
+
+ newNode.selectTreeNode();
+JS;
+
+ } else {
+
+ Director::redirectBack();
+
+ }
+ }
+
+ /**
+ * Delete a folder
+ */
+ public function deletefolder() {
+ $script = '';
+ $ids = split(' *, *', $_REQUEST['csvIDs']);
+
+ foreach($ids as $id) {
+
+ if(is_numeric($id)) {
+
+ $record = DataObject::get_by_id($this->stat('tree_class'), $id);
+
+if(!$record)
+
+ Debug::message( "Record appears to be null" );
+
+
+
+ /*if($record->hasMethod('BackLinkTracking')) {
+ $brokenPages = $record->BackLinkTracking();
+
+ foreach($brokenPages as $brokenPage) {
+
+ $brokenPageList .= "" . $brokenPage->Breadcrumbs(3, true) . " ";
+
+ $brokenPage->HasBrokenLink = true;
+
+ $notifications[$brokenPage->OwnerID][] = $brokenPage;
+
+ $brokenPage->write();
+
+ }
+
+ }*/
+
+ $record->delete();
+ $record->destroy();
+
+
+
+ // DataObject::delete_by_id($this->stat('tree_class'), $id);
+
+ $script .= $this->deleteTreeNodeJS($record);
+
+ }
+
+ }
+
+
+
+/*if($notifications) foreach($notifications as $memberID => $pages) {
+
+ $email = new Page_BrokenLinkEmail();
+
+ $email->populateTemplate(new ArrayData(array(
+
+ "Recipient" => DataObject::get_by_id("Member", $memberID),
+
+ "BrokenPages" => new DataObjectSet($pages),
+
+ )));
+
+ $email->debug();
+
+ $email->send();
+
+ }*/
+
+
+
+ $s = (sizeof($ids) > 1) ? "s" :"";
+
+ $message = sizeof($ids) . " folder$s deleted.";
+ //
+ if(isset($brokenPageList)) $message .= " The following pages now have broken links:" . addslashes($brokenPageList) . " Their owners have been emailed and they will fix up those pages.";
+ $script .= "statusMessage('$message');";
+ echo $script;
+ }
+
+ public function removefile(){
+ if($fileID = $this->urlParams['ID']){
+ $file = DataObject::get_by_id('File', $fileID);
+ $file->delete();
+ $file->destroy();
+
+ if(Director::is_ajax()) {
+ echo <<dataFieldByName('Title')->value = $form->dataFieldByName('Name')->value;
+
+ return parent::save($urlParams, $form);
+ }
+}
\ No newline at end of file
diff --git a/code/AssetTableField.php b/code/AssetTableField.php
new file mode 100755
index 00000000..b08d47fc
--- /dev/null
+++ b/code/AssetTableField.php
@@ -0,0 +1,130 @@
+sourceSort = "Title";
+ $this->Markable = true;
+ }
+
+ function setFolder($folder) {
+ $this->folder = $folder;
+ $this->sourceFilter .= ($this->sourceFilter) ? " AND " : "";
+ $this->sourceFilter .= " ParentID = '" . $folder->ID . "' AND ClassName <> 'Folder'";
+ }
+
+ function Folder() {
+ return $this->folder;
+ }
+
+ function sourceID() {
+ return $this->folder->ID;
+ }
+
+ function DetailForm() {
+ $ID = (isset($_REQUEST['ctf']['ID'])) ? Convert::raw2xml($_REQUEST['ctf']['ID']) : null;
+ $childID = (isset($_REQUEST['ctf']['childID'])) ? Convert::raw2xml($_REQUEST['ctf']['childID']) : null;
+ $childClass = (isset($_REQUEST['fieldName'])) ? Convert::raw2xml($_REQUEST['fieldName']) : null;
+ $methodName = (isset($_REQUEST['methodName'])) ? $_REQUEST['methodName'] : null;
+
+ if(!$childID) {
+ user_error("AssetTableField::DetailForm Please specify a valid ID");
+ return null;
+ }
+
+ if($childID) {
+ $childData = DataObject::get_by_id("File", $childID);
+ }
+
+ if(!$childData) {
+ user_error("AssetTableField::DetailForm No record found");
+ return null;
+ }
+
+ if($childData->ParentID) {
+ $folder = DataObject::get_by_id('File', $childData->ParentID );
+ } else {
+ $folder = singleton('Folder');
+ }
+
+ $urlLink = "";
+
+ $detailFormFields = new FieldSet(
+ new TabSet("BottomRoot",
+ new Tab("Main",
+ new TextField("Title"),
+ new TextField("Name", "Filename"),
+ new LiteralField("AbsoluteURL", $urlLink),
+ new ReadonlyField("FileType", "Type"),
+ new ReadonlyField("Size", "Size", $childData->getSize()),
+ new DropdownField("OwnerID", "Owner", Member::mapInCMSGroups( $folder->CanEdit() ) ),
+ new DateField_Disabled("Created", "First uploaded"),
+ new DateField_Disabled("LastEdited", "Last changed")
+ )
+ )
+ );
+
+ if(is_a($childData,'Image')) {
+ $big = $childData->URL;
+ $thumbnail = $childData->getFormattedImage('AssetLibraryPreview')->URL;
+
+ $detailFormFields->addFieldToTab("BottomRoot.Main",
+ new ReadonlyField("Dimensions"),
+ "Created"
+ );
+
+ $detailFormFields->addFieldToTab("BottomRoot",
+ new Tab("Image",
+ new LiteralField("ImageFull",
+ " "
+ )
+ ),
+ 'Main'
+ );
+ }
+
+ if($childData && $childData->hasMethod('BackLinkTracking')) {
+ $links = $childData->BackLinkTracking();
+ if($links->exists()) {
+ foreach($links as $link) {
+ $backlinks[] = "ID\">" . $link->Breadcrumbs(null,true). " ";
+ }
+ $backlinks = "The following pages link to this file:
" . implode("",$backlinks) . " ";
+ }
+ if(!isset($backlinks)) $backlinks = "
This file hasn't been linked to from any pages.
";
+ $detailFormFields->addFieldToTab("BottomRoot.Links", new LiteralField("Backlinks", $backlinks));
+ }
+
+ // the ID field confuses the Controller-logic in finding the right view for ReferencedField
+ $detailFormFields->removeByName('ID');
+ // add a namespaced ID instead thats "converted" by saveComplexTableField()
+ $detailFormFields->push(new HiddenField("ctf[childID]","",$childID));
+ $detailFormFields->push(new HiddenField("ctf[ClassName]","",$this->sourceClass));
+
+ $readonly = ($this->methodName == "show");
+ $form = new ComplexTableField_Popup($this, "DetailForm", $detailFormFields, $this->sourceClass, $readonly);
+
+ if (is_numeric($childID)) {
+ if ($methodName == "show" || $methodName == "edit") {
+ $form->loadDataFrom($childData);
+ }
+ }
+
+ if( !$folder->userCanEdit() || $methodName == "show") {
+ $form->makeReadonly();
+ }
+
+ return $form;
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/code/BulkLoader.php b/code/BulkLoader.php
new file mode 100755
index 00000000..f90e359c
--- /dev/null
+++ b/code/BulkLoader.php
@@ -0,0 +1,67 @@
+stat('title')) return $title;
+ else return $this->class;
+ }
+
+ /**
+ * Process every record in the file
+ * @param filename The name of the CSV file to process
+ * @param preview If true, we'll just output a summary of changes but not actually do anything
+ *
+ * @returns A DataObjectSet containing a list of all the reuslst
+ */
+ function processAll($filename, $preview = false) {
+ // TODO
+ // Get the first record out of the CSV and store it as headers
+ // Get each record out of the CSV
+ // Remap the record so that it's keyed by headers
+ // Pass it to $this->processRecord, and get the results
+ // Put the results inside an ArrayData and push that onto a DataObjectSet for returning
+ }
+
+
+ /*----------------------------------------------------------------------------------------
+ * Next, we have some abstract functions that let subclasses say what kind of batch operation they're
+ * going to do
+ *----------------------------------------------------------------------------------------
+ */
+
+
+ /**
+ * Return a FieldSet containing all the options for this form; this
+ * doesn't include the actual upload field itself
+ */
+ abstract function getOptionFields();
+
+ /**
+ * Process a single record from the CSV file.
+ * @param record An map of the CSV data, keyed by the header field
+ * @param preview
+ *
+ * @returns A 2 value array.
+ * - The first element should be "add", "edit" or "", depending on the operation performed in response to this record
+ * - The second element is a free-text string that can optionally provide some more information about what changes have
+ * been made
+ */
+ abstract function processRecord($record, $preview = false);
+
+ /*----------------------------------------------------------------------------------------
+ * Next, we have a library of helper functions (Brian to build as necessary)
+ *----------------------------------------------------------------------------------------
+ */
+
+}
+
+?>
\ No newline at end of file
diff --git a/code/BulkLoaderAdmin.php b/code/BulkLoaderAdmin.php
new file mode 100755
index 00000000..ba668a94
--- /dev/null
+++ b/code/BulkLoaderAdmin.php
@@ -0,0 +1,123 @@
+getOptionFields();
+ if(!$fields) $fields = new FieldSet();
+
+ $fields->push(new FileField("File", "CSV File"));
+ $fields->push(new HiddenField('LoaderClass', '', $loader->class));
+
+ return new Form($this, "EditForm",
+ $fields,
+ new FieldSet(
+ new FormAction('preview', "Preview")
+ )
+ );
+
+ }
+ }
+
+ public function preview() {
+ $className = $_REQUEST['LoaderClass'];
+ if(is_subclass_of($className, 'BulkLoader')) {
+ $loader = new $className();
+
+ $results = $loader->processAll($_FILES['File']['tmp_name'], false);
+
+ return $this->customise(array(
+ "Message" => "Press continue to load this data in",
+ "Results" => $results,
+ "ConfirmForm" => $this->getConfirmFormFor($loader, $file),
+ ))->renderWith("BulkLoaderAdmin_preview");
+ }
+ }
+
+ /**
+ * Generate a confirmation form for the given file/class
+ * Will copy the file to a suitable temporary location
+ * @param loader A BulkLoader object
+ * @param file The name of the temp file
+ */
+ public function getConfirmFormFor($loader, $file) {
+ $tmpFile = tempnam(TEMP_FOLDER,'batch-upload-');
+ copy($file,$tmpFile);
+
+ return new Form($this, "ConfirmForm", new FieldSet(
+ new HiddenField("File", "", $tmpFile),
+ new HiddenField("LoaderClass", "", $loader->class)
+ ), new FieldSet(
+ new FormAction('process', 'Confirm bulk load')
+ ));
+ }
+ /**
+ * Stub to return the form back after pressing the button.
+ */
+ public function ConfirmForm() {
+ $className = $_REQUEST['LoaderClass'];
+ return $this->getConfirmFormFor(new $className(), $_REQUEST['File']);
+ }
+
+ /**
+ * Process the data and display the final "finished" message
+ */
+ public function process() {
+ $className = $_REQUEST['LoaderClass'];
+ if(is_subclass_of($className, 'BulkLoader')) {
+ $loader = new $className();
+
+ $results = $loader->processAll($_REQUEST['Filename'], true);
+
+ return $this->customise(array(
+ "Message" => "This data has been loaded in",
+ "Results" => $results,
+ "ConfirmForm" => " ",
+ ))->renderWith("BulkLoaderAdmin_preview");
+ }
+ }
+
+}
+
+?>
diff --git a/code/CMSActionOptionsForm.php b/code/CMSActionOptionsForm.php
new file mode 100755
index 00000000..90e369bc
--- /dev/null
+++ b/code/CMSActionOptionsForm.php
@@ -0,0 +1,17 @@
+actions->First()->Name();
+ return "{$action}_options";
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/CMSHelp.php b/code/CMSHelp.php
new file mode 100755
index 00000000..3de78abb
--- /dev/null
+++ b/code/CMSHelp.php
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/code/CMSMain.php b/code/CMSMain.php
new file mode 100644
index 00000000..9e15d722
--- /dev/null
+++ b/code/CMSMain.php
@@ -0,0 +1,1132 @@
+ project() . "/css/editor.css",
+ "BaseURL" => Director::absoluteBaseURL(),
+ ));
+ }
+ }
+
+ //------------------------------------------------------------------------------------------//
+ // Main controllers
+
+ //------------------------------------------------------------------------------------------//
+ // Main UI components
+
+ /**
+ * Return the entire site tree as a nested set of ULs
+ */
+ public function SiteTreeAsUL() {
+ $this->generateDataTreeHints();
+ $this->generateTreeStylingJS();
+
+ return $this->getSiteTreeFor("SiteTree");
+ }
+
+ public function generateDataTreeHints() {
+ $classes = ClassInfo::subclassesFor( $this->stat('tree_class') );
+
+ $def['Root'] = array();
+
+ foreach($classes as $class) {
+ $obj = singleton($class);
+ if($obj instanceof HiddenClass) continue;
+
+ $allowedChildren = $obj->allowedChildren();
+ if($allowedChildren != "none") $def[$class]['allowedChildren'] = $allowedChildren;
+ $def[$class]['defaultChild'] = $obj->defaultChild();
+ $def[$class]['defaultParent'] = isset(DataObject::get_by_url($obj->defaultParent())->ID) ? DataObject::get_by_url($obj->defaultParent())->ID : null;
+
+ if(is_array($allowedChildren)) foreach($allowedChildren as $allowedChild) {
+ $def[$allowedChild]['allowedParents'][] = $class;
+ }
+
+ if($obj->stat('can_be_root')) {
+ $def['Root']['allowedChildren'][] = $class;
+ }
+ }
+
+ // Put data hints into a script tag at the top
+ Requirements::customScript("siteTreeHints = " . $this->jsDeclaration($def) . ";");
+ }
+
+ public function generateTreeStylingJS() {
+ $classes = ClassInfo::subclassesFor('SiteTree');
+ foreach($classes as $class) {
+ $obj = singleton($class);
+ if($obj instanceof HiddenClass) continue;
+ if($icon = $obj->stat('icon')) $iconInfo[$class] = $icon;
+ }
+ $iconInfo['BrokenLink'] = 'cms/images/treeicons/brokenlink';
+
+
+ $js = "var _TREE_ICONS = [];\n";
+
+
+ foreach($iconInfo as $class => $icon) {
+ // SiteTree::$icon can be set to array($icon, $option)
+ // $option can be "file" or "folder" to force the icon to always be the file or the folder form
+ $option = null;
+ if(is_array($icon)) list($icon, $option) = $icon;
+
+ $fileImage = ($option == "folder") ? $icon . '-openfolder.gif' : $icon . '-file.gif';
+ $openFolderImage = $icon . '-openfolder.gif';
+ if(!Director::fileExists($openFolderImage) || $option = "file") $openFolderImage = $fileImage;
+ $closedFolderImage = $icon . '-closedfolder.gif';
+ if(!Director::fileExists($closedFolderImage) || $option = "file") $closedFolderImage = $fileImage;
+
+ $js .= <<
$v) {
+ $parts[] = "$k : " . $this->jsDeclaration($v);
+ }
+ return " {\n " . implode(", \n", $parts) . " }\n";
+ } else {
+ foreach($array as $part) $parts[] = $this->jsDeclaration($part);
+ return " [ " . implode(", ", $parts) . " ]\n";
+ }
+ } else {
+ return "'" . addslashes($array) . "'";
+ }
+ }
+
+ /**
+ * Populates an array of classes in the CMS which allows the
+ * user to change the page type.
+ */
+ public function PageTypes() {
+ $classes = ClassInfo::getValidSubClasses();
+ array_shift($classes);
+ $result = new DataObjectSet();
+ $kill_ancestors[] = null;
+
+ // figure out if there are any classes we don't want to appear
+ foreach($classes as $class) {
+ $instance = singleton($class);
+
+ // do any of the progeny want to hide an ancestor?
+ if ($ancestor_to_hide = $instance->stat('hide_ancestor')){
+ // note for killing later
+ $kill_ancestors[] = $ancestor_to_hide;
+ }
+ }
+
+ // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to requirements.
+ if ($kill_ancestors) {
+ foreach ($kill_ancestors as $mark) {
+ // unset from $classes
+ unset($classes[$mark]);
+ }
+ }
+
+ foreach($classes as $class) {
+ $instance = singleton($class);
+ if($instance instanceof HiddenClass) continue;
+
+ if( !$instance->canCreate() ) continue;
+
+ // skip this type if it is restricted
+ if($instance->stat('need_permission') && !$this->can( singleton($class)->stat('need_permission') ) ) continue;
+
+ $addAction = $instance->uninherited('add_action', true);
+ if(!$addAction) $addAction = "a $class";
+
+ $result->push(new ArrayData(array(
+ "ClassName" => $class,
+ "AddAction" => "Create $addAction",
+ )));
+ }
+ return $result;
+ }
+
+ /**
+ * Get a databsae record to be managed by the CMS
+ */
+ public function getRecord($id) {
+
+ $treeClass = $this->stat('tree_class');
+
+ if($id && is_numeric($id)) {
+ $record = DataObject::get_one( $treeClass, "`$treeClass`.ID = $id");
+
+ if(!$record) {
+ // $record = Versioned::get_one_by_stage($treeClass, "Live", "`$treeClass`.ID = $id");
+ Versioned::reading_stage('Live');
+ singleton($treeClass)->flushCache();
+ $record = DataObject::get_one( $treeClass, "`$treeClass`.ID = $id");
+ if($record) {
+ $record->DeletedFromStage = true;
+ } else {
+ Versioned::reading_stage(null);
+ }
+ }
+ return $record;
+
+ } else if(substr($id,0,3) == 'new') {
+ return $this->getNewItem($id);
+ }
+ }
+
+ public function getEditForm($id) {
+ $record = $this->getRecord($id);
+
+ if($record) {
+ if($record->DeletedFromStage) $record->Status = "Removed from the draft site";
+
+ $fields = $record->getCMSFields($this);
+ $fields->push($idField = new HiddenField("ID"));
+ $fields->push($liveURLField = new HiddenField("LiveURLSegment"));
+ $fields->push($stageURLField = new HiddenField("StageURLSegment"));
+
+ /*if( substr($record->ID, 0, 3 ) == 'new' )*/
+ $fields->push(new HiddenField('Sort','', $record->Sort ));
+
+ $idField->setValue($id);
+
+ if($record->ID && is_numeric( $record->ID ) ) $liveURLField->setValue(DB::query("SELECT URLSegment FROM SiteTree_Live WHERE ID = $id")->value());
+ if(!$record->DeletedFromStage) $stageURLField->setValue($record->URLSegment);
+
+ // getAllCMSActions can be used to completely redefine the action list
+ if($record->hasMethod('getAllCMSActions')) {
+ $actions = $record->getAllCMSActions();
+ } else {
+ $actions = new FieldSet();
+
+ if($record->DeletedFromStage) {
+ if($record->can('CMSEdit')) {
+ $actions->push(new FormAction('revert','Restore'));
+ $actions->push(new FormAction('deletefromlive','Delete from the published site'));
+ }
+ } else {
+ if($record->hasMethod('getCMSActions')) {
+ $extraActions = $record->getCMSActions();
+ if($extraActions) foreach($extraActions as $action) $actions->push($action);
+ }
+
+ if($record->canEdit()) {
+ $actions->push(new FormAction('save','Save draft'));
+ }
+ }
+ }
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom($record);
+ $form->disableDefaultAction();
+
+ if(!$record->canEdit() || $record->DeletedFromStage) $form->makeReadonly();
+
+ return $form;
+ } else if($id) {
+ return new Form($this, "EditForm", new FieldSet(
+ new LabelField("This page doesn't exist")), new FieldSet());
+
+ }
+ }
+
+
+
+ //------------------------------------------------------------------------------------------//
+ // Data saving handlers
+
+
+ public function addpage() {
+ $className = $_REQUEST['PageType'] ? $_REQUEST['PageType'] : "Page";
+ $parent = $_REQUEST['ParentID'] ? $_REQUEST['ParentID'] : 0;
+ $suffix = $_REQUEST['Suffix'] ? "-" . $_REQUEST['Suffix'] : null;
+
+
+ if(is_numeric($parent)) $parentObj = DataObject::get_by_id("SiteTree", $parent);
+ if(!$parentObj || !$parentObj->ID) $parent = 0;
+
+ $p = $this->getNewItem("new-$className-$parent".$suffix );
+ // $p->write();
+
+ $p->CheckedPublicationDifferences = $p->AddedToStage = true;
+ return $this->returnItemToUser($p);
+ }
+
+ public function getNewItem($id, $setID = true) {
+ list($dummy, $className, $parentID, $suffix) = explode('-',$id);
+ $newItem = new $className();
+
+ if( !$suffix ) {
+ $sessionTag = "NewItems." . $parentID . "." . $className;
+ if(Session::get($sessionTag)) {
+ $suffix = '-' . Session::get($sessionTag);
+ Session::set($sessionTag, Session::get($sessionTag) + 1);
+ }
+ else
+ Session::set($sessionTag, 1);
+
+ $id = $id . $suffix;
+ }
+
+ $newItem->Title = "New $className";
+ $newItem->URLSegment = "new-" . strtolower($className);
+ $newItem->ClassName = $className;
+ $newItem->ParentID = $parentID;
+
+ if($newItem->fieldExists('Sort')) {
+ $newItem->Sort = DB::query("SELECT MAX(Sort) FROM SiteTree WHERE ParentID = '" . Convert::raw2sql($parentID) . "'")->value() + 1;
+ }
+
+ if( Member::currentUser() )
+ $newItem->OwnerID = Member::currentUser()->ID;
+
+ if($setID) $newItem->ID = $id;
+
+ return $newItem;
+ }
+
+ public function Link($action = null) {
+ return "admin/$action";
+ }
+
+ public function publish($urlParams, $form) {
+ $id = $_REQUEST['ID'];
+ $record = DataObject::get_by_id("SiteTree", $id);
+ $form->saveInto($record);
+ $this->performPublish($record);
+
+ return $this->tellBrowserAboutPublicationChange($record, "Published '$record->Title' successfully");
+ }
+
+ public function deletefromlive($urlParams, $form) {
+ $id = $_REQUEST['ID'];
+ Versioned::reading_stage('Live');
+ $record = DataObject::get_by_id("SiteTree", $id);
+
+ // before deleting the records, get the descendants of this tree
+ if($record) {
+ $descendantIDs = $record->getDescendantIDList('SiteTree');
+
+ // then delete them from the live site too
+ foreach( $descendantIDs as $descID )
+ if( $descendant = DataObject::get_by_id('SiteTree', $descID) ) {
+ $descendant->delete();
+ $descendantsRemoved++;
+ }
+
+ // delete the record
+ $record->delete();
+ }
+
+ Versioned::reading_stage('Stage');
+
+ if($descendantsRemoved) {
+ $descRemoved = " and $descendantsRemoved descendants";
+ }
+
+ $title = Convert::raw2js($record->Title);
+ FormResponse::add($this->deleteTreeNodeJS($record));
+ FormResponse::status_message("Deleted '$title'$descRemoved from live site", 'good');
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Actually perform the publication step
+ */
+ public function performPublish($record) {
+ $record->AssignedToID = 0;
+ $record->RequestedByID = 0;
+ $record->Status = "Published";
+ //$record->PublishedByID = Member::currentUser()->ID;
+ $record->write();
+ $record->publish("Stage", "Live");
+
+ // Fix the sort order for this page's siblings
+ DB::query("UPDATE SiteTree_Live
+ INNER JOIN SiteTree ON SiteTree_Live.ID = SiteTree.ID
+ SET SiteTree_Live.Sort = SiteTree.Sort
+ WHERE SiteTree_Live.ParentID = " . sprintf('%d', $record->ParentID));
+ }
+
+ public function revert($urlParams, $form) {
+ $id = $_REQUEST['ID'];
+
+ Versioned::reading_stage('Live');
+ $obj = DataObject::get_by_id("SiteTree", $id);
+ Versioned::reading_stage('Stage');
+ $obj->publish("Live", "Stage");
+
+ $title = Convert::raw2js($obj->Title);
+ FormResponse::get_page($id);
+ FormResponse::add("$('sitetree').setNodeTitle($id, '$title');");
+ FormResponse::status_message("Restored '$title' successfully",'good');
+
+ return FormResponse::respond();
+ }
+
+ public function delete($urlParams, $form) {
+ $id = $_REQUEST['ID'];
+ $record = DataObject::get_one("SiteTree", "SiteTree.ID = $id");
+ $record->delete();
+ Director::redirectBack();
+ }
+
+ //------------------------------------------------------------------------------------------//
+ // Workflow handlers
+
+ /**
+ * Send this page on to another user for review
+ */
+ function submit() {
+
+ $page = DataObject::get_by_id("SiteTree", $_REQUEST['ID']);
+ $recipient = DataObject::get_by_id("Member", $_REQUEST['RecipientID']);
+ if(!$recipient) user_error("CMSMain::submit() Can't find recipient #$_REQUEST[RecipientID]", E_USER_ERROR);
+
+ $comment = new WorkflowPageComment();
+ $comment->Comment = $_REQUEST['Message'];
+ $comment->PageID = $page->ID;
+ $comment->AuthorID = Member::currentUserID();
+ $comment->Recipient = $recipient;
+ $comment->Action = $_REQUEST['Status'];
+ $comment->write();
+
+ $emailData = $page->customise(array(
+ "Message" => $_REQUEST['Message'],
+ "Recipient" => $recipient,
+ "Sender" => Member::currentUser(),
+ "ApproveLink" => "admin/approve/$page->ID",
+ "EditLink" => "admin/show/$page->ID",
+ "StageLink" => "$page->URLSegment/?stage=Stage",
+ ));
+
+ $email = new Page_WorkflowSubmitEmail();
+ $email->populateTemplate($emailData);
+ $email->send();
+
+ $page->AssignedToID = $recipient->ID;
+ $page->RequestedByID = Member::currentUserID();
+ $page->Status = $_REQUEST['Status'];
+ $page->writeWithoutVersion();
+
+ FormResponse::status_message("Sent to $recipient->FirstName $recipient->Surname for approval.","good");
+
+ return FormResponse::respond();
+ }
+
+ function getpagemembers() {
+ $relationName = $_REQUEST['SecurityLevel'];
+ $pageID = $this->urlParams['ID'];
+ $page = DataObject::get_by_id('SiteTree',$pageID);
+ if($page) {
+ foreach($page->$relationName() as $editorGroup) $groupIDs[] = $editorGroup->ID;
+ if($groupIDs) {
+ $groupList = implode(", ", $groupIDs);
+ $members = DataObject::get("Member","","",
+ "INNER JOIN `Group_Members` ON `Group_Members`.MemberID = `Member`.ID AND `Group_Members`.GroupID IN ($groupList)");
+ }
+
+ if($members) {
+
+ if( $page->RequestedByID )
+ $members->shift( $page->RequestedBy() );
+
+ foreach($members as $editor) {
+ $options .= "ID\">$editor->FirstName $editor->Surname ($editor->Email) ";
+ }
+ } else {
+ $options = "(no-one available) ";
+ }
+
+ return <<Send to
+ $options
+HTML;
+ } else {
+ user_error("CMSMain::getpagemembers() Cannot find page #$pageID", E_USER_ERROR);
+ }
+ }
+
+ function getMembersByGroup() {
+
+ $group = DataObject::get_by_id("Group", $this->urlParams['ID']);
+ if($group){
+ $memberList = new MemberList('Users', $group);
+ $memberList->setController($this);
+ return $memberList->renderWith('MemberList');
+ }else{
+ return user_error("CMSMain::getpagemembers() Cannot find Group #$group->ID", E_USER_ERROR);
+ }
+
+ }
+
+ function addmember() {
+ SecurityAdmin::addmember($this->stat('subitem_class'));
+ }
+
+ function tasklist() {
+ $tasks = DataObject::get("Page", "AssignedToID = " . Member::currentUserID(), "Created DESC");
+ if($tasks) {
+ $data = new ArrayData(array(
+ "Tasks" => $tasks,
+ "Message" => "You have work to do on these " . $tasks->Count() . " pages.",
+ ));
+ } else {
+ $data = new ArrayData(array(
+ "Message" => "You have nothing assigned to you.",
+ ));
+ }
+ return $data->renderWith("TaskList");
+ }
+
+ function waitingon() {
+ $tasks = DataObject::get("Page", "RequestedByID = " . Member::currentUserID(), "Created DESC");
+ if($tasks) {
+ $data = new ArrayData(array(
+ "Tasks" => $tasks,
+ "Message" => "You are waiting on other people to work on these " . $tasks->Count() . " pages.",
+ ));
+ } else {
+ $data = new ArrayData(array(
+ "Message" => "You aren't waiting on anybody.",
+ ));
+ }
+ return $data->renderWith("WaitingOn");
+ }
+
+ function comments() {
+ if($this->urlParams['ID']) {
+ $comments = DataObject::get("WorkflowPageComment", "PageID = " . $this->urlParams['ID'], "Created DESC");
+ $data = new ArrayData(array(
+ "Comments" => $comments,
+ ));
+ return $data->renderWith("CommentList");
+ }
+ }
+
+ /**
+ * Return a dropdown for selecting reports
+ */
+ function ReportSelector() {
+ $reports = ClassInfo::subclassesFor("SideReport");
+
+ $options[""] = "(Choose a report)";
+ foreach($reports as $report) {
+ if($report != 'SideReport') $options[$report] = singleton($report)->title();
+ }
+ return new DropdownField("ReportSelector","Report",$options);
+ }
+ /**
+ * Get the content for a side report
+ */
+ function sidereport() {
+ $reportClass = $this->urlParams['ID'];
+ $report = new $reportClass();
+ return $report->getHTML();
+ }
+ /**
+ * Get the versions of the current page
+ */
+ function versions() {
+ $pageID = $this->urlParams['ID'];
+ $page = $this->getRecord($pageID);
+ if($page) {
+ $versions = $page->allVersions($_REQUEST['unpublished'] ? "" : "`SiteTree_versions`.WasPublished = 1");
+ return array(
+ 'Versions' => $versions,
+ );
+ } else {
+ return "Can't find page #$pageID";
+ }
+ }
+
+ /**
+ * Roll a page back to a previous version
+ */
+ function rollback() {
+ if($_REQUEST['Version']) {
+ $record = $this->performRollback($_REQUEST['ID'], $_REQUEST['Version']);
+ echo "Rolled back to version #$_REQUEST[Version]. New version number is #$record->Version";
+ } else {
+ $record = $this->performRollback($_REQUEST['ID'], "Live");
+ echo "Rolled back to published version. New version number is #$record->Version";
+ }
+ }
+
+ function unpublish() {
+ $SQL_id = Convert::raw2sql($_REQUEST['ID']);
+
+ $page = DataObject::get_by_id("SiteTree", $SQL_id);
+ $page->deleteFromStage('Live');
+ $page->flushCache();
+
+ $page = DataObject::get_by_id("SiteTree", $SQL_id);
+ $page->Status = "Unpublished";
+ $page->write();
+
+ return $this->tellBrowserAboutPublicationChange($page, "Removed '$page->Title' from the published site");
+ }
+
+
+ /**
+ * Return a few pieces of information about a change to a page
+ * - Send the new status message
+ * - Update the action buttons
+ * - Update the treenote
+ * - Send a status message
+ */
+ function tellBrowserAboutPublicationChange($page, $statusMessage) {
+
+ $JS_title = Convert::raw2js($page->TreeTitle());
+
+ $JS_stageURL = Convert::raw2js(DB::query("SELECT URLSegment FROM SiteTree WHERE ID = $page->ID")->value());
+ $JS_liveURL = Convert::raw2js(DB::query("SELECT URLSegment FROM SiteTree_Live WHERE ID = $page->ID")->value());
+ FormResponse::add($this->getActionUpdateJS($page));
+ FormResponse::update_status($page->Status);
+ FormResponse::add("\$('sitetree').setNodeTitle($page->ID, '$JS_title')");
+ FormResponse::status_message($statusMessage, 'good');
+ FormResponse::add("$('Form_EditForm').elements.StageURLSegment.value = '$JS_stageURL'");
+ FormResponse::add("$('Form_EditForm').elements.LiveURLSegment.value = '$JS_liveURL'");
+ FormResponse::add("$('Form_EditForm').notify('PagePublished', $('Form_EditForm').elements.ID.value);");
+
+ return FormResponse::respond();
+ }
+
+ function performRollback($id, $version) {
+ $record = DataObject::get_by_id($this->stat('tree_class'), $id);
+ $record->publish($version, "Stage", true);
+ $record->AssignedToID = 0;
+ $record->RequestedByID = 0;
+ $record->Status = "Saved (update)";
+ $record->writeWithoutVersion();
+ return $record;
+ }
+
+ function getversion() {
+ $id = $this->urlParams['ID'];
+ $version = $this->urlParams['OtherID'];
+ $record = Versioned::get_version("SiteTree", $id, $version);
+
+ if($record) {
+ $fields = $record->getCMSFields($this);
+ $fields->removeByName("Status");
+
+ $fields->push(new HiddenField("ID"));
+ $fields->push(new HiddenField("Version"));
+ $fields->insertBefore(new HeaderField("You are viewing version #$version, created " . $record->obj('LastEdited')->Ago()), "Root");
+
+ $actions = new FieldSet(
+ new FormAction("email", "Email"),
+ new FormAction("print", "Print"),
+ new FormAction("rollback", "Roll back to this version")
+ );
+
+ // encode the message to appear in the body of the email
+ $archiveURL = Director::absoluteBaseURL() . $record->URLSegment . '?archiveDate=' . $record->obj('LastEdited')->URLDate();
+ $archiveEmailMessage = urlencode( $this->customise( array( 'ArchiveDate' => $record->obj('LastEdited'), 'ArchiveURL' => $archiveURL ) )->renderWith( 'ViewArchivedEmail' ) );
+
+ $archiveEmailMessage = preg_replace( '/\+/', '%20', $archiveEmailMessage );
+
+ $fields->push( new HiddenField( 'ArchiveEmailMessage', '', $archiveEmailMessage ) );
+ $fields->push( new HiddenField( 'ArchiveEmailSubject', '', preg_replace( '/\+/', '%20', urlencode( 'Archived version of ' . $record->Title ) ) ) );
+ $fields->push( new HiddenField( 'ArchiveURL', '', $archiveURL ) );
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom($record);
+ $form->loadDataFrom(array(
+ "ID" => $id,
+ "Version" => $version,
+ ));
+ $form->makeReadonly();
+
+ $templateData = $this->customise(array(
+ "EditForm" => $form
+ ));
+
+ SSViewer::setOption('rewriteHashlinks', false);
+ $result = $templateData->renderWith($this->class . '_right');
+ $parts = split('?form[^>]*>', $result);
+ return $parts[sizeof($parts)-2];
+ }
+ }
+
+ function compareversions() {
+ $id = $this->urlParams['ID'];
+ $version1 = $_REQUEST['From'];
+ $version2 = $_REQUEST['To'];
+
+ if( $version1 > $version2 ) {
+ $toVersion = $version1;
+ $fromVersion = $version2;
+ } else {
+ $toVersion = $version2;
+ $fromVersion = $version1;
+ }
+
+ $page = DataObject::get_by_id("SiteTree", $id);
+ $record = $page->compareVersions($fromVersion, $toVersion);
+ if($record) {
+ $fields = $record->getCMSFields($this);
+ $fields->push(new HiddenField("ID"));
+ $fields->push(new HiddenField("Version"));
+ $fields->insertBefore(new HeaderField("You are comparing versions #$fromVersion and #$toVersion"), "Root");
+
+ $actions = new FieldSet();
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom($record);
+ $form->loadDataFrom(array(
+ "ID" => $id,
+ "Version" => $version,
+ ));
+ $form->makeReadonly();
+ foreach($form->Fields()->dataFields() as $field) {
+ $field->dontEscape = true;
+ }
+
+ return $this->sendFormToBrowser(array(
+ "EditForm" => $form
+ ));
+ }
+ }
+
+ function sendFormToBrowser($templateData) {
+ if(Director::is_ajax()) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ $result = $this->customise($templateData)->renderWith($this->class . '_right');
+ $parts = split('?form[^>]*>', $result);
+ return $parts[sizeof($parts)-2];
+ } else {
+ return array(
+ "Right" => $this->customise($templateData)->renderWith($this->class . '_right'),
+ );
+ }
+ }
+
+ function dialog() {
+ Requirements::clear();
+
+ $buttons = new DataObjectSet;
+ if($_REQUEST['Buttons']) foreach($_REQUEST['Buttons'] as $button) {
+ list($name, $title) = explode(',',$button,2);
+ $buttons->push(new ArrayData(array(
+ "Name" => $name,
+ "Title" => $title,
+ )));
+ }
+
+ return array(
+ "Message" => htmlentities($_REQUEST['Message']),
+ "Buttons" => $buttons,
+ "Modal" => $_REQUEST['Modal'] ? true : false,
+ );
+ }
+
+ function savedialog() {
+ Requirements::clear();
+ Requirements::css('cms/css/dialog.css');
+ Requirements::javascript('jsparty/prototype.js');
+ Requirements::javascript('jsparty/behaviour.js');
+ Requirements::javascript('jsparty/prototype_improvements.js');
+ Requirements::javascript('cms/javascript/dialog.js');
+
+ $message = "You have unsaved changes. Would you like to save them?";
+ $buttons = "Save changes Discard changes Stay on this page ";
+
+ return $this->customise( array(
+ 'Message' => $message,
+ 'Buttons' => $buttons,
+ 'DialogType' => 'alert'
+ ))->renderWith( 'Dialog' );
+ }
+
+ function canceldraftchangesdialog() {
+ Requirements::clear();
+ Requirements::css('cms/css/dialog.css');
+ Requirements::javascript('jsparty/prototype.js');
+ Requirements::javascript('jsparty/behaviour.js');
+ Requirements::javascript('jsparty/prototype_improvements.js');
+ Requirements::javascript('cms/javascript/dialog.js');
+
+ $message = "Do you really want to copy the published content to the stage site?";
+ $buttons = "OK Cancel ";
+
+ return $this->customise( array(
+ 'Message' => $message,
+ 'Buttons' => $buttons,
+ 'DialogType' => 'alert'
+ ))->renderWith('Dialog');
+ }
+
+ /**
+ * Delete a number of items.
+ * This code supports notification
+ */
+ public function deleteitems() {
+ // This method can't be called without ajax.
+ if(!Director::is_ajax()) {
+ Director::redirectBack();
+ return;
+ }
+
+ $ids = split(' *, *', $_REQUEST['csvIDs']);
+
+ $notifications = array();
+
+ $idList = array();
+
+ // make sure all the ids are numeric.
+ // Add all the children to the list of IDs if they are missing
+ foreach($ids as $id) {
+ $brokenPageList = '';
+ if(is_numeric($id)) {
+ $record = DataObject::get_by_id($this->stat('tree_class'), $id);
+
+ // if(!$record) Debug::message( "Can't find record #$id" );
+
+ if($record) {
+
+ // add all the children for this record if they are not already in the list
+ // this check is a little slower but will prevent circular dependencies
+ // (should they exist, which they probably shouldn't) from causing
+ // the function to not terminate
+ $children = $record->AllChildren();
+
+ if( $children )
+ foreach( $children as $child )
+ if( array_search( $child->ID, $ids ) !== FALSE )
+ $ids[] = $child->ID;
+
+ if($record->hasMethod('BackLinkTracking')) {
+ $brokenPages = $record->BackLinkTracking();
+ foreach($brokenPages as $brokenPage) {
+ $brokenPageList .= "" . $brokenPage->Breadcrumbs(3, true) . " ";
+ $brokenPage->HasBrokenLink = true;
+ $notifications[$brokenPage->OwnerID][] = $brokenPage;
+ $brokenPage->writeWithoutVersion();
+ }
+ }
+
+ $record->delete();
+ $record->destroy();
+
+ // DataObject::delete_by_id($this->stat('tree_class'), $id);
+ $record->CheckedPublicationDifferences = $record->DeletedFromStage = true;
+
+ // check to see if the record exists on the live site, if it doesn't remove the tree node
+ // $_REQUEST['showqueries'] = 1 ;
+ $liveRecord = Versioned::get_one_by_stage( $this->stat('tree_class'), 'Live', "`{$this->stat('tree_class')}`.`ID`={$id}");
+
+ if($liveRecord) {
+ $title = Convert::raw2js($record->TreeTitle());
+ FormResponse::add("$('sitetree').setNodeTitle($record->OldID, '$title');");
+ FormResponse::add("$('Form_EditForm').reloadIfSetTo($record->OldID);");
+ } else {
+ FormResponse::add("var node = $('sitetree').getTreeNodeByIdx('$id');");
+ FormResponse::add("if(node.parentTreeNode) node.parentTreeNode.removeTreeNode(node);");
+ FormResponse::add("$('Form_EditForm').reloadIfSetTo($record->OldID);");
+ }
+ }
+ }
+ }
+
+ if($notifications) foreach($notifications as $memberID => $pages) {
+ if(class_exists('Page_BrokenLinkEmail')) {
+ $email = new Page_BrokenLinkEmail();
+ $email->populateTemplate(new ArrayData(array(
+ "Recipient" => DataObject::get_by_id("Member", $memberID),
+ "BrokenPages" => new DataObjectSet($pages),
+ )));
+ $email->debug();
+ $email->send();
+ }
+ }
+
+ $s = sizeof($ids) > 1 ? "s" : "";
+ $message = sizeof($ids) . " page$s deleted.";
+ if($brokenPageList != '') {
+ $message .= " The following pages now have broken links:" . addslashes($brokenPageList) . " Their owners have been emailed and they will fix up those pages.";
+ }
+
+ FormResponse::status_message($message);
+
+ return FormResponse::respond();
+ }
+
+ function buildbrokenlinks() {
+ if($this->urlParams['ID']) {
+ $newPageSet[] = DataObject::get_by_id("Page", $this->urlParams['ID']);
+ } else {
+ $pages = DataObject::get("Page");
+ foreach($pages as $page) $newPageSet[] = $page;
+ $pages = null;
+ }
+
+ $content = new HtmlEditorField('Content');
+ $download = new HtmlEditorField('Download');
+
+ foreach($newPageSet as $i => $page) {
+ $page->HasBrokenLink = 0;
+ $page->HasBrokenFile = 0;
+
+ $lastUsage = (memory_get_usage() - $lastPoint);
+ $lastPoint = memory_get_usage();
+ $content->setValue($page->Content);
+ $content->saveInto($page);
+
+ $download->setValue($page->Download);
+ $download->saveInto($page);
+
+ echo "$page->Title (link:$page->HasBrokenLink, file:$page->HasBrokenFile)";
+
+ $page->writeWithoutVersion();
+ $page->destroy();
+ $newPageSet[$i] = null;
+ }
+ }
+
+ function AddPageOptionsForm() {
+ $pageTypes = array();
+
+ foreach( $this->PageTypes() as $arrayData ) {
+ $pageTypes[$arrayData->getField('ClassName')] = $arrayData->getField('AddAction');
+ }
+
+ return new Form($this, "AddPageOptionsForm", new FieldSet(
+ new HiddenField("ParentID"),
+ new DropdownField("PageType", "", $pageTypes)
+ // "Page to copy" => new TreeDropdownField("DuplicateSection", "", "SiteTree"),
+ ),
+ new FieldSet(
+ new FormAction("addpage", "Go")
+ ));
+ }
+
+ /**
+ * Helper function to get page count
+ */
+ function getpagecount() {
+ ini_set('max_execution_time',300);
+ $excludePages = split(" *, *", $_GET['exclude']);
+
+ $pages = DataObject::get("SiteTree", "ParentID = 0");
+ foreach($pages as $page) $pageArr[] = $page;
+
+ while(list($i,$page) = each($pageArr)) {
+ if(!in_array($page->URLSegment, $excludePages)) {
+ if($children = $page->AllChildren()) {
+ foreach($children as $child) $pageArr[] = $child;
+ }
+
+
+ if(!$_GET['onlywithcontent'] || strlen(Convert::xml2raw($page->Content)) > 100) {
+ echo " " . $page->Breadcrumbs(null, true) . " ";
+ $count++;
+ } else {
+ echo "" . $page->Breadcrumbs(null, true) . " - no content ";
+ }
+
+ }
+ }
+
+ echo "Total pages: $count
";
+ }
+
+ function publishall() {
+ ini_set("memory_limit","100M");
+ ini_set('max_execution_time', 300);
+
+ if(isset($_POST['confirm'])) {
+ $pages = DataObject::get("SiteTree");
+ $count = 0;
+ foreach($pages as $page) {
+ $this->performPublish($page);
+ $page->destroy();
+ unset($page);
+ $count++;
+ echo "$count";
+ }
+
+ echo "Done: Published $count pages";
+
+ } else {
+ echo <<"Publish All" functionality
+ Pressing this button will do the equivalent of going to every page and pressing "publish". It's
+ intended to be used after there have been massive edits of the content, such as when the site was
+ first built.
+
+HTML;
+ }
+ }
+
+ function restorepage() {
+ if($id = $this->urlParams['ID']) {
+ $restoredPage = Versioned::get_latest_version("SiteTree", $id);
+ $restoredPage->ID = $restoredPage->RecordID;
+ if(!DB::query("SELECT ID FROM SiteTree WHERE ID = $restoredPage->ID")->value()) {
+ DB::query("INSERT INTO SiteTree SET ID = $restoredPage->ID");
+ }
+ $restoredPage->forceChange();
+ $restoredPage->writeWithoutVersion();
+ Debug::show($restoredPage);
+ } else {
+ echo "visit restorepage/(ID)";
+ }
+ }
+
+ function duplicate() {
+ if(($id = $this->urlParams['ID']) && is_numeric($id)) {
+ $page = DataObject::get_by_id("SiteTree", $id);
+
+ $newPage = $page->duplicate();
+
+ return $this->returnItemToUser($newPage);
+ } else {
+ user_error("CMSMain::duplicate() Bad ID: '$id'", E_USER_WARNING);
+ }
+ }
+
+ // HACK HACK HACK - Dont remove without telling simon ;-)
+
+ /**
+ * This is only used by parents inc.
+ * TODO Work out a better way of handling control to the individual page objects.
+ */
+ function sethottip($data,$form) {
+ $page = DataObject::get_by_id("SiteTree", $_REQUEST['ID']);
+ return $page->sethottip($data,$form);
+ }
+ /**
+ * This is only used by parents inc.
+ * TODO Work out a better way of handling control to the individual page objects.
+ */
+ function notifyInvitation($data,$form) {
+ $page = DataObject::get_by_id("SiteTree", $_REQUEST['ID']);
+ return $page->notifyInvitation($data,$form);
+ }
+ function testInvitation($data,$form) {
+ $page = DataObject::get_by_id("SiteTree", $_REQUEST['ID']);
+ return $page->testInvitation($data,$form);
+ }
+
+ /**
+ * Use this as an action handler for custom CMS buttons.
+ */
+ function callPageMethod($data, $form) {
+ $methodName = $form->buttonClicked()->extraData();
+ $record = $this->CurrentPage();
+ return $record->$methodName($data, $form);
+ }
+
+ /**
+ * Provide the permission codes used by LeftAndMain.
+ * Can't put it on LeftAndMain since that's an abstract base class.
+ */
+ function providePermissions() {
+ $classes = ClassInfo::subclassesFor('LeftAndMain');
+
+ foreach($classes as $class) {
+ $perms["CMS_ACCESS_" . $class] = "Access to $class in CMS";
+ }
+ return $perms;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/Diff.php b/code/Diff.php
new file mode 100755
index 00000000..d935db33
--- /dev/null
+++ b/code/Diff.php
@@ -0,0 +1,795 @@
+
+// You may copy this code freely under the conditions of the GPL.
+//
+
+// FIXME: possibly remove assert()'s for production version?
+
+// PHP3 does not have assert()
+define('USE_ASSERTS', function_exists('assert'));
+
+class _DiffOp {
+ var $type;
+ var $orig;
+ var $final;
+
+ function reverse() {
+ trigger_error("pure virtual", E_USER_ERROR);
+ }
+
+ function norig() {
+ return $this->orig ? sizeof($this->orig) : 0;
+ }
+
+ function nfinal() {
+ return $this->final ? sizeof($this->final) : 0;
+ }
+}
+
+class _DiffOp_Copy extends _DiffOp {
+ var $type = 'copy';
+
+ function _DiffOp_Copy ($orig, $final = false) {
+ if (!is_array($final))
+ $final = $orig;
+ $this->orig = $orig;
+ $this->final = $final;
+ }
+
+ function reverse() {
+ return new _DiffOp_Copy($this->final, $this->orig);
+ }
+}
+
+class _DiffOp_Delete extends _DiffOp {
+ var $type = 'delete';
+
+ function _DiffOp_Delete ($lines) {
+ $this->orig = $lines;
+ $this->final = false;
+ }
+
+ function reverse() {
+ return new _DiffOp_Add($this->orig);
+ }
+}
+
+class _DiffOp_Add extends _DiffOp {
+ var $type = 'add';
+
+ function _DiffOp_Add ($lines) {
+ $this->final = $lines;
+ $this->orig = false;
+ }
+
+ function reverse() {
+ return new _DiffOp_Delete($this->final);
+ }
+}
+
+class _DiffOp_Change extends _DiffOp {
+ var $type = 'change';
+
+ function _DiffOp_Change ($orig, $final) {
+ $this->orig = $orig;
+ $this->final = $final;
+ }
+
+ function reverse() {
+ return new _DiffOp_Change($this->final, $this->orig);
+ }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ * http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * Finally, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * @author Geoffrey T. Dairiki
+ * @access private
+ */
+class _DiffEngine
+{
+ function diff ($from_lines, $to_lines) {
+ $n_from = sizeof($from_lines);
+ $n_to = sizeof($to_lines);
+
+ $this->xchanged = $this->ychanged = array();
+ $this->xv = $this->yv = array();
+ $this->xind = $this->yind = array();
+ unset($this->seq);
+ unset($this->in_seq);
+ unset($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) {
+ USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+ USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+ // 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 _DiffOp_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 _DiffOp_Change($delete, $add);
+ elseif ($delete)
+ $edits[] = new _DiffOp_Delete($delete);
+ elseif ($add)
+ $edits[] = new _DiffOp_Add($add);
+ }
+ return $edits;
+ }
+
+
+ /* Divide 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 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];
+ reset($matches);
+ while (list ($junk, $y) = each($matches))
+ if (empty($this->in_seq[$y])) {
+ $k = $this->_lcs_pos($y);
+ USE_ASSERTS && assert($k > 0);
+ $ymids[$k] = $ymids[$k-1];
+ break;
+ }
+ while (list ($junk, $y) = each($matches)) {
+ if ($y > $this->seq[$k-1]) {
+ USE_ASSERTS && assert($y < $this->seq[$k]);
+ // 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);
+ USE_ASSERTS && assert($k > 0);
+ $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;
+ }
+
+ USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+ $this->in_seq[$this->seq[$end]] = false;
+ $this->seq[$end] = $ypos;
+ $this->in_seq[$ypos] = 1;
+ return $end;
+ }
+
+ /* Find 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;
+ }
+ }
+ }
+
+ /* Adjust 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;
+
+ USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+ $len = sizeof($lines);
+ $other_len = sizeof($other_changed);
+
+ while (1) {
+ /*
+ * Scan forwards to find 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]) {
+ USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+ $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--;
+ USE_ASSERTS && assert('$j > 0');
+ while ($other_changed[--$j])
+ continue;
+ USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+ }
+
+ /*
+ * 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++;
+
+ USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+ $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;
+ USE_ASSERTS && assert('$j > 0');
+ while ($other_changed[--$j])
+ continue;
+ USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+ }
+ }
+ }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ */
+class Diff
+{
+ var $edits;
+
+ /**
+ * Constructor.
+ * Computes diff between sequences of strings.
+ *
+ * @param $from_lines array An array of strings.
+ * (Typically these are lines from a file.)
+ * @param $to_lines array An array of strings.
+ */
+ function Diff($from_lines, $to_lines) {
+ $eng = new _DiffEngine;
+ $this->edits = $eng->diff($from_lines, $to_lines);
+ //$this->_check($from_lines, $to_lines);
+ }
+
+ /**
+ * Compute reversed Diff.
+ *
+ * SYNOPSIS:
+ *
+ * $diff = new Diff($lines1, $lines2);
+ * $rev = $diff->reverse();
+ * @return object A Diff object representing the inverse of the
+ * original diff.
+ */
+ function reverse () {
+ $rev = $this;
+ $rev->edits = array();
+ foreach ($this->edits as $edit) {
+ $rev->edits[] = $edit->reverse();
+ }
+ return $rev;
+ }
+
+ /**
+ * Check for empty diff.
+ *
+ * @return bool True iff two sequences were identical.
+ */
+ function isEmpty () {
+ foreach ($this->edits as $edit) {
+ if ($edit->type != 'copy')
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Compute the length of the Longest Common Subsequence (LCS).
+ *
+ * This is mostly for diagnostic purposed.
+ *
+ * @return int The length of the LCS.
+ */
+ function lcs () {
+ $lcs = 0;
+ foreach ($this->edits as $edit) {
+ if ($edit->type == 'copy')
+ $lcs += sizeof($edit->orig);
+ }
+ return $lcs;
+ }
+
+ /**
+ * Get the original set of lines.
+ *
+ * This reconstructs the $from_lines parameter passed to the
+ * constructor.
+ *
+ * @return array The original sequence of strings.
+ */
+ function orig() {
+ $lines = array();
+
+ foreach ($this->edits as $edit) {
+ if ($edit->orig)
+ array_splice($lines, sizeof($lines), 0, $edit->orig);
+ }
+ return $lines;
+ }
+
+ /**
+ * Get the final set of lines.
+ *
+ * This reconstructs the $to_lines parameter passed to the
+ * constructor.
+ *
+ * @return array The sequence of strings.
+ */
+ function finaltext() {
+ $lines = array();
+
+ foreach ($this->edits as $edit) {
+ if ($edit->final)
+ array_splice($lines, sizeof($lines), 0, $edit->final);
+ }
+ return $lines;
+ }
+
+ /**
+ * Check a Diff for validity.
+ *
+ * This is here only for debugging purposes.
+ */
+ function _check ($from_lines, $to_lines) {
+ if (serialize($from_lines) != serialize($this->orig()))
+ trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+ if (serialize($to_lines) != serialize($this->finaltext()))
+ trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
+
+ $rev = $this->reverse();
+ if (serialize($to_lines) != serialize($rev->orig()))
+ trigger_error("Reversed original doesn't match", E_USER_ERROR);
+ if (serialize($from_lines) != serialize($rev->finaltext()))
+ trigger_error("Reversed final doesn't match", E_USER_ERROR);
+
+
+ $prevtype = 'none';
+ foreach ($this->edits as $edit) {
+ if ( $prevtype == $edit->type )
+ trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+ $prevtype = $edit->type;
+ }
+
+ $lcs = $this->lcs();
+ trigger_error("Diff okay: LCS = $lcs", E_USER_NOTICE);
+ }
+
+
+
+ static function compareHTML($from, $to) {
+ // First split up the content into words and tags
+ $set1 = self::getHTMLChunks($from);
+ $set2 = self::getHTMLChunks($to);
+
+ // Diff that
+ $diff = new Diff($set1, $set2);
+
+ // Go through everything, converting edited tags (and their content) into single chunks. Otherwise
+ // the generated HTML gets crusty
+ foreach($diff->edits as $edit) {
+ //echo " $edit->type: " . htmlentities(implode(" " ,$edit->orig));
+ //if($edit->final) echo ' / ' . htmlentities(implode(" " ,$edit->final));
+
+ switch($edit->type) {
+ case 'copy':
+ $lookForTag = false;
+ $stuffFor[1] = $edit->orig;
+ $stuffFor[2] = $edit->orig;
+ break;
+
+ case 'change':
+ $lookForTag = true;
+ $stuffFor[1] = $edit->orig;
+ $stuffFor[2] = $edit->final;
+ break;
+
+ case 'add':
+ $lookForTag = true;
+ $stuffFor[1] = null;
+ $stuffFor[2] = $edit->final;
+ break;
+
+ case 'delete':
+ $lookForTag = true;
+ $stuffFor[1] = $edit->orig;
+ $stuffFor[2] = null;
+ break;
+ }
+
+ foreach($stuffFor as $listName => $chunks) {
+ if($chunks) {
+ foreach($chunks as $item) {
+ // $tagStack > 0 indicates that we should be tag-building
+ if($tagStack[$listName]) $rechunked[$listName][sizeof($rechunked[$listName])-1] .= ' ' . $item;
+ else $rechunked[$listName][] = $item;
+
+ if($lookForTag && $item[0] == "<" && substr($item,0,2) != "") {
+ $tagStack[$listName] = 1;
+ } else if($tagStack[$listName]) {
+ if(substr($item,0,2) == "") $tagStack[$listName]--;
+ else if($item[0] == "<") $tagStack[$listName]++;
+ }
+
+ // echo " " . htmlentities($item) . " -> " .$tagStack[$listName];
+ }
+ }
+ }
+ }
+
+
+ // Diff the re-chunked data, turning it into maked up HTML
+ $diff = new Diff($rechunked[1], $rechunked[2]);
+ foreach($diff->edits as $edit) {
+ // echo " $edit->type: " . htmlentities(implode(" " ,$edit->orig));
+ // if($edit->final) echo ' / ' . htmlentities(implode(" " ,$edit->final));
+
+ switch($edit->type) {
+ case 'copy':
+ $content .= " " . implode(" ", $edit->orig) . " ";
+ break;
+
+ case 'change':
+ $content .= " " . implode(" ", $edit->final) . " ";
+ $content .= " " . implode(" ", $edit->orig) . " ";
+ break;
+
+ case 'add':
+ $content .= " " . implode(" ", $edit->final) . " ";
+ break;
+
+ case 'delete':
+ $content .= " " . implode(" ", $edit->orig) . " ";
+ break;
+ }
+ }
+ // echo "" . htmlentities($content) . "
";
+ return $content;
+ }
+ static function getHTMLChunks($content) {
+ $content = str_replace(array(" ","<", ">"),array(" "," <", "> "),$content);
+ $candidateChunks = split("[\t\r\n ]+", $content);
+ while(list($i,$item) = each($candidateChunks)) {
+ if($item[0] == "<") {
+ $newChunk = $item;
+ while($item[strlen($item)-1] != ">") {
+ list($i,$item) = each($candidateChunks);
+ $newChunk .= ' ' . $item;
+ }
+ $chunks[] = $newChunk;
+ } else {
+ $chunks[] = $item;
+ }
+ }
+ return $chunks;
+ }
+
+}
+
+
+
+
+/**
+ * FIXME: bad name.
+ */
+class MappedDiff
+extends Diff
+{
+ /**
+ * Constructor.
+ *
+ * Computes 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 $from_lines array An array of strings.
+ * (Typically these are lines from a file.)
+ *
+ * @param $to_lines array An array of strings.
+ *
+ * @param $mapped_from_lines array 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 $mapped_to_lines array This array should
+ * have the same number of elements as $to_lines.
+ */
+ function MappedDiff($from_lines, $to_lines,
+ $mapped_from_lines, $mapped_to_lines) {
+
+ assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+ assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+ $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+ $xi = $yi = 0;
+ // Optimizing loop invariants:
+ // http://phplens.com/lens/php-book/optimizing-debugging-php.php
+ for ($i = 0, $max = sizeof($this->edits); $i < $max; $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);
+ }
+ }
+ }
+}
+
+
+?>
\ No newline at end of file
diff --git a/code/FileList.php b/code/FileList.php
new file mode 100755
index 00000000..dbfc1c1d
--- /dev/null
+++ b/code/FileList.php
@@ -0,0 +1,17 @@
+folder = $folder;
+ parent::__construct($name, "File", array("Title" => "Title", "LinkedURL" => "URL"), "", "Title");
+ $this->Markable = true;
+ }
+
+ function sourceItems() {
+ return DataObject::get("File", "ParentID = '" . $this->folder->ID . "' AND ClassName <> 'Folder'", "Title");
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/GenericDataAdmin.php b/code/GenericDataAdmin.php
new file mode 100755
index 00000000..08cc8035
--- /dev/null
+++ b/code/GenericDataAdmin.php
@@ -0,0 +1,650 @@
+ 'AccountName'
+ * )
+ *
+ * Format "list":
+ * see {@DataObject->buildNestedUL}
+ */
+ static $result_columns;
+
+ /**
+ * @var string
+ * Either "table" or "list". List-format also supports second level results.
+ */
+ static $result_format = "table";
+
+ static $csv_columns;
+
+ private $results;
+
+ function __construct() {
+ $this->result_actions = new FieldSet(
+ new FormAction("export","Export as CSV")
+ );
+
+ parent::__construct();
+ }
+
+ /**
+ * Sets Requirements and checks for Permissions.
+ * Subclass this function to add custom Requirements.
+ */
+ function init() {
+ parent::init();
+
+ Requirements::javascript(MCE_ROOT . "tiny_mce_src.js");
+ Requirements::javascript("jsparty/tiny_mce_improvements.js");
+
+ Requirements::javascript("jsparty/hover.js");
+ Requirements::javascript("jsparty/scriptaculous/controls.js");
+
+ Requirements::javascript("cms/javascript/SecurityAdmin.js");
+ Requirements::javascript("cms/javascript/CMSMain_left.js");
+
+ Requirements::javascript("cms/javascript/GenericDataAdmin_left.js");
+ Requirements::javascript("cms/javascript/GenericDataAdmin_right.js");
+ Requirements::javascript("cms/javascript/SideTabs.js");
+
+ // We don't want this showing up in every ajax-response, it should always be present in a CMS-environment
+ if(!Director::is_ajax()) {
+ Requirements::javascriptTemplate("cms/javascript/tinymce.template.js", array(
+ "ContentCSS" => project() . "/css/editor.css",
+ "BaseURL" => Director::absoluteBaseURL(),
+ ));
+ }
+
+ Requirements::css("cms/css/GenericDataAdmin.css");
+
+ //For wrightgroup workshop
+ Requirements::css("writework/css/WorkshopCMSLayout.css");
+ }
+
+ function Link() {
+ $args = func_get_args();
+ return call_user_func_array( array( &$this, 'getLink' ), $args );
+ }
+
+ /**
+ * @return String
+ */
+ function DataTypeSingular() {
+ return singleton($this->stat('data_type'))->singular_name();
+ }
+
+ /**
+ * @return String
+ */
+ function DataTypePlural() {
+ return singleton($this->stat('data_type'))->plural_name();
+ }
+
+ /**
+ * @return Form
+ */
+ function CreationForm() {
+ $plural_name = singleton($this->stat('data_type'))->plural_name();
+ $singular_name = singleton($this->stat('data_type'))->singular_name();
+ return new Form($this, 'CreationForm', new FieldSet(), new FieldSet(new FormAction("createRecord", "Create {$singular_name}")));
+ }
+
+ /**
+ * @return Form
+ */
+ function EditForm() {
+ $id = isset($_REQUEST['ID']) ? $_REQUEST['ID'] : Session::get('currentPage');
+ if($id && DataObject::get_by_id($this->stat('data_type'), $id)) {
+ return $this->getEditForm($id);
+ }
+ }
+
+ // legacy
+ function ExportForm() {
+ return $this->EditForm();
+ }
+
+ /**
+ * @return Form
+ */
+ function SearchForm() {
+
+ $fields = $this->getSearchFields();
+ $actions = new FieldSet($action = new FormAction("getResults", "Go"));
+
+ $searchForm = new Form($this, "SearchForm", $fields, $actions);
+ $searchForm->loadDataFrom($_REQUEST);
+ return $searchForm;
+ }
+
+ /**
+ * Determines fields and actions for the given {$data_type}, and populates
+ * these fields with values from {$data_type} and any connected {$data_type_extra}.
+ * Adds default actions ("save" and "delete") if no custom actions are found.
+ * Returns an empty form if no fields or actions are found (on first load).
+ *
+ * @param $id Number
+ * @return Form
+ */
+ function getEditForm($id) {
+ if($_GET['debug_profile']) Profiler::mark('getEditForm');
+
+ $genericData = DataObject::get_by_id($this->stat('data_type'), $id);
+
+ $fields = (method_exists($genericData, getCMSFields)) ? $genericData->getCMSFields() : new FieldSet();
+
+ if(!$fields->dataFieldByName('ID')) {
+
+ $fields->push($idField = new HiddenField("ID","ID",$id));
+ $idField->setValue($id);
+ }
+
+ if(method_exists($genericData, getGenericStatus)){
+ $genericDataStatus = $genericData->getGenericStatus();
+ if($genericDataStatus){
+ $fields->push($dataStatusField = new ReadonlyField("GenericDataStatus", "", $genericDataStatus));
+ $dataStatusField -> dontEscape = true;
+ }
+ }
+
+
+ $actions = (method_exists($genericData, getCMSActions)) ? $genericData->getCMSActions() : new FieldSet();
+ if(!$actions->fieldByName('action_save')) {
+ $actions->push(new FormAction('save', 'Save','ajaxAction-save'));
+ }
+ if(!$actions->fieldByName('action_delete')) {
+ $actions->push(new FormAction('delete', 'Delete','ajaxAction-delete'));
+ }
+ $form = new Form($this, "EditForm", $fields, $actions);
+
+ if($this->stat('data_type_extra')) {
+ foreach ($this->stat('data_type_extra') as $oneRelated) {
+ $oneExtra = $genericData-> $oneRelated();
+ if($oneExtra) {
+ $allFields = $oneExtra->getAllFields();
+ foreach ($allFields as $k => $v) {
+ $fieldname = $oneRelated . "[" . $k . "]";
+ $allFields[$fieldname] = $v;
+ unset ($allFields[$k]);
+ }
+
+ $form->loadDataFrom($allFields);
+ }
+ }
+ }
+
+ $form->loadDataFrom($genericData);
+ $form->disableDefaultAction();
+
+ if($_GET['debug_profile']) Profiler::unmark('getEditForm');
+ return $form;
+ }
+
+ /**
+ * Display the results of the search.
+ * @return String
+ */
+ function Results() {
+ $ret = "";
+
+ $singular_name = singleton($this->stat('data_type'))->singular_name();
+ $plural_name = singleton($this->stat('data_type'))->plural_name();
+ $this->filter = array(
+ "ClassName" => $this->stat('data_type')
+ );
+
+ $results = $this->performSearch();
+ if($results) {
+ $name = ($results->Count() > 1) ? $plural_name : $singular_name;
+ $ret .= "{$results->Count()} {$name} found: ";
+
+ switch($this->stat('result_format')) {
+ case 'table':
+ $ret .= $this->getResultTable($results);
+ break;
+ case 'list':
+ $ret .= $this->getResultList($results);
+ break;
+ }
+ $ret .= $this->getResultActionsForm($results);
+ } else {
+ if($this->hasMethod('isEmptySearch') && $this->isEmptySearch()) {
+ $ret .="Please choose some search criteria and press 'Go'. ";
+ } else {
+ $ret .="Sorry, no {$plural_name} found by this search. ";
+ }
+ }
+ return $ret;
+ }
+
+ function getResults() {
+ if(Director::is_ajax()) {
+ echo $this->Results();
+ } else {
+ return $this->Results();
+ }
+ }
+
+ function getResultList($results, $link = true) {
+ $listBody = $results->buildNestedUL($this->stat('result_columns'));
+
+ return <<
+ $listBody
+
+HTML;
+ }
+
+ /**
+ * @param $results
+ * @param $link Link the rows to their according result (evaluated by javascript)
+ * @return String Result-Table as HTML
+ */
+ function getResultTable($results, $link = true) {
+ $tableHeader = $this->columnheader();
+
+ $tableBody = $this->columnbody($results);
+
+ return <<
+
+ $tableHeader
+
+
+ $tableBody
+
+
+HTML;
+ }
+
+ protected function columnheader(){
+ $content = "";
+ foreach( array_keys($this->stat('result_columns')) as $field ) {
+ $content .= $this->htmlTableCell($field);
+ }
+ return $this->htmlTableRow($content);
+ }
+
+ protected function columnbody($results=null) {
+ // shouldn't be called here, but left in for legacy
+ if(!$results) {
+ $results = $this->performSearch();
+ }
+
+ $body = "";
+ if($results){
+ $i=0;
+ foreach($results as $result){
+ $i++;
+ $html = "";
+ foreach($this->stat('result_columns') as $field) {
+ $value = $this->buildResultFieldValue($result, $field);
+ $html .= $this->htmlTableCell($value, $this->Link("show", $result->ID), "show", true);
+
+ }
+ $evenOrOdd = ($i%2)?"even":"odd";
+ $row = $this->htmlTableRow($html, null, $evenOrOdd);
+ $body .= $row;
+ }
+ }
+ return $body;
+ }
+
+ protected function listbody($results=null) {
+
+ }
+
+ /**
+ * @param $results Array
+ * @return String Form-Content
+ */
+ function getResultActionsForm($results) {
+ $ret = "";
+
+ $csvValues = array();
+ foreach($results as $record) {
+ $csvValues[] = $record->ID;
+ }
+
+ $form = new Form(
+ $this,
+ "ExportForm",
+ new FieldSet(
+ new HiddenField("csvIDs","csvIDs",implode(",",$csvValues))
+ ),
+ $this->result_actions
+ );
+
+ $ret = <<
+{$form->renderWith("Form")}
+
+HTML;
+
+ return $ret;
+ }
+
+ /**
+ * @param $result
+ * @param $field Mixed can be array: eg: array("FirstName", "Surname"), in which case two fields
+ * in database table should concatenate to one cell for report table.
+ * The field could be "Total->Nice" or "Order.Created->Date", intending to show its specific format.
+ * Caster then is "Nice" "Date" etc.
+ */
+ protected function buildResultFieldValue($result, $field) {
+ if(is_array($field)) {
+ $i = 0;
+ foreach($field as $each) {
+ $value .= $i == 0 ? "" : "_";
+ $value .= $this->buildResultFieldValue($result, $each);
+ $i++;
+ }
+ } else {
+ list ($field, $caster) = explode("->", $field);
+ if(preg_match('/^(.+)\.(.+)$/', $field, $matches)) {
+ $field = $matches[2];
+ }
+
+ if($caster) {
+ // When the intending value is Created.Date, the obj need to be casted as Datetime explicitely.
+ if ($field == "Created" || $field == "LastEdited") {
+ $created = Object::create('Datetime', $result->Created, "Created");
+ // $created->setVal();
+ $value = $created->val($caster);
+ } else // Dealing with other field like "Total->Nice", etc.
+ $value = $result->obj($field)->val($caster);
+ } else { // Simple field, no casting
+ $value = $result->val($field);
+ }
+ }
+
+ return $value;
+ }
+
+ protected function htmlTableCell($value, $link = false, $class = "", $id = null) {
+ if($link) {
+ return "" . htmlentities($value) . " ";
+ } else {
+ return "" . htmlentities($value) . " ";
+ }
+ }
+
+ protected function htmlTableRow($value, $link = null, $evenOrOdd = null) {
+ if ($link) {
+ return "" . $value . " ";
+ } else {
+ return "" . $value . " ";
+ }
+ }
+
+ /**
+ * Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField).
+ * Uses {$csv_columns} if present, and falls back to {$result_columns}.
+ */
+ function export() {
+
+ $now = Date("s-i-H");
+ $fileName = "export-$now.csv";
+
+ $csv_columns = ($this->stat('csv_columns')) ? array_values($this->stat('csv_columns')) : array_values($this->stat('result_columns'));
+
+ $fileData = "";
+ $fileData .= "\"" . implode("\";\"",$csv_columns) . "\"";
+ $fileData .= "\n";
+
+ $records = $this->performSearch();
+ if($records) {
+ foreach($records as $record) {
+ $columnData = array();
+ foreach($csv_columns as $column) {
+ $tmpColumnData = "\"" . str_replace("\"", "\"\"", $record->$column) . "\"";
+ $tmpColumnData = str_replace(array("\r", "\n"), "", $tmpColumnData);
+ $columnData[] = $tmpColumnData;
+ }
+ $fileData .= implode(",",$columnData);
+ $fileData .= "\n";
+ }
+
+ HTTP::sendFileToBrowser($fileData, $fileName);
+ } else {
+ user_error("No records found", E_USER_ERROR);
+ }
+
+ }
+
+
+ /**
+ * Save genetric data handler
+ *
+ * @return String Statusmessage
+ */
+ function save($urlParams, $form) {
+
+ $className = $this->stat('data_type');
+
+ $id = $_REQUEST['ID'];
+
+ if(substr($id, 0, 3) != 'new') {
+ $generic = DataObject::get_one($className, "`$className`.ID = $id");
+ $generic->Status = "Saved (Update)";
+ } else {
+ $generic = new $className();
+ $generic->Status = "Saved (New)";
+ }
+
+ $form->saveInto($generic, true);
+ $id = $generic->write();
+
+ if($this->stat('data_type_extra')) {
+ foreach($this->stat('data_type_extra') as $oneRelated) {
+ $oneExtra = $generic->$oneRelated();
+ if($_REQUEST[$oneExtra->class]) {
+ foreach($_REQUEST[$oneExtra->class] as $field => $value) {
+ $oneExtra->setField($field, $value);
+ }
+ $oneExtra->write();
+ }
+ }
+ }
+
+ FormResponse::status_message('Saved', 'good');
+ FormResponse::update_status($generic->Status);
+
+ if (method_exists($this, "saveAfterCall")) {
+ $this->saveAfterCall($generic, $urlParams, $form);
+ }
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Show a single record
+ *
+ * @return Array Editing Form
+ */
+ function show() {
+
+ Session::set('currentPage', $this->urlParams['ID']);
+
+ $editForm = $this->getEditForm($this->urlParams['ID']);
+
+ if(Director::is_ajax()) {
+ return $editForm->formHtmlContent();
+ } else {
+ return array (
+ 'EditForm' => $editForm
+ );
+ }
+ }
+
+ /**
+ * Add a new DataObject
+ *
+ * @return String
+ */
+ function createRecord() {
+ $baseClass = $this->stat('data_type');
+ $obj = new $baseClass();
+ $obj->write();
+
+ $editForm = $this->getEditForm($obj->ID);
+
+ return (Director::is_ajax()) ? $editForm->formHtmlContent() : array ('EditForm' => $editForm);
+ }
+
+ /**
+ * Delete a given Dataobjebt by ID
+ *
+ * @param $urlParams Array
+ * @param $form Form
+ * @return String
+ */
+ function delete($urlParams, $form) {
+ $id = Convert::raw2sql($_REQUEST['ID']);
+ $obj = DataObject::get_by_id($this->stat('data_type'), $id);
+ if ($obj) {
+ $obj->delete();
+ }
+
+ // clear session data
+ Session::clear('currentPage');
+
+ FormResponse::status_message('Successfully deleted', 'good');
+ FormResponse::add("$('Form_EditForm').deleteEffect();");
+
+ return FormResponse::respond();
+ }
+
+ protected function getRelatedData() {
+
+ $relatedName = $_REQUEST['RelatedClass'];
+ $id = $_REQUEST[$relatedName]['ID'];
+ $baseClass = $this->stat('data_type');
+ $relatedClasses = singleton($baseClass)->stat('has_one');
+ if($id){
+ $relatedObject = DataObject::get_by_id($relatedClasses[$relatedName], $id);
+ $response .= <<ParentID = $parentID;
+ }
+ $id = $relatedObject->write();
+ $response .= <<ParentID = $parentID;
+ }
+ $response .= << $v) {
+ $JS_newKey = Convert::raw2js($relatedName . '[' . $k . ']');
+ $JS_newValue = Convert::raw2js($relatedObject-> $k);
+ $response .=<<stat('data_type'))->$funcName();
+ $relatedKeyDropdown->extraClass = "relatedDataKey";
+ echo $relatedKeyDropdown->FieldHolder();
+ } else {
+ Director::redirectBack();
+ }
+ }
+
+ /**
+ * Execute a query based on {$filter} and build a DataObjectSet
+ * out of the results.
+ *
+ * @return DataObjectSet
+ */
+ abstract function performSearch();
+
+ /**
+ * Form fields which trigger {getResults} and {peformSearch}.
+ * Provide HTML in the following format to get auto-collapsing "advanced search"-fields.
+ *
+ *
+ *
+ *
+ * @return FieldSet
+ */
+ abstract function getSearchFields();
+
+ /**
+ * Provide custom link.
+ *
+ * @return String
+ */
+ abstract function getLink();
+
+ //abstract function create();
+
+ /**
+ * Legacy
+ */
+ function AddForm() {
+ return $this->CreationForm();
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/ImprintStats.php b/code/ImprintStats.php
new file mode 100755
index 00000000..2213982c
--- /dev/null
+++ b/code/ImprintStats.php
@@ -0,0 +1,66 @@
+CurrentPage();
+
+ if($page){
+ $description = str_replace(array("\n","\r"),"",$page->MetaDescription);
+
+ $title = Convert::raw2js($page->Title);
+
+ $level1 = $page->Level(1);
+ $level2 = $page->Level(2);
+ if($level1->URLSegment == 'modes') {
+ $level1 = $level2;
+ $level2 = $page->Level(3);
+ }
+ if($level2 && $level1->URLSegment == 'home') {
+ $level1 = $level2;
+ $level2 = $page->Level(3);
+ }
+
+
+ $section = $level1->Title;
+ $service = $level2 ? $level2->Title : $section;
+
+ $fullURL = $page->Link();
+
+ $imprintID = ImprintStats::$imprintID;
+
+ return "
+
+
+
+
+
+
+ ";
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/LeftAndMain.php b/code/LeftAndMain.php
new file mode 100644
index 00000000..e462cefe
--- /dev/null
+++ b/code/LeftAndMain.php
@@ -0,0 +1,761 @@
+class")) {
+ // When access /admin/, we should try a redirect to another part of the admin rather than a
+ if($this->class == 'CMSMain') {
+ $menu = $this->MainMenu();
+ if(($first = $menu->First()) && $first->Link) {
+ Director::redirect($first->Link);
+ exit();
+ }
+ }
+
+
+ $messageSet = array(
+ 'default' => "Enter your email address and password to access the CMS.",
+ 'alreadyLoggedIn' => "I'm sorry, but you can't access that part of the CMS. If you want to log in as someone else, do so below",
+ 'logInAgain' => "You have been logged out of the CMS. If you would like to log in again, enter a username and password below.",
+ );
+
+ Security::permissionFailure($this, $messageSet);
+ exit;
+ }
+
+ Requirements::javascript("jsparty/prototype.js");
+ Requirements::javascript("jsparty/behaviour.js");
+ Requirements::javascript("jsparty/prototype_improvements.js");
+ Requirements::javascript("jsparty/loader.js");
+
+ Requirements::javascript("jsparty/layout_helpers.js");
+ Requirements::javascript("jsparty/tree/tree.js");
+ Requirements::css("jsparty/tree/tree.css");
+ Requirements::javascript("jsparty/scriptaculous/effects.js");
+ Requirements::javascript("jsparty/scriptaculous/dragdrop.js");
+
+ Requirements::javascript("jsparty/tabstrip/tabstrip.js");
+ Requirements::css("jsparty/tabstrip/tabstrip.css");
+
+
+ Requirements::css("jsparty/greybox/greybox.css");
+ Requirements::javascript("jsparty/greybox/AmiJS.js");
+ Requirements::javascript("jsparty/greybox/greybox.js");
+
+ Requirements::javascript("cms/javascript/LeftAndMain.js");
+ Requirements::javascript("cms/javascript/LeftAndMain_left.js");
+ Requirements::javascript("cms/javascript/LeftAndMain_right.js");
+
+ Requirements::javascript("jsparty/calendar/calendar.js");
+ Requirements::javascript("jsparty/calendar/lang/calendar-en.js");
+ Requirements::javascript("jsparty/calendar/calendar-setup.js");
+ Requirements::css("sapphire/css/CalendarDateField.css");
+ Requirements::css("jsparty/calendar/calendar-win2k-1.css");
+
+ Requirements::javascript('sapphire/javascript/Validator.js');
+
+ Requirements::css("sapphire/css/SubmittedFormReportField.css");
+ }
+
+ /**
+ * Returns true if the current user can access the CMS
+ */
+ function canAccessCMS() {
+
+ $member = Member::currentUser();
+
+ if($member) {
+ if($groups = $member->Groups()) {
+ foreach($groups as $group) if($group->CanCMS) return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the current user has administrative rights in the CMS
+ */
+ function canAdminCMS() {
+ if($member = Member::currentUser()) return $member->isAdmin();
+ }
+
+ //------------------------------------------------------------------------------------------//
+ // Main controllers
+
+ /**
+ * You should implement a Link() function in your subclass of LeftAndMain,
+ * to point to the URL of that particular controller.
+ */
+ abstract public function Link();
+
+ public function show($params) {
+ if($params['ID']) $this->setCurrentPageID($params['ID']);
+ if($params['OtherID'])
+ Session::set('currentMember', $params['OtherID']);
+
+ if(Director::is_ajax()) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ return $this->EditForm()->formHtmlContent();
+
+ } else {
+ return array();
+ }
+ }
+
+
+ public function getitem() {
+ $this->setCurrentPageID($_REQUEST['ID']);
+ SSViewer::setOption('rewriteHashlinks', false);
+ // Changed 3/11/2006 to not use getLastFormIn because that didn't have _form_action, _form_name, etc.
+
+ $form = $this->EditForm();
+ if($form) return $form->formHtmlContent();
+ else return "";
+ }
+ public function getLastFormIn($html) {
+ $parts = split('?form[^>]*>', $html);
+ return $parts[sizeof($parts)-2];
+ }
+
+ //------------------------------------------------------------------------------------------//
+ // Main UI components
+
+ /**
+ * Returns the main menu of the CMS. This is also used by init() to work out which sections the user
+ * has access to.
+ */
+ public function MainMenu() {
+ // Don't accidentally return a menu if you're not logged in - it's used to determine access.
+ if(!Member::currentUserID()) return new DataObjectSet();
+
+ // Built-in modules
+
+ // array[0]: Name of the icon
+ // array[1]: URL to visi
+ // array[2]: The controller class for this menu, used to check permisssions. If blank, it's assumed that this is public, and always shown to
+ // users who have the rights to access some other part of the admin area.
+ $menuSrc = array(
+ "Site Content" => array("content", "admin/", "CMSMain"),
+ "Files & Images" => array("files", "admin/assets/", "AssetAdmin"),
+ "Newsletters" => array("newsletter", "admin/newsletter/", "NewsletterAdmin"),
+ "Reports" => array("report", "admin/reports/", "ReportAdmin"),
+ "Security" => array("security", "admin/security/", "SecurityAdmin"),
+ "Help" => array("help", "http://userhelp.silverstripe.com"),
+ );
+
+ if(!$this->hasReports()) unset($menuSrc['Reports']);
+
+ // Extra modules
+ if($removed = $this->stat('removed_menu_items')) {
+ foreach($removed as $remove) {
+ foreach($menuSrc as $k => $v) {
+ if($v[0] == $remove) {
+ unset($menuSrc[$k]);
+ break;
+ }
+ }
+ }
+ }
+
+ // replace menu items
+ if($replaced = $this->stat('replaced_menu_items') ) {
+
+ $newMenu = array();
+
+ reset( $menuSrc );
+
+ for( $i = 0; $i < count( $menuSrc ); $i++ ) {
+ $existing = current($menuSrc);
+
+ if( $replacement = $replaced[$existing[0]] ) {
+ $newMenu = array_merge( $newMenu, $replacement );
+ } else
+ $newMenu = array_merge( $newMenu, array( key( $menuSrc) => current( $menuSrc ) ) );
+
+ next( $menuSrc );
+ }
+
+ reset( $menuSrc );
+
+ $menuSrc = $newMenu;
+ }
+
+ // Extra modules
+ if($extra = $this->stat('extra_menu_items')) {
+ foreach($extra as $k => $v) {
+ if(!is_array($v)) $extra[$k] = array($k, $v, 'title' => $k);
+ else $extra[$k]['title'] = $k;
+ }
+
+ array_splice($menuSrc, count($menuSrc)-2, 0, $extra);
+ }
+
+ // Encode into DO set
+ $menu = new DataObjectSet();
+ $itemsWithPermission = 0;
+ foreach($menuSrc as $title => $menuItem) {
+ if(is_numeric($title) && isset($menuItem['title'])) $title = $menuItem['title'];
+ if(!isset($menuItem[2]) || Permission::check("CMS_ACCESS_$menuItem[2]")) {
+ // Count up the number of items that have specific permission settings
+ if(isset($menuItem[2])) $itemsWithPermission++;
+
+ $linkingmode = "";
+ if(!(strpos($this->Link(), $menuItem[1]) === false)) {
+ if($menuItem[0] == "content") {
+ if($this->Link() == "admin/")
+ $linkingmode = "current";
+ }
+ else
+ $linkingmode = "current";
+ }
+
+ $menu->push(new ArrayData(array(
+ "Title" => Convert::raw2xml($title),
+ "Code" => $menuItem[0],
+ "Link" => $menuItem[1],
+ "LinkingMode" => $linkingmode
+ )));
+ }
+ }
+
+ // Only return a menu if there is at least one permission-requiring item. Otherwise, the menu will just be the "help" icon.
+ if($itemsWithPermission > 0) return $menu;
+ else return new DataObjectSet();
+ }
+
+ /**
+ * Return a list of appropriate templates for this class, with the given suffix
+ */
+ protected function getTemplatesWithSuffix($suffix) {
+ $classes = array_reverse(ClassInfo::ancestry($this->class));
+ foreach($classes as $class) {
+ $templates[] = $class . $suffix;
+ if($class == 'LeftAndMain') break;
+ }
+ return $templates;
+ }
+
+ public function Left() {
+ return $this->renderWith($this->getTemplatesWithSuffix('_left'));
+ }
+ public function Right() {
+ return $this->renderWith($this->getTemplatesWithSuffix('_right'));
+ }
+ public function RightBottom() {
+ if(SSViewer::hasTemplate($this->getTemplatesWithSuffix('_rightbottom'))) {
+ return $this->renderWith($this->getTemplatesWithSuffix('_rightbottom'));
+ }
+ }
+
+
+ public function getRecord($id, $className = null) {
+ if(!$className) $className = $this->stat('tree_class');
+ return DataObject::get_by_id($className, $rootID);
+ }
+
+ function getSiteTreeFor($className, $rootID = null) {
+ $obj = $rootID ? $this->getRecord($rootID) : singleton($className);
+ $obj->markPartialTree();
+ if($p = $this->currentPage()) $obj->markToExpose($p);
+
+ // getChildrenAsUL is a flexible and complex way of traversing the tree
+ $siteTree = $obj->getChildrenAsUL("", '
+ "ID\" class=\"" . $child->CMSTreeClasses($extraArg) . "\">" .
+ "Link(),0,-1), "show", $child->ID) . "\" " . (($child->canEdit() || $child->canAddChildren()) ? "" : "class=\"disabled\"") . ">" .
+ ($child->TreeTitle()) .
+ " "
+'
+ ,$this, true);
+
+ // Wrap the root if needs be.
+
+ if(!$rootID) {
+ $rootLink = $this->Link() . '0';
+ $siteTree = "";
+ }
+
+ return $siteTree;
+ }
+
+ public function getsubtree() {
+ $results = $this->getSiteTreeFor($this->stat('tree_class'), $_REQUEST['ID']);
+ return substr(trim($results), 4,-5);
+ }
+
+ /**
+ * Allows you to returns a new data object to the tree (subclass of sitetree)
+ * and updates the tree via javascript.
+ */
+ public function returnItemToUser($p) {
+ if(Director::is_ajax()) {
+ // Prepare the object for insertion.
+ $parentID = (int)$p->ParentID;
+ $id = $p->ID ? $p->ID : "new-$p->class-$p->ParentID";
+ $treeTitle = Convert::raw2js($p->TreeTitle());
+ $hasChildren = is_numeric( $id ) && $p->AllChildren() ? ' unexpanded' : '';
+
+ // Ensure there is definitly a node avaliable. if not, append to the home tree.
+ $response = <<class}{$hasChildren}");
+ node = tree.getTreeNodeByIdx($parentID);
+ if(!node){ node = tree.getTreeNodeByIdx(0); }
+ node.open();
+ node.appendTreeNode(newNode);
+ newNode.selectTreeNode();
+JS;
+ FormResponse::add($response);
+ }
+
+ return FormResponse::respond();
+ }
+
+
+ /**
+ * Save page handler
+ */
+ public function save($urlParams, $form) {
+ $className = $this->stat('tree_class');
+ $result = '';
+
+ $id = $_REQUEST['ID'];
+ if(substr($id,0,3) != 'new') {
+ $record = DataObject::get_one($className, "`$className`.ID = $id");
+ } else {
+ $record = $this->getNewItem($id, false);
+ }
+
+ // We don't want to save a new version if there are no changes
+ $dataFields_new = $form->Fields()->dataFields();
+ $dataFields_old = $record->getAllFields();
+ $changed = false;
+ $hasNonRecordFields = false;
+ foreach($dataFields_new as $datafield) {
+ // if the form has fields not belonging to the record
+ if(!isset($dataFields_old[$datafield->Name()])) {
+ $hasNonRecordFields = true;
+ }
+ // if field-values have changed
+ if(!isset($dataFields_old[$datafield->Name()]) || $dataFields_old[$datafield->Name()] != $datafield->dataValue()) {
+ $changed = true;
+ }
+ }
+
+ if(!$changed && !$hasNonRecordFields) {
+ // Tell the user we have saved even though we haven't, as not to confuse them
+ if(is_a($record, "Page")) {
+ $record->Status = "Saved (update)";
+ }
+ FormResponse::status_message("Saved", "good");
+ FormResponse::update_status($record->Status);
+ return FormResponse::respond();
+ }
+
+ $form->dataFieldByName('ID')->Value = 0;
+
+ if(isset($urlParams['Sort']) && is_numeric($urlParams['Sort'])) {
+ $record->Sort = $urlParams['Sort'];
+ }
+
+ // HACK: This should be turned into something more general
+ $originalClass = $record->ClassName;
+ $originalStatus = $record->Status;
+
+ $record->HasBrokenLink = 0;
+ $record->HasBrokenFile = 0;
+
+ $record->writeWithoutVersion();
+
+ // HACK: This should be turned into something more general
+ $originalURLSegment = $record->URLSegment;
+
+ $form->saveInto($record, true);
+
+ if(is_a($record, "Page")) {
+ $record->Status = ($record->Status == "New page" || $record->Status == "Saved (new)") ? "Saved (new)" : "Saved (update)";
+ }
+
+
+
+ // $record->write();
+
+ if(Director::is_ajax()) {
+ if($id != $record->ID) {
+ FormResponse::add("$('sitetree').setNodeIdx(\"$id\", \"$record->ID\");");
+ FormResponse::add("$('Form_EditForm').elements.ID.value = \"$record->ID\";");
+ }
+
+ $title = Convert::raw2js($record->TreeTitle());
+
+ if($added = DataObjectLog::getAdded('SiteTree')) {
+ foreach($added as $page) {
+ if($page->ID != $record->ID) $result .= $this->addTreeNodeJS($page);
+ }
+ }
+ if($deleted = DataObjectLog::getDeleted('SiteTree')) {
+ foreach($deleted as $page) {
+ if($page->ID != $record->ID) $result .= $this->deleteTreeNodeJS($page);
+ }
+ }
+ if($changed = DataObjectLog::getChanged('SiteTree')) {
+ foreach($changed as $page) {
+ if($page->ID != $record->ID) {
+ $title = Convert::raw2js($page->TreeTitle());
+ FormResponse::add("$('sitetree').setNodeTitle($page->ID, \"$title\")");
+ }
+ }
+ }
+
+ FormResponse::add("$('sitetree').setNodeTitle(\"$record->ID\", \"$title\");");
+ $message = "Saved.";
+
+
+ // Update the icon if the class has changed
+ if($originalClass != $record->ClassName) {
+ $record->setClassName( $record->ClassName );
+ $newClass = $record->ClassName;
+ $record = $record->newClassInstance( $newClass );
+
+ FormResponse::add("if(\$('sitetree').setNodeIcon) \$('sitetree').setNodeIcon($record->ID, '$originalClass', '$record->ClassName');");
+ }
+
+ // HACK: This should be turned into somethign more general
+ if( ($record->class == 'VirtualPage' && $originalURLSegment != $record->URLSegment) ||
+ ($originalClass != $record->ClassName) || self::$ForceReload == true) {
+ FormResponse::add("$('Form_EditForm').getPageFromServer($record->ID);");
+ }
+
+ if( ($record->class != 'VirtualPage') && $originalURLSegment != $record->URLSegment) {
+ $message .= " Changed URL to '$record->URLSegment'";
+ FormResponse::add("\$('Form_EditForm').elements.URLSegment.value = \"$record->URLSegment\";");
+ FormResponse::add("\$('Form_EditForm_StageURLSegment').value = \"{$record->URLSegment}\";");
+ }
+
+ // After reloading action
+ if($originalStatus != $record->Status) {
+ $message .= " Status changed to '$record->Status'";
+ }
+
+ $record->write();
+
+ $result .= $this->getActionUpdateJS($record);
+ FormResponse::status_message($message, "good");
+
+ FormResponse::update_status($record->Status);
+
+
+ }
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Return a piece of javascript that will update the actions of the main form
+ */
+ public function getActionUpdateJS($record) {
+ // Get the new action buttons
+ $tempForm = $this->getEditForm($record->ID);
+ $actionList = '';
+ foreach($tempForm->Actions() as $action) {
+ $actionList .= $action->Field() . ' ';
+ }
+
+ FormResponse::add("$('Form_EditForm').loadActionsFromString('" . Convert::raw2js($actionList) . "');");
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Return JavaScript code to generate a tree node for the given page, if visible
+ */
+ public function addTreeNodeJS($page, $select = false) {
+ $parentID = (int)$page->ParentID;
+ $title = Convert::raw2js($page->TreeTitle());
+ $response = <<ID, \"$title\", \"$page->class\");
+var parentNode = $('sitetree').getTreeNodeByIdx($parentID);
+if(parentNode) parentNode.appendTreeNode(newNode);\n"
+($selected ? "newNode.selectTreeNode();\n" : "") ;
+JS;
+ FormResponse::add($response);
+ return FormResponse::respond();
+ }
+ /**
+ * Return JavaScript code to remove a tree node for the given page, if it exists.
+ */
+ public function deleteTreeNodeJS($page) {
+ $id = $page->ID ? $page->ID : $page->OldID;
+ $response = <<stat('tree_class'), $id);
+ if($node){
+ $node->ParentID = $parentID;
+ $node->Status = "Saved (update)";
+ $node->write();
+
+ if(is_numeric($_REQUEST['CurrentlyOpenPageID'])) {
+ $currentPage = DataObject::get_by_id($this->stat('tree_class'), $_REQUEST['CurrentlyOpenPageID']);
+ if($currentPage) {
+ $cleanupJS = $currentPage->cmsCleanup_parentChanged();
+ }
+ }
+
+ FormResponse::status_message('saved', 'good');
+ FormResponse::add($cleanupJS);
+
+ }else{
+ FormResponse::status_message("Please Save Page: This page could not be upated because it hasn't been saved yet.","good");
+ }
+
+
+ return FormResponse::respond();
+ } else {
+ user_error("Error in ajaxupdateparent request; id=$id, parentID=$parentID", E_USER_ERROR);
+ }
+ }
+
+ /**
+ * Ajax handler for updating the order of a number of tree nodes
+ * $_GET[ID]: An array of node ids in the correct order
+ * $_GET[MovedNodeID]: The node that actually got moved
+ */
+ public function ajaxupdatesort() {
+ $className = $this->stat('tree_class');
+ $counter = 0;
+ $js = '';
+ $_REQUEST['ajax'] = 1;
+
+ if(is_array($_REQUEST['ID'])) {
+ $movedNode = DataObject::get_by_id($className, $_REQUEST['MovedNodeID']);
+
+ foreach($_REQUEST['ID'] as $id) {
+ if($id == $movedNode->ID) {
+ $movedNode->Sort = ++$counter;
+ $movedNode->Status = "Saved (update)";
+ $movedNode->write();
+
+ $title = Convert::raw2js($movedNode->TreeTitle());
+ $js .="$('sitetree').setNodeTitle($movedNode->ID, \"$title\")\n";
+
+ // Nodes that weren't "actually moved" shouldn't be registered as having been edited; do a direct SQL update instead
+ } else if(is_numeric($id)) {
+ ++$counter;
+ DB::query("UPDATE `$className` SET `Sort` = $counter WHERE `ID` = '$id'");
+ }
+ }
+ // Virtual pages require selected to be null if the page is the same.
+ FormResponse::add(
+ "if( $('sitetree').selected && $('sitetree').selected[0]){
+ var idx = $('sitetree').selected[0].getIdx();
+ if(idx){
+ $('Form_EditForm').getPageFromServer(idx);
+ }
+ }\n" . $js
+ );
+ FormResponse::status_message('saved', 'good');
+ } else {
+ FormResponse::error("Error in request");
+ }
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Delete a number of items
+ */
+ public function deleteitems() {
+ $ids = split(' *, *', $_REQUEST['csvIDs']);
+
+ $script = "st = \$('sitetree'); \n";
+ foreach($ids as $id) {
+ if(is_numeric($id)) {
+ DataObject::delete_by_id($this->stat('tree_class'), $id);
+ $script .= "node = st.getTreeNodeByIdx($id); if(node) node.parentTreeNode.removeTreeNode(node); $('Form_EditForm').closeIfSetTo($id); \n";
+ }
+ }
+ FormResponse::add($script);
+
+ return FormResponse::respond();
+ }
+
+ public function EditForm() {
+ $id = isset($_REQUEST['ID']) ? $_REQUEST['ID'] : $this->currentPageID();
+ if($id) return $this->getEditForm($id);
+ }
+
+ public function printable() {
+ $id = $_REQUEST['ID'] ? $_REQUEST['ID'] : $this->currentPageID();
+
+ if($id) $form = $this->getEditForm($id);
+ $form->transform(new PrintableTransformation());
+ $form->actions = null;
+
+ Requirements::clear();
+ Requirements::css('cms/css/LeftAndMain_printable.css');
+ return array(
+ "PrintForm" => $form
+ );
+ }
+
+ public function currentPageID() {
+ if(isset($_REQUEST['ID']) && is_numeric($_REQUEST['ID'])) {
+ return $_REQUEST['ID'];
+ } elseif (is_numeric($this->urlParams['ID'])) {
+ return $this->urlParams['ID'];
+ } elseif(Session::get("{$this->class}.currentPage")) {
+ return Session::get("{$this->class}.currentPage");
+ } else {
+ return null;
+ }
+ }
+
+ public function setCurrentPageID($id) {
+ Session::set("{$this->class}.currentPage", $id);
+ }
+
+ public function currentPage() {
+ $id = $this->currentPageID();
+ if($id && is_numeric($id)) {
+ return DataObject::get_by_id($this->stat('tree_class'), $id);
+ }
+ }
+
+ public function isCurrentPage(DataObject $page) {
+ return $page->ID == Session::get("{$this->class}.currentPage");
+ }
+
+ /**
+ * Return the CMS's HTML-editor toolbar
+ */
+ public function EditorToolbar() {
+ return new HtmlEditorField_Toolbar($this, "EditorToolbar");
+ }
+
+ /**
+ * Return the version number of this application
+ */
+ public function CMSVersion() {
+ $sapphireVersionFile = file_get_contents('../sapphire/silverstripe_version');
+ $jspartyVersionFile = file_get_contents('../jsparty/silverstripe_version');
+ $cmsVersionFile = file_get_contents('../cms/silverstripe_version');
+
+ if(strstr($sapphireVersionFile, "/sapphire/trunk")) {
+ $sapphireVersion = "trunk";
+ } else {
+ preg_match("/sapphire\/(?:(?:branches)|(?:tags))(?:\/rc)?\/([A-Za-z0-9._-]+)\/silverstripe_version/", $sapphireVersionFile, $matches);
+ $sapphireVersion = $matches[1];
+ }
+
+ if(strstr($jspartyVersionFile, "/jsparty/trunk")) {
+ $jspartyVersion = "trunk";
+ } else {
+ preg_match("/jsparty\/(?:(?:branches)|(?:tags))(?:\/rc)?\/([A-Za-z0-9._-]+)\/silverstripe_version/", $jspartyVersionFile, $matches);
+ $jspartyVersion = $matches[1];
+ }
+
+ if(strstr($cmsVersionFile, "/cms/trunk")) {
+ $cmsVersion = "trunk";
+ } else {
+ preg_match("/cms\/(?:(?:branches)|(?:tags))(?:\/rc)?\/([A-Za-z0-9._-]+)\/silverstripe_version/", $cmsVersionFile, $matches);
+ $cmsVersion = $matches[1];
+ }
+
+ if($sapphireVersion == $jspartyVersion && $jspartyVersion == $cmsVersion) {
+ return $sapphireVersion;
+ } else {
+ return "cms: $cmsVersion, sapphire: $sapphireVersion, jsparty: $jspartyVersion";
+ }
+ }
+
+ /**
+ * The application name is customisable by calling
+ * LeftAndMain::setApplicationName("Something New")
+ */
+ static $application_name = "SilverStripe CMS", $application_logo_text = "SilverStripe CMS";
+ static function setApplicationName($name, $logoText = null) {
+ self::$application_name = $name;
+ self::$application_logo_text = $logoText ? $logoText : $name;
+ }
+ function ApplicationName() {
+ return self::$application_name;
+ }
+ function ApplicationLogoText() {
+ return self::$application_logo_text;
+ }
+
+ static $application_logo = "cms/images/mainmenu/logo.gif", $application_logo_style = "";
+
+ static function setLogo($logo, $logoStyle) {
+ self::$application_logo = $logo;
+ self::$application_logo_style = $logoStyle;
+ self::$application_logo_text = "";
+ }
+ function LogoStyle() {
+ return "background-image: url(" . self::$application_logo . "); " . self::$application_logo_style;
+ }
+
+ /**
+ * Determine if we have reports and need to display the Reports main menu item
+ *
+ * @return boolean
+ */
+ function hasReports() {
+ $subclasses = ClassInfo::subclassesFor('Report');
+ foreach($subclasses as $class){
+ if($class != 'Report') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the base directory of the tiny_mce codebase
+ */
+ function MceRoot() {
+ return MCE_ROOT;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/MemberList.php b/code/MemberList.php
new file mode 100755
index 00000000..e26f0609
--- /dev/null
+++ b/code/MemberList.php
@@ -0,0 +1,271 @@
+group = $group;
+ else $this->group = DataObject::get_by_id('Group',$group);
+ }
+
+ $this->pageSize = $pageLimit;
+
+ if($members) $this->members = $this->memberListWithGroupID($members, $group);
+
+ $this->hidePassword = $hidePassword;
+
+ parent::__construct(null);
+ }
+
+ function memberListWithGroupID($members, $group) {
+ $newMembers = new DataObjectSet();
+ foreach($members as $member) {
+ $newMembers->push($member->customise(array("GroupID" => $group->ID)));
+ }
+ return $newMembers;
+ }
+
+ function FieldHolder() {
+ return $this->renderWith("MemberList");
+ }
+ function setGroup($group) {
+ $this->group = $group;
+ }
+ function setController($controller) {
+ $this->controller = $controller;
+ }
+
+ function GroupID() {
+ if( $this->group )
+ return $this->group->ID;
+
+ return '0';
+ }
+
+ function GetControllerName() {
+ return $this->controller->class;
+ }
+ function Members() {
+ /*if($this->members)
+ $members = $this->members;
+ else if($this->group) {
+ $members = $this->group->Members( $this->pageSize, 0 );
+ $allMembers = $this->group->Members();
+
+ if( $allMembers )
+ $total = $allMembers->Count();
+ } else
+ return null;*/
+
+ if( $this->members )
+ return $this->members;
+
+ if( !$baseGroup )
+ $baseGroup = $this->group->ID;
+
+ // Debug::message( $_REQUEST['MemberListOrderByField'] );
+
+ // construct the filter and sort
+
+ if( $_REQUEST['MemberListOrderByField'] )
+ $sort = "`" . $_REQUEST['MemberListOrderByField'] . "`" . addslashes( $_REQUEST['MemberListOrderByOrder'] );
+
+ $whereClauses = array();
+
+ $search = addslashes( $_REQUEST['MemberListSearch'] );
+
+ if( is_numeric( $_REQUEST['MemberListStart'] ) )
+ $limitClause = ( $_REQUEST['MemberListStart'] ) . ", {$this->pageSize}";
+ else
+ $limitClause = "0, {$this->pageSize}";
+
+
+ if( !empty($_REQUEST['MemberListSearch']) )
+ $whereClauses[] = "( `Email`='$search' OR `FirstName`='$search' OR `Surname`='$search' )";
+
+ if( is_numeric( $_REQUEST['MemberListBaseGroup'] ) )
+ $baseGroup = $_REQUEST['MemberListBaseGroup'];
+
+ $whereClauses[] = "`GroupID`='".$baseGroup."'";
+ $join = "INNER JOIN `Group_Members` ON `MemberID`=`Member`.`ID`";
+
+ // $_REQUEST['showqueries'] = 1;
+
+ $members = DataObject::get('Member', implode( ' AND ', $whereClauses ), $sort, $join, $limitClause );
+
+ // $_REQUEST['showqueries'] = 0;
+
+ if( is_numeric( $_REQUEST['MemberListGroup'] ) ) {
+ $baseMembers = new DataObjectSet();
+
+ if( $members )
+ foreach( $members as $member )
+ if( $member->inGroup( $_REQUEST['MemberListGroup'] ) )
+ $baseMembers->push( $member );
+ } else
+ $baseMembers = $members;
+
+ if($members){
+ $members->setPageLimits( $_REQUEST['MemberListStart'], $this->pageSize, $total );
+
+ $this->members = $this->memberListWithGroupID($members, $this->group);
+ }
+ return $this->members;
+ }
+
+ function FirstLink() {
+ if( !$_REQUEST['MemberListStart'] )
+ return null;
+
+ return "admin/security/listmembers?MemberListStart=0{$this->filterString()}";
+ }
+
+ function filterString() {
+
+ foreach( $_REQUEST as $key => $value ) {
+ if( strpos( $key, 'MemberList' ) === 0 && $key != 'MemberListStart' )
+ $filterString .= "&$key=$value";
+ }
+
+ if( !$_REQUEST['MemberListBaseGroup'] )
+ $filterString .= '&MemberListBaseGroup=' . $this->group->ID;
+
+ return $filterString;
+ }
+
+ function MemberListStart() {
+ return $_REQUEST['MemberListStart'];
+ }
+
+ function PrevLink() {
+ if( !$_REQUEST['MemberListStart'] )
+ return null;
+
+ $prevStart = $_REQUEST['MemberListStart'] - $this->pageSize;
+
+ if( $prevStart < 0 )
+ $prevStart = 0;
+
+ return "admin/security/listmembers?MemberListStart=$prevStart{$this->filterString()}";
+ }
+
+ function NextLink() {
+ $total = $this->TotalMembers();
+
+ $lastStart = $total - ( $total % $this->pageSize );
+
+ if( $_REQUEST['MemberListStart'] >= $lastStart )
+ return null;
+
+ return "admin/security/listmembers?MemberListStart={$this->pageSize}{$this->filterString()}";
+ }
+
+ function LastLink() {
+
+ $total = $this->TotalMembers();
+
+ $lastStart = $total - ( $total % $this->pageSize );
+
+ if( $_REQUEST['MemberListStart'] >= $lastStart )
+ return null;
+
+ return "admin/security/listmembers?MemberListStart=$lastStart{$this->filterString()}";
+ }
+
+ function PageSize() {
+ return $this->pageSize;
+ }
+
+ function FirstMember() {
+ return $_REQUEST['MemberListStart'] + 1;
+ }
+
+ function LastMember() {
+ return $_REQUEST['MemberListStart'] + min( $_REQUEST['MemberListStart'] + $this->pageSize, $this->TotalMembers() - $_REQUEST['MemberListStart'] );
+ }
+
+ function TotalMembers() {
+ if($this->group) $members = $this->group->Members();
+
+ if( !$members )
+ return null;
+
+ return $members->TotalItems();
+ }
+
+ function DontShowPassword(){
+ if( $this->hidePassword )
+ return true;
+
+ return $this->controller->class=='CMSMain'||$this->controller->class=='NewsletterAdmin';
+ }
+
+ function AddRecordForm() {
+ if($this->DontShowPassword())
+ {
+ return new TabularStyle(new Form($this->controller,'AddRecordForm',
+ new FieldSet(
+ new TextField("FirstName", "First Name"),
+ new TextField("Surname", "Surname"),
+ new TextField("Email", "Email"),
+ new HiddenField("GroupID", null, $this->group->ID)
+ ),
+ new FieldSet(
+ new FormAction("addmember", "Add")
+ )
+ ));
+
+ } else {
+ return new TabularStyle(new Form($this->controller,'AddRecordForm',
+ new FieldSet(
+ new TextField("FirstName", "First Name"),
+ new TextField("Surname", "Surname"),
+ new TextField("Email", "Email"),
+ new TextField("Password", "Password"),
+ new HiddenField("GroupID", null, $this->group->ID)
+ ),
+ new FieldSet(
+ new FormAction("addmember", "Add")
+ )
+ ));
+ }
+ }
+
+ function SearchField() {
+ $field = new TextField( 'MemberListSearch', 'Search' );
+ return $field->FieldHolder();
+ }
+
+ function OrderByField() {
+ $fields = new FieldGroup( new DropdownField('MemberListOrderByField','', array(
+ 'FirstName' => 'FirstName',
+ 'Surname' => 'Surname',
+ 'Email' => 'Email'
+ )),
+ new DropdownField('MemberListOrderByOrder','',array(
+ 'ASC' => 'Ascending',
+ 'DESC' => 'Descending'
+ )));
+
+ $field = new FieldGroup( new LabelField( 'Order by' ), $fields );
+ return $field->FieldHolder();
+ }
+
+ function GroupFilter() {
+
+ $groups = DataObject::get('Group');
+
+ $groupArray = array( '' => 'Any group' );
+
+ foreach( $groups as $group )
+ $groupArray[$group->ID] = $group->Title;
+
+ $field = new DropdownField('MemberListGroup','Filter by group',$groupArray );
+ return $field->FieldHolder();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/MemberTableField.php b/code/MemberTableField.php
new file mode 100755
index 00000000..c89ee59e
--- /dev/null
+++ b/code/MemberTableField.php
@@ -0,0 +1,412 @@
+group = $group;
+ } else if(is_numeric($group)){
+ $this->group = DataObject::get_by_id('Group',$group);
+ }
+ } else if(is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) {
+ $this->group = DataObject::get_by_id('Group',$_REQUEST['ctf'][$this->Name()]["ID"]);
+ }
+
+
+ $sourceClass = $this->stat("data_class");
+
+ foreach( self::$addedPermissions as $permission )
+ array_push( $this->permissions, $permission );
+
+ $fieldList = array(
+ "FirstName" => "Firstname",
+ "Surname" => "Surname",
+ "Email" => "Email"
+ );
+
+ $csvFieldList = $fieldList;
+ foreach( self::$addedCsvFields as $key => $value ) {
+ $csvFieldList[$key] = $value;
+ }
+
+ foreach( self::$addedFields as $key => $value ) {
+ $fieldList[$key] = $value;
+ }
+
+ if(!$hidePassword) {
+ $fieldList["Password"] = "Password";
+ }
+
+ if(isset($_REQUEST['ctf']['childID']) && $memberID = $_REQUEST['ctf']['childID']) {
+ $SNG_member = DataObject::get_by_id($this->stat("data_class"),$_REQUEST['ctf']['childID']);
+ } else {
+ $SNG_member = singleton(Object::getCustomClass($this->stat("data_class")));
+ }
+ $detailFormFields = $SNG_member->getCMSFields();
+ $this->detailFormValidator = $SNG_member->getValidator();
+
+ $this->pageSize = $pageLimit;
+
+ // Legacy: Use setCustomSourceItems() instead.
+ if($members) {
+ $this->customSourceItems = $this->memberListWithGroupID($members, $group);
+ }
+
+ $this->hidePassword = $hidePassword;
+
+ parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields);
+
+ Requirements::javascript("cms/javascript/MemberTableField.js");
+
+ // construct the filter and sort
+ if(isset($_REQUEST['MemberOrderByField'])) {
+ $this->sourceSort = "`" . Convert::raw2sql($_REQUEST['MemberOrderByField']) . "`" . Convert::raw2sql( $_REQUEST['MemberOrderByOrder'] );
+ }
+
+ // search
+ $search = isset($_REQUEST['MemberSearch']) ? Convert::raw2sql($_REQUEST['MemberSearch']) : null;
+ if(!empty($_REQUEST['MemberSearch'])) {
+ //$this->sourceFilter[] = "( `Email` LIKE '%$search%' OR `FirstName` LIKE '%$search%' OR `Surname` LIKE '%$search%' )";
+ $sourceF = "( ";
+ foreach( $fieldList as $k => $v )
+ $sourceF .= "`$k` LIKE '%$search%' OR ";
+ $this->sourceFilter[] = substr( $sourceF, 0, -3 ) . ")";
+ }
+
+ // filter by groups
+ // TODO Not implemented yet
+ if(isset($_REQUEST['ctf'][$this->Name()]['GroupID']) && is_numeric($_REQUEST['ctf'][$this->Name()]['GroupID'])) {
+ $this->sourceFilter[] = "`GroupID`='{$_REQUEST['ctf'][$this->Name()]['GroupID']}'";
+ } elseif($this->group) {
+ //$this->sourceFilter[] = "`GroupID`='{$this->group->ID}'";
+ // If the table is not clean (without duplication), the total and navigation wil not work well, so uncheck the big line below
+ $this->sourceFilter[] = "`Group_Members`.`ID` IN (SELECT `ID` FROM `Group_Members` WHERE `GroupID`='{$this->group->ID}' GROUP BY `MemberID` HAVING MIN(`ID`))";
+ }
+
+ $this->sourceJoin = " INNER JOIN `Group_Members` ON `MemberID`=`Member`.`ID`";
+
+ $this->setFieldListCsv( $csvFieldList );
+ }
+
+ /**
+ * Overridden functions
+ */
+
+
+ function sourceID() {
+ return $this->group->ID;
+ }
+
+ function AddLink() {
+ return "{$this->PopupBaseLink()}&methodName=add";
+ }
+
+ function DetailForm() {
+ $ID = Convert::raw2xml($_REQUEST['ctf']['ID']);
+ $childID = isset($_REQUEST['ctf']['childID']) ? Convert::raw2xml($_REQUEST['ctf']['childID']) : 0;
+ $childClass = Convert::raw2xml($_REQUEST['fieldName']);
+ $methodName = isset($_REQUEST['methodName']) ? $_REQUEST['methodName'] : '';
+
+ if($methodName == "add") {
+ $parentIdName = $this->getParentIdName($childClass,$this->getParentClass());
+ if(!$parentIdName) {
+ user_error("ComplexTableField::DetailForm() Dataobject does not seem to have an 'has-one'-relationship", E_USER_WARNING);
+ return;
+ }
+ $this->detailFormFields->push(new HiddenField('parentClass'," ",$this->getParentClass()));
+ }
+
+ // the ID field confuses the Controller-logic in finding the right view for ReferencedField
+ $this->detailFormFields->removeByName('ID');
+
+ $this->detailFormFields->push(new HiddenField("ctf[ID]"," ",$ID));
+ // add a namespaced ID instead thats "converted" by saveComplexTableField()
+ $this->detailFormFields->push(new HiddenField("ctf[childID]","",$childID));
+ $this->detailFormFields->push(new HiddenField("ClassName","",$this->sourceClass));
+
+ $form = new MemberTableField_Popup($this, "DetailForm", $this->detailFormFields, $this->sourceClass, $methodName == "show", $this->detailFormValidator);
+
+ if (is_numeric($childID)) {
+ if ($methodName == "show" || $methodName == "edit") {
+ $childData = DataObject::get_by_id($this->sourceClass, $childID);
+ $form->loadDataFrom($childData);
+ }
+ }
+
+ if ($methodName == "show") {
+ $form->makeReadonly();
+ }
+
+ return $form;
+ }
+
+ function SearchForm() {
+ $searchFields = new FieldGroup(
+ new TextField('MemberSearch', 'Search'),
+ new HiddenField("ctf[ID]",'',$this->group->ID),
+ new HiddenField('MemberFieldName','',$this->name),
+ new HiddenField('MemberDontShowPassword','',$this->hidePassword)
+ );
+
+ $orderByFields = new FieldGroup(
+ new LabelField('Order by'),
+ new FieldSet(
+ new DropdownField('MemberOrderByField','', array(
+ 'FirstName' => 'FirstName',
+ 'Surname' => 'Surname',
+ 'Email' => 'Email'
+ )),
+ new DropdownField('MemberOrderByOrder','',array(
+ 'ASC' => 'Ascending',
+ 'DESC' => 'Descending'
+ ))
+ )
+ );
+
+ $groups = DataObject::get('Group');
+ $groupArray = array('' => 'Any group');
+ foreach( $groups as $group ) {
+ $groupArray[$group->ID] = $group->Title;
+ }
+ $groupFields = new DropdownField('MemberGroup','Filter by group',$groupArray );
+
+ $actionFields = new LiteralField('MemberFilterButton',' ');
+
+ $fieldContainer = new FieldGroup(
+ $searchFields,
+ // $orderByFields,
+ // $groupFields,
+ $actionFields
+ );
+
+ return $fieldContainer->FieldHolder();
+ }
+
+
+ /**
+ * Add existing member to group rather than creating a new member
+ */
+ function addtogroup() {
+ $data = $_REQUEST;
+ unset($data['ID']);
+
+ if(!is_numeric($data['ctf']['ID'])) {
+ FormResponse::status_messsage('Adding failed', 'bad');
+ }
+
+ $className = $this->stat('data_class');
+ $record = new $className();
+
+ $record->update($data);
+ $record->write();
+
+ // To Avoid duplication in the Group_Members table if the ComponentSet.php is not modified just uncomment le line below
+
+ //if( ! $record->isInGroup( $data['ctf']['ID'] ) )
+ $record->Groups()->add( $data['ctf']['ID'] );
+
+ $this->sourceItems();
+
+ // TODO add javascript to highlight added row (problem: might not show up due to sorting/filtering)
+ FormResponse::update_dom_id($this->id(), $this->renderWith($this->template), true);
+ FormResponse::status_message('Added member to group', 'good');
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Custom delete implementation:
+ * Remove member from group rather than from the database
+ */
+ function delete() {
+ $groupID = Convert::raw2sql($_REQUEST["ctf"]["ID"]);
+ $memberID = Convert::raw2sql($_REQUEST["ctf"]["childID"]);
+ if(is_numeric($groupID) && is_numeric($memberID)) {
+ $member = DataObject::get_by_id('Member', $memberID);
+ $member->Groups()->remove($groupID);
+ } else {
+ user_error("MemberTableField::delete: Bad parameters: Group=$groupID, Member=$memberID", E_USER_ERROR);
+ }
+
+ return FormResponse::respond();
+
+ }
+
+
+
+ /**
+ * #################################
+ * Utility Functions
+ * #################################
+ */
+ function getParentClass() {
+ return "Group";
+ }
+
+ function getParentIdName($childClass,$parentClass){
+ return "GroupID";
+ }
+
+
+ /**
+ * #################################
+ * Custom Functions
+ * #################################
+ */
+ function memberListWithGroupID($members, $group) {
+ $newMembers = new DataObjectSet();
+ foreach($members as $member) {
+ $newMembers->push($member->customise(array("GroupID" => $group->ID)));
+ }
+ return $newMembers;
+ }
+
+ function setGroup($group) {
+ $this->group = $group;
+ }
+ function setController($controller) {
+ $this->controller = $controller;
+ }
+
+ function GetControllerName() {
+ return $this->controller->class;
+ }
+
+ /**
+ * Add existing member to group by name (with JS-autocompletion)
+ */
+ function AddRecordForm() {
+ $fields = new FieldSet();
+ foreach($this->FieldList() as $fieldName=>$fieldTitle) {
+ $fields->push(new TextField($fieldName));
+ }
+ $fields->push(new HiddenField("ctf[ID]", null, $this->group->ID));
+
+ return new TabularStyle(new Form($this->controller,'AddRecordForm',
+ $fields,
+ new FieldSet(
+ new FormAction("addtogroup", "Add")
+ )
+ ));
+ }
+
+ /**
+ * Cached version for getting the appropraite members for this particular group.
+ *
+ * This includes getting inherited groups, such as groups under groups.
+ */
+ function sourceItems(){
+ // Caching.
+ if($this->sourceItems) {
+ return $this->sourceItems;
+ }
+
+ // Setup limits
+ $limitClause = "";
+ if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
+ $limitClause = ($_REQUEST['ctf'][$this->Name()]['start']) . ", {$this->pageSize}";
+ } else {
+ $limitClause = "0, {$this->pageSize}";
+
+ }
+
+ // We use the group to get the members, as they already have the bulk of the look up functions
+ $start = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
+ $this->sourceItems = $this->group->Members(
+ $this->pageSize, // limit
+ $start, // offset
+ $this->sourceFilter,
+ $this->sourceSort
+ );
+ $this->unpagedSourceItems = $this->group->Members( "", "", $this->sourceFilter, $this->sourceSort );
+ $this->totalCount = ($this->sourceItems) ? $this->sourceItems->TotalItems() : 0;
+ return $this->sourceItems;
+ }
+
+ function TotalCount() {
+ $this->sourceItems(); // Called for its side-effect of setting total count
+ return $this->totalCount;
+ }
+}
+
+
+
+
+
+class MemberTableField_Popup extends ComplexTableField_Popup {
+ function __construct($controller, $name, $fields, $sourceClass, $readonly=false, $validator = null) {
+
+ // DO NOT CHANGE THE ORDER OF THESE JS FILES. THESE ARE ONLY REQUIRED FOR THIS INSTANCE !!!11onetwo
+
+ parent::__construct($controller, $name, $fields, $sourceClass, $readonly, $validator);
+
+ Requirements::javascript("cms/javascript/MemberTableField.js");
+ Requirements::javascript("cms/javascript/MemberTableField_popup.js");
+ }
+
+
+ function saveComplexTableField() {
+ $id = Convert::raw2sql($_REQUEST['ctf']['childID']);
+
+ if (is_numeric($id)) {
+ $childObject = DataObject::get_by_id($this->sourceClass, $id);
+ } else {
+ $childObject = new $this->sourceClass();
+ }
+ $this->saveInto($childObject);
+ $childObject->write();
+
+ $childObject->Groups()->add($_REQUEST['ctf']['ID']);
+
+ // if ajax-call in an iframe, close window by javascript, else redirect to referrer
+ if(!Director::is_ajax()) {
+ Director::redirect(substr($_SERVER['REQUEST_URI'],0,strpos($_SERVER['REQUEST_URI'],"?")));
+ }
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/BatchProcess.php b/code/Newsletter/BatchProcess.php
new file mode 100755
index 00000000..93430717
--- /dev/null
+++ b/code/Newsletter/BatchProcess.php
@@ -0,0 +1,93 @@
+current = 0;
+
+ if( $collection ) {
+ if( is_array( $collection ) )
+ $this->objects = $collection;
+ elseif( is_a( $collection, 'DataObjectSet' ) ) {
+ $this->objects = $collection->toArray();
+
+ } else
+ $this->objects = array( $collection );
+ }
+ }
+
+ function runToCompletion() {
+ $this->scriptOutput = false;
+ $this->current = 0;
+ $ignore = $this->next( count( $this->objects ) );
+
+ $this->complete();
+ }
+
+ function getID() {
+ return $this->id;
+ }
+
+ function next() {
+ self::addProcess( $this );
+ return $this->id.':'.$this->current.'/'.count( $this->objects );
+ }
+
+ function start() {
+ $this->current = 0;
+ $this->id = self::generateID();
+
+ if( !$this->objects || count( $this->objects ) === 0 )
+ return $this->complete();
+
+ return $this->next();
+ }
+
+ function complete() {
+ self::removeProcess( $this );
+ }
+
+ static function generateID() {
+ return count(Session::get('BatchProcesses')) + 1;
+ }
+
+ static function addProcess( $process ) {
+ Session::set('BatchProcesses.' . ($process->getID() - 1), serialize($process));
+ }
+
+ static function removeProcess( $process ) {
+ Session::clear('BatchProcesses.' . ($process->getID() - 1));
+ }
+}
+
+class BatchProcess_Controller extends Controller {
+
+ function next() {
+
+ $processID = $this->urlParams['ID'];
+
+ if( !$processID ) {
+ return "ERROR:Could not continue process";
+ }
+
+ $process = unserialize(Session::get('BatchProcesses.' . ($this->urlParams['ID'] - 1)));
+
+ if( !$process ) {
+ return "ERROR:Could not continue process";
+ }
+
+ if( $this->urlParams['Batch'] )
+ return $process->next( $this->urlParams['Batch'] );
+ else
+ return $process->next();
+ }
+}
+?>
diff --git a/code/Newsletter/BouncedList.php b/code/Newsletter/BouncedList.php
new file mode 100755
index 00000000..7c88b3fe
--- /dev/null
+++ b/code/Newsletter/BouncedList.php
@@ -0,0 +1,46 @@
+nlType = $newsletterType;
+ else
+ $this->nlType = DataObject::get_by_id( 'NewsletterType', $newsletterType );
+ }
+
+ function setController($controller) {
+ $this->controller = $controller;
+ }
+
+ function FieldHolder() {
+ return $this->renderWith( 'NewsletterAdmin_BouncedList' );
+ }
+
+ function Entries() {
+
+ $id = $this->nlType->GroupID;
+
+ $bounceRecords = DataObject::get( 'Email_BounceRecord', "`GroupID`='$id'", null, "INNER JOIN `Group_Members` USING(`MemberID`)" );
+
+ //user_error($id, E_USER_ERROR );
+
+ if( !$bounceRecords )
+ return null;
+
+ foreach( $bounceRecords as $bounceRecord ) {
+ if( $bounceRecord ) {
+ $bouncedUsers[] = new ArrayData( array(
+ 'Record' => $bounceRecord,
+ 'Member' => DataObject::get_by_id( 'Member', $bounceRecord->MemberID )
+ ));
+ }
+ }
+
+ return new DataObjectSet( $bouncedUsers );
+ }
+}
+?>
diff --git a/code/Newsletter/Newsletter.php b/code/Newsletter/Newsletter.php
new file mode 100755
index 00000000..533f59c4
--- /dev/null
+++ b/code/Newsletter/Newsletter.php
@@ -0,0 +1,99 @@
+Parent()->GroupID);
+
+ $ret = new FieldSet(
+ new TabSet("Root",
+ $mailTab = new Tab("Mail",
+ new TextField("Subject", "Subject", $this->Subject),
+ new HtmlEditorField("Content", "Content")
+ )
+ )
+ );
+
+ if( $this->Status != 'Draft' ) {
+ $mailTab->push( new ReadonlyField("SendDate", "Sent at", $this->SendDate) );
+ }
+
+
+ return $ret;
+ }
+
+ function getTitle() {
+ return $this->getField('Subject');
+ }
+
+ function getNewsletterType() {
+ return DataObject::get_by_id('NewsletterType', $this->ParentID);
+ }
+
+ static $db = array(
+ "Status" => "Enum('Draft, Send', 'Draft')",
+ "Content" => "HTMLText",
+ "Subject" => "Varchar(255)",
+ "SentDate" => "Datetime",
+
+ );
+
+ static $has_one = array(
+ "Parent" => "NewsletterType",
+ );
+
+ static $has_many = array(
+ "Recipients" => "Newsletter_Recipient",
+ );
+
+ static function newDraft( $parentID, $subject, $content ) {
+ if( is_numeric( $parentID ) ) {
+ $newsletter = new Newsletter();
+ $newsletter->Status = 'Draft';
+ $newsletter->Title = $newsletter->Subject = $subject;
+ $newsletter->ParentID = $parentID;
+ $newsletter->Content = $content;
+ $newsletter->write();
+ } else {
+ user_error( $parentID, E_USER_ERROR );
+ }
+
+ return $newsletter;
+ }
+}
+
+class Newsletter_Recipient extends DataObject {
+ static $db = array(
+ "ParentID" => "Int",
+ );
+ static $has_one = array(
+ "Member" => "Member",
+ );
+}
+
+class Newsletter_Email extends Email_Template {
+ protected $nlType;
+
+ function __construct($nlType) {
+ $this->nlType = $nlType;
+ parent::__construct();
+ }
+
+ function setTemplate( $template ) {
+ $this->ss_template = $template;
+ }
+
+ function UnsubscribeLink(){
+ $emailAddr = $this->To();
+ $nlTypeID = $this->nlType->ID;
+ return Director::absoluteBaseURL()."unsubscribe/$emailAddr/$nlTypeID";
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/NewsletterEmailProcess.php b/code/Newsletter/NewsletterEmailProcess.php
new file mode 100755
index 00000000..40031361
--- /dev/null
+++ b/code/Newsletter/NewsletterEmailProcess.php
@@ -0,0 +1,97 @@
+subject = $subject;
+ $this->body = $body;
+ $this->from = $from;
+ $this->newsletter = $newsletter;
+ $this->nlType = $nlType;
+ $this->messageID = $messageID;
+
+ $groupID = $nlType->GroupID;
+
+ parent::__construct( DataObject::get( 'Member', "`GroupID`='$groupID'", null, "INNER JOIN `Group_Members` ON `MemberID`=`Member`.`ID`" ) );
+
+ }
+
+ function next( $count = 10 ) {
+ $max = $this->current + $count;
+
+ $max = $max < count( $this->objects ) ? $max : count( $this->objects );
+
+ while($this->current < $max) {
+ $index = $this->current++;
+ $member = $this->objects[$index];
+
+ // check to see if the user has unsubscribed from the mailing list
+ // TODO Join in the above query first
+ $unsubscribeRecord = DataObject::get_one('Member_UnsubscribeRecord', "`MemberID`='{$member->ID}' AND `NewsletterTypeID`='{$this->nlType->ID}'");
+
+ if( !$unsubscribeRecord ) {
+
+ $address = $member->Email;
+
+ /**
+ * Email Blacklisting Support
+ */
+ if($member->BlacklistedEmail && Email_BlackList::isBlocked($this->to)){
+ $bounceRecord = new Email_BounceRecord();
+ $bounceRecord->BounceEmail = $member->Email;
+ $bounceRecord->BounceTime = date("Y-m-d H:i:s",time());
+ $bounceRecord->BounceMessage = "BlackListed Email";
+ $bounceRecord->MemberID = $member->ID;
+ $bounceRecord->write();
+ continue;
+ }
+
+ $e = new Newsletter_Email($this->nlType);
+ $e->setBody( $this->body );
+ $e->setSubject( $this->subject );
+ $e->setFrom( $this->from );
+ $e->setTemplate( $this->nlType->Template );
+
+
+ $e->populateTemplate( array( 'Member' => $member, 'FirstName' => $member->FirstName ) );
+ $this->sendToAddress( $e, $address, $this->messageID );
+ }
+ }
+
+ if( $this->current >= count( $this->objects ) )
+ return $this->complete();
+ else
+ return parent::next();
+ }
+
+ private function sendToAddress( $email, $address, $messageID = null ) {
+ $email->setTo( $address );
+ $email->send( $messageID );
+ }
+
+ function complete() {
+ parent::complete();
+
+ if( $this->newsletter->SentDate )
+ $resent = true;
+
+ $this->newsletter->SentDate = 'now';
+ $this->newsletter->Status = 'Send';
+ $this->newsletter->write();
+
+ $message = "statusMessage('Sent " . count( $this->objects ) . " emails successfully','good');";
+
+ if( $resent )
+ return $message."resent_ok( '{$this->nlType->ID}', '{$this->newsletter->ID}' )";
+ else
+ return $message."draft_sent_ok( '{$this->nlType->ID}', '{$this->newsletter->ID}' )";
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/NewsletterList.php b/code/Newsletter/NewsletterList.php
new file mode 100755
index 00000000..46fcacd7
--- /dev/null
+++ b/code/Newsletter/NewsletterList.php
@@ -0,0 +1,57 @@
+mailType = $mailtype;
+ else $this->mailType = DataObject::get_by_id('NewsletterType',$mailtype);
+ $this->status = $status;
+ parent::__construct(null);
+ }
+
+ function FieldHolder() {
+ return $this->renderWith("NewsletterList");
+ }
+
+
+ function setMailType($mailtype) {
+ $this->mailType = $mailtype;
+ }
+
+ function setController($controller) {
+ $this->controller = $controller;
+ }
+
+ function Newsletters() {
+ return DataObject::get( 'Newsletter', "`ParentID`='{$this->mailType->ID}' AND `Status`='{$this->status}'" );
+ }
+
+ function DraftNewsletters() {
+ return $this->mailType->DraftNewsletters();
+ }
+
+ function SentNewsletters() {
+ return $this->mailType->SentNewsletters();
+ }
+
+ function Status() {
+ return $this->status;
+ }
+
+
+/* function AddRecordForm() {
+ return new TabularStyle(new Form($this->controller,'AddRecordForm',
+ new FieldSet(
+ new TextField("FirstName", "First Name"),
+ new TextField("Surname", "Surname"),
+ new TextField("Email", "Email"),
+ new TextField("Password", "Password"),
+ new HiddenField("GroupID", null, $this->group->ID)
+ ),
+ new FieldSet(
+ new FormAction("addmember", "Add")
+ )
+ ));
+ }*/
+}
+
+?>
diff --git a/code/Newsletter/NewsletterType.php b/code/Newsletter/NewsletterType.php
new file mode 100755
index 00000000..70ef1b35
--- /dev/null
+++ b/code/Newsletter/NewsletterType.php
@@ -0,0 +1,55 @@
+ "Varchar",
+ "Template" => "Varchar",
+ "FromEmail" => "Varchar",
+ "Sent" => "Datetime"
+ );
+ static $has_one = array(
+ "Parent" => "SiteTree",
+ "Group" => "Group",
+ );
+ static $has_many = array(
+ "Newsletters" => "Newsletter",
+ );
+
+ function DraftNewsletters() {
+ return DataObject::get("Newsletter","ParentID={$this->ID} AND Status ='Draft'");
+ }
+
+ function SentNewsletters() {
+ return DataObject::get("Newsletter","ParentID={$this->ID} AND Status ='Send'");
+ }
+
+ function Recipients() {
+ return DataObject::get("Member", "Group_Members.GroupID = {$this->GroupID}", "", "JOIN Group_Members on Group_Members.MemberID = Member.ID");
+ }
+
+ function delete() {
+ foreach( $this->Newsletters() as $newsletter )
+ $newsletter->delete();
+
+ parent::delete();
+ }
+
+ /**
+ * Updates the group so the security section is also in sync with
+ * the curent newsletters.
+ */
+ function onBeforeWrite(){
+ if($this->ID){
+ $group = $this->Group();
+ if($group->Title != "$this->Title"){
+ $group->Title = "Mailing List: " . $this->Title;
+ // Otherwise the code would have mailing list in it too :-(
+ $group->Code = SiteTree::generateURLSegment($this->Title);
+ $group->write();
+ }
+ }
+ parent::onBeforeWrite();
+ }
+}
+?>
diff --git a/code/Newsletter/RecipientImportField.php b/code/Newsletter/RecipientImportField.php
new file mode 100755
index 00000000..0c3b62a4
--- /dev/null
+++ b/code/Newsletter/RecipientImportField.php
@@ -0,0 +1,372 @@
+ array( 'title', 'salutation' ),
+ 'FirstName' => array( 'firstname', 'christianname', 'givenname' ),
+ 'Surname' => array( 'lastname','surname', 'familyname' ),
+ 'Email' => array( 'email', 'emailaddress' ),
+ 'Address' => array( 'address' ),
+ 'PhoneNumber' => array('phone','phonenumber'),
+ 'JobTitle' => array( 'jobtitle' ),
+ 'Organisation' => array( 'organisation', 'organization' ),
+ 'EmailType' => array( 'htmlorplaintext', 'emailtype' )
+ );
+
+ static $custom_set_fields = array();
+
+ public static function setCustomField( $getMapCode, $fieldName ) {
+ self::$custom_set_fields[] = array(
+ 'Code' => $getMapCode,
+ 'Field' => $fieldName
+ );
+ }
+
+ function __construct( $name, $title, $memberGroup, $memberClass = 'Member', $form = null ) {
+ $this->memberGroup = $memberGroup;
+ $this->memberClass = $memberClass;
+ parent::__construct( $name, $title, null, $form );
+ }
+
+ function Field() {
+ $frameURL = Director::absoluteBaseURL() . 'admin/newsletter/displayfilefield/' . $this->typeID;
+
+ return "";
+ }
+
+ function setTypeID( $id ) {
+ $this->typeID = $id;
+ }
+
+ function CustomSetFields() {
+ $fields = new FieldSet();
+
+
+
+ foreach( self::$custom_set_fields as $customField ) {
+ eval( '$map = ' . $customField['Code'] .';' );
+
+ $noneMap = array( 0 => '(Not set)' );
+
+ $map = $noneMap + $map;
+
+ $fields->push( new DropdownField( 'Set['.$customField['Field'].']', $customField['Field'], $map ) );
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Returns HTML to be displayed inside the IFrame
+ */
+ static function fileupload() {
+
+ }
+
+ /**
+ * Returns the table of results to be displayed in the table of
+ * details loaded from the file
+ */
+ function displaytable() {
+
+ // Check that a file was uploaded
+
+ $tempFile = fopen( $_FILES['ImportFile']['tmp_name'], 'r' );
+
+ // display some error if the file cannot be opened
+
+ $this->clientFileName = $_FILES['ImportFile']['name'];
+
+ while( ( $row = fgetcsv( $tempFile ) ) !== false ) {
+
+ if( !$this->tableColumns ) {
+ $this->parseTableHeader( $row );
+ } else {
+ $newRow = array();
+ $newSessionTableRow = array();
+
+ foreach( $row as $cell ) {
+ $newRow[] = $this->parseCSVCell( $cell );
+ $newSessionTableRow[] = $cell;
+ }
+
+ $cells = new DataObjectSet( $newRow );
+ $table[] = $cells->customise( array( 'Cells' => $cells ) );
+
+ $sessionTable[] = $newSessionTableRow;
+ }
+ }
+
+ fclose( $tempFile );
+
+ $this->table = new DataObjectSet( $table );
+
+ Session::set("ImportFile.{$_REQUEST['ID']}", $sessionTable);
+
+ return $this->renderWith( 'Newsletter_RecipientImportField_Table' );
+ }
+
+ /**
+ * Determines what type each column is
+ */
+ function parseTableHeader( $columns ) {
+
+ $columnSource = array_combine(
+ array_keys( self::$column_types ),
+ array_keys( self::$column_types )
+ );
+
+ $columnSource = array_merge( array( 'Unknown' => 'Unknown' ), $columnSource );
+
+ foreach( $columns as $cell ) {
+ $columnType = $this->getColumnType( $this->parseCSVCell( $cell )->Value() );
+ $this->tableColumns[] = new DropdownField( 'ImportFileColumns[' . (string)( $colCount++ ) . ']', '', $columnSource, $columnType );
+ }
+ }
+
+ function parseCSVCell( $cell ) {
+ return new RecipientImportField_Cell( $cell );
+ }
+
+ function getColumnType( $cell ) {
+ $cell = strtolower( $cell );
+ $escapedValue = preg_replace( '/[^a-z]/', '', $cell );
+
+ foreach( self::$column_types as $type => $aliases ) {
+ if( in_array( $escapedValue, $aliases ) )
+ return $type;
+ }
+
+ return 'Unknown';
+ }
+
+ function setController( $controller ) {
+ $this->controller = $controller;
+ }
+
+ /**
+ * Set of table column headers
+ */
+ function ColumnHeaders() {
+ return new DataObjectSet( $this->tableColumns );
+ }
+
+ function Rows() {
+ return $this->table;
+ }
+
+ function FileName() {
+ return $this->clientFileName;
+ }
+
+ function TypeID() {
+ return $this->typeID;
+ }
+}
+
+class RecipientImportField_Cell extends ViewableData {
+ protected $value;
+
+ function __construct( $value ) {
+ $this->value = $value;
+ }
+
+ function Value() {
+ return $this->value;
+ }
+}
+
+class RecipientImportField_UploadForm extends Form {
+ function import( $data, $form ) {
+ $id = $data['ID'];
+ $mailType = DataObject::get_one("NewsletterType", "ID = $id");
+ if($mailType->GroupID)
+ $group = DataObject::get_one("Group", "ID = $mailType->GroupID");
+
+ $recipientField = new RecipientImportField("ImportFile","Import from file", $group );
+ $recipientField->setTypeID( $id );
+
+ // if the file is not valid, then return an error
+ if( empty( $_FILES ) || empty( $_FILES['ImportFile'] ) || $_FILES['ImportFile']['size'] == 0 )
+ return $this->customise( array( 'ID' => $id, "UploadForm" => $this->controller->UploadForm( $id ), 'ErrorMessage' => 'Please choose a CSV file to import' ) )->renderWith('Newsletter_RecipientImportField');
+ elseif( !$this->isValidCSV( $_FILES['ImportFile'] ) ) {
+ /*if( file_exists( $_FILES['ImportFile']['tmp_name'] ) ) unlink( $_FILES['ImportFile']['tmp_name'] );
+ unset( $_FILES['ImportFile'] );*/
+ return $this->customise( array( 'ID' => $id, "UploadForm" => $this->controller->UploadForm( $id ), 'ErrorMessage' => 'The selected file was not a CSV file' ) )->renderWith('Newsletter_RecipientImportField');
+ } else
+ return $recipientField->displaytable();
+ }
+
+ function isValidCSV( $file ) {
+ return preg_match( '/.*\.csv$/', $file['name'] ) > 0;
+ }
+
+ function confirm( $data, $form ) {
+ $id = $data['ID'];
+ $mailType = DataObject::get_one("NewsletterType", "ID = $id");
+ if($mailType->GroupID)
+ $group = DataObject::get_one("Group", "ID = $mailType->GroupID");
+ return $this->importMembers( $id, $group, $data['ImportFileColumns'], $data['Set'] );
+ }
+
+ function cancel( $data, $form ) {
+ $newForm = $this->controller->UploadForm( $data['ID'] );
+ return $newForm->forTemplate();
+ }
+
+ function action_import( $data, $form ) {
+ return $this->import( $data, $form );
+ }
+
+ function action_confirm( $data, $form ) {
+ return $this->confirm( $data, $form );
+ }
+
+ function action_cancel( $data, $form ) {
+ return $this->cancel( $data, $form );
+ }
+
+ /**
+ * Import members from the uploaded file
+ */
+ protected function importMembers( $id, $group, $cols, $setFields, $primaryColType = 'Email' ) {
+
+ $startTime = time();
+
+ $importData = Session::set("ImportFile.{$id}");
+
+ $validColumns = array_keys( RecipientImportField::$column_types );
+
+ $columnMap = array_flip( $cols );
+
+ // Debug::show($columnMap);
+
+ // locate the primary column's index
+ $primaryColumn = /*array_search( $primaryColType, $validColumns );*/ $columnMap[$primaryColType];
+
+ // changed fields
+ $changedFields = array();
+
+ // intersect the list of valid columns with the column map to find the columns we need
+ $importColumns = array_intersect( $validColumns, $cols );
+
+ // statistics
+ $newMembers = 0;
+ $changedMembers = 0;
+ $skippedMembers = 0;
+
+ // the class that the imported members will become
+ $newMemberClass = Object::getCustomClass( 'Member' );
+
+ // for each row, add a new member or overwrite an existing member
+ foreach( $importData as $newMemberRow ) {
+
+ // skip rows with an empty value for the primary column
+ if( empty( $newMemberRow[$primaryColumn] ) ) {
+ $skippedMembers++;
+ continue;
+ }
+
+ // remember to check if the user has unsubscribed
+ $trackChanges = true;
+
+ // TODO: Write DataObject::update
+ $member = $this->findMember( $newMemberRow[$primaryColumn] );
+
+ if( !$member ) {
+ $newMembers++;
+ $trackChanges = false;
+ $member = Object::create("Member");
+ } else {
+ // skip this member if the are unsubscribed
+ if( $member->Unsubscribed ) {
+ $skippedMembers++;
+ continue;
+ }
+
+ if( $member->class != $newMemberClass )
+ $member->setClassName( $newMemberClass );
+
+ $changedMembers++;
+ }
+
+ // add each of the valid columns
+ foreach( $importColumns as $datum ) {
+
+ // perform any required conversions
+ $newValue = trim( $newMemberRow[$columnMap[$datum]] );
+ $oldValue = trim( $member->$datum );
+
+ // Debug::message( "$datum@{$columnMap[$datum]}" );
+
+ // track the modifications to the member data
+ if( $trackChanges && $newValue != $oldValue && $datum != $primaryColumn ) {
+ $changedFields[] = array(
+ $newMemberRow[$primaryColumn],
+ "$datum:\n$oldValue",
+ "$datum:\n$newValue"
+ );
+
+ $numChangedFields++;
+ }
+
+ $member->$datum = $newValue;
+ }
+
+ // set any set fields
+ if( $setFields )
+ foreach( $setFields as $fieldName => $fieldValue )
+ $member->$fieldName = $fieldValue;
+
+ // add member to group
+ $member->write();
+ $member->Groups()->add( $group->ID );
+ }
+
+ $numChangedFields = count( $changedFields );
+ $this->notifyChanges( $changedFields );
+
+ // TODO Refresh window
+ $customData = array(
+ 'ID' => $id,
+ "UploadForm" => $this->controller->UploadForm( $id ),
+ 'ImportMessage' => 'Imported new members',
+ 'NewMembers' => (string)$newMembers,
+ 'ChangedMembers' => (string)$changedMembers,
+ 'ChangedFields' => (string)$numChangedFields,
+ 'SkippedRecords' => (string)$skippedMembers,
+ 'Time' => time() - $startTime
+ );
+ return $this->customise( $customData )->renderWith('Newsletter_RecipientImportField');
+ }
+
+ function findMember( $email ) {
+ $email = addslashes( $email );
+ return DataObject::get_one( 'Member', "`Email`='$email'" );
+ }
+
+ function notifyChanges( $changes ) {
+ $email = new Email( Email::getAdminEmail(), Email::getAdminEmail(), 'Changed Fields' );
+
+ $body = "";
+
+ foreach( $changes as $change ) {
+ $body .= "-------------------------------\n";
+ $body .= implode( ' ', $change ) . "\n";
+ }
+
+ $email->setBody( $body );
+ $email->send();
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/SubscribeForm.php b/code/Newsletter/SubscribeForm.php
new file mode 100755
index 00000000..6d2aa954
--- /dev/null
+++ b/code/Newsletter/SubscribeForm.php
@@ -0,0 +1,254 @@
+ 'EditableEmailField(CustomParameter=Email,CanDelete=0)',
+ 'First name' => 'EditableTextField(CustomParameter=FirstName)',
+ 'Last name' => 'EditableTextField(CustomParameter=Surname)',
+ 'Address' => 'EditableTextField(Rows=3,CustomParameter=Address)',
+ 'Job title' => 'EditableTextField(CustomParameter=JobTitle)',
+ 'Organisation' => 'EditableTextField(CustomParameter=Organisation)',
+ 'Mail format' => 'EditableRadioField(Options=1:HTML,Options=0:Text__only,CustomParameter=HTMLEmail)'
+ );
+
+ static $db = array(
+ 'Subscribe' => 'Boolean',
+ 'AllNewsletters' => 'Boolean',
+ 'Subject' => 'Varchar'
+ );
+
+ static $defaults = array(
+ "OnCompleteMessage" => "Thanks, you have been added to our mailing list.
",
+ );
+
+ static $has_many = array(
+ 'Newsletters' => 'NewsletterType'
+ );
+
+ function __construct( $data = null, $isSingleton = false ) {
+ parent::__construct( $data, $isSingleton );
+
+ if( $data || $isSingleton )
+ return;
+
+ $this->addDefaultFields();
+ }
+
+ private function addDefaultFields() {
+ $f = $this->Fields();
+
+ // check that the required fields exist
+ $count = 1;
+
+ foreach( self::$required_fields as $defaultName => $typeString ) {
+
+ list( $type, $typeValue ) = $this->parseType( $typeString );
+ $newField = new $type();
+ $newField->Name = "Field" . (string)$count;
+ $newField->Title = $defaultName;
+
+ // TODO Map the field to a particular action
+
+ if( !empty( $typeValue ) ) {
+ $newField->prepopulate( $typeValue );
+ }
+
+ $newField->setField('ID', "new-" . $count);
+
+ $newField->Sort = $count;
+ $f->addWithoutWrite($newField);
+ $count++;
+ }
+ }
+
+ public function Newsletters() {
+ $components = $this->getComponents('Newsletters');
+ return $components;
+ }
+
+ public function customFormActions( $isReadonly = false ) {
+
+ $fields = parent::customFormActions( $isReadonly );
+
+ // get the newsletters in the system
+ $newsletterTypes = DataObject::get( 'NewsletterType' );
+
+ $availableNewsletters = array();
+
+ foreach( $this->Newsletters() as $subscribeTo )
+ $availableNewsletters[] = $subscribeTo->ID;
+
+ // create a checkbox for each newsletter
+ if($newsletterTypes && is_object($newsletterTypes)) {
+ foreach( $newsletterTypes as $newsletterType )
+ $nlCheckboxes[] = new CheckboxField( "CustomNewsletters[{$newsletterType->ID}]", $newsletterType->Title, in_array( $newsletterType->ID, $availableNewsletters ) );
+ }
+ $fields->push( new OptionsetField( 'AllNewsletters', '', array(
+ 1 => 'All newsletters',
+ 0 => 'Specific newsletters'
+ ),
+ $this->AllNewsletters
+ ));
+
+ $fields->push( new CompositeField( $nlCheckboxes ));
+ $fields->push( new TextField( 'Subject', 'Subject line on confirmation', $this->Subject ) );
+
+ return $fields;
+ }
+
+ function parseType( $typeString ) {
+ if( preg_match('/^([A-Za-z_]+)\(([^)]+)\)$/', $typeString, $match ) )
+ return array( $match[1], $match[2] );
+
+ return array( $typeString, null );
+ }
+
+ /**
+ *
+ */
+ public function processNewFormFields() {
+
+ }
+
+ /**
+ * Saves data related to any custom actions this form performs when submitted
+ */
+ public function customFormSave( $data, $form ) {
+
+ $newsletters = $this->Newsletters();
+
+ $this->Subscribe = !empty( $data['Subscribe'] );
+ $this->AllNewsletters = !empty( $data['AllNewsletters'] );
+ $this->Subject = $data['Subject'];
+ // Note: $data['CustomNewsletters'] was changed to $_REQUEST['CustomNewsletters'] in order to fix
+ // to fix "'Specific newsletters' option in 'newsletter subscription form' page type does not work" bug
+ // See: http://www.silverstripe.com/bugs/flat/1675
+ if( !empty( $_REQUEST['CustomNewsletters'] ) ) {
+ /*foreach( $newsletters as $newsletter ) {
+ if( $data['CustomNewsletters'][$newsletter->ID] == 'on' ) {
+ $newsletters->add( $newsletter->ID );
+ } else {
+ $newsletters->remove( $newsletter->ID );
+ }
+ }*/
+
+ if( isset($_REQUEST['CustomNewsletters'][0]) )
+ unset( $_REQUEST['CustomNewsletters'][0] );
+
+ $newsletters->setByIDList( array_keys( $_REQUEST['CustomNewsletters'] ) );
+ } else {
+ $this->AllNewsletters = true;
+ $newsletters->removeAll();
+ }
+ }
+}
+
+class SubscribeForm_SubscribeEmail extends Email_Template {
+ protected $to = '$Email';
+ protected $subject = '$Subject';
+ protected $ss_template = 'SubscribeEmail';
+ protected $from = '';
+}
+
+class SubscribeForm_Controller extends UserDefinedForm_Controller {
+
+ function process( $data, $form ) {
+
+ // Add the user to the mailing list
+ $member = Object::create("Member");
+
+ // $_REQUEST['showqueries'] = 1;
+
+ // map the editables to the data
+
+ foreach( $this->Fields() as $editable ) {
+
+ $field = $editable->CustomParameter;
+ if( !$field )
+ continue;
+
+ // Debug::message( $editable->Name . '->' . $field );
+
+ // if( $member->hasField( $field ) )
+ $member->$field = $data[$editable->Name];
+ }
+
+ // need to write the member record before adding the user to groups
+ $member->write();
+
+ $newsletters = array();
+
+ // Add member to the selected newsletters
+ if( $data['Newsletters'] ) foreach( $data['Newsletters'] as $listID ) {
+
+ if( !is_numeric( $listID ) )
+ continue;
+
+ // get the newsletter
+ $newsletter = DataObject::get_by_id( 'NewsletterType', $listID );
+
+ if( !$newsletter )
+ continue;
+
+ $newsletters[] = $newsletter;
+ // Debug::show( $newsletter->GroupID );
+
+ $member->Groups()->add( $newsletter->GroupID );
+ }
+
+ // email the user with their details
+ $templateData = array(
+ 'Email' => $member->Email,
+ 'FirstName' => $member->FirstName,
+ 'Newsletters' => new DataObjectSet( $newsletters ),
+ 'UnsubscribeLink' => Director::baseURL() . 'unsubscribe/' . $member->Email
+ );
+
+ $email = new SubscribeForm_SubscribeEmail();
+ $email->setFrom( Email::getAdminEmail() );
+ $email->setSubject( $this->Subject );
+
+ $email->populateTemplate( $templateData );
+ $email->send();
+
+ $parentHTML = parent::process( $data, $form );
+
+ $templateData['Link'] = $data['Referrer'];
+ $templateData['UnsubscribeLink'] = Director::baseURL() . 'unsubscribe';
+
+ $custom = $this->customise(array(
+ "Content" => $this->customise( $templateData )->renderWith('SubscribeSubmission')
+ ));
+
+ return $custom->renderWith('Page');
+ }
+
+ function Form() {
+ $form = parent::Form();
+
+ if( $this->AllNewsletters )
+ $newsletterList = DataObject::get('NewsletterType');
+ else
+ $newsletterList = $this->Newsletters();
+
+ $newsletters = array();
+
+ // Debug::show($newsletterList);
+
+ // get the newsletter types to display on the form
+ foreach( $newsletterList as $newsletter )
+ $newsletters[] = $newsletter;
+
+ // Debug::show( $newsletters );
+
+ $form->Fields()->push( new CheckboxSetField( 'Newsletters', 'Subscribe to lists', $newsletters ) );
+
+ $validator = $form->getValidator();
+ $validator->addRequiredField( 'Newsletters' );
+ $form->setValidator( $validator );
+
+ return $form;
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/TemplateList.php b/code/Newsletter/TemplateList.php
new file mode 100755
index 00000000..d4169f0e
--- /dev/null
+++ b/code/Newsletter/TemplateList.php
@@ -0,0 +1,40 @@
+templatePath = $path;
+ parent::__construct( $name, $title, $this->getTemplates(), $value, $form );
+ }
+
+ private function getTemplates() {
+ $templates = array( "" => "None" );
+
+ $absPath = Director::baseFolder();
+ if( $absPath{strlen($absPath)-1} != "/" )
+ $absPath .= "/";
+
+ $absPath .= $this->templatePath;
+ if(is_dir($absPath)) {
+ $templateDir = opendir( $absPath );
+
+ // read all files in the directory
+ while( ( $templateFile = readdir( $templateDir ) ) !== false ) {
+ // *.ss files are templates
+ if( preg_match( '/(.*)\.ss$/', $templateFile, $match ) )
+ $templates[$match[1]] = $match[1];
+ }
+ }
+
+ return $templates;
+ }
+
+ function setController( $controller ) {
+ $this->controller = $controller;
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/Newsletter/Unsubscribe.php b/code/Newsletter/Unsubscribe.php
new file mode 100755
index 00000000..d5ca329f
--- /dev/null
+++ b/code/Newsletter/Unsubscribe.php
@@ -0,0 +1,178 @@
+urlParams['Email'] );
+ $mailingListID = addslashes( $this->urlParams['MailingList'] );
+
+ if(is_numeric($mailingListID)) {
+ $mailingList = DataObject::get_by_id("NewsletterType", $mailingListID);
+ }
+
+ // try to find the user
+ if($emailAddress)
+ $member = DataObject::get_one( 'Member', "`Email`='$emailAddress'" );
+
+ // if the email address and mailing list is given in the URL and both are valid,
+ // then unsubscribe the user
+ if( $member && $mailingList && $member->inGroup( $mailingList->GroupID ) ) {
+ $this->unsubscribeFromList( $member, $mailingList );
+ $url = "unsubscribe"."/done/".$member->Email."/".$mailingList->Title;
+ Director::redirect($url);
+ } elseif( $member ) {
+ $listForm = $this->MailingListForm( $member );
+ } else {
+ $listForm = $this->EmailAddressForm();
+ }
+
+ if($this->urlParams['Email'] == "done")
+ $listForm->sessionMessage("Thank you. You have been removed from the selected groups", "good");
+
+ return $this->customise( array( 'Content' => $listForm->forTemplate() ) )->renderWith('Page');
+ }
+
+ /**
+ * Display a form with all the mailing lists that the user is subscribed to
+ */
+ function MailingListForm( $member = null ) {
+ $email = $this->urlParams['Email'];
+ return new Unsubscribe_MailingListForm($this, 'MailingListForm', $member, $email);
+ }
+
+ /**
+ * Display a form allowing the user to input their email address
+ */
+ function EmailAddressForm() {
+ return new Unsubscribe_EmailAddressForm( $this, 'EmailAddressForm' );
+ }
+
+ /**
+ * Show the lists for the user with the given email address
+ */
+ function showlists( $data, $form ) {
+ $member = DataObject::get_one( 'Member', "`Email`='{$data['Email']}'" );
+
+
+ $mailingListForm = new Unsubscribe_MailingListForm( $this, 'MailingListForm', $member, $data['Email']);
+
+ return $this->customise( array( 'Content' => $mailingListForm->forTemplate() ) )->renderWith('Page');
+ }
+
+ /**
+ * Unsubscribe the user from the given lists.
+ */
+ function unsubscribe($data, $form) {
+ $email = $this->urlParams['Email'];
+ $member = DataObject::get_one( 'Member', "`Email`='$email'" );
+ if(!$member){
+ $member = DataObject::get_one('Member', "`EmailAddress` = '$email'");
+ }
+
+ if( $data['MailingLists'] ) {
+ foreach( array_keys( $data['MailingLists'] ) as $listID ){
+
+ $nlType = DataObject::get_by_id( 'NewsletterType', $listID );
+ $nlTypeTitles[]= $nlType->Title;
+ $this->unsubscribeFromList( $member, DataObject::get_by_id( 'NewsletterType', $listID ) );
+ }
+
+ $sORp = (sizeof($nlTypeTitles)>1)?"newsletters ":"newsletter ";
+ //means single or plural
+ $nlTypeTitles = $sORp.implode(", ", $nlTypeTitles);
+ $url = "unsubscribe/done/".$member->Email."/".$nlTypeTitles;
+ Director::redirect($url);
+ } else {
+ $form->addErrorMessage('MailingLists', 'You need to select at least one mailing list to unsubscribe from.', 'bad');
+ Director::redirectBack();
+ }
+ }
+
+ protected function unsubscribeFromList( $member, $list ) {
+ // track unsubscriptions
+ $member->Groups()->remove( $list->GroupID );
+ }
+}
+
+class Unsubscribe_MailingListForm extends Form {
+
+ protected $memberEmail;
+
+ function __construct( $controller, $name, $member, $email ) {
+
+ $this->memberEmail = $member->Email;
+
+ $fields = new FieldSet();
+ $actions = new FieldSet();
+
+ // get all the mailing lists for this user
+ $lists = $this->getMailingLists( $member );
+
+ if( $lists ) {
+ $fields->push( new LabelField( 'You are subscribed to the following lists:' ) );
+
+ foreach( $lists as $list ) {
+ $fields->push( new CheckboxField( "MailingLists[{$list->ID}]", $list->Title ) );
+ }
+
+ $actions->push( new FormAction('unsubscribe', 'Unsubscribe' ) );
+ } else {
+ $fields->push( new LabelField( "I'm sorry, but $email doesn't appear to be in any of our mailing lists." ) );
+ }
+
+ parent::__construct( $controller, $name, $fields, $actions );
+ }
+
+ function FormAction() {
+ return $this->controller->Link() . "{$this->memberEmail}?executeForm=" . $this->name;
+ }
+
+ protected function getMailingLists( $member ) {
+ // get all the newsletter types that the member is subscribed to
+ return DataObject::get( 'NewsletterType', "`MemberID`='{$member->ID}'", null, "LEFT JOIN `Group_Members` USING(`GroupID`)" );
+ }
+}
+
+class Unsubscribe_EmailAddressForm extends Form {
+
+ function __construct( $controller, $name ) {
+
+ $fields = new FieldSet(
+ new EmailField( 'Email', 'Email address' )
+ );
+
+ $actions = new FieldSet(
+ new FormAction( 'showlists', 'Show lists' )
+ );
+
+ parent::__construct( $controller, $name, $fields, $actions );
+ }
+
+ function FormAction() {
+ return parent::FormAction() . ( $_REQUEST['showqueries'] ? '&showqueries=1' : '' );
+ }
+}
+
+class Unsubscribe_Successful extends Form {
+ function __construct($controller, $name){
+ $fields = new FieldSet();
+ $actions = new FieldSet();
+ parent::__construct($controller, $name, $fields, $actions);
+ }
+ function setSuccessfulMessage($email, $newsletterTypes) {
+ Requirements::css(project() . "/css/form.css");
+ $this->setMessage("Thank you. $email will no longer receive the $newsletterTypes.", "good");
+ }
+}
+
+?>
diff --git a/code/Newsletter/UnsubscribedList.php b/code/Newsletter/UnsubscribedList.php
new file mode 100755
index 00000000..43bd06d8
--- /dev/null
+++ b/code/Newsletter/UnsubscribedList.php
@@ -0,0 +1,49 @@
+nlType = $newsletterType;
+ else
+ $this->nlType = DataObject::get_by_id( 'NewsletterType', $newsletterType );
+ }
+
+ function FieldHolder() {
+ return $this->renderWith( 'NewsletterAdmin_UnsubscribedList' );
+ }
+
+ function Entries() {
+
+ $id = $this->nlType->ID;
+
+ $unsubscribeRecords = DataObject::get( 'Member_UnsubscribeRecord', "`NewsletterTypeID`='$id'" );
+
+ // user_error($id, E_USER_ERROR );
+
+ if( !$unsubscribeRecords )
+ return null;
+
+ foreach( $unsubscribeRecords as $unsubscribeRecord ) {
+ if( $unsubscribeRecord ) {
+ $unsubscribedUsers[] = new ArrayData( array(
+ 'Record' => $unsubscribeRecord,
+ 'Member' => DataObject::get_by_id( 'Member', $unsubscribeRecord->MemberID )
+ ));
+ }
+ }
+
+ return new DataObjectSet( $unsubscribedUsers );
+ }
+
+ function setController($controller) {
+ $this->controller = $controller;
+ }
+}
+?>
diff --git a/code/NewsletterAdmin.php b/code/NewsletterAdmin.php
new file mode 100755
index 00000000..5ae702f5
--- /dev/null
+++ b/code/NewsletterAdmin.php
@@ -0,0 +1,686 @@
+isAdmin()) Security::permissionFailure($this);
+
+ parent::init();
+ /*
+ if(!$this->can('AdminCMS')) {
+ $messageSet = array(
+ 'default' => "Enter your email address and password to access the CMS.",
+ 'alreadyLoggedIn' => "I'm sorry, but you can't access that part of the CMS. If you want to log in as someone else, do so below",
+ 'logInAgain' => "You have been logged out of the CMS. If you would like to log in again, enter a username and password below.",
+ );
+
+ Security::permissionFailure($this, $messageSet);
+ return;
+ }*/
+
+ Requirements::javascript(MCE_ROOT . "tiny_mce_src.js");
+ Requirements::javascript("jsparty/tiny_mce_improvements.js");
+
+ Requirements::javascript("jsparty/hover.js");
+ Requirements::javascript("jsparty/scriptaculous/controls.js");
+
+ Requirements::javascript("cms/javascript/SecurityAdmin.js");
+
+ Requirements::javascript("cms/javascript/LeftAndMain_left.js");
+ Requirements::javascript("cms/javascript/LeftAndMain_right.js");
+ Requirements::javascript("cms/javascript/CMSMain_left.js");
+
+ Requirements::javascript("cms/javascript/NewsletterAdmin_left.js");
+ Requirements::javascript("cms/javascript/NewsletterAdmin_right.js");
+ Requirements::javascript("sapphire/javascript/ProgressBar.js");
+
+ // We don't want this showing up in every ajax-response, it should always be present in a CMS-environment
+ if(!Director::is_ajax()) {
+ Requirements::javascriptTemplate("cms/javascript/tinymce.template.js", array(
+ "ContentCSS" => project() . "/css/editor.css",
+ "BaseURL" => Director::absoluteBaseURL(),
+ ));
+ }
+
+ // needed for MemberTableField (Requirements not determined before Ajax-Call)
+ Requirements::javascript("jsparty/greybox/AmiJS.js");
+ Requirements::javascript("jsparty/greybox/greybox.js");
+ Requirements::javascript("sapphire/javascript/TableListField.js");
+ Requirements::javascript("sapphire/javascript/TableField.js");
+ Requirements::javascript("sapphire/javascript/ComplexTableField.js");
+ Requirements::javascript("cms/javascript/MemberTableField.js");
+ Requirements::css("jsparty/greybox/greybox.css");
+ Requirements::css("sapphire/css/ComplexTableField.css");
+ }
+
+ public function remove() {
+ $ids = explode( ',', $_REQUEST['csvIDs'] );
+
+ foreach( $ids as $id ) {
+ if( preg_match( '/^mailtype_(\d+)$/', $id, $matches ) )
+ $record = DataObject::get_by_id( 'NewsletterType', $matches[1] );
+ else if( preg_match( '/^[a-z]+_\d+_(\d+)$/', $id, $matches ) )
+ $record = DataObject::get_by_id( 'Newsletter', $matches[1] );
+
+ if($record) {
+ $record->delete();
+ }
+
+ FormResponse::add("removeTreeNodeByIdx(\$('sitetree'), '$id' );");
+ }
+
+ FormResponse::status_message('Deleted $count items','good');
+
+ return FormResponse::respond();
+ }
+
+ public function getformcontent(){
+ Session::set('currentPage', $_REQUEST['ID']);
+ Session::set('currentType', $_REQUEST['type']);
+ if($_REQUEST['otherID'])
+ Session::set('currentOtherID', $_REQUEST['otherID']);
+ SSViewer::setOption('rewriteHashlinks', false);
+ $result = $this->renderWith($this->class . "_right");
+ return $this->getLastFormIn($result);
+ }
+
+ public function shownewsletter($params) {
+ return $this->showWithEditForm( $params, $this->getNewsletterEditForm( $params['ID'] ) );
+ }
+
+ public function showtype($params) {
+ return $this->showWithEditForm( $params, $this->getNewsletterTypeEditForm( $params['ID'] ) );
+ }
+
+ public function removenewsletter($params) {
+ if( !is_numeric( $params['ID'] ) )
+ return '';
+
+ $newsletter = DataObject::get_by_id( 'Newsletter', $params['ID'] );
+ $newsletter->delete();
+ return 'letter-' . $params['ID'];
+ }
+
+ private function showWithEditForm( $params, $editForm ) {
+ if(isset($params['ID'])) {
+ Session::set('currentPage', $params['ID']);
+ }
+ if(isset($params['OtherID'])) {
+ Session::set('currentMember', $params['OtherID']);
+ }
+ if(Director::is_ajax()) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ return $editForm->formHtmlContent();
+ } else {
+ return array();
+ }
+ }
+
+ public function getEditForm( $id ) {
+ return $this->getNewsletterTypeEditForm( $id );
+ }
+
+ /**
+ * Get the EditForm
+ */
+ public function EditForm() {
+ if((isset($_REQUEST['ID']) && $_REQUEST['Type'] == 'Newsletter') || isset($_REQUEST['action_savenewsletter'])) {
+ return $this->NewsletterEditForm();
+ } else {
+ return $this->TypeEditForm();
+ }
+ }
+
+ public function NewsletterEditForm() {
+ $id = $_REQUEST['ID'] ? $_REQUEST['ID'] : $this->currentPageID();
+ if(!is_numeric($id)) {
+ $id = 0;
+ }
+ return $this->getNewsletterEditForm($id);
+ }
+
+ public function TypeEditForm() {
+ $id = isset($_REQUEST['ID']) ? $_REQUEST['ID'] : $this->currentPageID();
+ if(!is_numeric($id)) {
+ $id = 0;
+ }
+ return $this->getNewsletterTypeEditForm($id);
+ }
+
+ public function getNewsletterTypeEditForm($id) {
+ if(!is_numeric($id)) {
+ $id = Session::get('currentPage');
+ }
+ if( is_a( $id, 'NewsletterType' ) ) {
+ $mailType = $id;
+ $id = $mailType->ID;
+ } else {
+ if($id && is_numeric($id)) {
+ $mailType = DataObject::get_by_id( 'NewsletterType', $id );
+ }
+ }
+
+ if(isset($mailType) && is_object($mailType) && $mailType->GroupID) {
+ $group = DataObject::get_one("Group", "ID = $mailType->GroupID");
+ }
+
+ if(isset($mailType)) {
+ $fields = new FieldSet(
+ new TextField("Title", "Newsletter Type"),
+ new TextField("FromEmail", "Send newsletters from"),
+ new TabSet("Root",
+ new Tab("Drafts",
+ $draftList = new NewsletterList("Draft", $mailType, "Draft")
+ ),
+ new TabSet("Sent",
+ new Tab("Sent",
+ $sendList = new NewsletterList("Send", $mailType, "Send")
+ ),
+ new Tab("Unsubscribed",
+ $unsubscribedList = new UnsubscribedList("Unsubscribed", $mailType)
+ ),
+ new Tab("Bounced",
+ $bouncedList = new BouncedList("Bounced", $mailType )
+ )
+ ),
+ new TabSet("Recipients",
+ new Tab( "Recipients",
+ $recipients = new MemberTableField(
+ $this,
+ "Recipients",
+ $group
+ )
+ ),
+ new Tab( "Import",
+ $importField = new RecipientImportField("ImportFile","Import from file", $group )
+ )
+ ),
+ new Tab("Template",
+ $templates = new TemplateList("Template","Template", $mailType->Template, self::template_path())
+ )
+ )
+ );
+
+ $draftList->setController($this);
+ $sendList->setController($this);
+ $recipients->setController($this);
+ $templates->setController($this);
+ $importField->setController($this);
+ $unsubscribedList->setController($this);
+ $bouncedList->setController($this);
+
+ $importField->setTypeID( $id );
+
+ $fields->push($idField = new HiddenField("ID"));
+ $fields->push( new HiddenField( "executeForm", "", "TypeEditForm" ) );
+ $idField->setValue($id);
+ // $actions = new FieldSet(new FormAction('adddraft', 'Add Draft'));
+
+ $actions = new FieldSet(new FormAction('save','Save'));
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom(array(
+ 'Title' => $mailType->Title,
+ 'FromEmail' => $mailType->FromEmail
+ ));
+ } else {
+ $form = false;
+ }
+
+ return $form;
+ }
+
+ /*
+ public function showmailinglist($params) {
+ return $this->showWithEditForm( $params, $this->getMailinglistEditForm( $params['ID'] ) );
+ }
+
+ public function getMailinglistEditForm($id) {
+ if(!is_numeric($id)) {
+ $id = $_SESSION['currentPage'];
+ }
+ if( is_a( $id, 'NewsletterType' ) ) {
+ $mailType = $id;
+ $id = $mailType->ID;
+ } else {
+ if($id && is_numeric($id)) {
+ $mailType = DataObject::get_by_id( 'NewsletterType', $id );
+ }
+ }
+
+ if($mailType->GroupID) {
+ $group = DataObject::get_one("Group", "ID = $mailType->GroupID");
+ }
+
+ if($mailType) {
+ $fields = new FieldSet(
+ new TabSet("Recipients",
+ new Tab( "Recipients",
+ $recipients = new MemberTableField(
+ $this,
+ "Recipients",
+ $group
+ )
+ ),
+ new Tab( "Import",
+ $importField = new RecipientImportField("ImportFile","Import from file", $group )
+ )
+ )
+ );
+
+ $recipients->setController($this);
+ $importField->setController($this);
+ $importField->setTypeID( $id );
+
+ $fields->push($idField = new HiddenField("ID"));
+ $fields->push( new HiddenField( "executeForm", "", "TypeEditForm" ) );
+ $idField->setValue($id);
+
+ $form = new Form($this, "EditForm", $fields, new FieldSet());
+ }
+ }
+ */
+
+ /**
+ * Reloads the list of recipients via ajax
+ */
+ function getrecipientslist() {
+ if( $_REQUEST['ajax'] ) {
+ $newsletterType = DataObject::get_by_id('NewsletterType', $this->urlParams['ID'] );
+ $memberList = new MemberTableField($this, "Recipients", $newsletterType->Group() );
+ return $memberList->FieldHolder();
+ }
+ }
+
+ public static function template_path() {
+ if(self::$template_path) return self::$template_path;
+ else return self::$template_path = project() . '/templates/email';
+ }
+
+ public function showdraft( $params ) {
+ return $this->showWithEditForm( $params, $this->getNewsletterEditForm( $params['ID'] ) );
+ }
+
+ public function getNewsletterEditForm($myId){
+
+ $email = DataObject::get_by_id("Newsletter", $myId);
+ if($email) {
+
+ $fields = $email->getCMSFields($this);
+ $fields->push($idField = new HiddenField("ID"));
+ $idField->setValue($myId);
+ $fields->push($typeField = new HiddenField("Type"));
+ $typeField->setValue('Newsletter');
+ //$fields->push(new HiddenField("executeForm", "", "EditForm") );
+
+ $actions = new FieldSet();
+
+ if( $email->SentDate )
+ $actions->push(new FormAction('send','Resend'));
+ else
+ $actions->push(new FormAction('send','Send'));
+
+ $actions->push(new FormAction('save','Save'));
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom($email);
+
+ if($email->Status != 'Draft') {
+ $form->makeReadonly();
+ }
+
+ // user_error( $form->FormAction(), E_USER_ERROR );
+
+ return $form;
+ } else {
+ user_error( 'Unknown Email ID: ' . $myId, E_USER_ERROR );
+ }
+ }
+
+ public function SendProgressBar() {
+ $progressBar = new ProgressBar( 'SendProgressBar', 'Sending emails...' );
+ return $progressBar->FieldHolder();
+ }
+
+ public function sendnewsletter( /*$data, $form = null*/ ) {
+
+ $id = $_REQUEST['ID'] ? $_REQUEST['ID'] : $_REQUEST['NewsletterID'];
+
+ if( !$id ) {
+ FormResponse::status_message('No newsletter specified','bad');
+ return FormResponse::respond();
+ }
+
+ $newsletter = DataObject::get_by_id("Newsletter", $id);
+ $nlType = $newsletter->getNewsletterType();
+
+ $e = new Newsletter_Email($nlType);
+ $e->Body = $body = $newsletter->Content;
+ $e->Subject = $subject = $newsletter->Subject;
+
+ // TODO Make this dynamic
+
+ if( $nlType && $nlType->FromEmail )
+ $e->From = $from = $nlType->FromEmail;
+ else
+ $e->From = $from = Email::getAdminEmail();
+
+ $e->To = $_REQUEST['TestEmail'];
+ $e->setTemplate( $nlType->Template );
+
+ $messageID = base64_encode( $newsletter->ID . '_' . date( 'd-m-Y H:i:s' ) );
+
+ switch($_REQUEST['SendType']) {
+ case "Test":
+ if($_REQUEST['TestEmail']) {
+ if( $nlType->Template ) {
+ self::sendToAddress( $e, $_REQUEST['TestEmail'], $messageID );
+ FormResponse::status_message('Sent test to ' . $_REQUEST['TestEmail'],'good');
+ } else {
+ FormResponse::status_message('No template selected','bad');
+ }
+ } else {
+ FormResponse::status_message('Please enter an email address','bad');
+ }
+ break;
+ case "List":
+ echo self::sendToList( $subject, $body, $from, $newsletter, $nlType, $messageID );
+ }
+
+ return FormResponse::respond();
+ }
+
+
+ static function sendToAddress( $email, $address, $messageID = null ) {
+ $email->To = $address;
+ $email->send();
+ }
+
+ static function sendToList( $subject, $body, $from, $newsletter, $nlType, $messageID = null ) {
+
+ $emailProcess = new NewsletterEmailProcess( $subject, $body, $from, $newsletter, $nlType, $messageID );
+ return $emailProcess->start();
+
+ /*$groupID = $nlType->GroupID;
+
+ $members = DataObject::get( 'Member', "`GroupID`='$groupID'", null, "INNER JOIN `Group_Members` ON `MemberID`=`Member`.`ID`" );
+
+ // user_error( $members, E_USER_ERROR );
+
+ if( !$members )
+ return "statusMessage('Unable to retrieve members from mailing list', 'bad' )";
+
+ foreach( $members as $member ) {
+ // check to see if the user has unsubscribed from the mailing list
+ $unsubscribeRecord = DataObject::get_one('Member_UnsubscribeRecord', "`MemberID`= {$member->ID} AND `NewsletterTypeID`={$nlType->ID}");
+
+ if( !$unsubscribeRecord ) {
+ $e = new Newsletter_Email($nlType);
+ $e->Body = $body;
+ $e->Subject =$subject;
+ $e->From = $from;
+ $e->setTemplate( $nlType->Template );
+
+ $e->populateTemplate( array( 'Member' => $member, 'FirstName' => $member->FirstName ) );
+ $this->sendToAddress( $e, $member->Email, $messageID );
+ }
+ }
+
+ if( $newsletter->Sent )
+ $resent = true;
+
+ $newsletter->Sent = 'now';
+ $newsletter->Status = 'Send';
+ $newsletter->write();
+
+ if( $resent )
+ return "resent_ok( '{$nlType->ID}', '{$newsletter->ID}' )";
+ else
+ return "draft_sent_ok( '{$nlType->ID}', '{$newsletter->ID}' )";*/
+ }
+
+ public function save($urlParams, $form) {
+ if( $_REQUEST['Type'] && $_REQUEST['Type'] == 'Newsletter' )
+ return $this->savenewsletter( $urlParams, $form );
+
+ $id = $_REQUEST['ID'];
+ $record = DataObject::get_one('NewsletterType', "`$className`.ID = $id");
+
+ // Is the template attached to the type, or the newsletter itself?
+
+ $record->Template = addslashes( $_REQUEST['Template'] );
+
+ $form->saveInto($record);
+ $record->write();
+
+ FormResponse::set_node_title("mailtype_$id", $record->Title);
+ FormResponse::status_message('Saved', 'good');
+
+ return FormResponse::respond();
+ }
+
+ public function savenewsletter($urlParams, $form) {
+
+ $id = $_REQUEST['ID'];
+ $record = DataObject::get_one('Newsletter', "`$className`.ID = $id");
+
+ // Is the template attached to the type, or the newsletter itself?
+ $type = $record->getNewsletterType();
+
+ $record->Subject = $urlParams['Subject'];
+ $record->Content = $urlParams['Content'];
+
+ $record->write();
+
+ $id = 'draft_'.$record->ParentID.'_'.$record->ID;
+
+ FormResponse::set_node_title($id, $record->Title);
+ FormResponse::status_message('Saved', 'good');
+
+ return FormResponse::respond();
+ }
+
+ function NewsletterAdminSiteTree() {
+ return $this->getsitetree();
+ }
+
+ function getsitetree() {
+ return $this->renderWith('NewsletterAdmin_SiteTree');
+ }
+
+ public function AddRecordForm() {
+ $m = new MemberTableField($this,"Members", $this->currentPageID());
+ return $m->AddRecordForm();
+ }
+
+ /**
+ * Ajax autocompletion
+ */
+ public function autocomplete() {
+ $fieldName = $this->urlParams['ID'];
+ $fieldVal = $_REQUEST[$fieldName];
+
+ $matches = DataObject::get("Member","$fieldName LIKE '" . addslashes($fieldVal) . "%'");
+ if($matches) {
+ echo "";
+ foreach($matches as $match) {
+ $data = $match->FirstName;
+ $data .= ",$match->Surname";
+ $data .= ",$match->Email";
+ $data .= ",$match->Password";
+ echo "" . $match->$fieldName . "($match->FirstName $match->Surname, $match->Email) $data ";
+ }
+ echo " ";
+ }
+ }
+
+ function savemember() {
+ $data = $_REQUEST;
+
+ $className = $this->stat('subitem_class');
+
+ $id = $_REQUEST['ID'];
+ if($id == 'new') $id = null;
+
+ if($id) {
+ $record = DataObject::get_one($className, "`$className`.ID = $id");
+ } else {
+ // send out an email to notify the user that they have been subscribed
+ $record = new $className();
+ }
+
+ $record->update($data);
+ $record->ID = $id;
+ $record->write();
+
+ $record->Groups()->add($data['GroupID']);
+
+ $FirstName = Convert::raw2js($record->FirstName);
+ $Surname = Convert::raw2js($record->Surname);
+ $Email = Convert::raw2js($record->Email);
+ $Password = Convert::raw2js($record->Password);
+ $response = <<ID, {
+ FirstName : "$FirstName",
+ Surname : "$Surname",
+ Email : "$Email"
+ });
+ $('MemberList').clearAddForm();
+JS;
+ FormResponse::add($response);
+ FormResponse::status_message('Saved', 'good');
+
+ return FormResponse::respond();
+ }
+
+
+ public function NewsletterTypes() {
+ return DataObject::get("NewsletterType","");
+ }
+
+ public function addgroup() {
+ $parent = $_REQUEST['ParentID'] ? $_REQUEST['ParentID'] : 0;
+ $p = new Group();
+ $p->Title = "New Group";
+ $p->Code = "new-group";
+ $p->ParentID = $parent;
+ $p->write();
+
+ $this->returnItemToUser($p);
+ }
+
+ public function addtype( $params ) {
+ switch( $_REQUEST['PageType'] ) {
+ case 'type':
+ $form = $this->getNewsletterTypeEditForm( $this->newNewsletterType() );
+ break;
+ default:
+ $form = $this->getNewsletterEditForm( $this->newDraft( $_REQUEST['ParentID'] ) );
+ }
+
+ return $this->showWithEditForm( $_REQUEST, $form );
+ }
+
+ public function adddraft( $data, $form ) {
+ $this->save( $data, $form );
+ $draftID = $this->newDraft( $_REQUEST['ID'] );
+ return $this->getNewsletterEditForm( $draftID );
+ }
+
+ /**
+ * Create a new newsletter type
+ */
+ private function newNewsletterType() {
+ // create a new group for the newsletter
+ $newGroup = new Group();
+ $newGroup->Title = "New mailing list";
+ $newGroup->Code = "new-mailing-list";
+ $newGroup->write();
+
+ // create the new type
+ $newsletterType = new NewsletterType();
+ $newsletterType->Title = 'New newsletter type';
+ $newsletterType->GroupID = $newGroup->ID;
+ $newsletterType->write();
+
+ // return the contents of the site tree
+ return $newsletterType;
+ }
+
+ private function newDraft( $parentID ) {
+ if(!$parentID || !is_numeric( $parentID)) {
+ $parent = DataObject::get_one("NewsletterType");
+ $parentID = $parent->ID;
+ }
+ if( $parentID && is_numeric( $parentID ) ) {
+ $newsletter = new Newsletter();
+ $newsletter->Status = 'Draft';
+ $newsletter->Title = $newsletter->Subject = 'New draft newsletter';
+ $newsletter->ParentID = $parentID;
+ $newsletter->write();
+ } else {
+ user_error( "You must first create a newsletter type before creating a draft", E_USER_ERROR );
+ }
+
+ return $newsletter->ID;
+ }
+
+ public function newmember() {
+ Session::clear('currentMember');
+ $newMemberForm = array(
+ "MemberForm" => $this->getMemberForm('new'),
+ );
+
+ if(Director::is_ajax()) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ $customised = $this->customise($newMemberForm);
+ $result = $customised->renderWith($this->class . "_rightbottom");
+ $parts = split('?form[^>]*>', $result);
+ echo $parts[1];
+
+ } else {
+ return $newMemberForm;
+ }
+ }
+
+ public function EditedMember() {
+ if(Session::get('currentMember'))
+ return DataObject::get_by_id("Member", Session::get('currentMember'));
+ }
+
+ public function Link($action = null) {
+ if(!$action) $action = "index";
+ return "admin/newsletter/$action/" . $this->currentPageID();
+ }
+
+ public function displayfilefield() {
+
+ $id = $this->urlParams['ID'];
+
+ return $this->customise( array( 'ID' => $id, "UploadForm" => $this->UploadForm() ) )->renderWith('Newsletter_RecipientImportField');
+ }
+
+ function UploadForm( $id = null ) {
+
+ if( !$id )
+ $id = $this->urlParams['ID'];
+
+ $fields = new FieldSet(
+ new FileField( "ImportFile", "" ),
+ //new HiddenField( "action_import", "", "1" ),
+ new HiddenField( "ID", "", $id )
+ );
+
+ $actions = new FieldSet(
+ new FormAction( "action_import", "Show contents" )
+ );
+
+ return new RecipientImportField_UploadForm( $this, "UploadForm", $fields, $actions );
+ }
+}
+
+?>
diff --git a/code/PageTypes/UserDefinedForm.php b/code/PageTypes/UserDefinedForm.php
new file mode 100755
index 00000000..75bba171
--- /dev/null
+++ b/code/PageTypes/UserDefinedForm.php
@@ -0,0 +1,320 @@
+ "Varchar",
+ "EmailOnSubmit" => "Boolean",
+ "SubmitButtonText" => "Varchar",
+ "OnCompleteMessage" => "HTMLText"
+ );
+
+ static $defaults = array(
+ "OnCompleteMessage" => "Thanks, we've received your submission.
",
+ );
+
+ static $has_many = array(
+ "Fields" => "EditableFormField",
+ "Submissions" => "SubmittedForm"
+ );
+
+ protected $fields;
+
+ function getCMSFields($cms) {
+ $fields = parent::getCMSFields($cms);
+
+ $fields->addFieldToTab("Root.Form", new FieldEditor("Fields", "Fields", "", $this ));
+ $fields->addFieldToTab("Root.Submissions", new SubmittedFormReportField( "Reports", "Received Submissions", "", $this ) );
+ $fields->addFieldToTab("Root.Content.On complete", new HtmlEditorField( "OnCompleteMessage", "Show on completion",3,"",$this->OnCompleteMessage, $this ) );
+
+ return $fields;
+ }
+
+ function FilterForm() {
+ // Build fields
+ $fields = new FieldSet();
+ $required = array();
+
+ foreach( $this->Fields() as $field ) {
+ $fields->push( $field->getFilterField() );
+ }
+
+ // Build actions
+ $actions = new FieldSet(
+ new FormAction( "filter", "Submit" )
+ );
+
+ // set the name of the form
+ return new Form( $this, "Form", $fields, $actions );
+ }
+
+ /**
+ * Filter the submissions by the given criteria
+ */
+ function filter( $data, $form ) {
+
+ $filterClause = array( "`SubmittedForm`.`ParentID` = '{$this->ID}'" );
+
+ $keywords = preg_split( '/\s+/', $data['FilterKeyword'] );
+
+ $keywordClauses = array();
+
+ // combine all keywords into one clause
+ foreach( $keywords as $keyword ) {
+
+ // escape %, \ and _ in the keyword. These have special meanings in a LIKE string
+ $keyword = preg_replace( '/([%_])/', '\\\\1', addslashes( $keyword ) );
+
+ $keywordClauses[] = "`Value` LIKE '%$keyword%'";
+ }
+
+ if( count( $keywordClauses ) > 0 ) {
+ $filterClause[] = "( " . implode( ' OR ', $keywordClauses ) . ")";
+ $searchQuery = 'keywords \'' . implode( "', '", $keywords ) . '\' ';
+ }
+
+ $fromDate = addslashes( $data['FilterFromDate'] );
+ $toDate = addslashes( $data['FilterToDate'] );
+
+ // use date objects to convert date to value expected by database
+ if( ereg('^([0-9]+)/([0-9]+)/([0-9]+)$', $fromDate, $parts) )
+ $fromDate = $parts[3] . '-' . $parts[2] . '-' . $parts[1];
+
+ if( ereg('^([0-9]+)/([0-9]+)/([0-9]+)$', $toDate, $parts) )
+ $toDate = $parts[3] . '-' . $parts[2] . '-' . $parts[1];
+
+ if( $fromDate ) {
+ $filterClause[] = "`SubmittedForm`.`Created` >= '$fromDate'";
+ $searchQuery .= 'from ' . $fromDate . ' ';
+ }
+
+ if( $toDate ) {
+ $filterClause[] = "`SubmittedForm`.`Created` <= '$toDate'";
+ $searchQuery .= 'to ' . $toDate;
+ }
+
+ $submittedValues = DataObject::get( 'SubmittedFormField', implode( ' AND ', $filterClause ), "", "INNER JOIN `SubmittedForm` ON `SubmittedFormField`.`ParentID`=`SubmittedForm`.`ID`" );
+
+ if( !$submittedValues || $submittedValues->Count() == 0 )
+ return "No matching results found";
+
+ $submissions = $submittedValues->groupWithParents( 'ParentID', 'SubmittedForm' );
+
+ if( !$submissions || $submissions->Count() == 0 )
+ return "No matching results found";
+
+ return $submissions->customise(
+ array( 'Submissions' => $submissions )
+ )->renderWith( 'SubmittedFormReportField_Reports' );
+ }
+
+ function ReportFilterForm() {
+ return new SubmittedFormReportField_FilterForm( $this, 'ReportFilterForm' );
+ }
+
+ function delete() {
+ // remove all the fields associated with this page
+ foreach( $this->Fields() as $field )
+ $field->delete();
+
+ parent::delete();
+ }
+
+ public function customFormActions( $isReadonly = false ) {
+ return new FieldSet( new TextField( "SubmitButtonText", "Text on submit button:", $this->SubmitButtonText ) );
+ }
+
+ /**
+ * Duplicate this UserDefinedForm page, and its form fields.
+ * Submissions, on the other hand, won't be duplicated.
+ */
+ public function duplicate() {
+ $page = parent::duplicate();
+ foreach($this->Fields() as $field) {
+ $newField = $field->duplicate();
+ $newField->ParentID = $page->ID;
+ $newField->write();
+ }
+ return $page;
+ }
+}
+
+class UserDefinedForm_Controller extends Page_Controller {
+
+ function init() {
+ Requirements::javascript('jsparty/prototype-safe.js');
+ Requirements::javascript('jsparty/behaviour.js');
+ Requirements::javascript('mot/javascript/UserDefinedForm.js');
+
+ parent::init();
+ }
+
+ function Form() {
+ // Build fields
+ $fields = new FieldSet();
+ $required = array();
+
+ if( !$this->SubmitButtonText )
+ $this->SubmitButtonText = 'Submit';
+
+ foreach( $this->Fields() as $field ) {
+ $fields->push( $field->getFormField() );
+ if( $field->Required )
+ $required[] = $field->Name;
+ }
+
+ $fields->push( new HiddenField( "Referrer", "", $_SERVER['HTTP_REFERER'] ) );
+
+ // Build actions
+ $actions = new FieldSet(
+ new FormAction( "process", $this->SubmitButtonText )
+ );
+
+ // set the name of the form
+ $form = new Form( $this, "Form", $fields, $actions, new RequiredFields( $required ) );
+ $form->loadDataFrom($this->failover);
+ return $form;
+ }
+
+ function ReportFilterForm() {
+ return new SubmittedFormReportField_FilterForm( $this, 'ReportFilterForm' );
+ }
+
+ function process( $data, $form ) {
+ $submittedForm = new SubmittedForm();
+ $submittedForm->SubmittedBy = Member::currentUser();
+ $submittedForm->ParentID = $this->ID;
+ $submittedForm->Recipient = $this->EmailTo;
+ $submittedForm->write();
+
+ $values = array();
+ $recipientAddresses = array();
+ $sendCopy = false;
+
+ $submittedFields = new DataObjectSet();
+ foreach( $this->Fields() as $field ) {
+ $submittedField = new SubmittedFormField();
+ $submittedField->ParentID = $submittedForm->ID;
+ $submittedField->Name = $field->Name;
+ $submittedField->Title = $field->Title;
+
+ if( $field->hasMethod( 'getValueFromData' ) )
+ $submittedField->Value = $field->getValueFromData( $data );
+ else
+ $submittedField->Value = $data[$field->Name];
+
+ $submittedField->write();
+ $submittedFields->push($submittedField);
+
+ if(!empty( $data[$field->Name])){
+ // execute the appropriate functionality based on the form field.
+ switch($field->ClassName){
+
+ case "EditableEmailField" :
+
+ if($field->SendCopy){
+ $recipientAddresses[] = $data[$field->Name];
+ $sendCopy = true;
+ $values[$field->Title] = ''.$data[$field->Name].' ';
+ }
+
+ break;
+
+ case "EditableFileField" :
+
+ // Returns a file type which we attach to the email.
+ $submittedfile = $field->createSubmittedField($data[$field->Name], $submittedForm);
+ $file = $submittedfile->UploadedFile();
+
+ $filename = $file->getFilename();
+
+ // Attach the file if its less than 1MB, provide a link if its over.
+ if($file->getAbsoluteSize() < 1024*1024*1){
+ $attachments[] = $file;
+ }
+
+ // Always provide the link if present.
+ if($file->ID) {
+ $submittedField->Value = $values[$field->Title] = "Uploaded to: ". Director::absoluteBaseURL(). $filename . " ";
+ } else {
+ $submittedField->Value = $values[$field->Title] = "";
+ }
+
+ break;
+ }
+
+ }elseif( $field->hasMethod( 'getValueFromData' ) ) {
+ $values[$field->Title] = Convert::linkIfMatch($field->getValueFromData( $data ));
+
+ } else {
+ $values[$field->Title] = Convert::linkIfMatch($data[$field->Name]);
+ }
+
+ }
+
+ if( $this->EmailOnSubmit || $sendCopy ) {
+ $emailData = array(
+ "Recipient" => $this->EmailTo,
+ "Sender" => Member::currentUser(),
+ "Fields" => $submittedFields,
+ );
+
+ $email = new UserDefinedForm_SubmittedFormEmail($submittedFields);
+ $email->populateTemplate($emailData);
+ $email->setTo( $this->EmailTo );
+ $email->setSubject( $this->Title );
+
+ // add attachments to email (<1MB)
+ if($attachments){
+ foreach($attachments as $file){
+ $email->attachFile($filename,$filename);
+ }
+ }
+
+ $email->send();
+
+ // send to each of email fields
+ foreach( $recipientAddresses as $addr ) {
+ $email->setTo( $addr );
+ $email->send();
+ }
+ }
+
+ $custom = $this->customise(array(
+ "Content" => $this->customise( array( 'Link' => $data['Referrer'] ) )->renderWith('ReceivedFormSubmission'),
+ "Form" => " ",
+ ));
+
+ return $custom->renderWith('Page');
+ }
+}
+
+class UserDefinedForm_SubmittedFormEmail extends Email_Template {
+ protected $ss_template = "SubmittedFormEmail";
+ protected $from = '$Sender.Email';
+ protected $to = '$Recipient.Email';
+ protected $subject = "Submission of form";
+ protected $data;
+
+ function __construct($values) {
+ parent::__construct();
+
+ $this->data = $values;
+ }
+
+ function Data() {
+ return $this->data;
+ }
+}
+
+?>
diff --git a/code/ReportAdmin.php b/code/ReportAdmin.php
new file mode 100755
index 00000000..4f971880
--- /dev/null
+++ b/code/ReportAdmin.php
@@ -0,0 +1,129 @@
+ project() . "/css/editor.css",
+ "BaseURL" => Director::absoluteBaseURL(),
+ ));
+ }
+ }
+
+ public function Link($action = null) {
+ return "admin/reports/$action";
+ }
+
+ public function Reports(){
+ $allReports= ClassInfo::subclassesFor("Report");
+ foreach($allReports as $report) {
+ if($report != 'Report') $processedReports[] = new $report();
+ }
+
+ $reports = new DataObjectSet($processedReports);
+ return $reports;
+ }
+
+ public function showreport($params) {
+ return $this->showWithEditForm( $params, $this->getReportEditForm( $params['ID'] ) );
+ }
+
+ protected function showWithEditForm( $params, $editForm ) {
+ if($params['ID']) {
+ Session::set('currentPage', $params['ID']);
+ }
+ if($params['OtherID']) {
+ Session::set('currentOtherID', $params['OtherID']);
+ }
+
+ if($_REQUEST['ajax']) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ $result = $this->customise( array( 'EditForm' => $editForm ) )->renderWith($this->getTemplatesWithSuffix("_right"));
+ return $this->getLastFormIn($result);
+ } else {
+ return array();
+ }
+ }
+
+ public function EditForm() {
+ $id = $_REQUEST['ID'] ? $_REQUEST['ID'] : Session::get('currentPage');
+
+ $subclasses = ClassInfo::subclassesFor('Report');
+
+ foreach($subclasses as $class){
+ if($class != 'Report') {
+ $obj = new $class();
+ $ids[] = $obj->getOwnerID();
+ }
+ }
+
+ // bdc: do we have any subclasses?
+ if(sizeof($ids) > 0){
+ if($id && in_array($id, $ids)) return $this->getReportEditForm($id);
+ }
+ else {
+ return null;
+ }
+
+ }
+
+ public function getReportEditForm($id){
+ if(is_numeric($id))
+ $page = DataObject::get_by_id("SiteTree", $id);
+ if($page) $reportClass = "Report_".$page->ClassName;
+
+ if(!$reportClass)
+ $reportClass = $id;
+
+ $obj = new $reportClass();
+ $fields = $obj->getCMSFields();
+
+ $fields->push($idField = new HiddenField("ID"));
+ $idField->setValue($id);
+
+ //$actions = new FieldSet(new FormAction('exporttocsv', 'Export to CVS'));
+ $actions = new FieldSet();
+ $form = new Form($this, "EditForm", $fields, $actions);
+
+ return $form;
+ }
+}
+
+?>
diff --git a/code/SecurityAdmin.php b/code/SecurityAdmin.php
new file mode 100644
index 00000000..f03e34ca
--- /dev/null
+++ b/code/SecurityAdmin.php
@@ -0,0 +1,334 @@
+isAdmin()) Security::permissionFailure($this);
+
+ parent::init();
+
+ Requirements::javascript("jsparty/hover.js");
+ Requirements::javascript("jsparty/scriptaculous/controls.js");
+
+ // needed for MemberTableField (Requirements not determined before Ajax-Call)
+ Requirements::javascript("sapphire/javascript/TableListField.js");
+ Requirements::javascript("sapphire/javascript/TableField.js");
+ Requirements::javascript("sapphire/javascript/ComplexTableField.js");
+ Requirements::javascript("cms/javascript/MemberTableField.js");
+ Requirements::css("jsparty/greybox/greybox.css");
+ Requirements::css("sapphire/css/ComplexTableField.css");
+
+ Requirements::javascript("cms/javascript/SecurityAdmin.js");
+ Requirements::javascript("cms/javascript/SecurityAdmin_left.js");
+ Requirements::javascript("cms/javascript/SecurityAdmin_right.js");
+
+ Requirements::javascript("jsparty/greybox/AmiJS.js");
+ Requirements::javascript("jsparty/greybox/greybox.js");
+ }
+
+ public function getEditForm($id) {
+ $record = DataObject::get_by_id("Group", $id);
+ if($record) {
+ $fields = new FieldSet(
+ new TabSet("Root",
+ new Tab("Members",
+ new TextField("Title", "Group name"),
+ $memberList = new MemberTableField(
+ $this,
+ "Members",
+ $record
+ )
+ ),
+
+ new Tab("Permissions",
+ new LiteralField("", "This section is for advanced users only.
+ See this page
+ for more information.
"),
+ new TableField(
+ "Permissions",
+ "Permission",
+ array("Code" => "Code", "Arg" => "Optional ID"),
+ array("Code" => "PermissionDropdownField", "Arg" => "TextField"),
+ "GroupID", $id
+ )
+ )
+ )
+ );
+
+ $memberList->setController($this);
+
+ $fields->push($idField = new HiddenField("ID"));
+ $idField->setValue($id);
+ $actions = new FieldSet(
+ new FormAction('addmember','Add Member')
+ );
+
+ $actions->push(new FormAction('save','Save'));
+
+ $form = new Form($this, "EditForm", $fields, $actions);
+ $form->loadDataFrom($record);
+ return $form;
+ }
+ }
+
+
+ public function AddRecordForm() {
+ $m = new MemberTableField(
+ $this,
+ "Members",
+ $this->currentPageID()
+ );
+ return $m->AddRecordForm();
+ }
+
+ /**
+ * Ajax autocompletion
+ */
+ public function autocomplete() {
+ $fieldName = $this->urlParams['ID'];
+ $fieldVal = $_REQUEST[$fieldName];
+
+ $matches = DataObject::get("Member","$fieldName LIKE '" . addslashes($fieldVal) . "%'");
+ if($matches) {
+ $result .= "";
+ foreach($matches as $match) {
+
+ $data = $match->FirstName;
+ $data .= ",$match->Surname";
+ $data .= ",$match->Email";
+ $data .= ",$match->Password";
+ $result .= "" . $match->$fieldName . "($match->FirstName $match->Surname, $match->Email) $data ";
+ }
+ $result .= " ";
+ return $result;
+ }
+ }
+
+ public function getmember() {
+ Session::set('currentMember', $_REQUEST['ID']);
+ SSViewer::setOption('rewriteHashlinks', false);
+ $result = $this->renderWith($this->class . "_rightbottom");
+ $parts = split('?form[^>]*>', $result);
+ echo $parts[1];
+ }
+
+
+ public function MemberForm() {
+ $id = $_REQUEST['ID'] ? $_REQUEST['ID'] : Session::get('currentMember');
+ if($id)
+ return $this->getMemberForm($id);
+ }
+
+ public function getMemberForm($id) {
+ if($id && $id != 'new') $record = DataObject::get_one("Member", "`Member`.ID = $id");
+ if($record || $id == 'new') {
+ $fields = new FieldSet(
+ new HiddenField('MemberListBaseGroup', '', $this->currentPageID() )
+ );
+
+ if( $extraFields = $record->getCMSFields() )
+ foreach( $extraFields as $extra )
+ $fields->push( $extra );
+
+ $fields->push($idField = new HiddenField("ID"));
+ $fields->push($groupIDField = new HiddenField("GroupID"));
+
+
+ $actions = new FieldSet();
+ $actions->push(new FormAction('savemember','Save'));
+
+ $form = new Form($this, "MemberForm", $fields, $actions);
+ if($record) $form->loadDataFrom($record);
+
+ $idField->setValue($id);
+ $groupIDField->setValue($this->currentPageID());
+
+ return $form;
+ }
+ }
+
+ function savemember() {
+ $data = $_REQUEST;
+
+ $className = $this->stat('subitem_class');
+
+ $id = $_REQUEST['ID'];
+ if($id == 'new') $id = null;
+
+ if($id) {
+ $record = DataObject::get_one($className, "`$className`.ID = $id");
+ } else {
+ $record = new $className();
+ }
+
+ $record->update($data);
+ $record->ID = $id;
+ $record->write();
+
+ $record->Groups()->add($data['GroupID']);
+
+
+ FormResponse::add("reloadMemberTableField();");
+
+ return FormResponse::respond();
+ }
+
+ function addmember($className=null) {
+ $data = $_REQUEST;
+ unset($data['ID']);
+ if($className == null);
+ $className = $this->stat('subitem_class');
+ $record = new $className();
+
+ $record->update($data);
+ $record->write();
+ if($data['GroupID'])
+ $record->Groups()->add($data['GroupID']);
+
+ FormResponse::add("reloadMemberTableField();");
+
+ return FormResponse::respond();
+ }
+
+ public function removememberfromgroup() {
+ $groupID = $this->urlParams['ID'];
+ $memberID = $this->urlParams['OtherID'];
+ if(is_numeric($groupID) && is_numeric($memberID)) {
+ $member = DataObject::get_by_id('Member', $memberID);
+ $member->Groups()->remove($groupID);
+ FormResponse::add("reloadMemberTableField();");
+
+ } else {
+ user_error("SecurityAdmin::removememberfromgroup: Bad parameters: Group=$groupID, Member=$memberID", E_USER_ERROR);
+ }
+
+ return FormResponse::respond();
+ }
+
+ /**
+ * Return the entire site tree as a nested set of ULs
+ */
+ public function SiteTreeAsUL() {
+ $className = "Group";
+
+ $obj = singleton($className);
+
+ // getChildrenAsUL is a flexible and complex way of traversing the tree
+ $siteTree = $obj->getChildrenAsUL("",
+ ' "ID\" class=\"$child->class " . ($child->Locked ? " nodelete" : "") . ' .
+ ' ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
+ ' "ID) . "\" >" . $child->Title . " " ',$this);
+
+ $siteTree = "";
+
+ return $siteTree;
+
+ }
+
+ public function addgroup() {
+ $parent = $_REQUEST['ParentID'] ? $_REQUEST['ParentID'] : 0;
+ $p = new Group();
+ $p->Title = "New Group";
+ $p->Code = "new-group";
+ $p->ParentID = $parent;
+ $p->write();
+ return $this->returnItemToUser($p);
+ }
+
+ public function newmember() {
+ Session::clear('currentMember');
+ $newMemberForm = array(
+ "MemberForm" => $this->getMemberForm('new'),
+ );
+ // This should be using FormResponse ;-)
+ if(Director::is_ajax()) {
+ SSViewer::setOption('rewriteHashlinks', false);
+ $customised = $this->customise($newMemberForm);
+ $result = $customised->renderWith($this->class . "_rightbottom");
+ $parts = split('?form[^>]*>', $result);
+ return $parts[1];
+
+ } else {
+ return $newMemberForm;
+ }
+ }
+
+ public function EditedMember() {
+ if(Session::get('currentMember'))
+ return DataObject::get_by_id("Member", Session::get('currentMember'));
+ }
+
+ public function Link($action = null) {
+ if(!$action) $action = "index";
+ return "admin/security/$action/" . $this->currentPageID();
+ }
+
+ public function listmembers( $baseGroup = null ) {
+
+ if( !$baseGroup )
+ $baseGroup = $this->urlParams['ID'];
+
+ // Debug::message( $_REQUEST['MemberListOrderByField'] );
+
+ // construct the filter and sort
+
+ if( $_REQUEST['MemberListOrderByField'] )
+ $sort = "`" . $_REQUEST['MemberListOrderByField'] . "`" . addslashes( $_REQUEST['MemberListOrderByOrder'] );
+
+ $whereClauses = array();
+
+ $search = addslashes( $_REQUEST['MemberListSearch'] );
+
+ if( $_REQUEST['MemberListPage'] ) {
+ $pageSize = 10;
+
+ $limitClause = ( $_REQUEST['MemberListPage'] ) . ", $pageSize";
+ }
+
+ if( !empty($_REQUEST['MemberListSearch']) )
+ $whereClauses[] = "( `Email`='$search' OR `FirstName`='$search' OR `Surname`='$search' )";
+
+ if( is_numeric( $_REQUEST['MemberListBaseGroup'] ) ) {
+ $whereClauses[] = "`GroupID`='".$_REQUEST['MemberListBaseGroup']."'";
+ $join = "INNER JOIN `Group_Members` ON `MemberID`=`Member`.`ID`";
+ }
+
+ // $_REQUEST['showqueries'] = 1;
+
+ $members = DataObject::get('Member', implode( ' AND ', $whereClauses ), $sort, $join, $limitClause );
+
+ if( is_numeric( $_REQUEST['MemberListGroup'] ) ) {
+ $baseMembers = new DataObjectSet();
+
+ if( $members )
+ foreach( $members as $member )
+ if( $member->inGroup( $_REQUEST['MemberListGroup'] ) )
+ $baseMembers->push( $member );
+ } else
+ $baseMembers = $members;
+
+ $baseMembers = null;
+
+ // user_error( $_REQUEST['MemberListBaseGroup'], E_USER_ERROR );
+
+ $memberListField = new MemberTableField(
+ $this,
+ 'MemberList',
+ $_REQUEST['MemberListBaseGroup'],
+ $baseMembers,
+ $_REQUEST['MemberListDontShowPassword']
+ );
+
+ return $memberListField->renderWith('MemberList_Table');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/SideReport.php b/code/SideReport.php
new file mode 100755
index 00000000..87d7a436
--- /dev/null
+++ b/code/SideReport.php
@@ -0,0 +1,64 @@
+records();
+ $fieldsToShow = $this->fieldsToShow();
+
+ if($records) {
+ $result = "class\">\n";
+
+ foreach($records as $record) {
+ $result .= "\n";
+ foreach($fieldsToShow as $fieldTitle => $fieldSource) {
+ $fieldName = ereg_replace('[^A-Za-z0-9]+','',$fieldTitle);
+ if(is_string($fieldSource)) {
+ $val = $record->$fieldSource;
+ } else {
+ $val = $record->val($fieldSource[0], $fieldSource[1]);
+ }
+
+ $result .= "ID\">$val ";
+ }
+ $result .= "\n \n";
+ }
+ $result .= " \n";
+ } else {
+ $result = 'The '.$this->title().' report is empty.';
+ }
+ return $result;
+ }
+}
+
+class SideReport_EmptyPages extends SideReport {
+ function title() {
+ return "Empty pages";
+ }
+ function records() {
+ return DataObject::get("SiteTree", "Content = '' OR Content IS NULL OR Content LIKE '
' OR Content LIKE '
'", "Title");
+ }
+ function fieldsToShow() {
+ return array(
+ "Title" => array("NestedTitle", array("2")),
+ );
+ }
+}
+
+class SideReport_RecentlyEdited extends SideReport {
+ function title() {
+ return "Pages edited in the last 2 weeks";
+ }
+ function records() {
+ return DataObject::get("SiteTree", "`SiteTree`.LastEdited > NOW() - INTERVAL 14 DAY", "`SiteTree`.`LastEdited` DESC");
+ }
+ function fieldsToShow() {
+ return array(
+ "Title" => array("NestedTitle", array("2")),
+ );
+ }
+}
+?>
\ No newline at end of file
diff --git a/code/StaticExporter.php b/code/StaticExporter.php
new file mode 100755
index 00000000..098a9467
--- /dev/null
+++ b/code/StaticExporter.php
@@ -0,0 +1,87 @@
+can('AdminCMS')) {
+ $messageSet = array(
+ 'default' => "Enter your email address and password to access the CMS.",
+ 'alreadyLoggedIn' => "I'm sorry, but you can't access that part of the CMS. If you want to log in as someone else, do so below",
+ 'logInAgain' => "You have been logged out of the CMS. If you would like to log in again, enter a username and password below.",
+ );
+
+ Security::permissionFailure($this, $messageSet);
+ exit;
+ }
+ }
+
+
+ function Link($action = null) {
+ return "StaticExporter/$action";
+ }
+
+ function index() {
+ echo "Static exporter ";
+ echo $this->StaticExportForm()->forTemplate();
+ }
+
+ function StaticExportForm() {
+ return new Form($this, 'StaticExportForm', new FieldSet(
+ new TextField('folder', 'Folder to export to'),
+ new TextField('baseurl', 'Base URL')
+ ), new FieldSet(
+ new FormAction('export', 'Export to that folder')
+ ));
+ }
+
+ function export() {
+
+ if($_REQUEST['baseurl']) {
+ $base = $_REQUEST['baseurl'];
+ if(substr($base,-1) != '/') $base .= '/';
+ Director::setBaseURL($base);
+ }
+
+ $folder = $_REQUEST['folder'];
+ if($folder && file_exists($folder)) {
+ $pages = DataObject::get("SiteTree");
+ foreach($pages as $page) {
+ $subfolder = "$folder/$page->URLSegment";
+ $contentfile = "$folder/$page->URLSegment/index.html";
+
+ echo " $page->URLSegment/index.html";
+
+ // Make the folder
+ if(!file_exists($subfolder)) {
+ mkdir($subfolder);
+ chmod($subfolder,02775);
+ }
+
+ // Run the page
+ Requirements::clear();
+ $controllerClass = "{$page->class}_Controller";
+ if(class_exists($controllerClass)) {
+ $controller = new $controllerClass($page);
+ $pageContent = $controller->run( array() );
+
+ // Write to file
+ if($fh = fopen($contentfile, 'w')) {
+ fwrite($fh, $pageContent);
+ fclose($fh);
+ }
+ }
+ }
+ } else {
+ echo "Please specify a folder that exists";
+ }
+
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/code/ThumbnailStripField.php b/code/ThumbnailStripField.php
new file mode 100755
index 00000000..1efd7e27
--- /dev/null
+++ b/code/ThumbnailStripField.php
@@ -0,0 +1,133 @@
+parentField = $parentField;
+ $this->updateMethod = $updateMethod;
+
+ parent::__construct($name);
+ }
+
+ function ParentField() {
+ return $this->form->FormName() . '_' . $this->parentField;
+ }
+
+ function FieldHolder() {
+ Requirements::javascript('cms/javascript/ThumbnailStripField.js');
+ return $this->renderWith('ThumbnailStripField');
+ }
+
+ function Images() {
+ //return DataObject::get("Image", "Paretn);
+ }
+
+ function UpdateMethod() {
+ return $this->updateMethod;
+ }
+
+ /**
+ * Populate the Thumbnail strip field, by looking for a folder,
+ * and the descendants of this folder.
+ */
+ function getimages() {
+ $result = '';
+ $folder = DataObject::get_by_id("Folder", $_GET['folderID']);
+
+ if( !$folder )
+ return 'This is not a folder';
+
+ $folderList = $folder->getDescendantIDList("Folder");
+
+ array_unshift($folderList, $folder->ID);
+
+ $images = DataObject::get("Image", "ParentID IN (" . implode(', ', $folderList) . ")");
+
+ if($images) {
+ $result .= '';
+ foreach($images as $image) {
+ $thumbnail = $image->getFormattedImage('StripThumbnail');
+
+ // Constrain the output image to a 600x600 square. This is passed to the destwidth/destheight in the class, which are then used to
+ // set width & height properties on the tag inserted into the CMS. Resampling is done after save
+ $width = $image->Width;
+ $height = $image->Height;
+ if($width > 600) {
+ $height *= (600/$width);
+ $width = 600;
+ }
+ if($height > 600) {
+ $width *= (600/$height);
+ $height = 600;
+ }
+
+ $result .=
+ '' .
+ '' .
+ ' ' .
+ ' ' .
+ ' ';
+ }
+ $result .= ' ';
+ }else{
+ $result = " No images found in ". $folder->Title. " ";
+ }
+
+ return $result;
+
+ }
+
+ function getflash() {
+
+ $folder = DataObject::get_by_id("Folder", $_GET['folderID']);
+
+ if( !$folder )
+ return 'This is not a folder';
+
+ $folderList = $folder->getDescendantIDList("Folder");
+ array_unshift($folderList, $folder->ID);
+
+ $width = Image::$strip_thumbnail_width - 10;
+ $height = Image::$strip_thumbnail_height - 10;
+
+ $flashObjects = DataObject::get("File", "ParentID IN (" . implode(', ', $folderList) . ") AND Filename LIKE '%.swf'");
+
+ if($flashObjects) {
+ $result .= '';
+ foreach($flashObjects as $flashObject) {
+ // doesn't work well because we can't stop/mute flash-files, AND IE does not bubble javascript-events
+ // over a flash-object grml
+// $result .= <<
+//
+//
+//
+//
+//HTML;
+ $mceRoot = MCE_ROOT .
+ $result .= <<
+
+
+
+ $flashObject->Name
+
+
+HTML;
+ }
+ $result .= '';
+ }
+
+ return $result;
+
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/sitefeatures/Akismet.php b/code/sitefeatures/Akismet.php
new file mode 100644
index 00000000..b5582414
--- /dev/null
+++ b/code/sitefeatures/Akismet.php
@@ -0,0 +1,397 @@
+Usage:
+ *
+ * $akismet = new Akismet('http://www.example.com/blog/', 'aoeu1aoue');
+ * $akismet->setCommentAuthor($name);
+ * $akismet->setCommentAuthorEmail($email);
+ * $akismet->setCommentAuthorURL($url);
+ * $akismet->setCommentContent($comment);
+ * $akismet->setPermalink('http://www.example.com/blog/alex/someurl/');
+ * if($akismet->isCommentSpam())
+ * // store the comment but mark it as spam (in case of a mis-diagnosis)
+ * else
+ * // store the comment normally
+ *
+ *
+ * @package akismet
+ * @name Akismet
+ * @version 0.2
+ * @author Alex Potsides
+ * @link http://www.achingbrain.net/
+ */
+class Akismet
+ {
+ private $version = '0.2';
+ private $wordPressAPIKey;
+ private $blogURL;
+ private $comment;
+ private $apiPort;
+ private $akismetServer;
+ private $akismetVersion;
+
+ // This prevents some potentially sensitive information from being sent accross the wire.
+ private $ignore = array('HTTP_COOKIE',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_FORWARDED_HOST',
+ 'HTTP_MAX_FORWARDS',
+ 'HTTP_X_FORWARDED_SERVER',
+ 'REDIRECT_STATUS',
+ 'SERVER_PORT',
+ 'PATH',
+ 'DOCUMENT_ROOT',
+ 'SERVER_ADMIN',
+ 'QUERY_STRING',
+ 'PHP_SELF' );
+
+
+ /**
+ * @throws Exception An exception is thrown if your API key is invalid.
+ * @param string Your WordPress API key.
+ * @param string $blogURL The URL of your blog.
+ */
+ public function __construct($blogURL, $wordPressAPIKey)
+ {
+ $this->blogURL = $blogURL;
+ $this->wordPressAPIKey = $wordPressAPIKey;
+
+ // Set some default values
+ $this->apiPort = 80;
+ $this->akismetServer = 'rest.akismet.com';
+ $this->akismetVersion = '1.1';
+
+ // Start to populate the comment data
+ $this->comment['blog'] = $blogURL;
+ $this->comment['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
+ $this->comment['referrer'] = $_SERVER['HTTP_REFERER'];
+
+ // This is necessary if the server PHP5 is running on has been set up to run PHP4 and
+ // PHP5 concurently and is actually running through a separate proxy al a these instructions:
+ // http://www.schlitt.info/applications/blog/archives/83_How_to_run_PHP4_and_PHP_5_parallel.html
+ // and http://wiki.coggeshall.org/37.html
+ // Otherwise the user_ip appears as the IP address of the PHP4 server passing the requests to the
+ // PHP5 one...
+ $this->comment['user_ip'] = $_SERVER['REMOTE_ADDR'] != getenv('SERVER_ADDR') ? $_SERVER['REMOTE_ADDR'] : getenv('HTTP_X_FORWARDED_FOR');
+
+ // Check to see if the key is valid
+ $response = $this->http_post('key=' . $this->wordPressAPIKey . '&blog=' . $this->blogURL, $this->akismetServer, '/' . $this->akismetVersion . '/verify-key');
+
+ if($response[1] != 'valid')
+ {
+ // Whoops, no it's not. Throw an exception as we can't proceed without a valid API key.
+ throw new Exception('Invalid API key. Please obtain one from http://wordpress.com/api-keys/');
+ }
+ }
+
+ private function http_post($request, $host, $path)
+ {
+ $http_request = "POST " . $path . " HTTP/1.1\r\n";
+ $http_request .= "Host: " . $host . "\r\n";
+ $http_request .= "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
+ $http_request .= "Content-Length: " . strlen($request) . "\r\n";
+ $http_request .= "User-Agent: Akismet PHP5 Class " . $this->version . " | Akismet/1.11\r\n";
+ $http_request .= "\r\n";
+ $http_request .= $request;
+
+ $socketWriteRead = new SocketWriteRead($host, $this->apiPort, $http_request);
+ $socketWriteRead->send();
+
+ return explode("\r\n\r\n", $socketWriteRead->getResponse(), 2);
+ }
+
+ // Formats the data for transmission echo $sql;
+ private function getQueryString()
+ {
+ foreach($_SERVER as $key => $value)
+ {
+ if(!in_array($key, $this->ignore))
+ {
+ if($key == 'REMOTE_ADDR')
+ {
+ $this->comment[$key] = $this->comment['user_ip'];
+ }
+ else
+ {
+ $this->comment[$key] = $value;
+ }
+ }
+ }
+
+ $query_string = '';
+
+ foreach($this->comment as $key => $data)
+ {
+ @$query_string .= $key . '=' . urlencode(stripslashes($data)) . '&';
+ }
+
+ return $query_string;
+ }
+
+ /**
+ * Tests for spam.
+ *
+ * Uses the web service provided by {@link http://www.akismet.com Akismet} to see whether or not the submitted comment is spam. Returns a boolean value.
+ *
+ * @return bool True if the comment is spam, false if not
+ */
+ public function isCommentSpam()
+ {
+ $response = $this->http_post($this->getQueryString(), $this->wordPressAPIKey . '.rest.akismet.com', '/' . $this->akismetVersion . '/comment-check');
+
+ return ($response[1] == 'true');
+ }
+
+ /**
+ * Submit spam that is incorrectly tagged as ham.
+ *
+ * Using this function will make you a good citizen as it helps Akismet to learn from its mistakes. This will improve the service for everybody.
+ */
+ public function submitSpam()
+ {
+ $this->http_post($this->getQueryString(), $this->wordPressAPIKey . '.' . $this->akismetServer, '/' . $this->akismetVersion . '/submit-spam');
+ }
+
+ /**
+ * Submit ham that is incorrectly tagged as spam.
+ *
+ * Using this function will make you a good citizen as it helps Akismet to learn from its mistakes. This will improve the service for everybody.
+ */
+ public function submitHam()
+ {
+ $this->http_post($this->getQueryString(), $this->wordPressAPIKey . '.' . $this->akismetServer, '/' . $this->akismetVersion . '/submit-ham');
+ }
+
+ /**
+ * To override the user IP address when submitting spam/ham later on
+ *
+ * @param string $userip An IP address. Optional.
+ */
+ public function setUserIP($userip)
+ {
+ $this->comment['user_ip'] = $userip;
+ }
+
+ /**
+ * To override the referring page when submitting spam/ham later on
+ *
+ * @param string $referrer The referring page. Optional.
+ */
+ public function setReferrer($referrer)
+ {
+ $this->comment['referrer'] = $referrer;
+ }
+
+ /**
+ * A permanent URL referencing the blog post the comment was submitted to.
+ *
+ * @param string $permalink The URL. Optional.
+ */
+ public function setPermalink($permalink)
+ {
+ $this->comment['permalink'] = $permalink;
+ }
+
+ /**
+ * The type of comment being submitted.
+ *
+ * May be blank, comment, trackback, pingback, or a made up value like "registration" or "wiki".
+ */
+ public function setCommentType($commentType)
+ {
+ $this->comment['comment_type'] = $commentType;
+ }
+
+ /**
+ * The name that the author submitted with the comment.
+ */
+ public function setCommentAuthor($commentAuthor)
+ {
+ $this->comment['comment_author'] = $commentAuthor;
+ }
+
+ /**
+ * The email address that the author submitted with the comment.
+ *
+ * The address is assumed to be valid.
+ */
+ public function setCommentAuthorEmail($authorEmail)
+ {
+ $this->comment['comment_author_email'] = $authorEmail;
+ }
+
+ /**
+ * The URL that the author submitted with the comment.
+ */
+ public function setCommentAuthorURL($authorURL)
+ {
+ $this->comment['comment_author_url'] = $authorURL;
+ }
+
+ /**
+ * The comment's body text.
+ */
+ public function setCommentContent($commentBody)
+ {
+ $this->comment['comment_content'] = $commentBody;
+ }
+
+ /**
+ * Defaults to 80
+ */
+ public function setAPIPort($apiPort)
+ {
+ $this->apiPort = $apiPort;
+ }
+
+ /**
+ * Defaults to rest.akismet.com
+ */
+ public function setAkismetServer($akismetServer)
+ {
+ $this->akismetServer = $akismetServer;
+ }
+
+ /**
+ * Defaults to '1.1'
+ */
+ public function setAkismetVersion($akismetVersion)
+ {
+ $this->akismetVersion = $akismetVersion;
+ }
+ }
+
+/**
+ * Utility class used by Akismet
+ *
+ * This class is used by Akismet to do the actual sending and receiving of data. It opens a connection to a remote host, sends some data and the reads the response and makes it available to the calling program.
+ *
+ * The code that makes up this class originates in the Akismet WordPress plugin, which is {@link http://akismet.com/download/ available on the Akismet website}.
+ *
+ * N.B. It is not necessary to call this class directly to use the Akismet class. This is included here mainly out of a sense of completeness.
+ *
+ * @package akismet
+ * @name SocketWriteRead
+ * @version 0.1
+ * @author Alex Potsides
+ * @link http://www.achingbrain.net/
+ */
+class SocketWriteRead
+ {
+ private $host;
+ private $port;
+ private $request;
+ private $response;
+ private $responseLength;
+ private $errorNumber;
+ private $errorString;
+
+ /**
+ * @param string $host The host to send/receive data.
+ * @param int $port The port on the remote host.
+ * @param string $request The data to send.
+ * @param int $responseLength The amount of data to read. Defaults to 1160 bytes.
+ */
+ public function __construct($host, $port, $request, $responseLength = 1160)
+ {
+ $this->host = $host;
+ $this->port = $port;
+ $this->request = $request;
+ $this->responseLength = $responseLength;
+ $this->errorNumber = 0;
+ $this->errorString = '';
+ }
+
+ /**
+ * Sends the data to the remote host.
+ *
+ * @throws An exception is thrown if a connection cannot be made to the remote host.
+ */
+ public function send()
+ {
+ $this->response = '';
+
+ $fs = fsockopen($this->host, $this->port, $this->errorNumber, $this->errorString, 3);
+
+ if($this->errorNumber != 0)
+ {
+ throw new Exception('Error connecting to host: ' . $this->host . ' Error number: ' . $this->errorNumber . ' Error message: ' . $this->errorString);
+ }
+
+ if($fs !== false)
+ {
+ @fwrite($fs, $this->request);
+
+ while(!feof($fs))
+ {
+ $this->response .= fgets($fs, $this->responseLength);
+ }
+
+ fclose($fs);
+ }
+
+ }
+
+ /**
+ * Returns the server response text
+ *
+ * @return string
+ */
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+ /**
+ * Returns the error number
+ *
+ * If there was no error, 0 will be returned.
+ *
+ * @return int
+ */
+ public function getErrorNumner()
+ {
+ return $this->errorNumber;
+ }
+
+ /**
+ * Returns the error string
+ *
+ * If there was no error, an empty string will be returned.
+ *
+ * @return string
+ */
+ public function getErrorString()
+ {
+ return $this->errorString;
+ }
+ }
+?>
\ No newline at end of file
diff --git a/code/sitefeatures/PageComment.php b/code/sitefeatures/PageComment.php
new file mode 100755
index 00000000..02011322
--- /dev/null
+++ b/code/sitefeatures/PageComment.php
@@ -0,0 +1,144 @@
+ "Varchar",
+ "Comment" => "Text",
+ "IsSpam" => "Boolean",
+ );
+
+ static $has_one = array(
+ "Parent" => "SiteTree",
+ );
+
+ static $casting = array(
+ "RSSTitle" => "Varchar",
+ );
+
+ // Number of comments to show before paginating
+ static $comments_per_page = 10;
+
+ /**
+ * Return a link to this comment
+ * @return string link to this comment.
+ */
+ function Link() {
+ return $this->Parent()->Link();
+ }
+
+
+ function DeleteLink() {
+ if(Permission::check('CMS_ACCESS_CMSMain')) {
+ return "PageComment/deletecomment/$this->ID";
+ }
+ }
+ function deletecomment() {
+ if(Permission::check('CMS_ACCESS_CMSMain')) {
+ $comment = DataObject::get_by_id("PageComment", $this->urlParams['ID']);
+ if($comment) {
+ $comment->delete();
+ }
+ }
+
+ if(Director::is_ajax()) {
+ echo "";
+ } else {
+ Director::redirectBack();
+ }
+ }
+
+ function SpamLink() {
+ $member = Member::currentUser();
+ if(SSAkismet::isEnabled() && Permission::check('CMS_ACCESS_CMSMain') && !$this->getField('IsSpam')) {
+ return "PageComment/reportspam/$this->ID";
+ }
+ }
+
+ function HamLink() {
+ $member = Member::currentUser();
+ if(SSAkismet::isEnabled() && Permission::check('CMS_ACCESS_CMSMain') && $this->getField('IsSpam')) {
+ return "PageComment/reportham/$this->ID";
+ }
+ }
+
+ function SpamClass() {
+ if($this->getField('IsSpam')) {
+ return 'spam';
+ } else {
+ return 'notspam';
+ }
+ }
+
+ function reportspam() {
+ if(SSAkismet::isEnabled() && Permission::check('CMS_ACCESS_CMSMain')) {
+ $comment = DataObject::get_by_id("PageComment", $this->urlParams['ID']);
+
+ if($comment) {
+ try {
+ $akismet = new SSAkismet();
+ $akismet->setCommentAuthor($comment->getField('Name'));
+ $akismet->setCommentContent($comment->getField('Comment'));
+
+ $akismet->submitSpam();
+ } catch (Exception $e) {
+ // Akismet didn't work, most likely the service is down.
+ }
+
+ if(SSAkismet::getSaveSpam()) {
+ $comment->setField('IsSpam', true);
+ $comment->write();
+ } else {
+ $comment->delete();
+ }
+ }
+ }
+
+ if(Director::is_ajax()) {
+ if(SSAkismet::getSaveSpam()) {
+ echo $comment->renderWith('PageCommentInterface_singlecomment');
+ } else {
+ echo '';
+ }
+ } else {
+ Director::redirectBack();
+ }
+ }
+
+ function reportham() {
+ if(SSAkismet::isEnabled() && Permission::check('CMS_ACCESS_CMSMain')) {
+ $comment = DataObject::get_by_id("PageComment", $this->urlParams['ID']);
+
+ if($comment) {
+ try {
+ $akismet = new SSAkismet();
+ $akismet->setCommentAuthor($comment->getField('Name'));
+ $akismet->setCommentContent($comment->getField('Comment'));
+
+ $akismet->submitHam();
+ } catch (Exception $e) {
+ // Akismet didn't work, most likely the service is down.
+ }
+
+ $comment->setField('IsSpam', false);
+ $comment->write();
+ }
+ }
+
+ if(Director::is_ajax()) {
+ echo $comment->renderWith('PageCommentInterface_singlecomment');
+ } else {
+ Director::redirectBack();
+ }
+ }
+
+ function RSSTitle() {
+ return "Comment by '$this->Name' on " . $this->Parent()->Title;
+ }
+ function rss() {
+ $rss = new RSSFeed(DataObject::get("PageComment", "ParentID > 0", "Created DESC", "", 10), "home/", "Page comments", "", "RSSTitle", "Comment", "Name");
+ $rss->outputToBrowser();
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/code/sitefeatures/PageCommentInterface.php b/code/sitefeatures/PageCommentInterface.php
new file mode 100755
index 00000000..d386f23b
--- /dev/null
+++ b/code/sitefeatures/PageCommentInterface.php
@@ -0,0 +1,121 @@
+controller = $controller;
+ $this->methodName = $methodName;
+ $this->page = $page;
+ }
+
+ function forTemplate() {
+ return $this->renderWith('PageCommentInterface');
+ }
+
+ function PostCommentForm() {
+ Requirements::javascript('jsparty/behaviour.js');
+ Requirements::javascript('jsparty/prototype.js');
+ Requirements::javascript('jsparty/scriptaculous/effects.js');
+ Requirements::javascript('cms/javascript/PageCommentInterface.js');
+
+ $form = new PageCommentInterface_Form($this->controller, $this->methodName . ".PostCommentForm", new FieldSet(
+ new HiddenField("ParentID", "ParentID", $this->page->ID),
+ new TextField("Name", "Your name"),
+ new TextareaField("Comment", "Comments")
+
+ ), new FieldSet(
+ new FormAction("postcomment", "Post")
+ ));
+
+ $form->loadDataFrom(array(
+ "Name" => Cookie::get("PageCommentInterface_Name"),
+ ));
+
+ return $form;
+ }
+
+ function Comments() {
+ // Comment limits
+ if(isset($_GET['commentStart'])) {
+ $limit = (int)$_GET['commentStart'].",".PageComment::$comments_per_page;
+ } else {
+ $limit = "0,".PageComment::$comments_per_page;
+ }
+
+ if(isset($_GET['showspam'])) {
+ $comments = DataObject::get("PageComment", "ParentID = '" . Convert::raw2sql($this->page->ID) . "'", "Created DESC", "", $limit);
+ } else {
+ $comments = DataObject::get("PageComment", "ParentID = '" . Convert::raw2sql($this->page->ID) . "' AND IsSpam = 0", "Created DESC", "", $limit);
+ }
+
+ if(is_null($comments)) {
+ return;
+ }
+
+ // This allows us to use the normal 'start' GET variables as well (In the weird circumstance where you have paginated comments AND something else paginated)
+ $comments->setPaginationGetVar('commentStart');
+
+ return $comments;
+ }
+
+}
+
+class PageCommentInterface_Form extends Form {
+ function postcomment($data) {
+ // Spam filtering
+ if(SSAkismet::isEnabled()) {
+ try {
+ $akismet = new SSAkismet();
+
+ $akismet->setCommentAuthor($data['Name']);
+ $akismet->setCommentContent($data['Comment']);
+
+ if($akismet->isCommentSpam()) {
+ if(SSAkismet::getSaveSpam()) {
+ $comment = Object::create('PageComment');
+ $this->saveInto($comment);
+ $comment->setField("IsSpam", true);
+ $comment->write();
+ }
+ echo "Spam detected!! ";
+ echo "If you believe this was in error, please email ";
+ echo ereg_replace("@", " _(at)_", Email::getAdminEmail());
+ echo ". The message you posted was: ";
+ echo $data['Comment'];
+
+ return;
+ }
+ } catch (Exception $e) {
+ // Akismet didn't work, continue without spam check
+ }
+ }
+
+
+ Cookie::set("PageCommentInterface_Name", $data['Name']);
+
+ $comment = Object::create('PageComment');
+ $this->saveInto($comment);
+ $comment->IsSpam = false;
+ $comment->write();
+
+ if(Director::is_ajax()) {
+ echo $comment->renderWith('PageCommentInterface_singlecomment');
+ } else {
+ Director::redirectBack();
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/code/sitefeatures/SSAkismet.php b/code/sitefeatures/SSAkismet.php
new file mode 100644
index 00000000..89d610bc
--- /dev/null
+++ b/code/sitefeatures/SSAkismet.php
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/css/GenericDataAdmin.css b/css/GenericDataAdmin.css
new file mode 100755
index 00000000..897bad0f
--- /dev/null
+++ b/css/GenericDataAdmin.css
@@ -0,0 +1,236 @@
+.right form div.tab {
+ padding-top: 1em;
+}
+
+form#Form_EditForm #Form_ExportForm fieldset {
+ height: auto;
+}
+
+
+.leftcolumn {
+ margin: 0;
+ width: 46%;
+ float: left;
+}
+
+.rightcolumn {
+ width: 46%;
+ margin: 0;
+ float: right;
+ clear: right;
+}
+
+.tab div.groupfield, .tab div.groupfield div.groupfield {
+ margin: 1.2em 0;
+ border: 1px solid #ffffff;
+}
+
+.tab div.field {
+ margin: .3em 0;
+}
+
+#left {
+ width:335px
+}
+ #LeftPane {
+ background-color: #FFF;
+ border-left: 1px #ccc solid;
+ padding: 0;
+ }
+
+ /* Generic form styling */
+
+ #LeftPane h3 {
+ font-size:1.2em;
+ letter-spacing:1px;
+ color:#4874C6;
+ }
+
+ #LeftPane form label.left {
+ font-size:1.1em;
+ font-weight:bold;
+ float:left;
+ width:100px;
+ padding-right:3px;
+ }
+
+ #LeftPane form div {
+ clear:left;
+ }
+
+ #LeftPane form div.field {
+ margin-bottom:5px;
+ }
+
+ #LeftPane form input {
+ padding:2px;
+ }
+
+ #LeftPane form input.text,
+ #LeftPane form select {
+ padding:2px;
+ }
+
+ #LeftPane form input.text {
+ width:145px;
+ }
+
+ #LeftPane form div.field select {
+ width:152px;
+ }
+
+ #LeftPane #AddForm {
+ padding: 5px 10px;
+ }
+
+ #Search_holder {
+ overflow: auto;
+ }
+
+ #Search_holder .ToggleAdvancedSearchFields {
+ margin: 3px 0;
+ }
+
+ #Search_holder .Actions {
+ margin: 5px 0;
+ }
+
+ #Search_holder h3 {
+ margin-bottom: 8px;
+ }
+
+ #LeftPane #Form_CreationForm {
+ margin: 0;
+ padding: 0;
+ }
+
+ #LeftPane #Form_CreationForm p.Actions {
+ margin-left: 1em;
+ }
+
+ #SearchForm_holder {
+ overflow: auto; /* TODO */
+ padding: 5px 10px;
+ }
+
+ #SearchForm_holder #DateRange .calendardate,
+ #SearchForm_holder #DateRange label{
+ float:left;
+ clear:none;
+ padding:1px;
+ }
+
+ #SearchForm_holder #DateRange input {
+ width:65px;
+ }
+
+ /* Result list layout */
+ div.ResultList {
+ padding: 1em;
+ }
+
+ div.ResultList ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ background:#eee;
+ }
+
+ div.ResultList li {
+ padding: .1em;
+ }
+
+ div.ResultList ul ul {
+ margin-left: 2em;
+ }
+
+ div.ResultList li.odd {
+ background:#f5f5f5;
+ }
+
+ div.ResultList li.even {
+ background:#eee;
+ }
+
+ /* Result table layout */
+ table.ResultTable table tbody tr.odd {
+ background:#f5f5f5;
+ }
+ table.ResultTable table tbody tr td {
+ padding:2px;
+ }
+
+ table.ResultTable {
+ padding: 1em;
+ width: 100%;
+ }
+ table.ResultTable * {
+ border:0;
+ font-size:1em;
+ }
+ table.ResultTable td {
+ padding: 3px;
+ }
+
+ table.ResultTable thead tr {
+ background:#eee;
+ }
+ table.ResultTable thead tr td {
+ font-weight:bold;
+ letter-spacing:1px;
+ font-size:1.1em;
+ }
+ table.ResultTable tbody tr {
+ background:#eee;
+ }
+ table.ResultTable tbody tr.even {
+ background:#f0f8ff;
+ }
+ table.ResultTable tbody tr.odd {
+ background:#fff;
+ }
+ table.ResultTable tbody tr td {
+ font-size:1em;
+ }
+
+ #Search_holder table.ResultTable tbody tr {
+ cursor: pointer;
+ }
+
+ #Form_export_action_export {
+ margin-top: 10px;
+ }
+
+#Form_EditForm_GenericDataStatus {
+ position: absolute;
+ z-index: 500;
+ top: -70px;
+ right: -10px;
+ text-align: right;
+ width: 150px;
+ height: 38px;
+
+ padding: 10px;
+
+ font-size: 14px;
+ font-weight: bold;
+
+ border: 1px solid #cc9;
+ color: #660;
+ background-color: #F9F9E3;
+}
+
+.clear:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.clear {display: inline-block;}
+
+/* Hides from IE-mac \*/
+* html .clear {height: 1%;}
+.clear {display: block;}
+/* End hide from IE-mac */
\ No newline at end of file
diff --git a/css/Image_iframe.css b/css/Image_iframe.css
new file mode 100755
index 00000000..96c9ac49
--- /dev/null
+++ b/css/Image_iframe.css
@@ -0,0 +1,146 @@
+@import url("typography.css");
+
+html,body {
+ padding: 0;
+ margin: 0;
+ border-style: none;
+ height: 100%;
+ overflow: hidden;
+}
+
+form {
+ margin: 0; padding: 0;
+}
+form fieldset{
+ padding: 0;
+ margin: 0;
+ border-style: none;
+}
+
+h2 {
+ margin: 0;
+ font-size: 1.4em;
+}
+
+/**
+ * Selection Groups
+ */
+.SelectionGroup {
+ padding: 0;
+ margin: 10px 0 0 0;
+}
+.SelectionGroup li {
+ list-style-type: none;
+ margin: 0;
+}
+.SelectionGroup li input.selector {
+ width: 20px;
+}
+
+
+.SelectionGroup li div.field {
+ display: none;
+}
+.SelectionGroup li.selected div.field {
+ margin-left: 30px;
+ display: block;
+ margin-bottom: 1em;
+}
+.SelectionGroup li.selected label.selector {
+ font-weight: bold;
+}
+
+
+
+/**
+ * TreeDropdownField stying
+ */
+div.TreeDropdownField {
+ width: 241px;
+ padding: 0;
+}
+html>body div.TreeDropdownField {
+ position:relative;
+}
+
+div.TreeDropdownField span.items {
+ display: block;
+ height: 100%;
+ border: 1px #7f9db9 solid;
+ cursor: pointer;
+ width: 220px;
+ float: left;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ background-color: white;
+}
+
+div.TreeDropdownField div.tree_holder {
+ clear: left;
+ cursor: default;
+ border: 1px black solid;
+ margin: 0;
+ height: 180px;
+ overflow: auto;
+ background-color: white;
+ /**
+ * HACK IE6, see http://www.hedgerwow.com/360/bugs/css-select-free.html
+ */
+ position:absolute;
+ z-index:10;
+ width:240px;/*must have for any value*/
+}
+
+html>body div.TreeDropdownField div.tree_holder {
+ top: 20px;
+ left: 0px;
+ z-index: 1000;
+}
+
+/**
+ * HACK IE6, see http://www.hedgerwow.com/360/bugs/css-select-free.html
+ */
+div.TreeDropdownField div.tree_holder iframe {
+ display:none;/* IE5*/
+ display/**/:block;/* IE5*/
+ position:absolute;
+ top:0;
+ left:0;
+ z-index:-1;
+ filter:mask();
+ width:180px; /*must have for any big value*/
+ height:200px/*must have for any big value*/;
+ border: 0;
+}
+
+div.TreeDropdownField a.editLink {
+ border-width: 1px 1px 1px 0;
+ background: url(../../sapphire/images/TreeDropdownField_button.gif) left top no-repeat;
+ width: 19px;
+ height: 21px;
+ margin: 0;
+ padding: 0;
+ float: left;
+ clear: right;
+ z-index: 0;
+ overflow: hidden;
+}
+
+
+
+.Actions {
+ text-align: right;
+ margin: 0;
+ position: absolute;
+ right: 5px;
+ bottom: 5px;
+}
+
+.mainblock {
+ float: left;
+ border: 1px #CCC solid;
+ padding: 5px;
+ margin-right: 5px;
+ height: 120px;
+ position: relative;
+}
\ No newline at end of file
diff --git a/css/LeftAndMain_printable.css b/css/LeftAndMain_printable.css
new file mode 100755
index 00000000..f6491e68
--- /dev/null
+++ b/css/LeftAndMain_printable.css
@@ -0,0 +1,44 @@
+* {
+ font-family: Helvetica, Verdana, Geneva, Arial, Helvetica, sans-serif;
+}
+
+h3 {
+ margin-top: 2em;
+}
+
+form .field {
+ margin-bottom: 1em;
+}
+
+form label.left {
+ float: left;
+ width: 15em;
+ font-weight: bold;
+}
+form div.field {
+ /*margin-left: 10em;*/
+}
+
+form .inlineformaction,
+form .inlineformaction_readonly,
+form .action,
+form .action_readonly
+{
+ display: none !important;
+}
+
+.pageStatusMessage {
+ position: absolute;
+ right: 2em;
+ top: 2em;
+ border: 1px #777 solid;
+}
+
+.TableListField table.data th {
+ background: #eee;
+}
+
+.TableListField table.data {
+ width: 100%;
+ border: 1px #aaa solid;
+}
\ No newline at end of file
diff --git a/css/cms_left.css b/css/cms_left.css
new file mode 100644
index 00000000..f564c4ca
--- /dev/null
+++ b/css/cms_left.css
@@ -0,0 +1,404 @@
+#TreeActions {
+ background-color: #EEE;
+ border-bottom: 1px #CCC solid;
+ padding: 0;
+ margin: 0;
+ /* height: 23px; */
+ float: left;
+ width: 100%;
+}
+#TreeActions li.action {
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 7px;
+ list-style-type: none;
+}
+#TreeActions li.action a {
+ padding: 0.3em;
+ display: block;
+}
+#TreeActions li.action a:hover {
+ background-color: #CCC;
+}
+#TreeActions li.selected a {
+ background-color: #CCC;
+}
+
+#left form.actionparams {
+ background-color: #CCC;
+ border-bottom: 1px #EEE solid;
+ margin: 0;
+ padding: 0.5em;
+}
+
+#left form.actionparams select {
+ width: 75%;
+ margin: 0;
+}
+#left form.actionparms input.action {
+ width: 5%;
+ margin: 0;
+}
+
+
+
+/**
+ * Selection Groups
+ */
+#left .SelectionGroup {
+ padding: 0;
+ margin: 0;
+ display: block;
+}
+#left .SelectionGroup li {
+ list-style-type: none;
+ clear: left;
+ padding: 0;
+ margin: 0;
+ border: 1px #CCC solid;
+}
+#left .SelectionGroup li input {
+ width: 20px;
+ float: left;
+ margin: 0;
+}
+#left .SelectionGroup li div.field {
+ display: none;
+ font-size: 1em;
+}
+#left .SelectionGroup li label {
+ margin: 0 0 0 2px;
+ padding: 0;
+ display: block;
+ height: 1.2em;
+ width: auto;
+ float: none;
+}
+#left .SelectionGroup li.selected div.field {
+ margin-left: 23px;
+ display: block;
+ margin-bottom: 1em;
+}
+#left div.field select {
+ width: 100%;
+}
+
+#left .SelectionGroup li.selected label.selector {
+ font-weight: bold;
+}
+
+
+ul.tree span.a {
+ cursor: pointer;
+}
+ul.tree span.a.over {
+ background-color: #FFFFBB;
+
+ /* these push the highlight out to the left of the window */
+ margin-left: -100px;
+ padding-left: 100px;
+ background-position: 100px 50%;
+}
+
+ul.tree span.a.current {
+ font-weight: bold;
+ background-color: #EEEEFF;
+ border-top: 1px #CCCCFF solid;
+ border-bottom: 1px #CCCCFF solid;
+
+ /* these push the highlight out to the left of the window */
+ margin-left: -100px;
+ padding-left: 100px;
+ background-position: 100px 50%;
+ /*position: relative;*/
+}
+
+
+
+/*
+ul.tree span.a.current span#currentNodeActions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ background-color: white;
+ padding: 0 3px;
+}
+*/
+
+ul.tree span.a.loading span.b span.c a {
+ background-image: url(../images/network-save.gif) !important;
+ margin-left: -3px;
+ padding-left: 21px;
+ background-position : 2px 2px;
+}
+
+ul.tree.multiselect span.a span.b a {
+ background-image: url(../images/tickbox-unticked.gif) !important;
+}
+ul.tree.multiselect span.a.nodelete span.b a {
+ background-image: url(../images/tickbox-canttick.gif) !important;
+}
+
+
+ul.tree.multiselect span.a.selected span.b span.c a {
+ background-image: url(../images/tickbox-ticked.gif) !important;
+}
+
+ul.tree.multiselect li.selected ul span.a a {
+ background-image: url(../images/tickbox-greyticked.gif) !important;
+}
+/*
+ul.tree.multiselect li.selected ul {
+ display: none;
+}
+*/
+
+/* Span-B: Plus/Minus icon */
+ul.tree.multiselect li.selected span.a.children span.b, ul.tree.multiselect li.selected span.a.unexpanded span.b {
+ background-image: none;
+ cursor: default;
+}
+
+ul.tree span.a a.disabled {
+ color: #999;
+ cursor: normal;
+}
+ ul.tree span.a a.disabled * {
+ padding: 0;
+ background: none;
+ color: #999;
+ cursor: normal;
+ }
+
+
+ul.tree .deleted {
+ color: red;
+ text-decoration: line-through;
+}
+/* Created on stage, never published */
+ul.tree ins,
+#publication_key ins {
+ color: orange;
+ padding-left : 16px;
+ background-image : url(../../sapphire/images/NewOnStage.png);
+ background-position : 0px 1px;
+ background-repeat : no-repeat;
+ text-decoration : none;
+}
+/* Deleated on stage */
+ul.tree del,
+#publication_key del {
+ color: red;
+ padding-left : 16px;
+ background-image : url(../../sapphire/images/TrashedOnStage.png);
+ background-repeat : no-repeat;
+}
+ul.tree span.modified,
+#publication_key span.modified {
+ color: green;
+ padding-left : 16px;
+ background-position : 1px 0px;
+ background-image : url(../../sapphire/images/EditedOnStage.png);
+ background-repeat : no-repeat;
+ text-decoration : none;
+}
+
+/**
+ * Side tabs
+ */
+#SideTabs {
+ float: left;
+ margin: 0;
+ padding: 0;
+ width: 18px;
+ height: 80%;
+}
+#SideTabs li {
+ clear: left;
+ width: 16px;
+ text-indent: -100em;
+ height: 70px;
+ overflow: hidden;
+ margin: 5px 0 5px 2px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+}
+#SideTabs li#sidetab_versions { background-image: url(../images/sidetabs/versions.gif); }
+#SideTabs li#sidetab_sitetree { background-image: url(../images/sidetabs/sitemap.gif); }
+#SideTabs li#sidetab_tasklist { background-image: url(../images/sidetabs/tasklist.gif); }
+#SideTabs li#sidetab_waitingon { background-image: url(../images/sidetabs/waitingon.gif); }
+#SideTabs li#sidetab_comments { background-image: url(../images/sidetabs/comments.gif); }
+#SideTabs li#sidetab_reports { background-image: url(../images/sidetabs/reports.gif); }
+#SideTabs li#sidetab_search { background-image: url(../images/sidetabs/search.gif); }
+#SideTabs li#sidetab_advertisements { background-image: url(../images/sidetabs/advertisements.gif); }
+
+#SideTabs li#sidetab_versions.selected { background-image: url(../images/sidetabs/versions_over.gif); }
+#SideTabs li#sidetab_sitetree.selected { background-image: url(../images/sidetabs/sitemap_over.gif); }
+#SideTabs li#sidetab_tasklist.selected { background-image: url(../images/sidetabs/tasklist_over.gif); }
+#SideTabs li#sidetab_waitingon.selected { background-image: url(../images/sidetabs/waitingon_over.gif); }
+#SideTabs li#sidetab_comments.selected { background-image: url(../images/sidetabs/comments_over.gif); }
+#SideTabs li#sidetab_reports.selected { background-image: url(../images/sidetabs/reports_over.gif); }
+#SideTabs li#sidetab_search.selected { background-image: url(../images/sidetabs/search_over.gif); }
+#SideTabs li#sidetab_advertisements.selected { background-image: url(../images/sidetabs/advertisements_over.gif); }
+
+#left {
+ background-color: #EEE;
+}
+
+#treepanes {
+ background-color: #FFF;
+ border-left: 1px #ccc solid;
+}
+
+.listpane p {
+ margin: 3px 3px 7px 3px;
+ font-size: 10px;
+}
+.listpane ul {
+ margin: 0;
+ padding: 0;
+}
+.listpane ul li {
+ list-style-type: none;
+ padding: 5px 2px 5px 2px;
+ font-size: 10px;
+ border-bottom: 1px #CCC dotted;
+ background-color: #FFF;
+}
+.listpane ul li.odd {
+ background-color: #EEE;
+}
+
+.listpane ul li .extra {
+ font-style: italic;
+ color: #666;
+}
+.listpane div.unitBody {
+ overflow: auto;
+ width: 100%;
+}
+
+
+#CommentList {
+ padding: 0;
+ margin: 0;
+}
+#CommentList li {
+ list-style-type: none;
+ background-color: #EEE;
+ border: 1px #CCC solid;
+ padding: 3px;
+ margin: 7px;
+}
+#CommentList li .extra {
+ margin-top: 3px;
+ font-size: 80%;
+ font-style: italic;
+}
+
+#ReportSelector_holder {
+ background-color: #EEE;
+ border-bottom: 1px #CCC solid;
+ margin: 0;
+ padding: 3px;
+}
+#ReportSelector_holder select {
+ width: 100%;
+}
+
+#treepanes .pane_actions {
+ margin: 7px 4px;
+}
+
+#treepanes .pane_actions a {
+ margin: 4px;
+ padding: 3px;
+ background-color: #EEE;
+ border: 1px #CCC solid;
+}
+#treepanes .pane_actions a.current {
+ background-color: #CCC;
+}
+
+
+#treepanes table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+#treepanes table thead td {
+ background-color: #777;
+ color: white;
+ border-bottom: 1px #CCC solid;
+}
+
+#treepanes table tbody tr {
+ cursor: pointer;
+}
+
+#treepanes table tbody tr.odd td {
+ background-color: #EEE;
+}
+#treepanes table tbody tr.over td {
+ background-color: #FFFFBB;
+}
+
+#treepanes table tbody tr.current td {
+ background-color: #BBBBFF;
+}
+
+
+#treepanes #Versions tbody tr.internal {
+ color: #777;
+}
+#treepanes #Versions tbody tr.published {
+ color: black;
+}
+
+/**
+ * Change the styling of the root tree node
+ */
+ul.tree span.a.Root, ul.tree span.a.last.Root, ul.tree span.a.children.Root {
+ background-image: none;
+}
+ul.tree span.a.Root span.c, ul.tree span.a.last.Root span.c, ul.tree span.a.children.Root span.c {
+ margin: 0;
+}
+ul#sitetree.tree ul {
+ margin-left: 0;
+}
+ul#sitetree.tree ul ul {
+ margin-left: 16px;
+}
+
+
+
+ul.tree span.a.MailType, ul.tree span.a.last.MailType, ul.tree span.a.children.MailType {
+ background-image: none;
+}
+ul.tree span.a.MailType span.c, ul.tree span.a.last.MailType span.c, ul.tree span.a.children.MailType span.c {
+ margin: 0;
+}
+
+.contextMenu {
+ padding: 0;
+ margin: 0;
+}
+.contextMenu li {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+.contextMenu li a {
+ display: block;
+ font-family: Tahoma, Verdana, Arial, Helvetica;
+ font-size: 11px;
+ padding: 5px 10px 5px 5px;
+ color: black;
+ text-decoration: none;
+}
+.contextMenu li a:hover {
+ background-color: #B6BDD2;
+ text-decoration: none;
+}
diff --git a/css/cms_right.css b/css/cms_right.css
new file mode 100644
index 00000000..457b1575
--- /dev/null
+++ b/css/cms_right.css
@@ -0,0 +1,765 @@
+
+.right form {
+ margin: 1em;
+ position: relative;
+ /*top: 70px;*/
+}
+html>body .right from {
+ /*top: 50px;*/
+}
+
+
+iframe {
+ border : none;
+}
+
+.right form div.field {
+ clear: both;
+ margin-left: 10em;
+ font-size: 1.2em;
+ vertical-align: middle;
+}
+.right form p.field {
+ clear: both;
+ font-size: 1.2em;
+ margin: 0;
+}
+
+.right form div.field.nolabel {
+ margin-left: 0;
+}
+
+.right form label.left {
+ float: left;
+ width: 10em;
+ margin-left: -10em;
+}
+
+.right form input.maxlength {
+ width: auto;
+}
+
+.right form textarea {
+ width: 98%;
+}
+
+.right form textarea.htmleditor {
+ width: 90%;
+ display : none;
+}
+html>body .right form textarea.htmleditor {
+ width: 98%;
+}
+
+.right form span.readonly {
+ border: 1px #CCC dotted;
+ background-color: #F7F7F7;
+ display: block;
+ width: 98%;
+ padding: 3px;
+ margin:5px 0;
+}
+
+.right form .validation,
+.right form .error,
+.right form .required
+{
+ border: 1px solid #f00;
+ background: #fcc;
+ padding: 0.5em;
+ width: 50%;
+}
+
+.right form .TableField .message {
+ width: auto;
+}
+
+.right form .creditCardfield input {
+ width: 4.415em;
+}
+
+.right form input.checkbox, .right form .optionset input, .right form .htmleditor select, .right form input.action {
+ width: auto;
+}
+.right form ul.optionset {
+ padding: 0;
+}
+.right form .optionset li {
+ list-style: none;
+}
+
+.right form h2 {
+ clear: both;
+}
+
+.right form .fieldgroup input, .right form .fieldgroup select {
+ width: auto;
+}
+
+
+
+/**
+ * TableField (and subclasses)
+ */
+table.TableField,
+table.TableListField,
+.TableListField table.data,
+table.CMSList {
+ border-collapse: collapse;
+ border-spacing: 0;
+ border : 1px solid #aaaaaa;
+ width : 100%;
+}
+
+/* HACK Preventing IE6 from showing double borders */
+body>div table.TableField,
+body>div table.TableListField,
+body>div .TableListField table.data,
+body>div table.CMSList {
+ border-collapse: separate;
+}
+
+table.TableField td,
+table.TableListField td,
+.TableListField table.data td,
+table.CMSList td {
+ border-style:none;
+}
+
+table.TableField th,
+table.TableListField th,
+.TableListField table.data th,
+table.CMSList th {
+ whitespace: no-wrap;
+}
+
+table.TableField thead th,
+.TableListField table.data thead th,
+table.CMSList thead th {
+ background-image : url(../../cms/images/tables/thead.png);
+ background-repeat : repeat-x;
+ background-position : left bottom;
+ background-color : #ebeadb;
+ height : 24px;
+ border-right : 1px solid #aca899;
+ border-left : 1px solid #ffffff;
+ padding-left : 5px;
+}
+
+table.TableField thead th span,
+.TableListField table.data thead th span {
+ display: block;
+ float: left;
+}
+
+table.TableField thead th a,
+.TableListField table.data thead th a,
+table.CMSList thead th a {
+ color: #000;
+}
+
+table.TableField thead th span.sortLink,
+.TableListField table.data thead th span.sortLink,
+table.CMSList thead th span.sortLink {
+ width: 16px;
+ height: 16px;
+ overflow: hidden;
+}
+
+table.TableField tfoot tr.summary td,
+.TableListField table.data tfoot tr.summary td,
+table.CMSList tfoot tr.summary td {
+ background-color : #ebeadb;
+}
+
+table.TableField tbody td,
+table.TableField tfoot td,
+.TableListField table.data tbody td,
+.TableListField table.data tfoot td,
+table.CMSList tbody td,
+table.CMSList tfoot td {
+ border : 1px solid #f1efe2;
+ padding-left : 5px;
+}
+
+.TableField td input,
+.TableListField td input {
+ width: 98%;
+}
+
+table.data tbody td input {
+ border:0 !important;
+}
+
+table.TableField tbody td.checkbox,
+.TableListField table.data tbody td.checkbox,
+table.CMSList tbody td.checkbox {
+ border : 1px solid #f1efe2;
+ padding-left : 5px;
+ background-image : url(../../cms/images/tables/checkbox.png);
+ background-repeat : repeat-x;
+ background-position : left bottom;
+}
+
+table.TableField tbody tr.over td,
+.TableListField table.data tbody tr.over td,
+table.CMSList tbody td.over td{
+ background-color: #FFC;
+}
+
+table.TableField tbody tr.current td,
+.TableListField table.data tbody tr.current td,
+table.CMSList tbody td.current td {
+ background-color: #316ac5;
+ color : white;
+}
+
+.TableListField table.data tfoot .addlink img {
+ vertical-align: middle;
+ margin: 3px 6px 3px 3px;
+}
+
+.right form .TableField span.readonly {
+ border: 0;
+ background: none;
+ padding: 0;
+ margin-bottom: 0;
+}
+
+.TableListField div.utility {
+ margin-top: 0.5em;
+}
+
+.TableListField div.utility a {
+ font-size: 9px;
+ border: 1px solid #aaa;
+ padding: 2px;
+}
+
+/**
+ * Selection Groups
+ */
+.SelectionGroup {
+ padding: 0px;
+ clear : both;
+}
+.SelectionGroup li {
+ list-style-type: none;
+ float : left;
+ clear : both;
+}
+.SelectionGroup li input {
+ width: 20px;
+ float : left;
+
+}
+
+.SelectionGroup li div.field {
+ display: none;
+ font-size: 1em;
+}
+.SelectionGroup li input, .right .SelectionGroup li label {
+ display: block;
+}
+.SelectionGroup li.selected div.field {
+ margin-left: 30px;
+ display: block;
+ margin-bottom: 1em;
+}
+
+.SelectionGroup li.selected label.selector {
+ font-weight: bold;
+}
+
+/**
+ * Composite Fields - raw concatenation of fields for programmatic purposes.
+ */
+
+
+.right form div.field.CompositeField {
+ margin-left: 7.5em;
+}
+.right form div.field.CompositeField div.field {
+ font-size: 1em;
+}
+
+.right form div.field.CompositeField {
+ clear: both;
+}
+.right form div.field.CompositeField label.left {
+ float: left;
+ width: 10em;
+ margin-left: -10em;
+}
+
+.right form div.column2 {
+ float: left;
+ width: 45%;
+ margin-right: 4%;
+}
+
+.right form div.multicolumn {
+ width: 100%;
+ float: left;
+ clear: left;
+}
+
+/**
+ * Checkbox set fields
+ */
+#right form .CheckboxSetField ul{
+ margin:0;
+ padding:0;
+}
+#right form .CheckboxSetField ul li{
+ list-style:none !important;
+ margin:0;
+ padding:0;
+}
+#right form .CheckboxSetField input{
+ width:auto;
+}
+
+
+
+.ajaxActions {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 0;
+ margin: 0;
+ text-align: right;
+ background-image:url(../images/textures/ToolBar.png);
+ height : 21px;
+ border-left : 1px solid #808080;
+ border-bottom : 1px solid #808080;
+}
+.ajaxActions input {
+ padding: 0;
+ border-style: none;
+ background-color:transparent;
+ cursor: pointer;
+ float : left;
+ height : 22px;
+ background-image:url(../images/textures/seperator.png);
+ background-repeat:no-repeat;
+ background-position:right;
+ padding-left: 10px;
+ padding-right : 10px;
+ margin : 0px;
+ color : #333333;
+}
+
+.right form div.tab {
+ clear: left;
+ overflow: auto;
+ border: 1px #AAA solid;
+ border-top: none;
+ position: relative;
+ top: -3px;
+ margin: 0;
+ padding: 10px;
+ /*width: 98%;*/
+}
+.right form div.tabset {
+ border: 1px solid #fff; /* Hack for FF1.5/Win Float-Bug */
+ clear: left;
+ margin: 0;
+ /*width: 100%;*/
+}
+
+/**
+ * Bottom-right
+ */
+
+#rightbottom {
+
+
+border : none;
+margin-top : 4px;
+margin-left : 1px;
+border : 1px solid #808080;
+
+}
+
+
+#rightbottom form {
+ margin: 1em;
+ /*position: relative;
+ top: 50px;*/
+}
+
+#rightbottom form div {
+ margin-top : 3px;
+}
+
+
+#rightbottom form div.field {
+ clear: both;
+ margin-left: 10em;
+ font-size: 1.2em;
+
+ border : none;
+}
+#rightbottom form label.left {
+ float: left;
+ width: 10em;
+ margin-left: -10em;
+
+
+}
+
+#rightbottom form input, #rightbottom form select, #rightbottom form textarea {
+ width: 90%;
+}
+
+#rightbottom form input.checkbox, #rightbottom form .optionset input, #rightbottom form .htmleditor select, #rightbottom form input.action {
+ width: auto;
+}
+#rightbottom form ul.optionset {
+ padding: 0;
+}
+#rightbottom form .optionset li {
+ list-style: none;
+}
+
+#rightbottom form h2 {
+ clear: both;
+}
+
+#rightbottom form .fieldgroup input, #rightbottom form .fieldgroup select {
+ width: auto;
+}
+
+/**
+ * RHS Action Parameters boxes
+ */
+#right form.actionparams {
+ border-bottom: 1px #777 solid;
+ border-left: 1px #777 solid;
+ background-color: #CCC;
+ position: absolute;
+ top: 23px;
+ right: 0px;
+ width: 300px;
+
+ z-index: 200;
+
+ margin: 0;
+ padding: 10px;
+
+}
+#right form.actionparams div.field {
+ margin-left: 6em;
+}
+
+#right form.actionparams label.left {
+ width: 6em;
+ margin-left: -6em;
+}
+#right form.actionparams input, #right form.actionparams select, #right form.actionparams textarea {
+ width: 100%;
+}
+#right form.actionparams input.checkbox, #right form.actionparams .optionset input {
+ width: auto;
+}
+
+#right form.actionparams p.label {
+ margin: 10px 0 0 0;
+}
+#right form.actionparams p.actions {
+ margin: 10px 0 0 0;
+ text-align: right;
+}
+#right form.actionparams p.actions input {
+ width: auto;
+}
+#right form.actionparams .sendingText {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ padding: 10% 0 0 0;
+ height: 60%;
+ background-color: #CCC;
+ font-size: 25px;
+ font-weight: bold;
+ color: white;
+ text-align: center;
+}
+
+#right form.actionparams div.TreeDropdownField {
+ width: 230px;
+
+}
+#right form.actionparams div.TreeDropdownField span.items {
+ width: 210px;
+}
+
+#right form.actionparams div.TreeDropdownField div.tree_holder {
+ width: 214px;
+}
+
+div.TreeDropdownField {
+ width: 35em;
+ padding: 0;
+}
+html>body div.TreeDropdownField {
+ position:relative;
+}
+
+div.TreeDropdownField span.items {
+ display: block;
+ height: 100%;
+ border: 1px #7f9db9 solid;
+ cursor: pointer;
+ width: 33em;
+ float: left;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ background-color: white;
+}
+
+div.TreeDropdownField div.tree_holder {
+ clear: left;
+ cursor: default;
+ border: 1px black solid;
+ margin: 0;
+ height: 200px;
+ overflow: auto;
+ background-color: white;
+ /**
+ * HACK IE6, see http://www.hedgerwow.com/360/bugs/css-select-free.html
+ */
+ position:absolute;
+ z-index:10;
+ width:33em;/*must have for any value*/;
+}
+
+html>body div.TreeDropdownField div.tree_holder {
+ top: 20px;
+ left: 0px;
+ z-index: 1000;
+}
+
+/**
+ * HACK IE6, see http://www.hedgerwow.com/360/bugs/css-select-free.html
+ */
+div.TreeDropdownField div.tree_holder iframe {
+ display:none;/* IE5*/
+ display/**/:block;/* IE5*/
+ position:absolute;
+ top:0;
+ left:0;
+ z-index:-1;
+ filter:mask();
+ width:31em;/*must have for any big value*/
+ height:200px/*must have for any big value*/;
+}
+
+div.TreeDropdownField a.editLink {
+ border-width: 1px 1px 1px 0;
+ background: url(../../sapphire/images/TreeDropdownField_button.gif) left top no-repeat;
+ width: 19px;
+ height: 21px;
+ margin: 0;
+ padding: 0;
+ float: left;
+ clear: right;
+ z-index: 0;
+ overflow: hidden;
+}
+
+
+.autocomplete {
+ background-color: #EEEEEE;
+ margin : 0px;
+ padding : 0px;
+
+
+
+}
+.autocomplete ul {
+ border: 1px #AAA solid;
+ margin : 0px;
+ padding : 0px;
+
+
+}
+.autocomplete li {
+ list-style-type: none;
+ cursor: pointer;
+ margin: 0px;
+ font-size : 12px;
+ padding : 3px;
+ white-space : nowrap;
+}
+.autocomplete .data {
+ display: none;
+}
+.autocomplete .informal {
+ font-style: italic;
+}
+
+/*
+ * Status
+ */
+
+#statusMessage {
+ position: absolute;
+ z-index: 500;
+ bottom: 15px;
+ right: 30px;
+ text-align: left;
+
+ padding: 15px 15px 15px 40px;
+
+ font-size: 16px;
+ font-weight: bold;
+
+ border: 1px solid #cc9;
+ color: #660;
+ background-color: #F9F9E3;
+}
+
+#statusMessage.good {
+ border-color: #9c9;
+ color: #060;
+ background: url(../images/alert-good.gif) #E2F9E3 7px no-repeat;
+}
+#statusMessage.bad {
+ border-color: #c99;
+ color: #fff;
+ background: url(../images/alert-bad.gif) #c00 7px no-repeat;
+ max-height: 600px;
+ overflow: auto;
+}
+.pageStatusMessage {
+ position: absolute;
+ top: 0;
+ right: 0;
+ background-color: #F9F9E3;
+ font-weight: bold;
+ font-size: 1.2em;
+ padding: 5px;
+ border: 1px solid #cc9;
+}
+
+
+
+
+
+#right ins * {
+ background-color: green;
+}
+
+
+
+
+
+#right del *{
+ background-color: red;
+}
+
+
+
+
+
+div#ImportFile iframe {
+ width: 80%;
+ height: auto;
+ border: none;
+}
+
+table.ReportField {
+ position: relative;
+ border: solid 1px #82C0FF;
+ border-collapse: collapse;
+}
+
+ table.ReportField thead th {
+ background-color: #82C0FF;
+ }
+
+ table.ReportField td, table.ReportField {
+ border: solid 1px #82C0FF;
+ }
+
+ table.ReportField tr {
+ border-bottom: solid 1px #82C0FF;
+ }
+
+div.ProgressBar {
+ text-align: center;
+ display: none;
+}
+
+ div.ProgressBar p.text {
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ div.ProgressBar div.BarHolder {
+ width: 100%;
+ border: solid 1px #777;
+ background-color: #FFF;
+ padding: 2px;
+ text-align: left;
+ }
+
+ div.ProgressBar div.BarHolder div.Bar {
+ width: 0%;
+ background-color: #0074C6;
+ }
+
+div.MemberListFilter,
+div.MemberFilter {
+ /*float: right;*/
+ width: 40em;
+ margin: 5px 0;
+ padding: 5px;
+ background-color : #ebeadb;
+ border: 1px solid #aca899;
+}
+
+ .right form div.MemberListFilter label,
+ .right form div.MemberFilter label {
+ display: block;
+ width: 10em;
+ float: left;
+ margin-left: 0em;
+ }
+
+ div.MemberListFilter div.field,
+ div.MemberFilter div.field {
+ margin-left: 0em;
+ }
+
+ div.MemberListFilter input,
+ div.MemberListFilter select,
+ div.MemberFilter input,
+ div.MemberFilter select {
+ width: auto;
+ }
+
+#right iframe.AWStatsReport {
+ width: 98%;
+ height: 85%;
+}
+
+#sitetree a.contents {
+ font-style: italic;
+}
+
+/**
+ * SecurityAdmin
+ */
+#Form_EditForm_Permissions .Arg,
+#Form_EditForm_Permissions .Enabled {
+ width: 6em;
+}
+
+#Form_EditForm_Permissions .Arg input {
+ text-align: right;
+}
\ No newline at end of file
diff --git a/css/dialog.css b/css/dialog.css
new file mode 100644
index 00000000..f43b9ee7
--- /dev/null
+++ b/css/dialog.css
@@ -0,0 +1,18 @@
+p.message {
+ /*background: url(../cms/images/dialogs/alert.gif) repeat top left;*/
+ padding-left: 60px;
+}
+
+img {
+ float: left;
+ margin-left: -60px;
+}
+
+span {
+ clear: right;
+}
+
+body {
+ font-family: Arial, sans-serif;
+ font-size: 1em;
+}
\ No newline at end of file
diff --git a/css/layout.css b/css/layout.css
new file mode 100644
index 00000000..f39e8f40
--- /dev/null
+++ b/css/layout.css
@@ -0,0 +1,433 @@
+/*
+ Tenative color scheme:
+ #0074C6 #95B2C6 #639DC6 #162F42 #2C5F84
+ 4A7695 819BAD
+ */
+
+html {
+ margin: 0;
+ height: 100%;
+ overflow:hidden;
+}
+body {
+ height: 100%;
+ margin: 0;
+ font-size: 62.5%;
+ background-color: #d4d0c8;
+}
+
+#top, #left, #bottom, div.title, div.mceToolbarExternal, ul.tabstrip, div.field label, #separator {
+
+/*-moz-user-select: none;*/ /*need to explicitly allow input box selection becuase of this */
+
+}
+
+
+
+
+form fieldset {
+ margin: 0;
+ padding: 0;
+ border-style: none;
+}
+form#Form_EditForm fieldset {
+ height: 100%;
+}
+
+
+body.stillLoading select {
+ display: none;
+}
+
+
+
+/** 3-PANEL LAYOUT **/
+#top {
+ font-size: 1.4em;
+ height: 51px;
+ padding: 3px;
+ background: url(../images/mainmenu/top-bg.gif) top left repeat-x;
+ color: #FFF;
+ border-bottom: 3px solid #d4d0c8;
+ overflow: hidden;
+}
+#left {
+ position: absolute;
+ top: 51px;
+ left: 3px;
+ width: 205px;
+ height: 92%;
+ z-index: 50;
+ border : 1px #808080 solid;
+ background-color:#FFFFFF;
+}
+#right, #rightbottom {
+ position: absolute;
+ left: 208px;
+ top: 51px;
+ height: 92%;
+ width: 600px;
+ overflow: auto;
+ z-index: 50;
+ border: 1px #808080 solid;
+ background-color: white;
+ margin-right : 3px;
+}
+
+#rightbottom {
+ height: 30%;
+}
+
+#separator {
+ position: absolute;
+ top: 51px;
+ left: 205px;
+ height: 92%;
+ width: 3px;
+ cursor: e-resize;
+ border-left : 1px solid #ffffff;
+}
+
+
+/**
+ * Hidden left-hand panel
+ */
+#left.hidden form, #left.hidden .title, #left.hidden #TreeActions {
+ display: none;
+}
+#left.hidden #SideTabs {
+ margin-top: 47px;
+}
+
+#left.hidden {
+ width: 18px;
+ display: block;
+}
+#separator.hidden {
+ display: none;
+}
+
+
+#left div.title, #right div.title, #rightbottom div.title{
+ border-top: 1px #77BBEE solid;
+ background-image:url(../images/textures/obar.gif);
+ height : 22px !important;
+ background-color : #0075C9;
+ background-repeat : repeat-x !important;
+ background-position: 0px 0px;
+
+
+
+}
+
+#rightbottom div.light {
+
+ background-image: url(../images/textures/obar-light.png) !important;
+
+}
+
+
+#left div.title div, #right div.title div, #rightbottom div.title div{
+ font-size : 14px;
+ font-weight : bold;
+ color: white;
+ padding: 2px 0 0 4px;
+ background-position: 2px 2px;
+ background-repeat:no-repeat;
+ padding-left : 20px;
+ border-top: 1px #77BBEE solid;
+
+}
+
+#left h2 {
+ margin: 0;
+ background-image:url(../images/textures/obar-18.gif);
+ height: 18px;
+ color: white;
+ font-size: 12px;
+ padding-left: 3px;
+}
+
+
+
+/** TOP PANEL **/
+#top #MainMenu {
+ margin: 0;
+ padding: 0;
+}
+#top #MainMenu li {
+ margin: 8px 8px 0 8px;
+ padding: 6px 8px 0 28px;
+ list-style-type: none;
+ float: left;
+ height: 32px;
+ background-repeat: no-repeat;
+ cursor: pointer;
+}
+html>body #top #MainMenu li {
+ height: 28px; /* 32 - 6 from the padding */
+}
+
+#top #MainMenu a {
+ color: white;
+}
+
+#top #MainMenu .current {
+ background-color: #d4d0c8;
+ background-position: 5px 5px;
+ position: relative;
+ top : -7px;
+ height : 31px;
+ padding-top : 12px;
+ padding-left : 30px;
+ -moz-border-radius-topright : 8px;
+ -moz-border-radius-topleft : 8px;
+ border : 1px solid #65686e;
+ border-bottom : none;
+
+
+}
+
+#top #MainMenu .current A:link,#top #MainMenu .current A:visited {
+
+ color : black;
+
+}
+
+
+
+#top #MainMenu #Menu-content { background-image: url(../images/mainmenu/content.gif); }
+#top #MainMenu #Menu-security { background-image: url(../images/mainmenu/members.gif); }
+#top #MainMenu #Menu-report { background-image: url(../images/mainmenu/reports.gif); }
+#top #MainMenu #Menu-newsletter { background-image: url(../images/mainmenu/emails.gif); }
+#top #MainMenu #Menu-files { background-image: url(../images/mainmenu/files.gif); }
+
+#top #MainMenu #Menu-help { background-image: url(../images/mainmenu/help.gif);
+ float: right;
+ /*padding: 6px 28px 0 8px;
+ background-position: 100% 0;*/
+ margin-right: 60px;
+}
+
+
+#top #Logo {
+ float: right;
+ margin: 0;
+ padding: 16px 60px 16px 0;
+ height: 32px;
+ font-size: 16px;
+ background: url(../images/mainmenu/logo.gif) right top no-repeat;
+}
+html>body #top #Logo {
+ padding-bottom: 0;
+}
+
+#top #Logo a {
+ color: white;
+}
+
+
+
+
+
+#bottom {
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ border-top: 3px solid #d4d0c8;
+ background-color:#81858d;
+ height: 18px;
+ overflow:hidden;
+ background-image:url(../images/textures/bottom.png);
+
+}
+
+#bottom .holder {
+
+ text-align: center;
+ padding-top : 3px;
+ padding-left : 3px;
+ padding-right : 6px;
+ font-size: 10px;
+ color: white;
+ border-top: 1px solid #555555;
+
+}
+
+#bottom #logInStatus {
+ float: right;
+}
+#bottom #switchView {
+ float: left;
+
+}
+
+#bottom .bottomTabs a {
+ display: block;
+ float : left;
+ height : 13px;
+ padding-left : 12px;
+ padding-right : 12px;
+ position:relative;
+ top : -3px;
+ border : 1px solid #65686e;
+ border-top : none;
+ cursor:pointer;
+ background-color: #cdc9c1;
+ color : #333333;
+ -moz-border-radius-bottomright : 4px;
+ -moz-border-radius-bottomleft : 4px;
+}
+
+#bottom .bottomTabs div.blank {
+ display: block;
+ float : left;
+ height : 13px;
+ padding-left : 12px;
+ padding-right : 12px;
+ position:relative;
+ top : -3px;
+ border : 1px solid #65686e;
+ border-top : none;
+ cursor:pointer;
+ background-color: #cdc9c1;
+ color : #333333;
+
+ border : none;
+ background-color: transparent;
+ padding-right: 2px;
+ padding-left: 2px;
+ padding-top : 2px;
+ color:#FFFFFF;
+}
+
+
+#bottom .bottomTabs a.current {
+ background-color : #d4d0c8;
+ padding-top : 1px;
+ top : -5px;
+ height : 15px;
+ font-weight:bold;
+ font-size : 11px;
+ border : 1px solid #555555;
+}
+
+
+/** LEFT PANEL **/
+
+#sitetree_holder {
+ height: 80%;
+ width: 100%;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+#sitetree {
+ width: 500px; /* IE's chocking right now */
+}
+html> #sitetree {
+ width: auto;
+}
+
+div.spacer, li.spacer {
+ list-style-type: none;
+ float: none;
+ clear: both;
+ background-color: transparent;
+ border-style: none;
+ padding: 0;
+ margin: -1px 0 0 0;
+ height: 1px;
+ font-size: 1px;
+ width: auto;
+}
+/** RIGHT PANEL **/
+
+.mceToolbarExternal {
+ background-color: #EEE;
+ border-bottom: 1px #CCC solid;
+ /*position: absolute;
+ top: 23px;
+ left: 0;*/
+ display: block;
+}
+
+
+#right form#Form_EditorToolbarLinkForm,
+#right form#Form_EditorToolbarImageForm,
+#right form#Form_EditorToolbarFlashForm {
+ background-color: #EEE;
+ border-bottom: 1px #CCC solid;
+ display: none;
+ margin: 1px 0 0 0;
+ padding: 5px;
+ height: 115px;
+ /*
+ * HACK IE (all versions): container needs to be higher stacking order
+ * than any DOM-elemnt under it.
+ * @see http://www.aplus.co.yu/lab/z-pos/
+ */
+ z-index: 1001;
+}
+
+#right form#Form_EditorToolbarImageForm {
+ height: 160px;
+}
+#right form#Form_EditorToolbarFlashForm {
+ height: 130px;
+}
+
+
+#right form#Form_EditorToolbarLinkForm ul.optionset {
+ height: 1em;
+ margin: 0;
+}
+#right form#Form_EditorToolbarLinkForm ul.optionset li {
+ float: left;
+}
+
+.thumbnailstrip {
+ height: 58px;
+ overflow: auto;
+ white-space: nowrap;
+ width: 100%;
+ float: left;
+ border : 1px solid #AAAAAA;
+}
+
+.thumbnailstrip ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.thumbnailstrip li {
+ float: left;
+ margin-right: 4px;
+}
+
+.indicator.inline {
+ display: inline;
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+p.Actions {
+ vertical-align: middle;
+}
+
+.indicator.block {
+ display: inline;
+}
+
+/* CMS specific icons for the tree */
+
+
+ul.tree li.Root span.Root span.c a {
+
+ background-image : url(../../cms/images/treeicons/root.png) !important;
+
+}
+
+
+
diff --git a/css/typography.css b/css/typography.css
new file mode 100644
index 00000000..267e1558
--- /dev/null
+++ b/css/typography.css
@@ -0,0 +1,49 @@
+body {
+ font-size: 62.5%;
+}
+* {
+ font-family: Arial;
+ font-size: 1em;
+
+}
+p, ul, td, th {
+ font-size: 1.2em;
+}
+ /* overrides (to avoid larger font-sizing) */
+ ul ul,
+ th p,
+ td p,
+ div ul.optionset {
+ font-size: 1em;
+ }
+
+ul ul {
+ font-size: 1em;
+}
+h1 {
+ font-size: 2em;
+ color: #0074C6;
+}
+h2 {
+ font-size: 1.8em;
+ color: #0074C6;
+}
+
+a {
+ color: #162F42;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a img {
+ border-style: none;
+}
+#Header a, #ProductGroups a, #Products a {
+ color: white;
+}
+
+
+#Header h1 {
+ color: white;
+}
\ No newline at end of file
diff --git a/images/add.gif b/images/add.gif
new file mode 100644
index 00000000..7492335c
Binary files /dev/null and b/images/add.gif differ
diff --git a/images/alert-bad.gif b/images/alert-bad.gif
new file mode 100755
index 00000000..33746c00
Binary files /dev/null and b/images/alert-bad.gif differ
diff --git a/images/alert-good.gif b/images/alert-good.gif
new file mode 100755
index 00000000..faf1717e
Binary files /dev/null and b/images/alert-good.gif differ
diff --git a/images/block.png b/images/block.png
new file mode 100755
index 00000000..1d744074
Binary files /dev/null and b/images/block.png differ
diff --git a/images/bullet_arrow_down.png b/images/bullet_arrow_down.png
new file mode 100755
index 00000000..9b23c06d
Binary files /dev/null and b/images/bullet_arrow_down.png differ
diff --git a/images/bullet_arrow_up.png b/images/bullet_arrow_up.png
new file mode 100755
index 00000000..24df0f42
Binary files /dev/null and b/images/bullet_arrow_up.png differ
diff --git a/images/button-bg.gif b/images/button-bg.gif
new file mode 100755
index 00000000..730d67c8
Binary files /dev/null and b/images/button-bg.gif differ
diff --git a/images/button-left.gif b/images/button-left.gif
new file mode 100755
index 00000000..25c2d9a9
Binary files /dev/null and b/images/button-left.gif differ
diff --git a/images/button-right.gif b/images/button-right.gif
new file mode 100755
index 00000000..6c82624c
Binary files /dev/null and b/images/button-right.gif differ
diff --git a/images/check.png b/images/check.png
new file mode 100755
index 00000000..67b695fa
Binary files /dev/null and b/images/check.png differ
diff --git a/images/delete-small.gif b/images/delete-small.gif
new file mode 100755
index 00000000..476fe8fe
Binary files /dev/null and b/images/delete-small.gif differ
diff --git a/images/delete.gif b/images/delete.gif
new file mode 100644
index 00000000..27dfea12
Binary files /dev/null and b/images/delete.gif differ
diff --git a/images/dialogs/alert.gif b/images/dialogs/alert.gif
new file mode 100644
index 00000000..87317ef5
Binary files /dev/null and b/images/dialogs/alert.gif differ
diff --git a/images/dialogs/alert.png b/images/dialogs/alert.png
new file mode 100644
index 00000000..36b560ba
Binary files /dev/null and b/images/dialogs/alert.png differ
diff --git a/images/down.gif b/images/down.gif
new file mode 100644
index 00000000..609bf6bc
Binary files /dev/null and b/images/down.gif differ
diff --git a/images/edit.gif b/images/edit.gif
new file mode 100755
index 00000000..c0e3d387
Binary files /dev/null and b/images/edit.gif differ
diff --git a/images/loading.gif b/images/loading.gif
new file mode 100755
index 00000000..3a020ddb
Binary files /dev/null and b/images/loading.gif differ
diff --git a/images/locked.gif b/images/locked.gif
new file mode 100755
index 00000000..30d5491c
Binary files /dev/null and b/images/locked.gif differ
diff --git a/images/mainmenu/content.gif b/images/mainmenu/content.gif
new file mode 100755
index 00000000..92c56ec0
Binary files /dev/null and b/images/mainmenu/content.gif differ
diff --git a/images/mainmenu/content.png b/images/mainmenu/content.png
new file mode 100755
index 00000000..7fae7489
Binary files /dev/null and b/images/mainmenu/content.png differ
diff --git a/images/mainmenu/emails.gif b/images/mainmenu/emails.gif
new file mode 100755
index 00000000..fe56b468
Binary files /dev/null and b/images/mainmenu/emails.gif differ
diff --git a/images/mainmenu/files.gif b/images/mainmenu/files.gif
new file mode 100755
index 00000000..1a16647f
Binary files /dev/null and b/images/mainmenu/files.gif differ
diff --git a/images/mainmenu/help.gif b/images/mainmenu/help.gif
new file mode 100755
index 00000000..4f87eac3
Binary files /dev/null and b/images/mainmenu/help.gif differ
diff --git a/images/mainmenu/help.png b/images/mainmenu/help.png
new file mode 100755
index 00000000..7324316a
Binary files /dev/null and b/images/mainmenu/help.png differ
diff --git a/images/mainmenu/logo-smallwhite.png b/images/mainmenu/logo-smallwhite.png
new file mode 100755
index 00000000..4a5b6395
Binary files /dev/null and b/images/mainmenu/logo-smallwhite.png differ
diff --git a/images/mainmenu/logo.gif b/images/mainmenu/logo.gif
new file mode 100755
index 00000000..bdf6e655
Binary files /dev/null and b/images/mainmenu/logo.gif differ
diff --git a/images/mainmenu/logo.png b/images/mainmenu/logo.png
new file mode 100755
index 00000000..c981e6ec
Binary files /dev/null and b/images/mainmenu/logo.png differ
diff --git a/images/mainmenu/members.gif b/images/mainmenu/members.gif
new file mode 100755
index 00000000..d1356484
Binary files /dev/null and b/images/mainmenu/members.gif differ
diff --git a/images/mainmenu/members.png b/images/mainmenu/members.png
new file mode 100755
index 00000000..0bf8e337
Binary files /dev/null and b/images/mainmenu/members.png differ
diff --git a/images/mainmenu/mgmt.png b/images/mainmenu/mgmt.png
new file mode 100755
index 00000000..7f249743
Binary files /dev/null and b/images/mainmenu/mgmt.png differ
diff --git a/images/mainmenu/reports.gif b/images/mainmenu/reports.gif
new file mode 100755
index 00000000..c697acec
Binary files /dev/null and b/images/mainmenu/reports.gif differ
diff --git a/images/mainmenu/top-bg.gif b/images/mainmenu/top-bg.gif
new file mode 100755
index 00000000..36979dc1
Binary files /dev/null and b/images/mainmenu/top-bg.gif differ
diff --git a/images/network-save-bw.gif b/images/network-save-bw.gif
new file mode 100755
index 00000000..cf718bcd
Binary files /dev/null and b/images/network-save-bw.gif differ
diff --git a/images/network-save.gif b/images/network-save.gif
new file mode 100644
index 00000000..d42f72c7
Binary files /dev/null and b/images/network-save.gif differ
diff --git a/images/network-saveOLD.gif b/images/network-saveOLD.gif
new file mode 100755
index 00000000..deac700d
Binary files /dev/null and b/images/network-saveOLD.gif differ
diff --git a/images/pagination/record-export.png b/images/pagination/record-export.png
new file mode 100755
index 00000000..dd34cf6e
Binary files /dev/null and b/images/pagination/record-export.png differ
diff --git a/images/pagination/record-first-g.png b/images/pagination/record-first-g.png
new file mode 100755
index 00000000..7ceeabc7
Binary files /dev/null and b/images/pagination/record-first-g.png differ
diff --git a/images/pagination/record-first.png b/images/pagination/record-first.png
new file mode 100755
index 00000000..88873dfc
Binary files /dev/null and b/images/pagination/record-first.png differ
diff --git a/images/pagination/record-last-g.png b/images/pagination/record-last-g.png
new file mode 100755
index 00000000..8654e336
Binary files /dev/null and b/images/pagination/record-last-g.png differ
diff --git a/images/pagination/record-last.png b/images/pagination/record-last.png
new file mode 100755
index 00000000..ba1d68ae
Binary files /dev/null and b/images/pagination/record-last.png differ
diff --git a/images/pagination/record-next-g.png b/images/pagination/record-next-g.png
new file mode 100755
index 00000000..74f72943
Binary files /dev/null and b/images/pagination/record-next-g.png differ
diff --git a/images/pagination/record-next.png b/images/pagination/record-next.png
new file mode 100755
index 00000000..e1c5340a
Binary files /dev/null and b/images/pagination/record-next.png differ
diff --git a/images/pagination/record-prev-g.png b/images/pagination/record-prev-g.png
new file mode 100755
index 00000000..7b04f4c1
Binary files /dev/null and b/images/pagination/record-prev-g.png differ
diff --git a/images/pagination/record-prev.png b/images/pagination/record-prev.png
new file mode 100755
index 00000000..33330922
Binary files /dev/null and b/images/pagination/record-prev.png differ
diff --git a/images/pagination/record-print.png b/images/pagination/record-print.png
new file mode 100755
index 00000000..02926897
Binary files /dev/null and b/images/pagination/record-print.png differ
diff --git a/images/panels/EditPage.png b/images/panels/EditPage.png
new file mode 100755
index 00000000..3585fc97
Binary files /dev/null and b/images/panels/EditPage.png differ
diff --git a/images/panels/MySite.png b/images/panels/MySite.png
new file mode 100755
index 00000000..6a26de5e
Binary files /dev/null and b/images/panels/MySite.png differ
diff --git a/images/right.gif b/images/right.gif
new file mode 100644
index 00000000..47e5ae7f
Binary files /dev/null and b/images/right.gif differ
diff --git a/images/show.png b/images/show.png
new file mode 100755
index 00000000..921d0775
Binary files /dev/null and b/images/show.png differ
diff --git a/images/sidetabs/advertisements.gif b/images/sidetabs/advertisements.gif
new file mode 100644
index 00000000..0b7cebc7
Binary files /dev/null and b/images/sidetabs/advertisements.gif differ
diff --git a/images/sidetabs/advertisements_over.gif b/images/sidetabs/advertisements_over.gif
new file mode 100644
index 00000000..5673cd9a
Binary files /dev/null and b/images/sidetabs/advertisements_over.gif differ
diff --git a/images/sidetabs/associations.gif b/images/sidetabs/associations.gif
new file mode 100644
index 00000000..5efe59a4
Binary files /dev/null and b/images/sidetabs/associations.gif differ
diff --git a/images/sidetabs/associations_over.gif b/images/sidetabs/associations_over.gif
new file mode 100644
index 00000000..7e91d8a3
Binary files /dev/null and b/images/sidetabs/associations_over.gif differ
diff --git a/images/sidetabs/categories.gif b/images/sidetabs/categories.gif
new file mode 100644
index 00000000..39ccaa72
Binary files /dev/null and b/images/sidetabs/categories.gif differ
diff --git a/images/sidetabs/categories_over.gif b/images/sidetabs/categories_over.gif
new file mode 100644
index 00000000..3adb1805
Binary files /dev/null and b/images/sidetabs/categories_over.gif differ
diff --git a/images/sidetabs/comments.gif b/images/sidetabs/comments.gif
new file mode 100755
index 00000000..7d4c913a
Binary files /dev/null and b/images/sidetabs/comments.gif differ
diff --git a/images/sidetabs/comments_over.gif b/images/sidetabs/comments_over.gif
new file mode 100755
index 00000000..66d8aac7
Binary files /dev/null and b/images/sidetabs/comments_over.gif differ
diff --git a/images/sidetabs/reports.gif b/images/sidetabs/reports.gif
new file mode 100755
index 00000000..aa479e66
Binary files /dev/null and b/images/sidetabs/reports.gif differ
diff --git a/images/sidetabs/reports_over.gif b/images/sidetabs/reports_over.gif
new file mode 100755
index 00000000..a3776465
Binary files /dev/null and b/images/sidetabs/reports_over.gif differ
diff --git a/images/sidetabs/search.gif b/images/sidetabs/search.gif
new file mode 100755
index 00000000..8b4e14c1
Binary files /dev/null and b/images/sidetabs/search.gif differ
diff --git a/images/sidetabs/search_over.gif b/images/sidetabs/search_over.gif
new file mode 100755
index 00000000..7733de1d
Binary files /dev/null and b/images/sidetabs/search_over.gif differ
diff --git a/images/sidetabs/sitemap.gif b/images/sidetabs/sitemap.gif
new file mode 100755
index 00000000..38f428f5
Binary files /dev/null and b/images/sidetabs/sitemap.gif differ
diff --git a/images/sidetabs/sitemap_over.gif b/images/sidetabs/sitemap_over.gif
new file mode 100755
index 00000000..d122af78
Binary files /dev/null and b/images/sidetabs/sitemap_over.gif differ
diff --git a/images/sidetabs/tasklist.gif b/images/sidetabs/tasklist.gif
new file mode 100755
index 00000000..48330bd8
Binary files /dev/null and b/images/sidetabs/tasklist.gif differ
diff --git a/images/sidetabs/tasklist_over.gif b/images/sidetabs/tasklist_over.gif
new file mode 100755
index 00000000..ae291460
Binary files /dev/null and b/images/sidetabs/tasklist_over.gif differ
diff --git a/images/sidetabs/template normal.psd b/images/sidetabs/template normal.psd
new file mode 100755
index 00000000..7f7723d8
Binary files /dev/null and b/images/sidetabs/template normal.psd differ
diff --git a/images/sidetabs/template over.psd b/images/sidetabs/template over.psd
new file mode 100755
index 00000000..0d3e8cfd
Binary files /dev/null and b/images/sidetabs/template over.psd differ
diff --git a/images/sidetabs/versions.gif b/images/sidetabs/versions.gif
new file mode 100755
index 00000000..f7546046
Binary files /dev/null and b/images/sidetabs/versions.gif differ
diff --git a/images/sidetabs/versions_over.gif b/images/sidetabs/versions_over.gif
new file mode 100755
index 00000000..89db3648
Binary files /dev/null and b/images/sidetabs/versions_over.gif differ
diff --git a/images/sidetabs/waitingon.gif b/images/sidetabs/waitingon.gif
new file mode 100755
index 00000000..0957bf2a
Binary files /dev/null and b/images/sidetabs/waitingon.gif differ
diff --git a/images/sidetabs/waitingon_over.gif b/images/sidetabs/waitingon_over.gif
new file mode 100755
index 00000000..8e5c6f9e
Binary files /dev/null and b/images/sidetabs/waitingon_over.gif differ
diff --git a/images/tables/checkbox.png b/images/tables/checkbox.png
new file mode 100755
index 00000000..71fbdd3b
Binary files /dev/null and b/images/tables/checkbox.png differ
diff --git a/images/tables/thead.png b/images/tables/thead.png
new file mode 100755
index 00000000..7a46a715
Binary files /dev/null and b/images/tables/thead.png differ
diff --git a/images/textures/ToolBar.png b/images/textures/ToolBar.png
new file mode 100755
index 00000000..dd4189b1
Binary files /dev/null and b/images/textures/ToolBar.png differ
diff --git a/images/textures/bottom.png b/images/textures/bottom.png
new file mode 100755
index 00000000..ca68f228
Binary files /dev/null and b/images/textures/bottom.png differ
diff --git a/images/textures/obar-18.gif b/images/textures/obar-18.gif
new file mode 100755
index 00000000..c4179591
Binary files /dev/null and b/images/textures/obar-18.gif differ
diff --git a/images/textures/obar-light.png b/images/textures/obar-light.png
new file mode 100755
index 00000000..bee14105
Binary files /dev/null and b/images/textures/obar-light.png differ
diff --git a/images/textures/obar.gif b/images/textures/obar.gif
new file mode 100755
index 00000000..679b9aa5
Binary files /dev/null and b/images/textures/obar.gif differ
diff --git a/images/textures/seperator.png b/images/textures/seperator.png
new file mode 100755
index 00000000..f3fd4189
Binary files /dev/null and b/images/textures/seperator.png differ
diff --git a/images/tickbox-canttick.gif b/images/tickbox-canttick.gif
new file mode 100755
index 00000000..10c65a39
Binary files /dev/null and b/images/tickbox-canttick.gif differ
diff --git a/images/tickbox-greyticked.gif b/images/tickbox-greyticked.gif
new file mode 100755
index 00000000..7fb9a9b3
Binary files /dev/null and b/images/tickbox-greyticked.gif differ
diff --git a/images/tickbox-ticked.gif b/images/tickbox-ticked.gif
new file mode 100644
index 00000000..b3ed56e3
Binary files /dev/null and b/images/tickbox-ticked.gif differ
diff --git a/images/tickbox-unticked.gif b/images/tickbox-unticked.gif
new file mode 100644
index 00000000..f5b440d4
Binary files /dev/null and b/images/tickbox-unticked.gif differ
diff --git a/images/treeicons/book-closedfolder.gif b/images/treeicons/book-closedfolder.gif
new file mode 100755
index 00000000..9433af8b
Binary files /dev/null and b/images/treeicons/book-closedfolder.gif differ
diff --git a/images/treeicons/book-file.gif b/images/treeicons/book-file.gif
new file mode 100755
index 00000000..9433af8b
Binary files /dev/null and b/images/treeicons/book-file.gif differ
diff --git a/images/treeicons/book-openfolder.gif b/images/treeicons/book-openfolder.gif
new file mode 100755
index 00000000..b8a56905
Binary files /dev/null and b/images/treeicons/book-openfolder.gif differ
diff --git a/images/treeicons/brokenlink-closedfolder.gif b/images/treeicons/brokenlink-closedfolder.gif
new file mode 100755
index 00000000..56ee5039
Binary files /dev/null and b/images/treeicons/brokenlink-closedfolder.gif differ
diff --git a/images/treeicons/brokenlink-file.gif b/images/treeicons/brokenlink-file.gif
new file mode 100755
index 00000000..a34b4a37
Binary files /dev/null and b/images/treeicons/brokenlink-file.gif differ
diff --git a/images/treeicons/brokenlink-openfolder.gif b/images/treeicons/brokenlink-openfolder.gif
new file mode 100755
index 00000000..211cb683
Binary files /dev/null and b/images/treeicons/brokenlink-openfolder.gif differ
diff --git a/images/treeicons/draft-file.png b/images/treeicons/draft-file.png
new file mode 100755
index 00000000..53bbc9f5
Binary files /dev/null and b/images/treeicons/draft-file.png differ
diff --git a/images/treeicons/draft-folder.png b/images/treeicons/draft-folder.png
new file mode 100755
index 00000000..e90517f3
Binary files /dev/null and b/images/treeicons/draft-folder.png differ
diff --git a/images/treeicons/element-closedfolder.gif b/images/treeicons/element-closedfolder.gif
new file mode 100755
index 00000000..f3c9240a
Binary files /dev/null and b/images/treeicons/element-closedfolder.gif differ
diff --git a/images/treeicons/element-file.gif b/images/treeicons/element-file.gif
new file mode 100755
index 00000000..93aa341a
Binary files /dev/null and b/images/treeicons/element-file.gif differ
diff --git a/images/treeicons/element-openfolder.gif b/images/treeicons/element-openfolder.gif
new file mode 100755
index 00000000..fa271365
Binary files /dev/null and b/images/treeicons/element-openfolder.gif differ
diff --git a/images/treeicons/folder-closedfolder.gif b/images/treeicons/folder-closedfolder.gif
new file mode 100755
index 00000000..d26f2dc9
Binary files /dev/null and b/images/treeicons/folder-closedfolder.gif differ
diff --git a/images/treeicons/folder-file.gif b/images/treeicons/folder-file.gif
new file mode 100755
index 00000000..d26f2dc9
Binary files /dev/null and b/images/treeicons/folder-file.gif differ
diff --git a/images/treeicons/folder-openfolder.gif b/images/treeicons/folder-openfolder.gif
new file mode 100755
index 00000000..8d00c394
Binary files /dev/null and b/images/treeicons/folder-openfolder.gif differ
diff --git a/images/treeicons/home-file (1).png b/images/treeicons/home-file (1).png
new file mode 100755
index 00000000..289a5ed2
Binary files /dev/null and b/images/treeicons/home-file (1).png differ
diff --git a/images/treeicons/home-file.png b/images/treeicons/home-file.png
new file mode 100755
index 00000000..68cd72f5
Binary files /dev/null and b/images/treeicons/home-file.png differ
diff --git a/images/treeicons/multi-user.gif b/images/treeicons/multi-user.gif
new file mode 100755
index 00000000..a2d220de
Binary files /dev/null and b/images/treeicons/multi-user.gif differ
diff --git a/images/treeicons/multi-user.png b/images/treeicons/multi-user.png
new file mode 100755
index 00000000..051d3c60
Binary files /dev/null and b/images/treeicons/multi-user.png differ
diff --git a/images/treeicons/page-gold-closedfolder.gif b/images/treeicons/page-gold-closedfolder.gif
new file mode 100755
index 00000000..07fbcfcd
Binary files /dev/null and b/images/treeicons/page-gold-closedfolder.gif differ
diff --git a/images/treeicons/page-gold-file.gif b/images/treeicons/page-gold-file.gif
new file mode 100755
index 00000000..9f52ae6b
Binary files /dev/null and b/images/treeicons/page-gold-file.gif differ
diff --git a/images/treeicons/page-gold-openfolder.gif b/images/treeicons/page-gold-openfolder.gif
new file mode 100755
index 00000000..1703ae07
Binary files /dev/null and b/images/treeicons/page-gold-openfolder.gif differ
diff --git a/images/treeicons/page-shortcut-file.gif b/images/treeicons/page-shortcut-file.gif
new file mode 100755
index 00000000..ac74a037
Binary files /dev/null and b/images/treeicons/page-shortcut-file.gif differ
diff --git a/images/treeicons/page-shortcut-gold-file.gif b/images/treeicons/page-shortcut-gold-file.gif
new file mode 100755
index 00000000..0cf7009e
Binary files /dev/null and b/images/treeicons/page-shortcut-gold-file.gif differ
diff --git a/images/treeicons/preferences-closedfolder.gif b/images/treeicons/preferences-closedfolder.gif
new file mode 100755
index 00000000..a3028d3b
Binary files /dev/null and b/images/treeicons/preferences-closedfolder.gif differ
diff --git a/images/treeicons/preferences-file.gif b/images/treeicons/preferences-file.gif
new file mode 100755
index 00000000..a3028d3b
Binary files /dev/null and b/images/treeicons/preferences-file.gif differ
diff --git a/images/treeicons/preferences-openfolder.gif b/images/treeicons/preferences-openfolder.gif
new file mode 100755
index 00000000..a3028d3b
Binary files /dev/null and b/images/treeicons/preferences-openfolder.gif differ
diff --git a/images/treeicons/reports-file.png b/images/treeicons/reports-file.png
new file mode 100755
index 00000000..523eb97e
Binary files /dev/null and b/images/treeicons/reports-file.png differ
diff --git a/images/treeicons/reports-foldericon.png b/images/treeicons/reports-foldericon.png
new file mode 100755
index 00000000..9ab17c3b
Binary files /dev/null and b/images/treeicons/reports-foldericon.png differ
diff --git a/images/treeicons/reports-openfoldericon.png b/images/treeicons/reports-openfoldericon.png
new file mode 100755
index 00000000..ebb359cd
Binary files /dev/null and b/images/treeicons/reports-openfoldericon.png differ
diff --git a/images/treeicons/root.png b/images/treeicons/root.png
new file mode 100755
index 00000000..4cc535b9
Binary files /dev/null and b/images/treeicons/root.png differ
diff --git a/images/treeicons/sent-file.gif b/images/treeicons/sent-file.gif
new file mode 100755
index 00000000..db2d51a5
Binary files /dev/null and b/images/treeicons/sent-file.gif differ
diff --git a/images/treeicons/sent-folder.png b/images/treeicons/sent-folder.png
new file mode 100755
index 00000000..e105b4df
Binary files /dev/null and b/images/treeicons/sent-folder.png differ
diff --git a/images/treeicons/task-file.gif b/images/treeicons/task-file.gif
new file mode 100755
index 00000000..a0570e5c
Binary files /dev/null and b/images/treeicons/task-file.gif differ
diff --git a/images/treeicons/user-file.gif b/images/treeicons/user-file.gif
new file mode 100755
index 00000000..8e67b243
Binary files /dev/null and b/images/treeicons/user-file.gif differ
diff --git a/images/unlocked.gif b/images/unlocked.gif
new file mode 100644
index 00000000..ead49916
Binary files /dev/null and b/images/unlocked.gif differ
diff --git a/images/unlockedled.gif b/images/unlockedled.gif
new file mode 100644
index 00000000..ead49916
Binary files /dev/null and b/images/unlockedled.gif differ
diff --git a/images/workflow/note_edit.png b/images/workflow/note_edit.png
new file mode 100755
index 00000000..ab87e650
Binary files /dev/null and b/images/workflow/note_edit.png differ
diff --git a/images/workflow/note_new.png b/images/workflow/note_new.png
new file mode 100755
index 00000000..5f4cfd52
Binary files /dev/null and b/images/workflow/note_new.png differ
diff --git a/images/workflow/note_view.png b/images/workflow/note_view.png
new file mode 100755
index 00000000..3fce8e0e
Binary files /dev/null and b/images/workflow/note_view.png differ
diff --git a/images/workflow/rubberstamp.png b/images/workflow/rubberstamp.png
new file mode 100755
index 00000000..7ee17e1e
Binary files /dev/null and b/images/workflow/rubberstamp.png differ
diff --git a/javascript/AssetAdmin.js b/javascript/AssetAdmin.js
new file mode 100755
index 00000000..b6532960
--- /dev/null
+++ b/javascript/AssetAdmin.js
@@ -0,0 +1,280 @@
+/**
+ * Configuration for the left hand tree
+ */
+if(typeof SiteTreeHandlers == 'undefined') SiteTreeHandlers = {};
+SiteTreeHandlers.parentChanged_url = 'admin/assets/ajaxupdateparent';
+SiteTreeHandlers.orderChanged_url = 'admin/assets/ajaxupdatesort';
+SiteTreeHandlers.loadPage_url = 'admin/assets/getitem';
+SiteTreeHandlers.loadTree_url = 'admin/assets/getsubtree';
+SiteTreeHandlers.showRecord_url = 'admin/assets/show/';
+var _HANDLER_FORMS = {
+ addpage : 'addpage_options',
+ deletepage : 'deletepage_options',
+ sortitems : 'sortitems_options'
+};
+
+/**
+ * Top-right actions
+ */
+
+function action_upload_right(e) {
+ if(frames['AssetAdmin_upload'].document && frames['AssetAdmin_upload'].document.getElementById('Form_UploadForm')) {
+ // make sure at least one file is selected for upload
+ var values = "";
+ var inputs = $A(frames['AssetAdmin_upload'].document.getElementsByTagName("input"));
+ inputs.each(function(input) {
+ if(input.type == "file") values += input.value;
+ }.bind(this));
+
+ if(values.length == 0) {
+ alert("Please select at least one file for uploading");
+ openTab("Root_Upload");
+ } else {
+ frames['AssetAdmin_upload'].document.getElementById('Form_UploadForm').submit();
+ }
+ }
+ Event.stop(e);
+ return false;
+}
+
+function action_deletemarked_right() {
+ $('action_deletemarked_options').onComplete = function() {}
+
+ if(confirm("Do you really want to delete the marked files?")) {
+ $('action_deletemarked_options').send();
+ }
+}
+function action_movemarked_right() {
+ $('action_movemarked_options').toggle();
+}
+
+MarkingPropertiesForm = Class.extend('ActionPropertiesForm');
+MarkingPropertiesForm.applyTo('#action_movemarked_options', "Please select some files to move!");
+MarkingPropertiesForm.applyTo('#action_deletemarked_options', "Please select some files to delete!");
+
+MarkingPropertiesForm.prototype = {
+ initialize: function(noneCheckedError) {
+ this.noneCheckedError = noneCheckedError;
+ },
+
+ send: function() {
+ var i, list = "", checkboxes = $('Form_EditForm').elements['Files[]'];
+ if(!checkboxes) checkboxes = [];
+ if(!checkboxes.length) checkboxes = [ checkboxes ];
+ for(i=0;i 0) {
+ count += newNodes.length;
+
+ if(confirm("Do you really want to delete the " + count + " marked pages?")) {
+ $(_HANDLER_FORMS.deletepage).elements.csvIDs.value = csvIDs;
+
+ statusMessage('deleting pages');
+
+ for( var idx = 0; idx < newNodes.length; idx++ ) {
+ var newNode = $('sitetree').getTreeNodeByIdx( newNodes[idx] );
+
+ if( newNode.parentTreeNode )
+ newNode.parentTreeNode.removeTreeNode( newNode );
+ else
+ alert( newNode.id + ' has no parent node');
+
+ $('Form_EditForm').reloadIfSetTo(idx);
+ }
+
+ newNodes = new Array();
+
+ Ajax.SubmitForm(_HANDLER_FORMS.deletepage, null, {
+ onSuccess : deletepage.submit_success,
+ onFailure : function(response) {
+ errorMessage('Error deleting pages', response);
+ }
+ });
+
+ $('deletepage').getElementsByTagName('a')[0].onclick();
+ }
+
+ } else {
+ alert("Please select at least 1 page.");
+ }
+
+ return false;
+ },
+ submit_success: function(response) {
+ deletepage.selectedNodes = {};
+
+ Ajax.Evaluator(response);
+ treeactions.closeSelection($('deletepage'));
+ }
+}
+
+/**
+ * Initialisation function to set everything up
+ */
+appendLoader(function () {
+ // Set up deleet page
+ if( !$('deletepage') )
+ return;
+
+ Observable.applyTo($(_HANDLER_FORMS.deletepage));
+ $('deletepage').onclick = deletepage.button_onclick;
+ $('deletepage').getElementsByTagName('a')[0].onclick = function() { return false; };
+ $(_HANDLER_FORMS.deletepage).onsubmit = deletepage.form_submit;
+});
+
+/**
+ * Tree context menu
+ */
+TreeContextMenu = {
+ 'Edit this page' : function(treeNode) {
+ treeNode.selectTreeNode();
+ },
+ 'Duplicate this page' : function(treeNode) {
+ new Ajax.Request(baseHref() + 'admin/duplicate/' + treeNode.getIdx() + '?ajax=1', {
+ method : 'get',
+ onSuccess : Ajax.Evaluator,
+ onFailure : function(response) {
+ errorMessage('Error: ', response);
+ }
+ });
+ },
+ 'Sort sub-pages' : function(treeNode) {
+ var children = treeNode.treeNodeHolder().childTreeNodes();
+ var sortedChildren = children.sort(function(a, b) {
+ var titleA = a.aTag.innerHTML.replace(/<[^>]*>/g,'');
+ var titleB = b.aTag.innerHTML.replace(/<[^>]*>/g,'');
+ return titleA < titleB ? -1 : (titleA > titleB ? 1 : 0);
+ });
+
+ var i,child;
+ for(i=0;child=sortedChildren[i];i++) {
+ treeNode.appendTreeNode(child);
+ }
+
+ treeNode.onOrderChanged(sortedChildren);
+ }
+};
\ No newline at end of file
diff --git a/javascript/CMSMain_right.js b/javascript/CMSMain_right.js
new file mode 100755
index 00000000..2d832f67
--- /dev/null
+++ b/javascript/CMSMain_right.js
@@ -0,0 +1,123 @@
+function action_publish_right() {
+ $('Form_EditForm').notify('BeforeSave');
+ Ajax.SubmitForm('Form_EditForm', 'action_publish', {
+ onSuccess : function(response) {
+ Ajax.Evaluator(response);
+ },
+ onFailure : function(response) {
+ errorMessage('Error publishing content', response);
+ }
+ });
+}
+function action_revert_right() {
+ Ajax.SubmitForm('Form_EditForm', 'action_revert', {
+ onSuccess : Ajax.Evaluator,
+ onFailure : function(response) {
+ errorMessage('Error reverting to live content', response);
+ }
+ });
+}
+/*function action_submit_right() {
+ $('action_submit_options').configure('CanApprove', 'Awaiting editor approval');
+ $('action_submit_options').toggle();
+}*/
+function action_reject_right() {
+ $('action_submit_options').configure('CanReject', 'Content declined');
+ $('action_submit_options').toggle();
+}
+function action_submit_right() {
+ $('action_submit_options').configure('CanPublish', 'Awaiting final approval');
+ $('action_submit_options').toggle();
+}
+function action_rollback_right() {
+ var options = {
+ OK: function() {
+ var pageID = $('Form_EditForm').elements.ID.value;
+
+ Ajax.SubmitForm('Form_EditForm', 'action_rollback', {
+ onSuccess : function(response) {
+ $('Form_EditForm').getPageFromServer(pageID);
+ statusMessage(response.responseText,'good');
+ },
+ onFailure : function(response) {
+ errorMessage('Error rolling back content', response);
+ }
+ });
+ },
+ Cancel:function() {
+ }
+ }
+
+ doYouWantToRollback(options);
+}
+
+/**
+ * Email containing the link to the archived version of the page
+ */
+function action_email_right() {
+ window.open( 'mailto:?subject=' + $('Form_EditForm_ArchiveEmailSubject').value + '&body=' + $('Form_EditForm_ArchiveEmailMessage').value, 'archiveemail' );
+}
+
+function action_print_right() {
+ var printURL = $('Form_EditForm').action.replace(/\?.*$/,'') + '/printable/' + $('Form_EditForm').elements.ID.value;
+ if(printURL.substr(0,7) != 'http://') printURL = baseHref() + printURL;
+
+ window.open(printURL, 'printable');
+}
+
+
+
+Submit_ActionPropertiesForm = Class.extend('ActionPropertiesForm');
+Submit_ActionPropertiesForm.applyTo('#action_submit_options');
+Submit_ActionPropertiesForm.prototype = {
+ /**
+ * Define how this form is to be used
+ */
+ configure : function(securityLevel, status) {
+ this.securityLevel = securityLevel;
+ this.elements.Status.value = status
+ this.elements.Status.parentNode.getElementsByTagName('span')[0].innerHTML= status;
+ }
+}
+
+function suggestStageSiteLink() {
+ var el = $('viewStageSite');
+ el.flasher = setInterval(flashColor.bind(el), 300);
+ setTimeout(stopFlashing.bind(el), 3000);
+}
+function flashColor() {
+ if(!this.style.color) this.style.color = '';
+ this.style.color = (this.style.color == '') ? '#00FF00' : '';
+}
+function stopFlashing() {
+ clearInterval(this.flasher);
+}
+
+
+Behaviour.register({
+ 'a.cmsEditlink' : {
+ onclick : function() {
+ if(this.href.match(/admin\/show\/([0-9]+)($|#|\?)/)) {
+ $('Form_EditForm').getPageFromServer(RegExp.$1);
+ return false;
+ }
+ }
+ }
+});
+
+Behaviour.register({
+ 'select#Form_EditForm_ClassName' : {
+ onchange: function() {
+ alert('The page type will be updated after the page is saved');
+ }
+ }
+});
+
+Behaviour.register({
+ '#Form_EditForm' : {
+ changeDetection_fieldsToIgnore : {
+ 'restricted-chars[Form_EditForm_URLSegment]' : true,
+ 'Sort' : true
+ }
+ }
+});
diff --git a/javascript/CommentList.js b/javascript/CommentList.js
new file mode 100755
index 00000000..4578c026
--- /dev/null
+++ b/javascript/CommentList.js
@@ -0,0 +1,13 @@
+CommentList = Class.extend('SidePanel');
+CommentList.prototype = {
+ destroy: function() {
+ if(this.SidePanel) this.SidePanel.destroy();
+ this.SidePanel = null;
+ },
+ onpagechanged : function() {
+ this.body.innerHTML = 'loading...
';
+ this.ajaxGetPanel(this.afterPanelLoaded);
+ }
+}
+
+CommentList.applyTo('#comments_holder');
\ No newline at end of file
diff --git a/javascript/ForumAdmin.js b/javascript/ForumAdmin.js
new file mode 100755
index 00000000..d316d18a
--- /dev/null
+++ b/javascript/ForumAdmin.js
@@ -0,0 +1,115 @@
+var _GroupID;
+Behaviour.register({
+ '#Form_EditForm_Type input':{
+ onclick:function(){
+ var requiredlogin = $('Form_EditForm_RequiredLogin');
+ var usersTab = $('Form_EditForm').getElementsByTagName('ul')[0].getElementsByTagName('li')[2];
+ if(this.value == 'consultation'){
+ if(requiredlogin.checked != 'checked')
+ requiredlogin.checked = 'checked';
+ usersTab.style.display = 'block';
+ if(_GroupID)
+ this.recoverGroupID();
+ Element.disable(requiredlogin);
+ }else{ // this.value == 'open'
+ usersTab.style.display = 'none';
+ this.treatGroupIDAs0();
+ Element.enable(requiredlogin);
+ }
+ },
+
+ treatGroupIDAs0:function(){
+ var groupIDDiv = $('GroupID');
+ var groupIDs = groupIDDiv.getElementsByTagName('option');
+ for(var i=0; iSilverStripe CMSWelcome to SilverStripe CMS! Please choose click on one of the items on the left pane.
",
+
+ initialize : function() {
+ Behaviour.register({
+ '#Form_EditForm_action_delete' : {
+ onclick: this.remove.bind(this)
+ }
+ });
+ },
+
+ updateCMSContent: function(el, currentTab, link, customCallBack) {
+ if(!customCallBack) customCallBack = function(){};
+
+ if(el || link){
+ var reqLink = (el.href) ? el.href : link;
+
+ if(typeof(currentTab) != 'undefined')
+ $('Form_EditForm').openTab = currentTab;
+
+ statusMessage("loading...", null, true);
+
+ new Ajax.Request(reqLink, {
+ asynchronous : true,
+ postBody : 'ajax=1',
+ onSuccess: customCallBack.bind(this),
+ onComplete : this.successfullyReceivedPage.bind(this),
+ onFailure : function(response) {
+ errorMessage('Error loading page',response);
+ }
+ });
+ }else{
+ $('Form_EditForm').innerHTML = this.welcomeMessage;
+ }
+ },
+
+ successfullyReceivedPage : function(response) {
+ $('Form_EditForm').loadNewPage(response.responseText);
+ $('Form_EditForm').initialize();
+ onload_init_tabstrip();
+ // TODO
+ // try to reopen saved tab (before ajax request)
+ if($('Form_EditForm').openTab) {
+ openTab($('Form_EditForm').openTab);
+ } else {
+ $('Form_EditForm').openTab = this.getCurrentTab();
+ openTab($('Form_EditForm').openTab);
+ }
+
+ clearStatusMessage();
+ },
+
+ remove: function(e) {
+ if(window.confirm('Are you sure you want to delete?')){
+ var el = Event.element(e);
+ Ajax.SubmitForm(el.ownerForm, el.name, {
+ postBody : 'ajax=1',
+ onSuccess: Ajax.Evaluator,
+ onFailure: ajaxErrorHandler
+ });
+ }
+ Event.stop(e);
+ return false;
+ },
+
+ deleteEffect: function() {
+ new Effect.Fade(this, {duration:2});
+ window.setTimeout(function() {
+ $('Form_EditForm').updateCMSContent();
+ new Effect.Appear($('Form_EditForm'), {duration:3});
+ }, 3000);
+ },
+
+ tabExists: function(tabName) {
+ if($('Root')){
+ var tags = $('Root').getElementsByTagName('li');
+ for(var i=0; i 0) url += "&ni_page=" + NI_PAGE;
+ else url += "?ni_page=" + NI_PAGE;
+ }
+
+ var p = "http"+(document.URL.indexOf('https:')==0?'s':'');
+ var t = new Date();
+ var u = p+"://"+server+"/Hit.aspx?tv=1&sc="+siteCode;
+ u+=A("lo",description);
+ u+=A("du",url);
+ u+=A("st",section);
+ u+=A("sv",service);
+ u+=A("ac",adCampaign);
+ u+=A("tr",trigger);
+ u+=A("ta",amount);
+ u+=A("ti",title);
+ u+=A("tz",t.getTimezoneOffset());
+ u+=A("ch",t.getHours());
+ u+=A("cb",CB());
+ u+=A("ru",window.document.referrer);
+ u+=A("js","1");
+ u+=A("ul",navigator.appName=="Netscape" ? navigator.language : navigator.userLanguage);
+ u+=A("ba", basketAdd);
+ u+=A("br", basketRemove);
+ u+=A("pm", parameters);
+
+ if (typeof(screen)=="object")
+ {
+ u+=A("sr",screen.width+"x"+screen.height);
+ }
+
+ if (layer == 1)
+ {
+ if (NI_IW == 0) { document.write(''); NI_IW = 1; }
+ else { u+=A("ir","1"); document.images.ni_tag.src = u; }
+ }
+ else
+ {
+ document.write(' '); NI_IW = 2;
+ }
+}
+
+/* The following function may be used any number of times in a page to load a file and track a hit
+ * against that file. This is useful when the file being loaded is not html,
+ * or is not under your control, so can't have an imprint tracking code inserted into it.
+ *
+ * E.g. Download catalogue
+ *
+ * If you consider clicking on the link to be the completion of a transaction, use the 'Sale' trigger
+ * E.g. Download catalogue
+ */
+function ni_LoadUrl(url, title, trigger)
+{
+ ni_TrackHit(NI_SE, NI_SC, "", NI_ST, NI_SV, trigger, "", "", title, url, NI_IW, "", "", "");
+ document.location.href = url;
+}
+
\ No newline at end of file
diff --git a/javascript/LeftAndMain.js b/javascript/LeftAndMain.js
new file mode 100644
index 00000000..af659019
--- /dev/null
+++ b/javascript/LeftAndMain.js
@@ -0,0 +1,816 @@
+var _AJAX_LOADING = false;
+
+/**
+ * Code for the separator bar between the two panes
+ */
+function DraggableSeparator() {
+ this.onmousedown = this.onmousedown.bindAsEventListener(this);
+ // this.onselectstart = this.onselectstart.bindAsEventListener(this);
+}
+DraggableSeparator.prototype = {
+ onmousedown : function(event) {
+ this.leftBase = $('left').offsetWidth - Event.pointerX(event);
+ this.separatorBase = getDimension($('separator'),'left') - Event.pointerX(event);
+ this.rightBase = getDimension($('right'),'left') - Event.pointerX(event);
+
+ document.onmousemove = this.document_mousemove.bindAsEventListener(this);
+ document.onmouseup = this.document_mouseup.bindAsEventListener(this);
+
+ document.body.onselectstart = this.body_selectstart.bindAsEventListener(this);
+ },
+ document_mousemove : function(event) {
+ $('left').style.width = (this.leftBase + Event.pointerX(event)) + 'px';
+ fixRightWidth();
+ },
+ document_mouseup : function(e) {
+ document.onmousemove = null;
+ },
+
+ body_selectstart : function(event) {
+ Event.stop(event);
+ return false;
+ }
+}
+
+function fixRightWidth() {
+ if( !$('right') )
+ return;
+
+ // Absolutely position all the elements
+ var sep = getDimension($('left'),'width') + getDimension($('left'),'left');
+ $('separator').style.left = (sep + 2) + 'px';
+ $('right').style.left = (sep + 6) + 'px';
+
+ // Give the remaining space to right
+ var rightWidth = parseInt(document.body.offsetWidth) - parseInt($('left').offsetWidth) - $('separator').offsetWidth - 8;
+
+ if( rightWidth >= 0 )
+ $('right').style.width = rightWidth + 'px';
+
+ var rb;
+ if(rb = $('rightbottom')) {
+ rb.style.left = $('right').style.left;
+ rb.style.width = $('right').style.width;
+ }
+}
+
+Behaviour.register({
+ '#separator' : DraggableSeparator,
+
+ '#left' : {
+ hide : function() {
+ if(!this.hidden) {
+ this.hidden = true;
+ this.style.width = null;
+ Element.addClassName(this,'hidden');
+ Element.addClassName('separator','hidden');
+ fixRightWidth();
+ }
+ },
+ show : function() {
+ if(this.hidden) {
+ this.hidden = false;
+ Element.removeClassName(this,'hidden');
+ Element.removeClassName('separator','hidden');
+ fixRightWidth();
+ }
+ }
+ },
+
+ '#MainMenu li' : {
+ onclick : function(event) {
+ window.location.href = this.getElementsByTagName('a')[0].href;
+ Event.stop(event);
+ }
+ },
+
+ '#Menu-help' : {
+ onclick : function() {
+ var w = window.open(this.getElementsByTagName('a')[0].href, 'help');
+ w.focus();
+ return false;
+ }
+ }
+
+})
+
+
+
+window.ontabschanged = function() {
+ var formEl = $('Form_EditForm');
+
+ if( !formEl )
+ return;
+
+ var fs = formEl.getElementsByTagName('fieldset')[0];
+ if(fs) fs.style.height = formEl.style.height;
+
+ // var divs = document.getElementsBySelector('#Form_EditForm div');
+ /*for(i=0;i_right"
+ if(window[this.name + '_' + tabName]) {
+ window[this.name + '_' + tabName](e);
+ } else {
+ statusMessage(ingize(this.value));
+ Ajax.SubmitForm(this.ownerForm, this.name, {
+ onSuccess: Ajax.Evaluator,
+ onFailure: ajaxErrorHandler
+ });
+ }
+ return false;
+ }
+ behaveAs(button, StatusTitle);
+ }
+}
+
+function ingize(val) {
+ if(!val) val = "process";
+ if(val.substr(val.length-1) == 'e') return val.substr(0, val.length-1) + 'ing...';
+ else return val + 'ing...';
+}
+
+/**
+ * Submit the given form and evaluate the Ajax response.
+ * Needs to be bound to an object with the following parameters to work:
+ * - form
+ * - action
+ * - verb
+ *
+ * The bound function can then be called, with the arguments passed
+ */
+
+function ajaxSubmitForm(automated, callAfter, form, action, verb) {
+ // tinyMCE.triggerSave(true);
+
+ var alreadySaved = false;
+ if($(form).elements.length < 2) alreadySaved = true;
+
+ if(alreadySaved) {
+ if(callAfter) callAfter();
+
+ } else {
+ statusMessage(verb + '...', '', true);
+
+ var success = function(response) {
+ Ajax.Evaluator(response);
+ if(callAfter) callAfter();
+ }
+
+ if(callAfter) success = success.bind({callAfter : callAfter});
+ Ajax.SubmitForm(form, action, {
+ onSuccess : success,
+ onFailure : function(response) {
+ errorMessage('Error ' + verb, response);
+ }
+ });
+ }
+
+ return false;
+};
+
+/**
+ * Post the given fields to the given url
+ */
+function ajaxSubmitFieldSet(href, fieldSet, extraData) {
+ // Build data
+ var i,field,data = "ajax=1";
+ for(i=0;field=fieldSet[i];i++) {
+ data += '&' + Form.Element.serialize(field);
+ }
+ if(extraData){
+ data += '&'+extraData;
+ }
+ // Send request
+ new Ajax.Request(href, {
+ method : 'post', postBody : data,
+ onSuccess : function(response) {
+ //alert(response.responseText);
+ Ajax.Evaluator(response);
+ },
+ onFailure : function(response) {
+ alert(response.responseText);
+ //errorMessage('Error: ', response);
+ }
+ });
+}
+
+/**
+ * Post the given fields to the given url
+ */
+function ajaxLink(href) {
+ // Send request
+ new Ajax.Request(href + (href.indexOf("?") == -1 ? "?" : "&") + "ajax=1", {
+ method : 'get',
+ onSuccess : Ajax.Evaluator,
+ onFailure : ajaxErrorHandler
+ });
+}
+
+/**
+ * Load a URL into the given form
+ */
+function ajaxLoadPage() {
+ statusMessage('loading...', 2, true);
+ new Ajax.Request(this.URL + '&ajax=1', {
+ method : 'get',
+ onSuccess : ajaxLoadPage_success.bind(this)
+ });
+}
+function ajaxLoadPage_success(response) {
+ statusMessage('loaded');
+ $(this.form).loadNewPage(response.responseText);
+}
+
+/**
+ * Behaviour of the statuts message.
+ */
+Behaviour.register({
+ '#statusMessage' : {
+ showMessage : function(message, type, waitTime, clearManually) {
+ if(this.fadeTimer) {
+ clearTimeout(this.fadeTimer);
+ this.fadeTimer = null;
+ }
+ if(this.currentEffect) {
+ this.currentEffect.cancel();
+ this.currentEffect = null;
+ }
+
+ this.innerHTML = message;
+ this.className = type;
+ Element.setOpacity(this, 1);
+
+ this.style.position = 'absolute';
+ this.style.display = '';
+ this.style.visibility = '';
+
+ if(!clearManually) {
+ this.fade(0.5,waitTime ? waitTime : 5);
+ }
+ },
+ clearMessage : function(waitTime) {
+ this.fade(0.5, waitTime);
+ },
+ fade: function(fadeTime, waitTime) {
+ if(!fadeTime) fadeTime = 0.5;
+
+ // Wait a bit before fading
+ if(waitTime) {
+ this.fadeTimer = setTimeout((function() {
+ this.fade(fadeTime);
+ }).bind(this), waitTime * 1000);
+
+ // Fade straight away
+ } else {
+ this.currentEffect = new Effect.Opacity(this,
+ { duration: 0.5,
+ transition: Effect.Transitions.linear,
+ from: 1.0, to: 0.0,
+ afterFinish : this.afterFade.bind(this) });
+ }
+ },
+ afterFade : function() {
+ this.style.visibility = 'hidden';
+ this.style.display = 'none';
+ this.innerHTML = '';
+ }
+ }
+});
+
+/**
+ * Show a status message.
+ *
+ * @param msg String
+ * @param type String (optional) can be 'good' or 'bad'
+ * @param clearManually boolean Don't automatically fade message.
+ */
+function statusMessage(msg, type, clearManually) {
+ var statusMessageEl = $('statusMessage');
+ if(statusMessageEl) {
+ if(msg) {
+ statusMessageEl.showMessage(msg, type, msg.length / 20, clearManually);
+ } else {
+ statusMessageEl.clearMessage();
+ }
+ }
+}
+
+function clearStatusMessage() {
+ $('statusMessage').clearMessage();
+}
+
+/**
+ * Called when something goes wrong
+ */
+function errorMessage(msg, fullMessage) {
+ // More complex error for developers
+ if(fullMessage && window.location.href.indexOf('//dev') != -1) {
+ // Get the message from an Ajax response object
+ try {
+ if(typeof fullMessage == 'object') fullMessage = fullMessage.status + '//' + fullMessage.responseText;
+ } catch(er) {
+ fullMessage = "";
+ }
+ msg = msg + ' ' + fullMessage.replace(/\n/g,' ');
+ }
+
+ $('statusMessage').showMessage(msg,'bad',60);
+}
+
+function ajaxErrorHandler(response) {
+ errorMessage('Error talking to server', response);
+}
+
+/**
+ * Applying StatusTitle to an element will mean that the title attribute is shown as a statusmessage
+ * upon hover
+ */
+StatusTitle = Class.create();
+StatusTitle.prototype = {
+ onmouseover : function() {
+ if(this.title) {
+ this.message = this.title;
+ this.title = null;
+ }
+ if(this.message) {
+ $('statusMessage').showMessage(this.message);
+ }
+ },
+ onmouseout : function() {
+ if(this.message) {
+ $('statusMessage').fade(0.3,1);
+ }
+ }
+}
+
+/**
+ * BaseForm is the base form class used in the CMS.
+ */
+BaseForm = Class.create();
+BaseForm.prototype = {
+ intitialize: function() {
+ this.visible = this.style.display == 'none' ? false : true;
+
+ // Collect all the buttons and attach handlers
+ this.buttons = [];
+ var i,input,allInputs = this.getElementsByTagName('input');
+ for(i=0;input=allInputs[i];i++) {
+ if(input.type == 'button' || input.type == 'submit') {
+ this.buttons.push(input);
+ input.holder = this;
+ input.onclick = function() { return this.holder.buttonClicked(this); }
+ }
+ }
+ },
+ show: function() {
+ this.visible = true;
+ Element.hide(show);
+ },
+ hide: function() {
+ this.visible = false;
+ Element.hide(this);
+ },
+ isVisible: function() {
+ return this.visible;
+ },
+ buttonClicked: function(button) {
+ return true;
+ }
+}
+
+
+/**
+ * ChangeTracker is a class that can be applied to forms to support change tracking on forms.
+ */
+ChangeTracker = Class.create();
+ChangeTracker.prototype = {
+ initialize: function() {
+ this.resetElements();
+ },
+
+ /**
+ * Reset all the 'changed field' data.
+ */
+ resetElements: function(debug) {
+ var elements = Form.getElements(this);
+ var i, element;
+ for(i=0;element=elements[i];i++) {
+ // Initialise each element
+ if(element.resetChanged) {
+ element.resetChanged();
+ } else {
+ element.originalSerialized = Form.Element.serialize(element);
+ }
+ }
+ },
+
+ field_changed: function() {
+ return this.originalSerialized != Form.Element.serialize(this);
+ },
+
+ /**
+ * Returns true if something in the form has been changed
+ */
+ isChanged: function() {
+ var elements = Form.getElements(this);
+ var i, element;
+ for(i=0;element=elements[i];i++) {
+ if(!element.isChanged) element.isChanged = this.field_changed;
+ if(!this.changeDetection_fieldsToIgnore[element.name] && element.isChanged()) {
+
+ //if( window.location.href.match( /^https?:\/\/dev/ ) )
+ // Debug.log('Changed:'+ element.id + '(' + this.originalSerialized +')->('+Form.Element.serialize(element)+')' );
+
+ return true;
+ }
+ }
+ return false;
+ },
+
+ changeDetection_fieldsToIgnore : {
+ 'Sort' : true
+ },
+
+ /**
+ * Serialize only the fields to change.
+ * You can specify the names of fields that must be included as arguments
+ */
+ serializeChangedFields: function() {
+ var elements = Form.getElements(this);
+ var queryComponent, queryComponents = new Array();
+ var i, element;
+
+ var forceFields = {};
+ if(arguments) {for(var i=0;i method; context-menu operations to get called
+ */
+function createContextMenu(event, owner, menuItems) {
+ if(_CURRENT_CONTEXT_MENU) {
+ document.body.removeChild(_CURRENT_CONTEXT_MENU);
+ _CURRENT_CONTEXT_MENU = null;
+ }
+
+ var menu = document.createElement("ul");
+ menu.className = 'contextMenu';
+ menu.style.position = 'absolute';
+ menu.style.left = event.clientX + 'px';
+ menu.style.top = event.clientY + 'px';
+
+ var menuItemName, menuItemTag, menuATag;
+ for(menuItemName in menuItems) {
+ menuItemTag = document.createElement("li");
+
+ menuATag = document.createElement("a");
+ menuATag.href = "#";
+ menuATag.onclick = menuATag.oncontextmenu = contextmenu_onclick;
+ menuATag.innerHTML = menuItemName;
+ menuATag.handler = menuItems[menuItemName];
+ menuATag.owner = owner;
+
+ menuItemTag.appendChild(menuATag);
+ menu.appendChild(menuItemTag);
+ }
+
+ document.body.appendChild(menu);
+
+ document.body.onclick = contextmenu_close;
+
+ _CURRENT_CONTEXT_MENU = menu;
+
+ return menu;
+}
+
+function contextmenu_close() {
+ if(_CURRENT_CONTEXT_MENU) {
+ document.body.removeChild(_CURRENT_CONTEXT_MENU);
+ _CURRENT_CONTEXT_MENU = null;
+ }
+}
+
+function contextmenu_onclick() {
+ this.handler(this.owner);
+ contextmenu_close();
+ return false;
+}
+
+/**
+ * Shows an ajax loading indicator.
+ *
+ * @param id String Identifier for the newly created image
+ * @param container ID/DOM Element
+ * @param imgSrc String (optional)
+ * @param insertionType Object (optional) Prototype-style insertion-classes, defaults to Insertion.Bottom
+ * @param displayType String (optional) "inline" or "block"
+ */
+function showIndicator(id, container, imgSrc, insertionType, displayType) {
+ if(!id || !$(container)) return false;
+ if(!imgSrc) imgSrc = "cms/images/network-save.gif";
+ if(!displayType) displayType = "inline";
+ if(!insertionType) insertionType = Insertion.Bottom;
+
+ if(!$(id)) {
+ var html = ' ';
+ new insertionType(container, html);
+ }
+
+ Effect.Appear(id);
+}
+
+function hideIndicator(id) {
+ Effect.Fade(id, {duration: 0.3});
+}
\ No newline at end of file
diff --git a/javascript/LeftAndMain_left.js b/javascript/LeftAndMain_left.js
new file mode 100755
index 00000000..492ebb9f
--- /dev/null
+++ b/javascript/LeftAndMain_left.js
@@ -0,0 +1,475 @@
+/**
+ * LeftAndMain_left.js
+ * Code for supporting the left-hand panel of all the 2-pane admin windows
+ * This includes code for the action panel at the top, and the draggable, ajax-linked tree.
+ */
+
+if(typeof SiteTreeHandlers == 'undefined') SiteTreeHandlers = {};
+SiteTreeHandlers.parentChanged_url = 'admin/ajaxupdateparent';
+SiteTreeHandlers.orderChanged_url = 'admin/ajaxupdatesort';
+SiteTreeHandlers.showRecord_url = 'admin/show/';
+
+var _HANDLER_FORMS = {
+ addpage : 'Form_AddPageOptionsForm',
+ deletepage : 'deletepage_options',
+ sortitems : 'sortitems_options'
+};
+
+
+/**
+ * Overload this with a real context menu if necessary
+ */
+var TreeContextMenu = null;
+
+/**
+ * Extra methods for the tree when used in the LHS of the CMS
+ */
+TreeAPI = Class.create();
+TreeAPI.prototype = {
+ /**
+ * Perform the given code on the each tree node with the given index.
+ * There could be more than one :-)
+ * @param idx The index of the tree node
+ * @param code A method to be executed, that will be passed the tree node
+ */
+ performOnTreeNode: function(idx, code) {
+ var treeNode = this.getTreeNodeByIdx(idx);
+ if(!treeNode) return;
+
+ if(treeNode.className.indexOf('manyparents') == -1) {
+ code(treeNode);
+
+ } else {
+ var i,item,allNodes = this.getElementsByTagName('li');
+ for(i=0;item=allNodes[i];i++) {
+ if(treeNode.id == item.id) code(item);
+ }
+ }
+ },
+
+ getTreeNodeByIdx : function(idx) {
+ if(!idx) idx = "0";
+ var node = document.getElementById('record-' + idx);
+ if(idx == "0" && !node) node = document.getElementById('record-root');
+ return node;
+ },
+ getIdxOf : function(treeNode) {
+ if(treeNode && treeNode.id && treeNode.id.match(/record-([0-9a-zA-Z\-]+)$/))
+ return RegExp.$1;
+ },
+ createTreeNode : function(idx, title, pageType) {
+ var i;
+ var node = document.createElement('li');
+ node.id = 'record-' + idx;
+ node.className = pageType;
+
+ var aTag = document.createElement('a');
+ aTag.href = SiteTreeHandlers.showRecord_url + idx;
+ aTag.innerHTML = title;
+ node.appendChild(aTag);
+
+ SiteTreeNode.create(node, this.options);
+
+ return node;
+ },
+
+ setNodeIdx : function(idx, newIdx) {
+ this.performOnTreeNode(idx, function(treeNode) {
+ treeNode.id = 'record-' + newIdx;
+ var aTag = treeNode.getElementsByTagName('a')[0];
+ aTag.href = aTag.href.replace(idx, newIdx);
+ });
+
+ var treeNode = this.getTreeNodeByIdx(idx);
+ },
+
+ setNodeTitle : function(idx, title) {
+ this.performOnTreeNode(idx, function(treeNode) {
+ var aTag = treeNode.getElementsByTagName('a')[0];
+ aTag.innerHTML = title;
+ });
+ },
+
+ setNodeIcon: function(idx, oldClassName, newClassName) {
+ this.performOnTreeNode(idx, function(treeNode) {
+ treeNode.className = treeNode.className.replace(oldClassName,newClassName);
+ treeNode.aSpan.className = 'a ' + treeNode.className.replace('closed','spanClosed');
+ treeNode.setIconByClass();
+ });
+ },
+
+ setCurrentByIdx : function(idx) {
+ if(this.selected) {
+ var i,item;
+ for(i=0;item=this.selected[i];i++) {
+ item.removeNodeClass('current');
+ }
+ }
+
+ __tree = this;
+ __tree.selected = [];
+
+ this.performOnTreeNode(idx, function(treeNode) {
+ __tree.selected.push(treeNode);
+ treeNode.expose();
+ treeNode.addNodeClass('current');
+ });
+ },
+
+ changeCurrentTo : function(newNode) {
+ if(this.selected) {
+ var i,item;
+ for(i=0;item=this.selected[i];i++) {
+ item.removeNodeClass('current');
+ }
+ }
+
+ newNode.addNodeClass('current');
+
+ this.selected = [newNode];
+ newNode.expose();
+ },
+
+ firstSelected : function() {
+ if(this.selected) return this.selected[0];
+ }
+};
+
+/**
+ * Extra methods for the tree node when used in the LHS of the CMS
+ */
+TreeNodeAPI = Class.create();
+TreeNodeAPI.prototype = {
+ selectTreeNode : function() {
+ if(this.getElementsByTagName('a')[0].href) {
+ _AJAX_LOADING = true;
+ if($('sitetree').notify('SelectionChanged', this)) {
+ autoSave(true, this.getPageFromServer.bind(this));
+ }
+ }
+ },
+
+ getPageFromServer : function() {
+ $('Form_EditForm').getPageFromServer(this.id.replace('record-',''), this);
+ _AJAX_LOADING = false;
+ },
+ ajaxExpansion : function() {
+ this.addNodeClass('loading');
+ var ul = this.treeNodeHolder(false);
+ ul.innerHTML = 'loading...';
+
+ // Any attempts to add children to this page should, in fact, cue them up for insertion later
+ ul.cuedNewNodes = [];
+ ul.appendTreeNode = function(node) {
+ this.cuedNewNodes[this.cuedNewNodes.length] = node;
+ }
+
+
+ new Ajax.Request(SiteTreeHandlers.loadTree_url + '&ajax=1&ID=' + this.getIdx(), {
+ onSuccess : this.installSubtree.bind(this),
+ onFailure : this.showSubtreeLoadingError
+ });
+ },
+ showSubtreeLoadingError: function(response) {
+ errorMessage('error loading subtree', response);
+ },
+
+ /**
+ * Context menu
+ */
+ oncontextmenu: function(event) {
+ if(TreeContextMenu) {
+ if(!event) event = window.event;
+ createContextMenu(event, this, TreeContextMenu);
+ Event.stop(event);
+ return false;
+ }
+ }
+}
+
+
+
+
+/**
+ * In the case of Tree & DraggableTree, the root tree and the sub-trees all use the same class.
+ * In this case, however, SiteTree has a much bigger API and so SiteSubTree is smaller.
+ */
+SiteSubTree = Class.extend('Tree').extend('TreeAPI');
+SiteSubTree.prototype = {
+ castAsTreeNode: function(li) {
+ behaveAs(li, SiteTreeNode, this.options);
+ }
+}
+
+
+/**
+ * Our SiteTree class extends the tree object with a richer manipulation API.
+ * The server will send a piece javascript that uses these functions. In this way, the server
+ * has flexibility over its operation, but the Script->HTML interface is kept client-side.
+ */
+SiteTree = Class.extend('SiteSubTree');
+SiteTree.prototype = {
+ initialize : function() {
+ this.Tree.initialize();
+
+ /*
+ if(!this.tree.selected) this.tree.selected = [];
+ var els = this.getElementsByTagName('li');
+ for(var i=0;i -1) {
+ this.tree.selected.push(els[i]);
+ break;
+ }
+ */
+
+ this.observeMethod('SelectionChanged', this.interruptLoading.bind(this) );
+ },
+ destroy: function () {
+ if(this.Tree) this.Tree.destroy();
+ this.Tree = null;
+ this.SiteSubTree = null;
+ this.TreeAPI = null;
+ this.selected = null;
+ },
+
+ /**
+ * Stop the currently loading node from loading.
+ */
+ interruptLoading: function( newLoadingNode ) {
+ if( this.loadingNode ) this.loadingNode.removeNodeClass('loading');
+ this.loadingNode = newLoadingNode;
+ }
+}
+
+SiteTreeNode = Class.extend('TreeNode').extend('TreeNodeAPI');
+SiteTreeNode.prototype = {
+ initialize: function(options) {
+ this.TreeNode.initialize(options);
+
+ if(this.className && this.className.match(/ *([^ ]+)( +|$)/)) {
+ if((typeof siteTreeHints != 'undefined') && siteTreeHints[ RegExp.$1 ]) {
+ this.hints = siteTreeHints[ RegExp.$1 ];
+ this.dropperOptions = {
+ accept : (this.hints.allowedChildren && (this.className.indexOf('nochildren') == -1))
+ ? this.hints.allowedChildren : 'none'
+ };
+ }
+ }
+
+ if(this.className.indexOf('current') > -1) {
+ if(!this.tree.selected) this.tree.selected = [];
+ this.tree.selected.push(this);
+ }
+
+ if(!this.hints) this.hints = {}
+ },
+
+ destroy: function () {
+ if(this.TreeNode) this.TreeNode.destroy();
+ this.TreeNode = null;
+ this.TreeNodeAPI = null;
+ },
+
+ castAsTree: function(childUL) {
+ behaveAs(childUL, SiteSubTree, this.options);
+ if(this.draggableObj) childUL.makeDraggable();
+ },
+
+ onselect: function() {
+ this.selectTreeNode();
+ return false;
+ },
+
+
+
+ /**
+ * Drag'n'drop handlers - Ajax saving
+ */
+ onParentChanged : function(node, oldParent, newParent) {
+ if(newParent.id.match(/^record-new/)) {
+ alert("You must save the page before dragging children into it");
+ return false;
+ }
+
+ if( node == newParent || node.getIdx() == newParent.getIdx() ) {
+ alert("You cannot add a page to itself");
+ return false;
+ }
+
+ if(node.innerHTML.toLowerCase().indexOf(' -1) {
+ alert("You can't moved deleted pages");
+ return false;
+ }
+
+ if( Element.hasClassName( newParent, 'nochildren' ) ) {
+ alert("You can't add children to that node");
+ return false;
+ }
+
+ var currentlyOpenPageID = 0;
+ if($('Form_EditForm').elements.ID) currentlyOpenPageID = $('Form_EditForm').elements.ID.value;
+
+ statusMessage('saving...', '', true);
+ new Ajax.Request(SiteTreeHandlers.parentChanged_url, {
+ method : 'post',
+ postBody : 'ID=' + node.getIdx() + '&ParentID=' + newParent.getIdx() + '&CurrentlyOpenPageID=' + currentlyOpenPageID,
+ onSuccess : Ajax.Evaluator,
+ onFailure : function(response) {
+ errorMessage('error saving parent', response);
+ }
+ });
+
+ return true;
+ },
+
+ /**
+ * Called when the tree has been resorted
+ * nodeList is a list of all the nodes in the correct rder
+ * movedNode is the node that actually got moved to trigger this resorting
+ */
+ onOrderChanged : function(nodeList, movedNode) {
+ statusMessage('saving...', '', true);
+
+ var i, parts = Array();
+ sort = 0;
+
+ for(i=0;i ' + response.responseText);
+ }
+ });
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/javascript/LeftAndMain_right.js b/javascript/LeftAndMain_right.js
new file mode 100755
index 00000000..a6b14061
--- /dev/null
+++ b/javascript/LeftAndMain_right.js
@@ -0,0 +1,463 @@
+
+CMSForm = Class.extend('ChangeTracker').extend('Observable');
+CMSForm.prototype = {
+ initialize : function(fn) {
+ this.ChangeTracker.initialize();
+ this.formName = fn;
+ this.prepareForm();
+ },
+
+ /**
+ * Processing called whenever a page is loaded in the right - including the initial one
+ */
+ prepareForm : function() {
+ ajaxActionsAtTop(this.id, 'form_actions_' + this.formName, this.formName);
+ },
+
+ /**
+ * Load actions from a string containing the HTML content
+ */
+ loadActionsFromString : function(actionHTML) {
+ var actionHolder = $('form_actions_' + this.formName);
+ actionHolder.innerHTML = actionHTML;
+ prepareAjaxActions(actionHolder, this.id, this.formName);
+ },
+
+ /**
+ * Close the form down without any corrective action, after its been deleted.
+ */
+ closeIfSetTo: function(id) {
+ if(this.elements.ID && this.elements.ID.value == id) {
+ this.innerHTML = "This page was deleted. To edit a page, select it from the left.
";
+ if(typeof tinyMCE != 'undefined') tinyMCE.instances = [];
+ }
+ },
+
+ /**
+ * Reload lose the form if the current page is open.
+ */
+ reloadIfSetTo: function(id) {
+ if(this.elements.ID && this.elements.ID.value == id) {
+ this.getPageFromServer(id);
+ }
+ },
+
+ close: function() {
+ this.innerHTML = "
";
+ var actions;
+ if(actions = $('form_actions_' + this.formName)) {
+ actions.parentNode.removeChild(actions);
+ }
+ },
+
+ updateStatus: function( newStatus ) {
+
+ if( $('Form_EditForm_Status') )
+ $('Form_EditForm_Status').innerHTML = "STATUS: " + newStatus;
+ },
+
+ /**
+ * Load a new page into the right-hand form.
+ *
+ * @param formContent string
+ * @param response object (optional)
+ * @param evalResponse boolean (optional)
+ */
+ loadNewPage : function(formContent, response, evalResponse) {
+ //alert('here: ' + formContent);
+ var rightHTML = formContent;
+
+ // Rewrite # links
+ rightHTML = rightHTML.replace(/href *= *"#/g, 'href="' + window.location.href.replace(/#.*$/,'') + '#');
+
+ // Rewrite iframe links (for IE)
+ rightHTML = rightHTML.replace(/(
";
+ }
+ },
+ select_success : function(response) {
+ Element.removeClassName(this, 'loading');
+ $('Form_MemberForm').loadNewPage(response.responseText);
+
+ statusMessage('loaded','good');
+ // for (var n in tinyMCE.instances) tinyMCE.removeMCEControl(n);
+ }
+ },
+
+ '#MemberList thead tr' : {
+ onmouseover : null,
+ onmouseout : null,
+ onclick : null
+ },
+
+ '#MemberList' : {
+ initialise : function() {
+ this.headerMap = [];
+
+ var i, item, headers = this.getElementsByTagName('thead')[0].getElementsByTagName('tr')[0].getElementsByTagName('td');
+ for(i=0;item=headers[i];i++) {
+ this.headerMap[i] = item.className;
+ }
+ },
+
+ setRecordDetails : function(id, details, groupID) {
+ var row = document.getElementById('member-' + id);
+ if(row) {
+ var i, item, cells = row.getElementsByTagName('td');
+ for(i=0;item=cells[i];i++) {
+ if(details[this.headerMap[i]]) {
+ item.innerHTML = details[this.headerMap[i]];
+ }
+ }
+ } else {
+ this.createRecord(id, details, groupID);
+ }
+ },
+
+ createRecord : function (id, details, groupId) {
+ var row = document.createElement('tr');
+ row.id = 'member-' + id;
+ var i, cell, cellField;
+ for(i=0;cellField=this.headerMap[i];i++) {
+ cell = document.createElement('td')
+ if(details[cellField]) {
+ cell.innerHTML = details[cellField];
+ }
+ row.appendChild(cell);
+ }
+
+ // Add the delete icon
+ if(typeof groupId == 'undefined')
+ var groupId = $('Form_EditForm').elements.ID.value;
+ cell = document.createElement('td')
+ cell.innerHTML = ' ';
+ cell.getElementsByTagName('0');
+ row.appendChild(cell);
+
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) tbody.insertBefore(row, addRow);
+ else tbody.appendChild(row);
+ Behaviour.apply(row, true);
+ },
+
+ clearAddForm : function() {
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) {
+ var i,field,fields = addRow.getElementsByTagName('input');
+ for(i=0;field=fields[i];i++) {
+ if(field.type != 'hidden' && field.type != 'submit') field.value = '';
+ }
+ }
+ },
+
+ removeMember : function(memberID) {
+ var record;
+ if(record = $('member-' + memberID)) {
+ record.parentNode.removeChild(record);
+ }
+ }
+ },
+
+ '#MemberList input' : AjaxMemberLookup,
+
+ '#MemberList a.deletelink' : {
+ onclick : function(event) {
+ if(confirm("Do you want to remove this member from the group?")) {
+ this.getElementsByTagName('img')[0].src = 'cms/images/network-save.gif';
+ ajaxLink(this.href);
+ }
+ Event.stop(event);
+ return false;
+ }
+ },
+
+ '#MemberList tr.addrow' : {
+ onmouseover : null,
+ onmouseout : null,
+ onclick : null
+ },
+
+ '#MemberList tr.addrow td.actions input' : {
+ initialise: function() {
+ data = this.parentNode.parentNode.getElementsByTagName('input');
+ var i,item,error = [];
+ for(i=0;item=data[i];i++) {
+ item.originalSerialized = Form.Element.serialize(item);
+ }
+ },
+
+ onclick : function(event) {
+ data = this.parentNode.parentNode.getElementsByTagName('input');
+ var i,item,error = [];
+ for(i=0;item=data[i];i++) {
+ if(item.name == 'Email' && !item.value) error[error.length] = "Email";
+ if(item.name == 'Password' && !item.value) error[error.length] = "Password";
+ }
+ if(error.length > 0) {
+ alert('Please enter a ' + error.join(' and a ') + ' to add a member.');
+
+ } else {
+ ajaxSubmitFieldSet('admin/security/addmember?MemberListBaseGroupID='.$('MemberListBaseGroupID') , data);
+ }
+
+ return false;
+ }
+ },
+
+ '#Form_EditForm' : {
+ changeDetection_fieldsToIgnore : {
+ 'MemberListBaseGroup' : true,
+ 'MemberListOrderByField' : true,
+ 'MemberListOrderByOrder' : true,
+ 'MemberListGroup' : true,
+ 'MemberListSearch' : true
+ }
+ }
+});
+
+MemberListFilterButton = Class.create();
+MemberListFilterButton.applyTo('div.MemberListField #MemberListFilterButton');
+MemberListFilterButton.prototype = {
+ initialize: function() {
+ this.inputFields = new Array();
+
+ var childNodes = this.parentNode.getElementsByTagName('input');
+
+ for( var index = 0; index < childNodes.length; index++ ) {
+ if( childNodes[index].tagName ) {
+ childNodes[index].resetChanged = function() { return false; }
+ childNodes[index].isChanged = function() { return false; }
+ this.inputFields.push( childNodes[index] );
+ }
+ }
+
+ childNodes = this.parentNode.getElementsByTagName('select');
+
+ for( var index = 0; index < childNodes.length; index++ ) {
+ if( childNodes[index].tagName ) {
+ childNodes[index].resetChanged = function() { return false; }
+ childNodes[index].field_changed = function() { return false; }
+ this.inputFields.push( childNodes[index] );
+ }
+ }
+ },
+
+ isChanged: function() {
+ return false;
+ },
+
+ onclick: function(event) {
+
+ var baseGroup = $('MemberListBaseGroupID').value;
+
+ var updateURL = 'admin/security/listmembers/' + baseGroup + '?';
+
+ for( var index = 0; index < this.inputFields.length; index++ ) {
+ if( this.inputFields[index].tagName ) {
+ updateURL += this.inputFields[index].name + '=' + encodeURIComponent( this.inputFields[index].value ) + '&';
+ }
+ }
+
+ updateURL += 'ajax=1';
+
+ new Ajax.Request( updateURL, {
+ onSuccess: function( response ) {
+ $('MemberList').innerHTML = response.responseText;
+ // Behaviour.debug();
+ Behaviour.apply( $('MemberList') );
+ },
+ onFailure: function( response ) {
+ errorMessage('Could not filter results: ' + response.responseText );
+ }
+ });
+
+ return false;
+ }
+}
+
+Behaviour.register({
+ 'div#MemberList div.PageControls a' : {
+ onclick: function() {
+ new Ajax.Request( this.href + '&ajax=1', {
+ onSuccess: function( response ) {
+ $('MemberList').innerHTML = response.responseText;
+ // Behaviour.debug();
+ Behaviour.apply( $('MemberList') );
+ },
+ onFailure: function( response ) {
+ errorMessage('Could not filter results: ' + response.responseText );
+ }
+ });
+
+ return false;
+ }
+ }
+});
+
+function reloadMemberList( groupID ) {
+
+ if( !groupID )
+ groupID = $('MemberListBaseGroupID').value;
+
+ if($('MemberListStart')) var listStart = $('MemberListStart').value;
+ else var listStart = 0;
+
+ new Ajax.Request( 'admin/security/listmembers?&ajax=1&MemberListBaseGroup=' + groupID + '&MemberListStart=' + listStart, {
+ onSuccess: function( response ) {
+ $('MemberList').innerHTML = response.responseText;
+ // Behaviour.debug();
+ Behaviour.apply( $('MemberList') );
+ },
+ onFailure: function( response ) {
+ errorMessage('Could not filter results: ' + response.responseText );
+ }
+ });
+}
+
+Behaviour.register({
+ '.MemberListFilter a.showhide' : {
+ initialise: function() {
+ this.open = !Element.hasClassName( this, 'closed' );
+ },
+
+ onclick: function() {
+ if( this.open )
+ this.openControls();
+ else
+ this.closeControls();
+
+ this.open = !this.open;
+
+ return false;
+ },
+
+ openControls: function() {
+ Element.removeClassName( this, 'closed' );
+ $('MemberListFilterControls').style.display = 'block';
+ },
+
+ closeControls: function() {
+ Element.removeClassName( this, 'closed' );
+ $('MemberListFilterControls').style.display = 'none';
+ }
+ }
+});
\ No newline at end of file
diff --git a/javascript/MemberTableField.js b/javascript/MemberTableField.js
new file mode 100755
index 00000000..8621cf23
--- /dev/null
+++ b/javascript/MemberTableField.js
@@ -0,0 +1,298 @@
+/**
+ * Modified 2006-10-05, Ingo Schommer
+ * This is more or less a copy of Member.js, with additions and changes
+ * to match the switch from Member.php to MemberTableField.php all over the UI.
+ * Eventually it will replace Member.js (please remove this message then).
+ */
+
+// no confirm message for removal from a group
+ComplexTableField.prototype.deleteConfirmMessage = null;
+
+/**
+ * Auto-lookup on ajax fields
+ */
+AjaxMemberLookup = {
+ initialise : function() {
+ var div = document.createElement('div');
+ div.id = this.id + '_ac';
+ div.className = 'autocomplete';
+ this.parentNode.appendChild(div);
+ if(this.id) {
+ new Ajax.Autocompleter(this.id, div.id, 'admin/security/autocomplete/' + this.name, {
+ afterUpdateElement : this.afterAutocomplete.bind(this)
+ });
+
+ }
+ },
+ afterAutocomplete : function(field, selectedItem) {
+ var data = selectedItem.getElementsByTagName('span')[1].innerHTML;
+ var items = data.split(",");
+ form = Element.ancestorOfType(field, 'form');
+ // TODO more flexible column-detection
+ form.elements.FirstName.value = items[0];
+ form.elements.Surname.value = items[1];
+ form.elements.Email.value = items[2];
+ if(items[3] && form.elements.Password)
+ form.elements.Password.value = items[3];
+
+ //var fieldSet = field.parentNode.parentNode.getElementsByTagName('input');
+ //ajaxSubmitFieldSet('admin/security/savemember?MemberBaseGroupID='.$('MemberBaseGroupID'), fieldSet);
+ }
+}
+
+MemberTableField = Class.create();
+MemberTableField.applyTo('#Form_EditForm div.MemberTableField');
+MemberTableField.prototype = {
+
+ initialize: function() {
+ Behaviour.register({
+ '#Form_EditForm div.MemberFilter input' : {
+ onkeypress : this.prepareSearch.bind(this)
+ },
+
+ '#Form_EditForm div.MemberTableField table.data tr.addtogrouprow input' : {
+ onkeypress : this.prepareAddToGroup.bind(this)
+ },
+
+ '#Form_EditForm div.MemberTableField table.data tr.addtogrouprow #Form_AddRecordForm_action_addtogroup' : {
+ onclick : this.prepareAddToGroup.bind(this)
+ },
+
+ '#Form_EditForm div.MemberTableField table.data tr.addtogrouprow td.actions input' : {
+ initialise: function() {
+ data = this.parentNode.parentNode.getElementsByTagName('input');
+ var i,item,error = [];
+ for(i=0;item=data[i];i++) {
+ item.originalSerialized = Form.Element.serialize(item);
+ }
+ },
+ onclick : this.addToGroup.bind(this)
+ },
+
+ //'#Form_EditForm div.MemberTableField input' : AjaxMemberLookup,
+
+ '#Form_EditForm' : {
+ changeDetection_fieldsToIgnore : {
+ 'ctf[start]' : true,
+ 'ctf[ID]' : true,
+ 'MemberOrderByField' : true,
+ 'MemberOrderByOrder' : true,
+ 'MemberGroup' : true,
+ 'MemberFilterButton' : true,
+ 'MemberFieldName' : true,
+ 'MemberDontShowPassword' : true,
+ 'Surname' : true,
+ 'FirstName' : true,
+ 'Email' : true,
+ 'MemberSearch' : true
+ }
+ }
+ });
+ },
+
+ // prevent submission of wrong form-button (MemberFilterButton)
+ prepareAddToGroup: function(e) {
+ // IE6 doesnt send an event-object with onkeypress
+ var event = (e) ? e : window.event;
+ var keyCode = (event.keyCode) ? event.keyCode : event.which;
+ if(keyCode == Event.KEY_RETURN) {
+ var el = Event.element(event);
+ this.addToGroup(event);
+ Event.stop(event);
+ return false;
+ }
+ },
+
+ // prevent submission of wrong form-button (MemberFilterButton)
+ prepareSearch: function(e) {
+ // IE6 doesnt send an event-object with onkeypress
+ var event = (e) ? e : window.event;
+ var keyCode = (event.keyCode) ? event.keyCode : event.which;
+
+ if(keyCode == Event.KEY_RETURN) {
+ var el = Event.element(event);
+ $('MemberFilterButton').onclick(event);
+ Event.stop(event);
+ return false;
+ }
+ },
+
+ addToGroup: function(e) {
+ // only submit parts of the form
+ var data = this.parentNode.parentNode.getElementsByTagName('input');
+ var i,item,error = [];
+ var form = Event.findElement(e,"form");
+
+ for(i=0;item=data[i];i++) {
+ if(item.name == 'Email' && !item.value) error[error.length] = "Email";
+ if(item.name == 'Password' && !item.value) error[error.length] = "Password";
+ }
+
+ if(error.length > 0) {
+ alert('Please enter a ' + error.join(' and a ') + ' to add a member.');
+ } else {
+ updateURL = "";
+ updateURL += Event.findElement(e,"form").action;
+ // we can't set "fieldName" as a HiddenField because there might be multiple ComplexTableFields in a single EditForm-container
+ updateURL += "&fieldName="+$('MemberFieldName').value;
+ updateURL += "&action_callfieldmethod&&methodName=addtogroup&";
+
+ ajaxSubmitFieldSet(updateURL, data);
+ }
+
+ return false;
+ }
+
+ /*
+ initialise : function() {
+ this.headerMap = [];
+
+ var i, item, headers = this.getElementsByTagName('thead')[0].getElementsByTagName('tr')[0].getElementsByTagName('td');
+ for(i=0;item=headers[i];i++) {
+ this.headerMap[i] = item.className;
+ }
+ },
+
+ setRecordDetails : function(id, details, groupID) {
+ var row = document.getElementById('member-' + id);
+ if(row) {
+ var i, item, cells = row.getElementsByTagName('td');
+ for(i=0;item=cells[i];i++) {
+ if(details[this.headerMap[i]]) {
+ item.innerHTML = details[this.headerMap[i]];
+ }
+ }
+ } else {
+ this.createRecord(id, details, groupID);
+ }
+ },
+ createRecord : function (id, details, groupId) {
+ var row = document.createElement('tr');
+ row.id = 'member-' + id;
+ var i, cell, cellField;
+ for(i=0;cellField=this.headerMap[i];i++) {
+ cell = document.createElement('td')
+ if(details[cellField]) {
+ cell.innerHTML = details[cellField];
+ }
+ row.appendChild(cell);
+ }
+
+ // Add the delete icon
+ if(typeof groupId == 'undefined')
+ var groupId = $('Form_EditForm').elements.ID.value;
+ cell = document.createElement('td')
+ cell.innerHTML = ' ';
+ cell.getElementsByTagName('0');
+ row.appendChild(cell);
+
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) tbody.insertBefore(row, addRow);
+ else tbody.appendChild(row);
+ Behaviour.apply(row, true);
+ },
+ clearAddForm : function() {
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) {
+ var i,field,fields = addRow.getElementsByTagName('input');
+ for(i=0;field=fields[i];i++) {
+ if(field.type != 'hidden' && field.type != 'submit') field.value = '';
+ }
+ }
+ },
+ removeMember : function(memberID) {
+ var record;
+ if(record = $('member-' + memberID)) {
+ record.parentNode.removeChild(record);
+ }
+ }
+ */
+}
+
+MemberFilterButton = Class.create();
+MemberFilterButton.applyTo('#Form_EditForm #MemberFilterButton');
+MemberFilterButton.prototype = {
+ initialize: function() {
+ this.inputFields = new Array();
+
+ var childNodes = this.parentNode.getElementsByTagName('input');
+
+ for( var index = 0; index < childNodes.length; index++ ) {
+ if( childNodes[index].tagName ) {
+ childNodes[index].resetChanged = function() { return false; }
+ childNodes[index].isChanged = function() { return false; }
+ this.inputFields.push( childNodes[index] );
+ }
+ }
+
+ childNodes = this.parentNode.getElementsByTagName('select');
+
+ for( var index = 0; index < childNodes.length; index++ ) {
+ if( childNodes[index].tagName ) {
+ childNodes[index].resetChanged = function() { return false; }
+ childNodes[index].field_changed = function() { return false; }
+ this.inputFields.push( childNodes[index] );
+ }
+ }
+ },
+
+ isChanged: function() {
+ return false;
+ },
+
+ onclick: function(e) {
+ if(!$('ctf-ID') || !$('MemberFieldName')) {
+ return false;
+ }
+
+ var updateURL = "";
+ updateURL += Event.findElement(e,"form").action;
+ // we can't set "fieldName" as a HiddenField because there might be multiple ComplexTableFields in a single EditForm-container
+ updateURL += "&fieldName="+$('MemberFieldName').value;
+ updateURL += "&action_callfieldmethod&&methodName=ajax_refresh&";
+ for( var index = 0; index < this.inputFields.length; index++ ) {
+ if( this.inputFields[index].tagName ) {
+ updateURL += this.inputFields[index].name + '=' + encodeURIComponent( this.inputFields[index].value ) + '&';
+ }
+ }
+ updateURL += 'ajax=1';
+
+ new Ajax.Request( updateURL, {
+ onSuccess: Ajax.Evaluator,
+ onFailure: function( response ) {
+ errorMessage('Could not filter results: ' + response.responseText );
+ }.bind(this)
+ });
+
+ return false;
+ }
+}
+
+// has to be external from initialize() because otherwise request will double on each reload - WTF
+Behaviour.register({
+ '#Form_EditForm div.MemberTableField table.data input' : AjaxMemberLookup
+});
+
+/*
+function reloadMemberTableField( groupID ) {
+
+ if( !groupID )
+ groupID = $('MemberBaseGroupID').value;
+
+ if($('MemberStart')) var listStart = $('MemberStart').value;
+ else var listStart = 0;
+
+ new Ajax.Request( 'admin/security/listmembers?&ajax=1&MemberBaseGroup=' + groupID + '&MemberStart=' + listStart, {
+ onSuccess: function( response ) {
+ $('MemberList').innerHTML = response.responseText;
+ // Behaviour.debug();
+ Behaviour.apply( $('MemberList') );
+ },
+ onFailure: function( response ) {
+ errorMessage('Could not filter results: ' + response.responseText );
+ }
+ });
+}
+*/
\ No newline at end of file
diff --git a/javascript/MemberTableField_popup.js b/javascript/MemberTableField_popup.js
new file mode 100755
index 00000000..b6f10ec9
--- /dev/null
+++ b/javascript/MemberTableField_popup.js
@@ -0,0 +1,42 @@
+MemberTableFieldPopupForm = Class.extend("ComplexTableFieldPopupForm");
+MemberTableFieldPopupForm.prototype = {
+ initialize: function() {
+ this.ComplexTableFieldPopupForm.initialize();
+
+ Behaviour.register({
+ "form#MemberTableField_Popup_DetailForm input.action": {
+ onclick: this.submitForm.bind(this)
+ },
+
+ 'form#MemberTableField_Popup_DetailForm input' : {
+ initialise : function() {
+ if(this.name == 'FirstName' || this.name == 'Surname' || this.name == 'Email') {
+ var div = document.createElement('div');
+ div.id = this.id + '_ac';
+ div.className = 'autocomplete';
+ this.parentNode.appendChild(div);
+ /*
+ new Ajax.Autocompleter(this.id, div.id, 'admin/security/autocomplete/' + this.name, {
+ afterUpdateElement : this.afterAutocomplete.bind(this)
+ });
+ */
+ }
+ },
+ afterAutocomplete : function(field, selectedItem) {
+ var data = selectedItem.getElementsByTagName('span')[1].innerHTML;
+ var items = data.split(",");
+
+ this.elements.FirstName.value = items[0];
+ this.elements.Surname.value = items[1];
+ this.elements.Email.value = items[2];
+ this.elements.Password.value = items[3];
+ }
+ }
+
+ //'form#MemberTableField_Popup_DetailForm input' : AjaxMemberLookup
+ });
+ },
+
+
+}
+MemberTableFieldPopupForm.applyTo('form#MemberTableField_Popup_DetailForm');
\ No newline at end of file
diff --git a/javascript/NewsletterAdmin_left.js b/javascript/NewsletterAdmin_left.js
new file mode 100755
index 00000000..a446a488
--- /dev/null
+++ b/javascript/NewsletterAdmin_left.js
@@ -0,0 +1,404 @@
+if(typeof SiteTreeHandlers == 'undefined') SiteTreeHandlers = {};
+SiteTreeHandlers.showNLDraft_url = 'admin/newsletter/shownewsletter/';
+SiteTreeHandlers.showNLType_url = 'admin/newsletter/showtype/';
+
+SiteTree.prototype = {
+ castAsTreeNode: function(li) {
+ behaveAs(li, SiteTreeNode, this.options);
+ },
+
+ getIdxOf : function(treeNode) {
+ if(treeNode && treeNode.id)
+ return treeNode.id;
+ },
+
+ getTreeNodeByIdx : function(idx) {
+ if(!idx) idx = "0";
+ return document.getElementById(idx);
+ },
+
+ initialise: function() {
+ this.observeMethod('SelectionChanged', this.changeCurrentTo);
+ },
+
+ createTreeNode : function(idx, title, pageType, secondIdx) {
+ var i;
+ var node = document.createElement('li');
+ node.className = pageType;
+
+ var aTag = document.createElement('a');
+
+ node.id = idx;
+
+ if( pageType == 'Draft' ) {
+ aTag.href = SiteTreeHandlers.showNLDraft_url + idx;
+ } else {
+ aTag.href = SiteTreeHandlers.showNLType_url + idx;
+ }
+
+ aTag.innerHTML = title;
+ node.appendChild(aTag);
+
+ SiteTreeNode.create(node, this.options);
+
+ return node;
+ },
+
+ addDraftNode: function( title, parentID, draftID ) {
+ var draftNode = this.createTreeNode( 'draft_' + parentID + '_' + draftID, title, 'Draft', parentID );
+ this.getTreeNodeByIdx('drafts_' + parentID).appendTreeNode( draftNode );
+ this.changeCurrentTo( draftNode );
+ },
+
+ addTypeNode: function( title, parentID ) {
+ var typeNode = this.createTreeNode( 'mailtype_' + parentID, title, 'MailType' );
+ var draftsNode = this.createTreeNode( 'drafts_' + parentID, 'Drafts', 'DraftFolder' );
+ var sentItemsNode = this.createTreeNode( 'sent_' + parentID, 'Sent Items', 'SentFolder' );
+ var mailingListNode = this.createTreeNode( 'recipients_' + parentID, 'Mailing List', 'Recipients' );
+ typeNode.appendTreeNode( draftsNode );
+ Element.addClassName( draftsNode, 'nodelete');
+ typeNode.appendTreeNode( sentItemsNode );
+ Element.addClassName(sentItemsNode,'nodelete');
+ typeNode.appendTreeNode( mailingListNode );
+ Element.addClassName(mailingListNode,'nodelete');
+ this.appendTreeNode( typeNode );
+ this.changeCurrentTo( typeNode );
+ }
+}
+
+
+/**
+ * Delete pages action
+ */
+deletedraft = {
+ button_onclick : function() {
+ if(treeactions.toggleSelection(this)) {
+ deletedraft.o1 = $('sitetree').observeMethod('SelectionChanged', deletedraft.treeSelectionChanged);
+ deletedraft.o2 = $('deletedrafts_options').observeMethod('Close', deletedraft.popupClosed);
+ addClass($('sitetree'),'multiselect');
+
+ var sel = $('sitetree').firstSelected();
+
+ if(sel) {
+ var selIdx = $('sitetree').getIdxOf(sel);
+ deletedraft.selectedNodes = { };
+ deletedraft.selectedNodes[selIdx] = true;
+ sel.removeNodeClass('current');
+ sel.addNodeClass('selected');
+ }
+ }
+ return false;
+ },
+
+ treeSelectionChanged : function(selectedNode) {
+ var idx = $('sitetree').getIdxOf(selectedNode);
+
+ if(selectedNode.selected) {
+ selectedNode.removeNodeClass('selected');
+ selectedNode.selected = false;
+ deletedraft.selectedNodes[idx] = false;
+
+ } else {
+ selectedNode.addNodeClass('selected');
+ selectedNode.selected = true;
+ deletedraft.selectedNodes[idx] = true;
+ }
+
+ return false;
+ },
+
+ popupClosed : function() {
+ removeClass($('sitetree'),'multiselect');
+ $('sitetree').stopObserving(deletedraft.o1);
+ $('deletedrafts_options').stopObserving(deletedraft.o2);
+
+ for(var idx in deletedraft.selectedNodes) {
+ if(deletedraft.selectedNodes[idx]) {
+ node = $('sitetree').getTreeNodeByIdx(idx);
+ if(node) {
+ node.removeNodeClass('selected');
+ node.selected = false;
+ }
+ }
+ }
+ },
+
+ form_submit : function() {
+ var csvIDs = "";
+ for(var idx in deletedraft.selectedNodes) {
+ if(deletedraft.selectedNodes[idx]) csvIDs += (csvIDs ? "," : "") + idx;
+ }
+
+ if(csvIDs) {
+ $('deletedrafts_options').elements.csvIDs.value = csvIDs;
+
+ Ajax.SubmitForm('deletedrafts_options', null, {
+ onSuccess : function(response) {
+ Ajax.Evaluator(response);
+
+ var sel;
+ if((sel = $('sitetree').selected) && sel.parentNode) sel.addNodeClass('current');
+// else $('Form_EditForm').innerHTML = "";
+
+ treeactions.closeSelection($('deletedrafts'));
+ },
+ onFailure : function(response) {
+ errorMessage('Error deleting drafts', response);
+ }
+ });
+
+ $('deletedrafts').getElementsByTagName('a')[0].onclick();
+
+ } else {
+ alert("Please select at least 1 page.");
+ }
+
+ return false;
+ },
+
+ treeSelectionChanged : function(selectedNode) {
+ var idx = $('sitetree').getIdxOf(selectedNode);
+
+ if( !deletedraft.selectedNodes )
+ deletedraft.selectedNodes = {};
+
+ if(selectedNode.className.indexOf('nodelete') == -1) {
+ if(selectedNode.selected) {
+ selectedNode.removeNodeClass('selected');
+ selectedNode.selected = false;
+ deletedraft.selectedNodes[idx] = false;
+
+ } else {
+ selectedNode.addNodeClass('selected');
+ selectedNode.selected = true;
+ deletedraft.selectedNodes[idx] = true;
+ }
+ }
+
+ return false;
+ }
+}
+
+SiteTreeNode.prototype.onselect = function() {
+ $('sitetree').changeCurrentTo(this);
+ if($('sitetree').notify('SelectionChanged', this)) {
+ if(typeof save != 'undefined'){
+ autoSave(true, this.getPageFromServer.bind(this));
+ }
+ else {
+ this.getPageFromServer();
+ }
+ }
+ return false;
+};
+
+SiteTreeNode.prototype.getPageFromServer = function() {
+
+ var match = this.id.match(/(mailtype|drafts|draft|sent|recipients)_([\d]+)$/);
+ var openTabName = null;
+ var currentPageID = null;
+
+ if( $('Form_EditForm_ID') )
+ currentPageID = $('Form_EditForm_ID').value;
+
+ var newPageID = null;
+ var otherID = null;
+ var type = null;
+
+ var forceReload = $('Form_EditForm_Type') && $('Form_EditForm_Type').value == 'Newsletter';
+
+ if( match ) {
+
+ // open the appropriate tab
+ switch( match[1] ) {
+ case 'recipients':
+ openTabName = 'Root_Recipients_set_Recipients';
+ break;
+ case 'drafts':
+ openTabName = 'Root_Drafts';
+ break;
+ case 'sent':
+ openTabName = 'Root_Sent_set_Sent';
+ break;
+ }
+
+ newPageID = match[2];
+ type = match[1];
+ } else if(this.id.match(/(mailtype|drafts|draft|sent|recipients)_([\d]+)_([\d]+)$/)) {
+ newPageID = RegExp.$2;
+ type = RegExp.$1;
+ otherID = RegExp.$3;
+ forceReload = true;
+ }
+
+ if( forceReload || currentPageID != newPageID )
+ $('Form_EditForm').getPageFromServer(newPageID, type, otherID, openTabName);
+ else
+ openTab( openTabName );
+};
+
+function draft_sent_ok( newsletterID, draftID ) {
+ var draftsListNode = $('drafts_' + newsletterID);
+ var sentListNode = $('sent_' + newsletterID);
+ var draftNode = $('draft_' + newsletterID + '_' + draftID );
+
+ draftsListNode.removeTreeNode( draftNode );
+ draftNode.id = 'sent_' + newsletterID + '_' + draftID;
+ sentListNode.appendTreeNode( draftNode, null );
+ statusMessage('Sent email to mailing list', 'good');
+}
+
+function resent_ok( newsletterID, sentID ) {
+ statusMessage('Resent email to mailing list', 'good');
+}
+
+function reloadSiteTree() {
+
+ new Ajax.Request( 'admin/newsletter/getsitetree', {
+ method: get,
+ onSuccess: function( response ) {
+ $('sitetree_holder').innerHTML = response.responseText;
+ },
+ onFailure: function( response ) {
+
+ }
+ });
+
+}
+
+_HANDLER_FORMS['addtype'] = 'addtype_options';
+_HANDLER_FORMS['deletedrafts'] = 'deletedrafts_options';
+
+AddForm = Class.extend('addpage');
+AddForm.applyTo('#addtype');
+AddForm.prototype = {
+ initialize: function () {
+ Observable.applyTo($(_HANDLER_FORMS[this.id]));
+ this.getElementsByTagName('a')[0].onclick = returnFalse;
+ $(_HANDLER_FORMS[this.id]).onsubmit = this.form_submit;
+ },
+
+ form_submit : function() {
+ var st = $('sitetree');
+
+ if( st.selected.length )
+ selectedNode = st.selected[0];
+ else
+ selectedNode = st.selected;
+
+ var parentID = null;
+
+ while( selectedNode && !parentID ) {
+ if( selectedNode && selectedNode.id && selectedNode.id.match(/mailtype_([0-9a-z\-]+)$/) )
+ parentID = RegExp.$1;
+ else
+ selectedNode = selectedNode.parentNode;
+ }
+
+ if(parentID && parentID.substr(0,3) == 'new') {
+ alert("You have to save a page before adding children underneath it");
+
+ } else {
+
+ this.elements.ParentID.value = parentID;
+
+ var type = 'draft';
+ var selectIDPrefix = 'draft_' + parentID + '_';
+
+ if( $('add_type').value == 'type' ) {
+ type = 'type';
+ selectIDPrefix = 'mailtype_';
+ }
+
+ var request = new Ajax.Request( this.action + '?ajax=1&PageType=' + type + '&ParentID=' + parentID, {
+ method: 'get',
+ asynchronous: true,
+ onSuccess : function( response ) {
+
+ $('Form_EditForm').loadNewPage(response.responseText);
+
+ // create a new node and add it to the site tree
+ if( type == 'draft' ) {
+ $('sitetree').addDraftNode('New draft newsletter', parentID, $('Form_EditForm_ID').value );
+ } else {
+ $('sitetree').addTypeNode('New newsletter type', $('Form_EditForm_ID').value );
+ }
+
+ statusMessage('Added new ' + type);
+ },
+
+ onFailure : function(response) {
+ alert(response.responseText);
+ statusMessage('Could not add new ' + type );
+ }
+ });
+ }
+
+ return false;
+ },
+
+ reloadSiteTree: function( response ) {
+ statusMessage('Added new newsletter type', 'good' );
+ $('sitetree_holder').innerHTML = response.responseText;
+ Behaviour.apply( $('sitetree_holder') );
+ },
+
+ button_onclick : function() {
+ if(treeactions.toggleSelection(this)) {
+ var selectedNode = $('sitetree').firstSelected();
+ if(selectedNode) {
+ while(selectedNode.parentTreeNode && !selectedNode.hints.defaultChild) {
+ $('sitetree').changeCurrentTo(selectedNode.parentTreeNode);
+ selectedNode = selectedNode.parentTreeNode;
+ }
+ }
+
+ this.o1 = $('sitetree').observeMethod('SelectionChanged', this.treeSelectionChanged.bind(this));
+ this.o2 = $(_HANDLER_FORMS[this.id]).observeMethod('Close', this.popupClosed.bind(this));
+ }
+ return false;
+ },
+
+ treeSelectionChanged: function( treeNode ) {
+ this.selected = treeNode;
+ },
+
+ popupClosed: function() {
+ $('sitetree').stopObserving(this.o1);
+ $(_HANDLER_FORMS.addtype).stopObserving(this.o2);
+ }
+}
+
+function removeTreeNodeByIdx( tree, nodeID ) {
+ var node = tree.getTreeNodeByIdx( nodeID );
+ if( node )
+ if( node.parentTreeNode )
+ node.parentTreeNode.removeTreeNode( node );
+ else
+ $('sitetree').removeTreeNode( node );
+}
+
+/**
+ * Initialisation function to set everything up
+ */
+Behaviour.addLoader(function () {
+ // Set up add draft
+ Observable.applyTo($('addtype_options'));
+
+ if( $('addtype') ) {
+ if( AddForm.button_click )
+ $('addtype').getElementsByTagName('a')[0].onclick = function() {return false;};
+ if( AddForm.button_click )
+ $('addtype_options').onsubmit = AddForm.form_submit;
+ }
+ // Set up delete drafts
+ Observable.applyTo($('deletedrafts_options'));
+
+ var deleteDrafts = $('deletedrafts');
+
+ if( deleteDrafts ) {
+ deleteDrafts.onclick = deletedraft.button_onclick;
+ deleteDrafts.getElementsByTagName('a')[0].onclick = function() {return false;};
+ $('deletedrafts_options').onsubmit = deletedraft.form_submit;
+ }
+});
\ No newline at end of file
diff --git a/javascript/NewsletterAdmin_right.js b/javascript/NewsletterAdmin_right.js
new file mode 100755
index 00000000..64a89dd3
--- /dev/null
+++ b/javascript/NewsletterAdmin_right.js
@@ -0,0 +1,536 @@
+function action_send_right() {
+ $('action_send_options').toggle();
+}
+
+CMSForm.applyTo('#Form_MemberForm','rightbottom');
+
+Behaviour.register({
+ '#Form_EditForm' : {
+ initialise : function() {
+ this.openTab = null;
+ this.prepareForm();
+ },
+
+ /**
+ * Processing called whenever a page is loaded in the right - including the initial one
+ */
+ prepareForm : function() {
+ ajaxActionsAtTop('Form_EditForm', 'form_actions', 'right');
+ },
+
+ /**
+ * Request a page from the server via Ajax
+ */
+ getPageFromServer : function(id, type, otherid,openTabName) {
+
+ this.openTab = openTabName;
+
+ if(id && parseInt(id) == id) {
+ this.receivingID = id;
+
+ // Treenode might not exist if that part of the tree is closed
+ var treeNode = $('sitetree').getTreeNodeByIdx(id);
+ if(treeNode) treeNode.addNodeClass('loading');
+ statusMessage("loading...");
+
+ var requestURL = 'admin/newsletter/showtype/' + id;
+
+ if( otherid )
+ requestURL = 'admin/newsletter/shownewsletter/' + otherid;
+
+ new Ajax.Request(requestURL, {
+ asynchronous : true,
+ method : 'post',
+ postBody : /*'ID=' + id + 'type=' + type + */'ajax=1'+ (otherid?('&otherid='+otherid):''),
+ onSuccess : this.successfullyReceivedPage.bind(this),
+ onFailure : function(response) {
+ errorMessage('error loading page',response);
+ }
+ });
+ } else {
+ throw("getPageFromServer: Bad page ID: " + id);
+ }
+ },
+
+ successfullyReceivedPage : function(response) {
+ this.loadNewPage(response.responseText);
+
+ // Treenode might not exist if that part of the tree is closed
+ var treeNode = $('sitetree').getTreeNodeByIdx(this.receivingID);
+ if(treeNode) {
+ $('sitetree').changeCurrentTo(treeNode);
+ treeNode.removeNodeClass('loading');
+ }
+ statusMessage('');
+
+ onload_init_tabstrip();
+
+ if( this.openTab ) {
+ openTab( this.openTab );
+ this.openTab = null;
+ }
+ },
+
+ didntReceivePage : function(response) {
+ errorMessage('error loading page', response);
+ $('sitetree').getTreeNodeByIdx(this.elements.ID.value).removeNodeClass('loading');
+ },
+
+ /**
+ * Load a new page into the right-hand form
+ */
+ loadNewPage : function(formContent) {
+ rightHTML = formContent;
+ rightHTML = rightHTML.replace(/href *= *"#/g, 'href="' + window.location.href.replace(/#.*$/,'') + '#');
+
+ // Prepare iframes for removal, otherwise we get loading bugs
+ var i, allIframes = this.getElementsByTagName('iframe');
+ if(allIframes) for(i=0;i= parseInt( processParts[3] ) ) {
+ this.onComplete( response );
+ return;
+ }
+
+
+ // update the progress bar
+ $('SendProgressBar').setProgress( ( parseInt( processParts[2] ) / parseInt( processParts[3] ) ) * 100 );
+ var estimate = $('SendProgressBar').estimateTime();
+ $('SendProgressBar').setText( 'Sent ' + processParts[2] + ' of ' + processParts[3] + '. ' + estimate + ' remaining...' );
+
+ // set the action to the batch process controller
+ var updateRequest = baseHref() + 'processes/next/' + processParts[1] + '/10?ajax=1';
+
+ var request = new Ajax.Request( updateRequest, {
+ onSuccess: this.incrementProcess.bind(this),
+ onFailure: function( response ) {
+ errorMessage( response.responseText );
+ }
+ });
+ },
+
+ /**
+ * Process the action's Ajax response
+ */
+ onComplete: function( response ) {
+ // $('SendProgressBar').setProgress( 100 );
+ // $('SendProgressBar').setText( 'Done!' );
+ $('SendProgressBar').reset();
+ // this.elements.Message.value = '';
+ this.close();
+ if( response )
+ Ajax.Evaluator(response);
+ },
+
+ onCompleteTest: function( response ) {
+ // this.sendingText.innerHTML = '';
+ if( this.sendingText.parentNode == this )
+ this.removeChild( this.sendingText );
+ this.close();
+ if( response )
+ Ajax.Evaluator(response);
+ }
+ }
+});
+
+
+function save(ifChanged, callAfter, confirmation) {
+ tinyMCE.triggerSave(true);
+
+ var form = $('Form_EditForm');
+
+ var alreadySaved = false;
+
+ if(ifChanged) {
+ if(form.elements.length < 2) alreadySaved = true;
+ else alreadySaved = !form.isChanged();
+ }
+
+ if(alreadySaved && ifChanged) { // pressing save always saves
+ if(!ifChanged && !confirmation) {
+ statusMessage("There's nothing to save");
+ }
+
+ if(callAfter) callAfter();
+
+ } else {
+ var options = {
+ save: (function() {
+ statusMessage('saving...', '', true);
+
+ var success = function(response) {
+ Ajax.Evaluator(response);
+ if(this.callAfter) this.callAfter();
+ }
+ if(callAfter) success = success.bind(this);
+
+ if( $('Form_EditForm_Type').value == 'Newsletter' )
+ var data = form.serializeChangedFields('ID','type') + '&ajax=1&action_savenewsletter=1';
+ else
+ var data = form.serializeChangedFields('ID','type') + '&ajax=1&action_save=1';
+
+ new Ajax.Request(form.action, {
+ method : form.method,
+ postBody: data,
+ onSuccess : success,
+ onFailure : function(response) {
+ errorMessage('Error saving content', response);
+ }
+ });
+ }).bind({callAfter : callAfter}),
+
+ discard: callAfter,
+ cancel: function() {
+ }
+ }
+
+ if(confirmation) doYouWantToSave(options);
+ else options.save();
+ }
+
+ return false;
+}
+
+Behaviour.register({
+ /**
+ * When the iframe has loaded, apply the listeners
+ */
+ 'div#ImportFile' : {
+ frameLoaded: function( doc ) {
+ this.showTable = true;
+ var fileField = doc.getElementsByTagName('input')[0];
+ var idField = doc.getElementsByTagName('input')[1];
+
+ idField.value = $('Form_EditForm_ID').value;
+
+ fileField.onchange = this.selectedFile.bind(this);
+ },
+ loadTable: function( doc ) {
+ this.innerHTML = doc.getElementsByTagName('body')[0].innerHTML;
+ }
+ }
+});
+
+NewsletterList = Class.create();
+NewsletterList.applyTo('table.NewsletterList');
+NewsletterList.prototype = {
+ initialize: function() {
+ this.tableBody = this.getElementsByTagName('tbody')[0];
+ this.deleteLinks = this.getElementsByTagName('a');
+
+ for( var i = 0; i < this.deleteLinks.length; i++ ) {
+ this.deleteLinks[i].onclick = this.deleteNewsletter.bindAsEventListener(this);
+ }
+ },
+
+ deleteNewsletter: function( event ) {
+ var link = event.target;
+
+ if( event.srcElement )
+ link = event.srcElement;
+
+ var rowNode = link.parentNode.parentNode;
+
+ new Ajax.Request( link.href, {
+ onSuccess: this.removeRow,
+ onFailure: function( response ) {
+ alert('The newsletter could not be deleted');
+ }
+ });
+ },
+
+ removeRow: function( response ) {
+ this.tableBody.removeChild( $(response.responseText) );
+ }
+}
+
+/**
+ * Add page action
+ * @todo Remove duplication between this and the CMSMain Add page action
+ */
+Behaviour.register( {
+ '#Form_EditForm_action_save': {
+ onclick : function() {
+ $('Form_EditForm').save();
+ return false;
+ }
+ }
+});
+
+/**
+ * Handle auto-saving. Detects if changes have been made, and if so save everything on the page.
+ * If confirmation is true it will ask for confirmation.
+ */
+function autoSave(confirmation, callAfter) {
+ if(typeof tinyMCE != 'undefined') tinyMCE.triggerSave();
+
+ var __forms = []
+ if($('Form_EditForm')) __forms.push($('Form_EditForm'));
+ if($('Form_SubForm')) __forms.push($('Form_SubForm'));
+ if($('Form_MemberForm')) __forms.push($('Form_MemberForm'));
+
+ var __somethingHasChanged = false;
+ var __callAfter = callAfter;
+
+ __forms.each(function(form) {
+ if(form.isChanged && form.isChanged()) {
+ __somethingHasChanged = true;
+ }
+ });
+
+ if(__somethingHasChanged) {
+ var options = {
+ save: function() {
+ statusMessage('saving...', '', true);
+ var i;
+ for(i=0;i<__forms.length;i++) {
+ if(__forms[i].isChanged && __forms[i].isChanged()) {
+ if(i == 0) __forms[i].save(true, __callAfter);
+ else __forms[i].save(true);
+ }
+ }
+ },
+ discard: function() {
+ __forms.each(function(form) { form.resetElements(false); });
+ if(__callAfter) __callAfter();
+ },
+ cancel: function() {
+ }
+ }
+
+ if(confirmation) doYouWantToSave(options);
+ else options.save();
+
+ } else {
+ if(__callAfter) __callAfter();
+ }
+}
+
+function reloadRecipientsList() {
+
+ var id = $('Form_EditForm_ID').value;
+
+ var request = new Ajax.Request( 'admin/newsletter/getrecipientslist/' + id + '?ajax=1', {
+ onSuccess: function( response ) {
+ $('MemberList').outerHTML = response.responseText;
+ Behaviour.apply( $('MemberList') );
+ },
+ onFailure: function(response) {
+ statusMessage('Could not automatically refresh recipients list', 'bad');
+ }
+ });
+}
+
+/*RecipientImportField = Class.create();
+RecipientImportField.applyTo('iframe.RecipientImportField');
+RecipientImportField.prototype = {
+ initialize: function() {
+ this.src = document.getElementsByTagName('base')[0].href + this.src;
+ }
+}*/
+
+/**
+ * We don't want hitting the enter key in the subject field
+ * to submit the form.
+ */
+ Behaviour.register({
+ '#Form_EditForm_Subject' : {
+ onkeypress : function(event) {
+ event = (event) ? event : window.event;
+ var kc = event.keyCode ? event.keyCode : event.charCode;
+ if(kc == 13) {
+ return false;
+ }
+ }
+ }
+ });
diff --git a/javascript/NewsletterMemberList.js b/javascript/NewsletterMemberList.js
new file mode 100755
index 00000000..3e755a90
--- /dev/null
+++ b/javascript/NewsletterMemberList.js
@@ -0,0 +1,172 @@
+/**
+ * Auto-lookup on ajax fields
+ */
+AjaxMemberLookup = {
+ initialise : function() {
+ var div = document.createElement('div');
+ div.id = this.id + '_ac';
+ div.className = 'autocomplete';
+ this.parentNode.appendChild(div);
+ new Ajax.Autocompleter(this.id, div.id, 'admin/security/autocomplete/' + this.name, {
+ afterUpdateElement : this.afterAutocomplete.bind(this)
+ });
+ },
+ afterAutocomplete : function(field, selectedItem) {
+ var data = selectedItem.getElementsByTagName('span')[1].innerHTML;
+ var items = data.split(",");
+ form = Element.ancestorOfType(field, 'form');
+
+ form.elements.FirstName.value = items[0];
+ form.elements.Surname.value = items[1];
+ form.elements.Email.value = items[2];
+ form.elements.Password.value = items[3];
+ var fieldSet = field.parentNode.parentNode.getElementsByTagName('input');
+ ajaxSubmitFieldSet('admin/newsletter/savemember', fieldSet);
+ }
+}
+
+/**
+ * Member list behaviour
+ */
+
+Behaviour.register({
+ '#MemberList tr' : {
+ onmouseover : hover_over,
+ onmouseout : hover_out,
+
+ onclick : function() {
+ if(this.className.indexOf('addrow') == -1) {
+ Element.addClassName(this, 'loading');
+ new Ajax.Request('admin/newsletter/getmember', {
+ method : 'post',
+ postBody : 'ID=' + this.id.replace('member-','') + '&ajax=1',
+ onSuccess : this.select_success.bind(this)
+ });
+
+ } else {
+ $('Form_MemberForm').innerHTML = "Choose a member from above.
";
+ }
+ },
+ select_success : function(response) {
+ Element.removeClassName(this, 'loading');
+ $('Form_MemberForm').loadNewPage(response.responseText);
+
+ statusMessage('loaded','good');
+ // for (var n in tinyMCE.instances) tinyMCE.removeMCEControl(n);
+ }
+ },
+
+ '#MemberList thead tr' : {
+ onmouseover : null,
+ onmouseout : null,
+ onclick : null
+ },
+
+ '#MemberList' : {
+ initialise : function() {
+ this.headerMap = [];
+
+ var i, item, headers = this.getElementsByTagName('thead')[0].getElementsByTagName('tr')[0].getElementsByTagName('td');
+ for(i=0;item=headers[i];i++) {
+ this.headerMap[i] = item.className;
+ }
+ },
+
+ setRecordDetails : function(id, details, groupID) {
+ var row = document.getElementById('member-' + id);
+ if(row) {
+ var i, item, cells = row.getElementsByTagName('td');
+ for(i=0;item=cells[i];i++) {
+ if(details[this.headerMap[i]]) {
+ item.innerHTML = details[this.headerMap[i]];
+ }
+ }
+ } else {
+ this.createRecord(id, details, groupID);
+ }
+ },
+
+ createRecord : function (id, details, groupId) {
+ var row = document.createElement('tr');
+ row.id = 'member-' + id;
+ var i, cell, cellField;
+ for(i=0;cellField=this.headerMap[i];i++) {
+ cell = document.createElement('td')
+ if(details[cellField]) {
+ cell.innerHTML = details[cellField];
+ }
+ row.appendChild(cell);
+ }
+
+ // Add the delete icon
+ if(typeof groupId == 'undefined')
+ var groupId = $('Form_EditForm').elements.ID.value;
+ cell = document.createElement('td')
+ cell.innerHTML = ' ';
+ cell.getElementsByTagName('0');
+ row.appendChild(cell);
+
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) tbody.insertBefore(row, addRow);
+ else tbody.appendChild(row);
+ Behaviour.apply(row, true);
+ },
+
+ clearAddForm : function() {
+ var tbody = this.getElementsByTagName('tbody')[0];
+ var addRow = document.getElementsByClassName('addrow',tbody)[0];
+ if(addRow) {
+ var i,field,fields = addRow.getElementsByTagName('input');
+ for(i=0;field=fields[i];i++) {
+ if(field.type != 'hidden' && field.type != 'submit') field.value = '';
+ }
+ }
+ },
+
+ removeMember : function(memberID) {
+ var record;
+ if(record = $('member-' + memberID)) {
+ record.parentNode.removeChild(record);
+ }
+ }
+ },
+
+ '#MemberList input' : AjaxMemberLookup,
+
+ '#MemberList a.deletelink' : {
+ onclick : function(event) {
+ if(confirm("Do you want to remove this member from the group?")) {
+ this.getElementsByTagName('img')[0].src = 'cms/images/network-save.gif';
+ ajaxLink(this.href);
+ }
+ Event.stop(event);
+ return false;
+ }
+ },
+
+ '#MemberList tr.addrow' : {
+ onmouseover : null,
+ onmouseout : null,
+ onclick : null
+ },
+
+ '#MemberList tr.addrow td.actions input' : {
+ onclick : function(event) {
+ data = this.parentNode.parentNode.getElementsByTagName('input');
+ var i,item,error = [];
+ for(i=0;item=data[i];i++) {
+ if(item.name == 'Email' && !item.value) error[error.length] = "Email";
+ if(item.name == 'Password' && !item.value) error[error.length] = "Password";
+ }
+ if(error.length > 0) {
+ alert('Please enter a ' + error.join(' and a ') + ' to add a member.');
+
+ } else {
+ ajaxSubmitFieldSet('admin/newsletter/addmember', data);
+ }
+
+ return false;
+ }
+ }
+});
diff --git a/javascript/Newsletter_UploadForm.js b/javascript/Newsletter_UploadForm.js
new file mode 100755
index 00000000..6d85f4ef
--- /dev/null
+++ b/javascript/Newsletter_UploadForm.js
@@ -0,0 +1,13 @@
+Behaviour.register({
+ 'body' : {
+ onload: function() {
+ top.getElementById('ImportFile').frameLoaded( document );
+ }
+ },
+
+ 'form input[type=file]' : {
+ onchange: function() {
+ this.form.submit();
+ }
+ }
+});
\ No newline at end of file
diff --git a/javascript/PageCommentInterface.js b/javascript/PageCommentInterface.js
new file mode 100755
index 00000000..07ee4a47
--- /dev/null
+++ b/javascript/PageCommentInterface.js
@@ -0,0 +1,186 @@
+/**
+ * Ajax to support the comment posting system
+ */
+
+PageCommentInterface = Class.create();
+
+PageCommentInterface.prototype = {
+ initialize: function() {
+ Behaviour.register({
+ '#PageCommentInterface_Form_PageCommentsPostCommentForm_action_postcomment' : {
+ onclick : this.postComment
+ },
+
+ '#PageComments a.deletelink' : {
+ onclick : this.deleteComment
+ },
+ '#PageComments a.spamlink' : {
+ onclick : this.reportSpam
+ },
+ '#PageComments a.hamlink' : {
+ onclick : this.reportHam
+ }
+ });
+ },
+
+ postComment: function() {
+ var form = $("PageCommentInterface_Form_PageCommentsPostCommentForm");
+ var message = $("PageCommentInterface_Form_PageCommentsPostCommentForm_error");
+
+ if(form.elements.Name.value && form.elements.Comment.value) {
+ if(noComments = $('NoComments')) {
+ Element.remove(noComments);
+ var pageComments = document.createElement('ul');
+ pageComments.id = 'PageComments';
+ $('CommentHolder').appendChild(pageComments);
+ }
+
+ message.style.display = 'none';
+
+ // Create a new for the post
+ var pageComments = $('PageComments').getElementsByTagName('li');
+ var __newComment = document.createElement('li');
+
+ // Add it to the list with a 'loading' message
+ $('PageComments').insertBefore(__newComment, pageComments[0]);
+ __newComment.innerHTML = ' Loading...
';
+
+ // Submit the form via ajax
+ Ajax.SubmitForm(form, "action_postcomment", {
+ onSuccess : function(response) {
+ // Load the response into the new
+ __newComment.innerHTML = response.responseText;
+ Behaviour.apply(__newComment);
+
+ // Flash it using Scriptaculous
+ new Effect.Highlight(__newComment, { endcolor: '#e9e9e9' } );
+ if(response.responseText.match('Spam detected!! ')) {
+ __newComment.className = 'spam';
+ }
+ },
+ onFailure : function(response) {
+ alert(response.responseText);
+ }
+ });
+
+ // Clear the fields
+
+ // form.elements.Name.value = "";
+ form.elements.Comment.value = "";
+
+
+
+ } else {
+ message.style.display = '';
+ message.innerHTML = "Please enter your name and a comment to be posted to the site.";
+
+ }
+
+ return false;
+ },
+
+ /**
+ * Ajax handler of moderation removal
+ */
+ deleteComment: function() {
+ var __comment = this.parentNode.parentNode.parentNode;
+
+ __comment.getElementsByTagName('span')[0].innerHTML = "Removing...";
+
+ new Ajax.Request(this.href + '?ajax=1', {
+ onSuccess : function(response) {
+ // Clear our wee status message
+ __comment.getElementsByTagName('span')[0].innerHTML = "Removing...";
+
+ // Remove it using Scriptaculous
+ new Effect.Highlight(__comment, {
+ startcolor: '#cc9999' , endcolor: '#e9e9e9', duration: 0.5,
+ afterFinish : function () {
+ var commentList = __comment.parentNode;
+ commentList.removeChild(__comment);
+ if(!commentList.firstChild) {
+ $('CommentHolder').innerHTML = "";
+ }
+ }
+ } );
+ },
+
+ onFailure : function(response) {
+ alert(response.responseText);
+ }
+ });
+
+ return false;
+ },
+
+ /**
+ * Ajax handler of spam reporting
+ */
+ reportSpam: function() {
+ var __comment = this.parentNode.parentNode.parentNode;
+
+ __comment.getElementsByTagName('span')[0].innerHTML = "Reporting spam...";
+
+
+ new Ajax.Request(this.href + '?ajax=1', {
+ onSuccess : function(response) {
+ if(response.responseText != '') {
+ // Load the response into the
+ __comment.innerHTML = response.responseText;
+ Behaviour.apply(__comment);
+
+ // Flash it using Scriptaculous
+ new Effect.Highlight(__comment, { endcolor: '#cc9999' } );
+
+ __comment.className = 'spam';
+ } else {
+ new Effect.Highlight(__comment, {
+ startcolor: '#cc9999' , endcolor: '#e9e9e9', duration: 0.5,
+ afterFinish : function() {
+ var commentList = __comment.parentNode;
+ commentList.removeChild(__comment);
+ if(!commentList.firstChild) {
+ $('CommentHolder').innerHTML = "";
+ }
+ }
+ } );
+ }
+ },
+
+ onFailure : function(response) {
+ alert(response.responseText);
+ }
+ });
+
+ return false;
+ },
+
+ /**
+ * Ajax handler of ham reporting
+ */
+ reportHam: function() {
+ var __comment = this.parentNode.parentNode.parentNode;
+
+ __comment.getElementsByTagName('span')[0].innerHTML = "Reporting as not spam...";
+
+ new Ajax.Request(this.href + '?ajax=1', {
+ onSuccess : function(response) {
+ // Load the response into the
+ __comment.innerHTML = response.responseText;
+ Behaviour.apply(__comment);
+
+ // Flash it using Scriptaculous
+ new Effect.Highlight(__comment, { endcolor: '#e9e9e9' } );
+ __comment.className = 'notspam';
+ },
+
+ onFailure : function(response) {
+ alert(response.responseText);
+ }
+ });
+
+ return false;
+ }
+}
+
+PageCommentInterface.applyTo("#PageComments_holder");
diff --git a/javascript/ReportAdmin_left.js b/javascript/ReportAdmin_left.js
new file mode 100755
index 00000000..a33da255
--- /dev/null
+++ b/javascript/ReportAdmin_left.js
@@ -0,0 +1,54 @@
+if(typeof SiteTreeHandlers == 'undefined') SiteTreeHandlers = {};
+
+SiteTree.prototype = {
+ castAsTreeNode: function(li) {
+ behaveAs(li, SiteTreeNode, this.options);
+ },
+
+ getIdxOf : function(treeNode) {
+ if(treeNode && treeNode.id)
+ return treeNode.id;
+ },
+
+ getTreeNodeByIdx : function(idx) {
+ if(!idx) idx = "0";
+ return document.getElementById(idx);
+ },
+
+ initialise: function() {
+ this.observeMethod('SelectionChanged', this.changeCurrentTo);
+ }
+
+};
+
+SiteTreeNode.prototype.onselect = function() {
+ $('sitetree').changeCurrentTo(this);
+ if($('sitetree').notify('SelectionChanged', this)) {
+ this.getPageFromServer();
+ }
+ return false;
+};
+
+SiteTreeNode.prototype.getPageFromServer = function() {
+ if(this.id)
+ $('Form_EditForm').getPageFromServer(this.id);
+};
+
+function reloadSiteTree() {
+
+ new Ajax.Request( 'admin/report/getsitetree', {
+ method: get,
+ onSuccess: function( response ) {
+ $('sitetree_holder').innerHTML = response.responseText;
+ },
+ onFailure: function( response ) {
+
+ }
+ });
+}
+
+appendLoader(function () {
+ if(typeof($('sitetree').selected) == 'undefined');
+ var selectedNode = $('sitetree').getElementsByTagName('li')[1];
+ selectedNode.onselect();
+});
\ No newline at end of file
diff --git a/javascript/ReportAdmin_right.js b/javascript/ReportAdmin_right.js
new file mode 100755
index 00000000..d8079cc3
--- /dev/null
+++ b/javascript/ReportAdmin_right.js
@@ -0,0 +1,118 @@
+
+Behaviour.register({
+ '#Form_EditForm' : {
+ initialise : function() {
+ this.openTab = null;
+ this.prepareForm();
+ },
+
+ /**
+ * Processing called whenever a page is loaded in the right - including the initial one
+ */
+ prepareForm : function() {
+ ajaxActionsAtTop('Form_EditForm', 'form_actions', 'right');
+ },
+
+ /**
+ * Request a page from the server via Ajax
+ */
+ getPageFromServer : function(id) {
+ if(id) {
+ this.receivingID = id;
+
+ // Treenode might not exist if that part of the tree is closed
+ var treeNode = $('sitetree').getTreeNodeByIdx(id);
+
+ if(treeNode) treeNode.addNodeClass('loading');
+
+ statusMessage("loading...");
+
+ var requestURL = 'admin/reports/showreport/' + id;
+ new Ajax.Request(requestURL, {
+ asynchronous : true,
+ method : 'post',
+ postBody : 'ajax=1',
+ onSuccess : this.successfullyReceivedPage.bind(this),
+ onFailure : function(response) {
+ errorMessage('error loading page',response);
+ }
+ });
+ } else {
+ throw("getPageFromServer: Bad page ID: " + id);
+ }
+ },
+
+ successfullyReceivedPage : function(response) {
+ this.loadNewPage(response.responseText);
+
+ // Treenode might not exist if that part of the tree is closed
+ var treeNode = $('sitetree').getTreeNodeByIdx(this.receivingID);
+ if(treeNode) {
+ $('sitetree').changeCurrentTo(treeNode);
+ treeNode.removeNodeClass('loading');
+ }
+ statusMessage('');
+
+ onload_init_tabstrip();
+
+ if( this.openTab ) {
+ openTab( this.openTab );
+ this.openTab = null;
+ }
+ },
+
+ didntReceivePage : function(response) {
+ errorMessage('error loading page', response);
+ $('sitetree').getTreeNodeByIdx(this.elements.ID.value).removeNodeClass('loading');
+ },
+
+ /**
+ * Load a new page into the right-hand form
+ */
+ loadNewPage : function(formContent) {
+ rightHTML = formContent;
+ rightHTML = rightHTML.replace(/href *= *"#/g, 'href="' + window.location.href.replace(/#.*$/,'') + '#');
+
+ // Prepare iframes for removal, otherwise we get loading bugs
+ var i, allIframes = this.getElementsByTagName('iframe');
+ if(allIframes) for(i=0;i 0) {
+ $('left').show();
+ var totalHeight = getFittingHeight(this.tabs[0].linkedPane, 0, otherPanes);
+ var eachHeight = totalHeight / numOpenPanes;
+ for(i=0;i -10);
+ this.holder = this.parentNode;
+ this.linkedPane.style.display = this.selected ? '' : 'none';
+ },
+
+ destroy: function() {
+ this.holder = null;
+ this.linkedPane = null;
+ this.onclick = null;
+ },
+
+ /**
+ * Handler for click
+ */
+ onclick: function(event) {
+ Event.stop(event);
+ this.toggle();
+ },
+ toggle: function() {
+ if(this.selected) this.close();
+ else this.open();
+ },
+ open: function() {
+ if(!this.selected) {
+ this.selected = true;
+ Element.addClassName(this,'selected');
+ this.linkedPane.style.display = '';
+ this.holder.resize();
+ if(this.linkedPane.onshow) {
+ this.linkedPane.onshow();
+ }
+ }
+ },
+ close: function() {
+ if(this.selected) {
+ this.selected = false;
+ Element.removeClassName(this,'selected');
+ this.linkedPane.style.display = 'none';
+ this.holder.resize();
+ if(this.holder.onhide) this.holder.onhide();
+ if(this.linkedPane.onclose) this.linkedPane.onclose();
+ }
+ }
+}
+
+// Application order is important - the Items must be created before the SideTabs object.
+SideTabItem.applyTo('#SideTabs li');
+SideTabs.applyTo('#SideTabs');
+
+/**
+ * Generic base class for all side panels
+ */
+SidePanel = Class.create();
+SidePanel.prototype = {
+ initialize : function() {
+ this.body = this.getElementsByTagName('div')[0];
+ },
+ destroy: function() {
+ this.body = null;
+ },
+ onshow : function() {
+ this.onresize();
+ this.body.innerHTML = 'loading...
';
+ this.ajaxGetPanel(this.afterPanelLoaded);
+ },
+ onresize : function() {
+ fitToParent(this.body);
+ },
+ ajaxGetPanel : function(onComplete) {
+ fitToParent(this.body);
+ new Ajax.Updater(this.body, this.ajaxURL(), {
+ onComplete : onComplete ? onComplete.bind(this) : null,
+ onFailure : this.ajaxPanelError
+ });
+ },
+
+ ajaxPanelError : function (response) {
+ errorMessage("error getting side panel", response);
+ },
+
+ ajaxURL : function() {
+ var srcName = this.id.replace('_holder','');
+ var id = $('Form_EditForm').elements.ID;
+ if(id) id = id.value; else id = "";
+ return 'admin/' + srcName + '/' + id + '?ajax=1';
+ },
+
+ afterPanelLoaded : function() {
+ },
+ onpagechanged : function() {
+ }
+}
+
+SidePanelRecord = Class.create();
+SidePanelRecord.prototype = {
+ onclick : function(event) {
+ Event.stop(event);
+ $('Form_EditForm').getPageFromServer(this.getID());
+ },
+ destroy: function() {
+ this.onclick = null;
+ },
+ getID : function() {
+ if(this.href.match(/\/([^\/]+)$/)) return parseInt(RegExp.$1);
+ }
+}
+
+
+/**
+ * Class that will add an 'over' class when the mouse is over the object
+ */
+Highlighter = Class.create();
+Highlighter.prototype = {
+ onmouseover: function() {
+ Element.addClassName(this,'over');
+ },
+ onmouseout: function() {
+ Element.removeClassName(this,'over');
+ },
+ destroy: function() {
+ this.onmouseover = null;
+ this.onmouseout = null;
+ },
+ select: function(dontHide) {
+ if(dontHide) {
+ Element.addClassName(this,'current');
+ this.parentNode.lastSelected = null;
+
+ } else {
+ if(this.parentNode.lastSelected) {
+ Element.removeClassName(this.parentNode.lastSelected,'current');
+ Element.addClassName(this,'current');
+
+ } else {
+ var i,item;
+ for(i=0;item=this.parentNode.childNodes[i];i++) {
+ if(item.tagName) {
+ if(item == this) Element.addClassName(item,'current');
+ else Element.removeClassName(item,'current');
+ }
+ }
+ }
+ this.parentNode.lastSelected = this;
+ }
+ }
+}
+
+
+/**
+ * Version list
+ */
+VersionList = Class.extend('SidePanel');
+VersionList.prototype = {
+ initialize : function() {
+ this.mode = 'view';
+ this.SidePanel.initialize();
+ },
+ destroy: function() {
+ this.SidePanel = null;
+ this.onclose = null;
+ },
+
+ ajaxURL : function() {
+ return this.SidePanel.ajaxURL() + '&unpublished=' + ($('versions_showall').checked ? 1 : 0);
+ },
+ afterPanelLoaded : function() {
+ this.idLoaded = $('Form_EditForm').elements.ID.value;
+ VersionItem.applyTo('#' + this.id + ' tbody tr');
+ },
+
+ select : function(pageID, versionID, sourceEl) {
+ if(this.mode == 'view') {
+ sourceEl.select();
+ $('Form_EditForm').loadURLFromServer('admin/getversion/' + pageID + '/' + versionID);
+ $('viewArchivedSite').style.display = '';
+ $('viewArchivedSite').getVars = '?archiveDate=' + sourceEl.getElementsByTagName('td')[1].className;
+
+ } else {
+ $('viewArchivedSite').style.display = 'none';
+
+ if(this.otherVersionID) {
+ sourceEl.select();
+ this.otherSourceEl.select(true);
+ statusMessage('Loading comparison...');
+ $('Form_EditForm').loadURLFromServer('admin/compareversions/' + pageID + '/?From=' + this.otherVersionID + '&To=' + versionID);
+ } else {
+ sourceEl.select();
+ }
+ }
+ this.otherVersionID = versionID;
+ this.otherSourceEl = sourceEl;
+ },
+ onpagechanged : function() {
+ if(this.idLoaded != $('Form_EditForm').elements.ID.value) {
+ this.body.innerHTML = 'loading...
';
+ this.ajaxGetPanel(this.afterPanelLoaded);
+ }
+ },
+ refresh : function() {
+ this.ajaxGetPanel(this.afterPanelLoaded);
+ },
+ onclose : function() {
+ $('Form_EditForm').getPageFromServer(this.idLoaded);
+ $('viewArchivedSite').style.display = 'none';
+ }
+}
+
+VersionItem = Class.extend('Highlighter');
+VersionItem.prototype = {
+ initialize : function() {
+ this.holder = $('versions_holder');
+ },
+
+ destroy: function() {
+ this.holder = null;
+ this.onclick = null;
+ },
+
+ onclick : function() {
+ this.holder.select(this.pageID(), this.versionID(), this);
+ },
+ idPair : function() {
+ if(this.id.match(/page-([^-]+)-version-([^-]+)$/)) {
+ return RegExp.$1 + '/' + RegExp.$2;
+ }
+ },
+ pageID : function() {
+ if(this.id.match(/page-([^-]+)-version-([^-]+)$/)) {
+ return RegExp.$1;
+ }
+ },
+ versionID : function() {
+ if(this.id.match(/page-([^-]+)-version-([^-]+)$/)) {
+ return RegExp.$2;
+ }
+ }
+}
+
+VersionAction = Class.create();
+VersionAction.prototype = {
+ initialize: function() {
+ this.holder = $('versions_holder');
+ this.selector = this.getElementsByTagName('select')[0];
+ this.selector.holder = this;
+ this.selector.onchange = function() { this.holder.changed(this.value,this); return false; }
+
+ this.showallCheckbox = $('versions_showall');
+ this.showallCheckbox.holder = this;
+ this.showallCheckbox.onclick = this.showall_change;
+ },
+
+ showall_change: function() {
+ this.holder.holder.refresh();
+ },
+
+ destroy: function() {
+ if(this.selector) {
+ this.selector.holder = null;
+ this.selector.onchange = null;
+ }
+ if(this.showallCheckbox) {
+ this.showallCheckbox.holder = null;
+ this.showallCheckbox.onchange = null;
+ }
+ this.holder = null;
+ this.selector = null;
+ },
+
+ /**
+ * Handler function whenever an action is clicked
+ */
+ changed: function(url, selector) {
+ switch(url) {
+ case 'view': return this.view();
+ case 'compare': return this.compare();
+ default: errorMessage("VersionAction.changed: I don't know about action '" + url + "'");
+ }
+ },
+
+ view: function() {
+ this.holder.mode = 'view';
+ },
+
+ compare: function() {
+ this.holder.mode = 'compare';
+ }
+}
+
+
+VersionItem.applyTo('#versions_holder tbody tr');
+VersionAction.applyTo('#versions_holder p.pane_actions');
+VersionList.applyTo('#versions_holder');
+
diff --git a/javascript/TaskList.js b/javascript/TaskList.js
new file mode 100755
index 00000000..e27dc6a0
--- /dev/null
+++ b/javascript/TaskList.js
@@ -0,0 +1,21 @@
+TaskList = Class.extend('SidePanel');
+TaskList.prototype = {
+ destroy: function() {
+ if(this.SidePanel) this.SidePanel.destroy();
+ this.SidePanel = null;
+ },
+ /**
+ * Called after the panel has been ajax-loaded in
+ */
+ afterPanelLoaded : function() {
+ TaskListRecord.applyTo('#' + this.id + ' a');
+ }
+}
+
+TaskListRecord = SidePanelRecord;
+
+TaskListRecord.applyTo('#tasklist_holder a');
+TaskList.applyTo('#tasklist_holder');
+
+TaskListRecord.applyTo('#waiting_holder a');
+TaskList.applyTo('#waitingon_holder');
diff --git a/javascript/ThumbnailStripField.js b/javascript/ThumbnailStripField.js
new file mode 100755
index 00000000..5adc9c0f
--- /dev/null
+++ b/javascript/ThumbnailStripField.js
@@ -0,0 +1,56 @@
+ThumbnailStripField = Class.create();
+// We do this instead of div.thumbnailstrip for efficiency. It means that ThumbnailStripField can only be used in the
+// CMS toolbar
+ThumbnailStripField.applyTo('#Image');
+ThumbnailStripField.applyTo('#Flash');
+ThumbnailStripField.prototype = {
+
+ /**
+ * @var updateMethod string Specifies the Ajax-call for getting files
+ * (currently either "getimages" or "getflash"). This can be specified
+ * in the PHP-constructor of ThumbnailStripField and is passed to the client
+ * as a fake css-class.
+ */
+ updateMethod: "getimages",
+
+ initialize: function() {
+ try {
+ this.updateMethod = this.className.match(/updatemethod=([^ ]+)/)[1];
+ } catch(err) {}
+
+ if(this.className.match(/parent=([^ ]+)/)) {
+ // HACK: This is hard-coded to only work with TreeDropdownFields
+ var parentField = $(RegExp.$1).parentNode;
+ if(parentField) {
+ parentField.observeMethod('Change', this.ajaxGetFiles.bind(this));
+ }
+ }
+ },
+
+ ajaxGetFiles: function(folderID) {
+ this.innerHTML = 'Loading... '
+ var ajaxURL = this.helperURLBase() + '&methodName='+this.updateMethod+'&folderID=' + folderID;
+ new Ajax.Updater(this, ajaxURL, {
+ method : 'get',
+ onComplete : this.reapplyBehaviour.bind(this),
+ onFailure : function(response) { errorMessage("Error getting files", response); }
+ })
+ },
+
+ reapplyBehaviour: function() {
+ Behaviour.apply(this);
+ },
+
+ helperURLBase: function() {
+ var fieldName = this.id; //this.id.replace(this.ownerForm().name + '_','');
+
+ return this.ownerForm().action + '&action_callfieldmethod=1&fieldName=' + fieldName + '&ajax=1'
+ },
+
+ ownerForm: function() {
+ var f =this.parentNode;
+ while(f && f.tagName.toLowerCase() != 'form') f = f.parentNode;
+ return f;
+ }
+
+}
\ No newline at end of file
diff --git a/javascript/dialog.js b/javascript/dialog.js
new file mode 100644
index 00000000..746def9b
--- /dev/null
+++ b/javascript/dialog.js
@@ -0,0 +1,10 @@
+Behaviour.register({
+ '.buttons button' : {
+ onclick: function() {
+ window.top._OPEN_DIALOG.execHandler( this.name );
+
+ return false;
+ }
+
+ }
+});
\ No newline at end of file
diff --git a/javascript/tinymce.template.js b/javascript/tinymce.template.js
new file mode 100755
index 00000000..e726e967
--- /dev/null
+++ b/javascript/tinymce.template.js
@@ -0,0 +1,42 @@
+function nullConverter(url) {
+ return url;
+}
+
+/**
+ * TinyMCE initialisation template.
+ * $ variables are replaced by string search & replace. It's pretty crude.
+ */
+tinyMCE.init({
+ mode : "specific_textareas",
+ textarea_trigger : "tinymce",
+ width: -1,
+ height: -1,
+ auto_resize : true,
+ theme : "advanced",
+ content_css : "$ContentCSS",
+ document_base_url: "$BaseURL",
+ urlconverter_callback : "nullConverter",
+
+ setupcontent_callback : "sapphiremce_setupcontent",
+ cleanup_callback : "sapphiremce_cleanup",
+
+ Theme_Advanced_Layout_manager : "SimpleLayout",
+ theme_advanced_toolbar_location : "manually_placed",
+ theme_advanced_toolbar_align : "left",
+ theme_advanced_toolbar_parent : "right",
+ plugins : "contextmenu,table,emotions,flash",
+ table_inline_editing : true,
+ theme_advanced_buttons1 : "italic,underline,strikethrough,separator,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,separator,bullist,numlist,outdent,indent,hr,charmap",
+ theme_advanced_buttons2 : "undo,redo,separator,cut,copy,paste,separator,search,replace,separator,flash",
+ theme_advanced_buttons3 : "",
+ theme_advanced_buttons3_add : "emotions",
+/*
+ theme_advanced_buttons1 : "bold,italic,underline,strikethrough,separator,justifyleft,justifycenter,justifyright,justifyfull,styleselect,formatselect,separator,bullist,numlist,outdent,indent,hr,charmap",
+ theme_advanced_buttons2 : "undo,redo,separator,cut,copy,paste,separator,search,replace,separator,link,unlink,anchor,image,separator,cleanup,removeformat,visualaid,code,separator,tablecontrols",
+*/
+ safari_warning : false,
+ relative_urls : true,
+ verify_html : true,
+ valid_elements : "+a[id|rel|rev|dir|tabindex|accesskey|type|name|href|target|title|class],-strong/-b[class],-em/-i[class],-strike[class],-u[class],#p[id|dir|class|align],-ol[class],-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align],-sub[class],-sup[class],-blockquote[dir|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|dir|id|style],-tr[id|dir|class|rowspan|width|height|align|valign|bgcolor|background|bordercolor|style],tbody[id|class|style],thead[id|class|style],tfoot[id|class|style],-td[id|dir|class|colspan|rowspan|width|height|align|valign|scope|style],-th[id|dir|class|colspan|rowspan|width|height|align|valign|scope|style],caption[id|dir|class],-div[id|dir|class|align],-span[class|align],-pre[class|align],address[class|align],-h1[id|dir|class|align],-h2[id|dir|class|align],-h3[id|dir|class|align],-h4[id|dir|class|align],-h5[id|dir|class|align],-h6[id|dir|class|align],hr[class],dd[id|class|title|dir],dl[id|class|title|dir],dt[id|class|title|dir]",
+ extended_valid_elements : "img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name]"
+});
diff --git a/silverstripe_version b/silverstripe_version
new file mode 100644
index 00000000..03d27cc0
--- /dev/null
+++ b/silverstripe_version
@@ -0,0 +1 @@
+$URL$
\ No newline at end of file
diff --git a/templates/AssetAdmin_uploadiframe.ss b/templates/AssetAdmin_uploadiframe.ss
new file mode 100755
index 00000000..1f47bec1
--- /dev/null
+++ b/templates/AssetAdmin_uploadiframe.ss
@@ -0,0 +1,24 @@
+
+
+
+<% base_tag %>
+
+
+
+<% if CanUpload %>
+$UploadForm
+<% else %>
+You do not have permission to upload files into this folder.
+<% end_if %>
+
\ No newline at end of file
diff --git a/templates/BulkLoaderAdmin_iframe.ss b/templates/BulkLoaderAdmin_iframe.ss
new file mode 100755
index 00000000..40443e04
--- /dev/null
+++ b/templates/BulkLoaderAdmin_iframe.ss
@@ -0,0 +1 @@
+$EditForm
\ No newline at end of file
diff --git a/templates/BulkLoaderAdmin_preview.ss b/templates/BulkLoaderAdmin_preview.ss
new file mode 100755
index 00000000..3a992d53
--- /dev/null
+++ b/templates/BulkLoaderAdmin_preview.ss
@@ -0,0 +1,9 @@
+$Message
+
+Results
+
+<% control Results %>
+list changes here
+<% end_control %>
+
+$ConfirmForm
\ No newline at end of file
diff --git a/templates/CMSMain_dialog.ss b/templates/CMSMain_dialog.ss
new file mode 100755
index 00000000..77de3c71
--- /dev/null
+++ b/templates/CMSMain_dialog.ss
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+$Message
+
+
+<% control Buttons %>
+
+<% end_control %>
+
+
+
+
\ No newline at end of file
diff --git a/templates/CMSMain_versions.ss b/templates/CMSMain_versions.ss
new file mode 100755
index 00000000..b5d4dc11
--- /dev/null
+++ b/templates/CMSMain_versions.ss
@@ -0,0 +1,30 @@
+
+
+
+ #
+ When
+ Author
+ Publisher
+
+
+
+ <% control Versions %>
+
+ $Version
+ $LastEdited.Nice
+ $Author.FirstName $Author.Surname.Initial
+
+ <% if Published %>
+ <% if Publisher %>
+ $Publisher.FirstName $Publisher.Surname.Initial
+ <% else %>
+ Unknown
+ <% end_if %>
+ <% else %>
+ Not published
+ <% end_if %>
+
+
+ <% end_control %>
+
+
\ No newline at end of file
diff --git a/templates/CommentList.ss b/templates/CommentList.ss
new file mode 100755
index 00000000..28b1b48e
--- /dev/null
+++ b/templates/CommentList.ss
@@ -0,0 +1,14 @@
+<% if Comments %>
+
+<% else %>
+ There are no comments on this page.
+ Comments are created whenever one of the 'workflow actions'
+ are undertaken - Publish, Reject, Submit.
+<% end_if %>
\ No newline at end of file
diff --git a/templates/Dialog.ss b/templates/Dialog.ss
new file mode 100644
index 00000000..cf22f564
--- /dev/null
+++ b/templates/Dialog.ss
@@ -0,0 +1,14 @@
+
+
+
+
+<% base_tag %>
+$Title
+
+
+ $Message
+
+ $Buttons
+
+
+
\ No newline at end of file
diff --git a/templates/Includes/AssetAdmin_left.ss b/templates/Includes/AssetAdmin_left.ss
new file mode 100755
index 00000000..ccea5f39
--- /dev/null
+++ b/templates/Includes/AssetAdmin_left.ss
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $SiteTreeAsUL
+
+
\ No newline at end of file
diff --git a/templates/Includes/AssetAdmin_right.ss b/templates/Includes/AssetAdmin_right.ss
new file mode 100755
index 00000000..1ea052bb
--- /dev/null
+++ b/templates/Includes/AssetAdmin_right.ss
@@ -0,0 +1,34 @@
+
+
+
+
+
+$MoveMarkedOptionsForm
+$DeleteMarkedOptionsForm
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+<% end_if %>
+
+
diff --git a/templates/Includes/AssetTableField.ss b/templates/Includes/AssetTableField.ss
new file mode 100755
index 00000000..4e377533
--- /dev/null
+++ b/templates/Includes/AssetTableField.ss
@@ -0,0 +1,27 @@
+
+ <% include TableListField_PageControls %>
+
+
+
+ <% if Markable %> <% end_if %>
+ <% control Headings %>
+ $Title
+ <% end_control %>
+
+
+
+
+ <% control Items %>
+
+ <% if Markable %>$MarkingCheckbox <% end_if %>
+ <% control Fields %>
+ $Value
+ <% end_control %>
+
+
+
+
+ <% end_control %>
+
+
+
\ No newline at end of file
diff --git a/templates/Includes/BulkLoaderAdmin_left.ss b/templates/Includes/BulkLoaderAdmin_left.ss
new file mode 100755
index 00000000..63261c05
--- /dev/null
+++ b/templates/Includes/BulkLoaderAdmin_left.ss
@@ -0,0 +1,20 @@
+
+
+
+
+ <% if BulkLoaders %>
+
+ <% end_if %>
+
+
diff --git a/templates/Includes/BulkLoaderAdmin_right.ss b/templates/Includes/BulkLoaderAdmin_right.ss
new file mode 100755
index 00000000..cbc6eadd
--- /dev/null
+++ b/templates/Includes/BulkLoaderAdmin_right.ss
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/templates/Includes/CMSLeft.ss b/templates/Includes/CMSLeft.ss
new file mode 100755
index 00000000..62beb76f
--- /dev/null
+++ b/templates/Includes/CMSLeft.ss
@@ -0,0 +1,28 @@
+ Site Content
+
+
+
+
+
+
+
+
+ $SiteTreeAsUL
+
\ No newline at end of file
diff --git a/templates/Includes/CMSMain_left.ss b/templates/Includes/CMSMain_left.ss
new file mode 100755
index 00000000..df580e02
--- /dev/null
+++ b/templates/Includes/CMSMain_left.ss
@@ -0,0 +1,130 @@
+
+
+
+ Site Map
+
+ <% if EnterpriseCMS %>
+ Task List
+ Waiting on
+ <% end_if %>
+ Versions
+ <% if EnterpriseCMS %>
+
+ <% end_if %>
+ Reports
+
+
+
+
+
+
+
+
+ <% control AddPageOptionsForm %>
+
+ <% end_control %>
+
+
+
+
+
+ <% control DuplicatePagesOptionsForm %>
+
+ <% end_control %>
+
+
+ Key:
+ new
+ deleted
+ changed
+
+
+
+ $SiteTreeAsUL
+
+
+
+ <% if EnterpriseCMS %>
+
+
+ <% end_if %>
+
+
History
+
+
+
+ View (click to see)
+ Compare (click 2 to see)
+
+
+
+ Show unpublished versions
+
+
+
+
+
+ <% if EnterpriseCMS %>
+
+ <% end_if %>
+
+
Reports
+
$ReportSelector
+
+
+
+
\ No newline at end of file
diff --git a/templates/Includes/CMSMain_right.ss b/templates/Includes/CMSMain_right.ss
new file mode 100755
index 00000000..efc0a3c6
--- /dev/null
+++ b/templates/Includes/CMSMain_right.ss
@@ -0,0 +1,38 @@
+
+<% include Editor_toolbar %>
+
+
+
+
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+ $ApplicationName
+
+ Welcome to $ApplicationName! Please choose a page from the left.
+
+<% end_if %>
+
+
diff --git a/templates/Includes/CMSMain_rightbottom.ss b/templates/Includes/CMSMain_rightbottom.ss
new file mode 100644
index 00000000..e69de29b
diff --git a/templates/Includes/CMSRight.ss b/templates/Includes/CMSRight.ss
new file mode 100755
index 00000000..2d3df14d
--- /dev/null
+++ b/templates/Includes/CMSRight.ss
@@ -0,0 +1,21 @@
+Edit Content
+
+
+
+
+
+
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+ $ApplicationName
+
+
+
+
+ Welcome to $ApplicationName! Please choose a page from the left.
+
+<% end_if %>
+
\ No newline at end of file
diff --git a/templates/Includes/Editor_toolbar.ss b/templates/Includes/Editor_toolbar.ss
new file mode 100755
index 00000000..834556c8
--- /dev/null
+++ b/templates/Includes/Editor_toolbar.ss
@@ -0,0 +1,41 @@
+<% control EditorToolbar %>
+
+
+$LinkForm
+
+$ImageForm
+
+$FlashForm
+
+<% end_control %>
\ No newline at end of file
diff --git a/templates/Includes/GenericDataAdmin_left.ss b/templates/Includes/GenericDataAdmin_left.ss
new file mode 100755
index 00000000..79de2a0b
--- /dev/null
+++ b/templates/Includes/GenericDataAdmin_left.ss
@@ -0,0 +1,28 @@
+
+
+
+
Add Listing
+
+ $AddForm
+
+
Search Listings
+
+ $SearchForm
+
+
+
+
+
Search Results
+
+ $Results
+
+
\ No newline at end of file
diff --git a/templates/Includes/GenericDataAdmin_right.ss b/templates/Includes/GenericDataAdmin_right.ss
new file mode 100755
index 00000000..fabbd046
--- /dev/null
+++ b/templates/Includes/GenericDataAdmin_right.ss
@@ -0,0 +1,16 @@
+
+<% include Editor_toolbar %>
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+ $ApplicationName
+
+ Welcome to $ApplicationName! Please choose click on one of the entries on the left pane.
+
+
+<% end_if %>
+
+
+
diff --git a/templates/Includes/LeftAndMain_rightbottom.ss b/templates/Includes/LeftAndMain_rightbottom.ss
new file mode 100644
index 00000000..e69de29b
diff --git a/templates/Includes/MemberList_PageControls.ss b/templates/Includes/MemberList_PageControls.ss
new file mode 100755
index 00000000..c880ffdf
--- /dev/null
+++ b/templates/Includes/MemberList_PageControls.ss
@@ -0,0 +1,15 @@
+
+
+ <% if LastLink %>
+ <% else %>
<% end_if %>
+ <% if FirstLink %>
+ <% else %>
<% end_if %>
+ <% if PrevLink %>
+ <% else %>
<% end_if %>
+
+ Displaying $FirstMember to $LastMember of $TotalMembers
+
+ <% if NextLink %>
+ <% else %>
<% end_if %>
+
+
diff --git a/templates/Includes/MemberList_Table.ss b/templates/Includes/MemberList_Table.ss
new file mode 100755
index 00000000..fd1db40b
--- /dev/null
+++ b/templates/Includes/MemberList_Table.ss
@@ -0,0 +1,42 @@
+<% include MemberList_PageControls %>
+
+
+
+ First Name
+ Surname
+ Email Address
+ <% if DontShowPassword %>
+ <% else %>
+ Password
+ <% end_if %>
+
+
+
+
+
+
+ <% if DontShowPassword %>
+ <% control Members %>
+
+ $FirstName
+ $Surname
+ $Email
+
+
+ <% end_control %>
+ <% else %>
+ <% control Members %>
+
+ $FirstName
+ $Surname
+ $Email
+ $Password
+
+
+ <% end_control %>
+ <% end_if %>
+
+
+ $AddRecordForm.AsTableRow
+
+
\ No newline at end of file
diff --git a/templates/Includes/MemberTableField.ss b/templates/Includes/MemberTableField.ss
new file mode 100755
index 00000000..65182c26
--- /dev/null
+++ b/templates/Includes/MemberTableField.ss
@@ -0,0 +1,77 @@
+
+
+ $SearchForm
+
+
+ <% include TableListField_PageControls %>
+
+
+
+ <% if Markable %> <% end_if %>
+ <% control Headings %>
+ $Title
+ <% end_control %>
+ <% if Can(show) %> <% end_if %>
+ <% if Can(edit) %> <% end_if %>
+ <% if Can(delete) %> <% end_if %>
+
+
+
+
+ <% if Markable %> <% end_if %>
+ $AddRecordForm.CellFields
+ $AddRecordForm.CellActions
+
+
+ <% if Markable %> <% end_if %>
+
+
+
+ <% if Can(show) %> <% end_if %>
+ <% if Can(edit) %> <% end_if %>
+ <% if Can(delete) %> <% end_if %>
+
+
+
+ <% if Items %>
+ <% control Items %>
+
+ <% if Markable %>$MarkingCheckbox <% end_if %>
+ <% control Fields %>
+ $Value
+ <% end_control %>
+ <% if Can(show) %>
+
+
+
+ <% end_if %>
+ <% if Can(edit) %>
+
+
+
+ <% end_if %>
+ <% if Can(delete) %>
+
+
+
+ <% end_if %>
+
+ <% end_control %>
+ <% else %>
+
+ <% if Markable %> <% end_if %>
+ No $NamePlural found
+ <% if Can(show) %> <% end_if %>
+ <% if Can(edit) %> <% end_if %>
+ <% if Can(delete) %> <% end_if %>
+
+ <% end_if %>
+
+
+
+ <% if Can(export) %>
+ $ExportButton
+ <% end_if %>
+
+
+
\ No newline at end of file
diff --git a/templates/Includes/NewsletterAdmin_BouncedList.ss b/templates/Includes/NewsletterAdmin_BouncedList.ss
new file mode 100755
index 00000000..38199653
--- /dev/null
+++ b/templates/Includes/NewsletterAdmin_BouncedList.ss
@@ -0,0 +1,24 @@
+<% if Entries %>
+
+
+
+ User name
+ Email address
+ Reason:
+ Last bounce at
+
+
+
+ <% control Entries %>
+
+ $Member.FirstName $Member.Surname
+ $Member.Email
+ $Record.BounceMessage
+ $Record.Created.Long
+
+ <% end_control %>
+
+
+<% else %>
+No emails sent have bounced.
+<% end_if %>
diff --git a/templates/Includes/NewsletterAdmin_SiteTree.ss b/templates/Includes/NewsletterAdmin_SiteTree.ss
new file mode 100755
index 00000000..45f7e0cd
--- /dev/null
+++ b/templates/Includes/NewsletterAdmin_SiteTree.ss
@@ -0,0 +1,30 @@
+<% if NewsletterTypes %>
+
+<% end_if %>
diff --git a/templates/Includes/NewsletterAdmin_UnsubscribedList.ss b/templates/Includes/NewsletterAdmin_UnsubscribedList.ss
new file mode 100755
index 00000000..293274d2
--- /dev/null
+++ b/templates/Includes/NewsletterAdmin_UnsubscribedList.ss
@@ -0,0 +1,20 @@
+<% if Entries %>
+
+
+
+ User name
+ Unsubscribed on
+
+ <% control Entries %>
+
+ $Member.FirstName $Member.Surname
+ $Record.Created.Long
+
+ <% end_control %>
+
+
+<% else %>
+
+ No users have unsubscribed from this newsletter.
+
+<% end_if %>
diff --git a/templates/Includes/NewsletterAdmin_left.ss b/templates/Includes/NewsletterAdmin_left.ss
new file mode 100755
index 00000000..82e3f6b3
--- /dev/null
+++ b/templates/Includes/NewsletterAdmin_left.ss
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+ Add new type
+ Add new draft
+
+
+
+
+
+ Select the drafts that you want to delete and then click the button below
+
+
+
+ <% include NewsletterAdmin_SiteTree %>
+
+
diff --git a/templates/Includes/NewsletterAdmin_right.ss b/templates/Includes/NewsletterAdmin_right.ss
new file mode 100755
index 00000000..66428707
--- /dev/null
+++ b/templates/Includes/NewsletterAdmin_right.ss
@@ -0,0 +1,32 @@
+
+
+<% include Editor_toolbar %>
+
+
+
+
+
+
+
+
+
+ $SendProgressBar
+
+
+
+
+
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+ Welcome to the $ApplicationName newsletter admininistration section. Please choose a folder from the left.
+
+<% end_if %>
+
+
diff --git a/templates/Includes/ReportAdmin_SiteTree.ss b/templates/Includes/ReportAdmin_SiteTree.ss
new file mode 100755
index 00000000..0ca6c0f1
--- /dev/null
+++ b/templates/Includes/ReportAdmin_SiteTree.ss
@@ -0,0 +1,14 @@
+<% if Reports %>
+
+
+<% end_if %>
\ No newline at end of file
diff --git a/templates/Includes/ReportAdmin_left.ss b/templates/Includes/ReportAdmin_left.ss
new file mode 100755
index 00000000..3f5799ef
--- /dev/null
+++ b/templates/Includes/ReportAdmin_left.ss
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ <% include ReportAdmin_SiteTree %>
+
+
diff --git a/templates/Includes/ReportAdmin_right.ss b/templates/Includes/ReportAdmin_right.ss
new file mode 100755
index 00000000..ffd861b6
--- /dev/null
+++ b/templates/Includes/ReportAdmin_right.ss
@@ -0,0 +1,11 @@
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+ Welcome to the $ApplicationName reporting section. Please choose a specific report from the left.
+
+<% end_if %>
+
+
diff --git a/templates/Includes/SecurityAdmin_left.ss b/templates/Includes/SecurityAdmin_left.ss
new file mode 100644
index 00000000..000765cd
--- /dev/null
+++ b/templates/Includes/SecurityAdmin_left.ss
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+ Select the pages that you want to delete and then click the button below
+
+
+
+
+
+
+ To reorganise your site, drag the pages around as desired.
+
+
+ $SiteTreeAsUL
+
+
\ No newline at end of file
diff --git a/templates/Includes/SecurityAdmin_right.ss b/templates/Includes/SecurityAdmin_right.ss
new file mode 100644
index 00000000..f5031764
--- /dev/null
+++ b/templates/Includes/SecurityAdmin_right.ss
@@ -0,0 +1,14 @@
+
+
+
+
+
+<% if EditForm %>
+ $EditForm
+<% else %>
+
+ Welcome to the $ApplicationName security admininistration section. Please choose a group from the left.
+
+<% end_if %>
+
+
diff --git a/templates/LeftAndMain.ss b/templates/LeftAndMain.ss
new file mode 100644
index 00000000..1752abe0
--- /dev/null
+++ b/templates/LeftAndMain.ss
@@ -0,0 +1,72 @@
+
+
+
+
+<% base_tag %>
+$ApplicationName
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
+
+ <% control MainMenu %>
+
+ <% end_control %>
+
+
+
+ $Left
+
+
+
+
+
+
+ $Right
+
+
+ <% if RightBottom %>
+
+ $RightBottom
+
+ <% end_if %>
+
+
+
+
+
Silverstripe CMS -
+
$CMSVersion
+ <% control CurrentMember %>
+ Logged in as $FirstName $Surname -
log out
+ <% end_control %>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/LeftAndMain_printable.ss b/templates/LeftAndMain_printable.ss
new file mode 100755
index 00000000..c327d282
--- /dev/null
+++ b/templates/LeftAndMain_printable.ss
@@ -0,0 +1,12 @@
+
+
+
+
+<% base_tag %>
+SilverStripe CMS - $Title
+
+
+
+ $PrintForm
+
+
\ No newline at end of file
diff --git a/templates/MemberList.ss b/templates/MemberList.ss
new file mode 100755
index 00000000..97a05da4
--- /dev/null
+++ b/templates/MemberList.ss
@@ -0,0 +1,17 @@
+
+
+
Filter
+
+
+
+ $SearchField
+ $OrderByField
+ $GroupFilter
+
+
+
+
+ <% include MemberList_Table %>
+
+
+
diff --git a/templates/NewsletterList.ss b/templates/NewsletterList.ss
new file mode 100755
index 00000000..2a282c66
--- /dev/null
+++ b/templates/NewsletterList.ss
@@ -0,0 +1,19 @@
+
+
+
+ Subject
+ Content
+
+
+
+
+ <% control Newsletters %>
+
+ $Subject
+ $Content
+
+ <% end_control %>
+
+
+
+
diff --git a/templates/Newsletter_RecipientImportField.ss b/templates/Newsletter_RecipientImportField.ss
new file mode 100755
index 00000000..5ccb2808
--- /dev/null
+++ b/templates/Newsletter_RecipientImportField.ss
@@ -0,0 +1,48 @@
+
+
+
+ <% base_tag %>
+ $MetaTags
+
+
+
+
+
+
+<% if ImportMessage %>
+
+<% end_if %>
+
+
+<% if ImportMessage %>
+
+ $ImportMessage
+
+ New members imported: $NewMembers
+ Members updated: $ChangedMembers
+ Number of details changed: $ChangedFields
+ Records skipped: $SkippedRecords
+ Time taken: $Time seconds
+
+
+<% end_if %>
+<% if ErrorMessage %>
+
+ $ErrorMessage
+
+<% end_if %>
+ $UploadForm
+
+
\ No newline at end of file
diff --git a/templates/Newsletter_RecipientImportField_Table.ss b/templates/Newsletter_RecipientImportField_Table.ss
new file mode 100755
index 00000000..8c89335d
--- /dev/null
+++ b/templates/Newsletter_RecipientImportField_Table.ss
@@ -0,0 +1,46 @@
+
+
+
+ <% base_tag %>
+ $MetaTags
+
+
+
+
+
+
+
+Contents of $FileName
+
+ <% control CustomSetFields %>
+ $FieldHolder
+ <% end_control %>
+
+
+
+
+
+
+ <% control ColumnHeaders %>
+
+ $Field
+
+ <% end_control %>
+
+ <% control Rows %>
+
+ <% control Cells %>
+ $Value
+ <% end_control %>
+
+ <% end_control %>
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/PageCommentInterface.ss b/templates/PageCommentInterface.ss
new file mode 100755
index 00000000..9f58c81a
--- /dev/null
+++ b/templates/PageCommentInterface.ss
@@ -0,0 +1,45 @@
+
+
diff --git a/templates/PageCommentInterface_singlecomment.ss b/templates/PageCommentInterface_singlecomment.ss
new file mode 100755
index 00000000..7075bec4
--- /dev/null
+++ b/templates/PageCommentInterface_singlecomment.ss
@@ -0,0 +1,16 @@
+
+
+ Posted by $Name, $Created.Nice ($Created.Ago)
+
+
+ <% if DeleteLink %>
+ remove this comment
+ <% end_if %>
+ <% if SpamLink %>
+ this comment is spam
+ <% end_if %>
+ <% if HamLink %>
+ this comment is not spam
+ <% end_if %>
+
+
diff --git a/templates/ReceivedFormSubmission.ss b/templates/ReceivedFormSubmission.ss
new file mode 100755
index 00000000..a8b0d36e
--- /dev/null
+++ b/templates/ReceivedFormSubmission.ss
@@ -0,0 +1 @@
+$OnCompleteMessage
\ No newline at end of file
diff --git a/templates/TaskList.ss b/templates/TaskList.ss
new file mode 100755
index 00000000..1db4bad9
--- /dev/null
+++ b/templates/TaskList.ss
@@ -0,0 +1,10 @@
+$Message
+
+
+ <% control Tasks %>
+
+ $Title
+
+
+ <% end_control %>
+
\ No newline at end of file
diff --git a/templates/ThumbnailStripField.ss b/templates/ThumbnailStripField.ss
new file mode 100755
index 00000000..db4f29c2
--- /dev/null
+++ b/templates/ThumbnailStripField.ss
@@ -0,0 +1,3 @@
+
+ (Choose a folder above)
+
\ No newline at end of file
diff --git a/templates/WaitingOn.ss b/templates/WaitingOn.ss
new file mode 100755
index 00000000..13108eed
--- /dev/null
+++ b/templates/WaitingOn.ss
@@ -0,0 +1,10 @@
+$Message
+
+
+ <% control Tasks %>
+
+ $Title
+
+
+ <% end_control %>
+
\ No newline at end of file
diff --git a/templates/email/SubmittedFormEmail.ss b/templates/email/SubmittedFormEmail.ss
new file mode 100755
index 00000000..0c5df1c5
--- /dev/null
+++ b/templates/email/SubmittedFormEmail.ss
@@ -0,0 +1,20 @@
+
+
+
+
+ $Subject
+
+ The following data was submitted to your website:
+
+ $Body
+
+
+ <% control Fields %>
+
+ $Title
+ $Value
+
+ <% end_control %>
+
+
+
diff --git a/templates/email/ViewArchivedEmail.ss b/templates/email/ViewArchivedEmail.ss
new file mode 100644
index 00000000..33f25b0c
--- /dev/null
+++ b/templates/email/ViewArchivedEmail.ss
@@ -0,0 +1,3 @@
+You have asked to view the content of our site on $ArchiveDate.Date
+
+You can access the archived site at this link: $ArchiveURL
\ No newline at end of file