Skip to content
This repository
Browse code

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
  • Loading branch information...
commit abb9a61d0ddc151151c4df4f45299a2b0002cb0f 0 parents
Hayden authored July 19, 2007

Showing 248 changed files with 18,088 additions and 0 deletions. Show diff stats Hide diff stats

  1. 19  _config.php
  2. 660  code/AssetAdmin.php
  3. 130  code/AssetTableField.php
  4. 67  code/BulkLoader.php
  5. 123  code/BulkLoaderAdmin.php
  6. 17  code/CMSActionOptionsForm.php
  7. 8  code/CMSHelp.php
  8. 1,132  code/CMSMain.php
  9. 795  code/Diff.php
  10. 17  code/FileList.php
  11. 650  code/GenericDataAdmin.php
  12. 66  code/ImprintStats.php
  13. 761  code/LeftAndMain.php
  14. 271  code/MemberList.php
  15. 412  code/MemberTableField.php
  16. 93  code/Newsletter/BatchProcess.php
  17. 46  code/Newsletter/BouncedList.php
  18. 99  code/Newsletter/Newsletter.php
  19. 97  code/Newsletter/NewsletterEmailProcess.php
  20. 57  code/Newsletter/NewsletterList.php
  21. 55  code/Newsletter/NewsletterType.php
  22. 372  code/Newsletter/RecipientImportField.php
  23. 254  code/Newsletter/SubscribeForm.php
  24. 40  code/Newsletter/TemplateList.php
  25. 178  code/Newsletter/Unsubscribe.php
  26. 49  code/Newsletter/UnsubscribedList.php
  27. 686  code/NewsletterAdmin.php
  28. 320  code/PageTypes/UserDefinedForm.php
  29. 129  code/ReportAdmin.php
  30. 334  code/SecurityAdmin.php
  31. 64  code/SideReport.php
  32. 87  code/StaticExporter.php
  33. 133  code/ThumbnailStripField.php
  34. 397  code/sitefeatures/Akismet.php
  35. 144  code/sitefeatures/PageComment.php
  36. 121  code/sitefeatures/PageCommentInterface.php
  37. 26  code/sitefeatures/SSAkismet.php
  38. 236  css/GenericDataAdmin.css
  39. 146  css/Image_iframe.css
  40. 44  css/LeftAndMain_printable.css
  41. 404  css/cms_left.css
  42. 765  css/cms_right.css
  43. 18  css/dialog.css
  44. 433  css/layout.css
  45. 49  css/typography.css
  46. BIN  images/add.gif
  47. BIN  images/alert-bad.gif
  48. BIN  images/alert-good.gif
  49. BIN  images/block.png
  50. BIN  images/bullet_arrow_down.png
  51. BIN  images/bullet_arrow_up.png
  52. BIN  images/button-bg.gif
  53. BIN  images/button-left.gif
  54. BIN  images/button-right.gif
  55. BIN  images/check.png
  56. BIN  images/delete-small.gif
  57. BIN  images/delete.gif
  58. BIN  images/dialogs/alert.gif
  59. BIN  images/dialogs/alert.png
  60. BIN  images/down.gif
  61. BIN  images/edit.gif
  62. BIN  images/loading.gif
  63. BIN  images/locked.gif
  64. BIN  images/mainmenu/content.gif
  65. BIN  images/mainmenu/content.png
  66. BIN  images/mainmenu/emails.gif
  67. BIN  images/mainmenu/files.gif
  68. BIN  images/mainmenu/help.gif
  69. BIN  images/mainmenu/help.png
  70. BIN  images/mainmenu/logo-smallwhite.png
  71. BIN  images/mainmenu/logo.gif
  72. BIN  images/mainmenu/logo.png
  73. BIN  images/mainmenu/members.gif
  74. BIN  images/mainmenu/members.png
  75. BIN  images/mainmenu/mgmt.png
  76. BIN  images/mainmenu/reports.gif
  77. BIN  images/mainmenu/top-bg.gif
  78. BIN  images/network-save-bw.gif
  79. BIN  images/network-save.gif
  80. BIN  images/network-saveOLD.gif
  81. BIN  images/pagination/record-export.png
  82. BIN  images/pagination/record-first-g.png
  83. BIN  images/pagination/record-first.png
  84. BIN  images/pagination/record-last-g.png
  85. BIN  images/pagination/record-last.png
  86. BIN  images/pagination/record-next-g.png
  87. BIN  images/pagination/record-next.png
  88. BIN  images/pagination/record-prev-g.png
  89. BIN  images/pagination/record-prev.png
  90. BIN  images/pagination/record-print.png
  91. BIN  images/panels/EditPage.png
  92. BIN  images/panels/MySite.png
  93. BIN  images/right.gif
  94. BIN  images/show.png
  95. BIN  images/sidetabs/advertisements.gif
  96. BIN  images/sidetabs/advertisements_over.gif
  97. BIN  images/sidetabs/associations.gif
  98. BIN  images/sidetabs/associations_over.gif
  99. BIN  images/sidetabs/categories.gif
  100. BIN  images/sidetabs/categories_over.gif
  101. BIN  images/sidetabs/comments.gif
  102. BIN  images/sidetabs/comments_over.gif
  103. BIN  images/sidetabs/reports.gif
  104. BIN  images/sidetabs/reports_over.gif
  105. BIN  images/sidetabs/search.gif
  106. BIN  images/sidetabs/search_over.gif
  107. BIN  images/sidetabs/sitemap.gif
  108. BIN  images/sidetabs/sitemap_over.gif
  109. BIN  images/sidetabs/tasklist.gif
  110. BIN  images/sidetabs/tasklist_over.gif
  111. BIN  images/sidetabs/template normal.psd
  112. BIN  images/sidetabs/template over.psd
  113. BIN  images/sidetabs/versions.gif
  114. BIN  images/sidetabs/versions_over.gif
  115. BIN  images/sidetabs/waitingon.gif
  116. BIN  images/sidetabs/waitingon_over.gif
  117. BIN  images/tables/checkbox.png
  118. BIN  images/tables/thead.png
  119. BIN  images/textures/ToolBar.png
  120. BIN  images/textures/bottom.png
  121. BIN  images/textures/obar-18.gif
  122. BIN  images/textures/obar-light.png
  123. BIN  images/textures/obar.gif
  124. BIN  images/textures/seperator.png
  125. BIN  images/tickbox-canttick.gif
  126. BIN  images/tickbox-greyticked.gif
  127. BIN  images/tickbox-ticked.gif
  128. BIN  images/tickbox-unticked.gif
  129. BIN  images/treeicons/book-closedfolder.gif
  130. BIN  images/treeicons/book-file.gif
  131. BIN  images/treeicons/book-openfolder.gif
  132. BIN  images/treeicons/brokenlink-closedfolder.gif
  133. BIN  images/treeicons/brokenlink-file.gif
  134. BIN  images/treeicons/brokenlink-openfolder.gif
  135. BIN  images/treeicons/draft-file.png
  136. BIN  images/treeicons/draft-folder.png
  137. BIN  images/treeicons/element-closedfolder.gif
  138. BIN  images/treeicons/element-file.gif
  139. BIN  images/treeicons/element-openfolder.gif
  140. BIN  images/treeicons/folder-closedfolder.gif
  141. BIN  images/treeicons/folder-file.gif
  142. BIN  images/treeicons/folder-openfolder.gif
  143. BIN  images/treeicons/home-file (1).png
  144. BIN  images/treeicons/home-file.png
  145. BIN  images/treeicons/multi-user.gif
  146. BIN  images/treeicons/multi-user.png
  147. BIN  images/treeicons/page-gold-closedfolder.gif
  148. BIN  images/treeicons/page-gold-file.gif
  149. BIN  images/treeicons/page-gold-openfolder.gif
  150. BIN  images/treeicons/page-shortcut-file.gif
  151. BIN  images/treeicons/page-shortcut-gold-file.gif
  152. BIN  images/treeicons/preferences-closedfolder.gif
  153. BIN  images/treeicons/preferences-file.gif
  154. BIN  images/treeicons/preferences-openfolder.gif
  155. BIN  images/treeicons/reports-file.png
  156. BIN  images/treeicons/reports-foldericon.png
  157. BIN  images/treeicons/reports-openfoldericon.png
  158. BIN  images/treeicons/root.png
  159. BIN  images/treeicons/sent-file.gif
  160. BIN  images/treeicons/sent-folder.png
  161. BIN  images/treeicons/task-file.gif
  162. BIN  images/treeicons/user-file.gif
  163. BIN  images/unlocked.gif
  164. BIN  images/unlockedled.gif
  165. BIN  images/workflow/note_edit.png
  166. BIN  images/workflow/note_new.png
  167. BIN  images/workflow/note_view.png
  168. BIN  images/workflow/rubberstamp.png
  169. 280  javascript/AssetAdmin.js
  170. 15  javascript/BulkLoaderAdmin.js
  171. 0  javascript/CMSMain.js
  172. 270  javascript/CMSMain_left.js
  173. 123  javascript/CMSMain_right.js
  174. 13  javascript/CommentList.js
  175. 115  javascript/ForumAdmin.js
  176. 187  javascript/GenericDataAdmin_left.js
  177. 116  javascript/GenericDataAdmin_right.js
  178. 84  javascript/ImprintStats.js
  179. 816  javascript/LeftAndMain.js
  180. 475  javascript/LeftAndMain_left.js
  181. 463  javascript/LeftAndMain_right.js
  182. 322  javascript/MemberList.js
  183. 298  javascript/MemberTableField.js
  184. 42  javascript/MemberTableField_popup.js
  185. 404  javascript/NewsletterAdmin_left.js
  186. 536  javascript/NewsletterAdmin_right.js
  187. 172  javascript/NewsletterMemberList.js
  188. 13  javascript/Newsletter_UploadForm.js
  189. 186  javascript/PageCommentInterface.js
  190. 54  javascript/ReportAdmin_left.js
  191. 118  javascript/ReportAdmin_right.js
  192. 147  javascript/SecurityAdmin_left.js
  193. 11  javascript/SecurityAdmin_right.js
  194. 70  javascript/SideReports.js
  195. 358  javascript/SideTabs.js
  196. 21  javascript/TaskList.js
  197. 56  javascript/ThumbnailStripField.js
  198. 10  javascript/dialog.js
  199. 42  javascript/tinymce.template.js
  200. 1  silverstripe_version
  201. 24  templates/AssetAdmin_uploadiframe.ss
  202. 1  templates/BulkLoaderAdmin_iframe.ss
  203. 9  templates/BulkLoaderAdmin_preview.ss
  204. 36  templates/CMSMain_dialog.ss
  205. 30  templates/CMSMain_versions.ss
  206. 14  templates/CommentList.ss
  207. 14  templates/Dialog.ss
  208. 52  templates/Includes/AssetAdmin_left.ss
  209. 34  templates/Includes/AssetAdmin_right.ss
  210. 27  templates/Includes/AssetTableField.ss
  211. 20  templates/Includes/BulkLoaderAdmin_left.ss
  212. 7  templates/Includes/BulkLoaderAdmin_right.ss
  213. 28  templates/Includes/CMSLeft.ss
  214. 130  templates/Includes/CMSMain_left.ss
  215. 38  templates/Includes/CMSMain_right.ss
  216. 0  templates/Includes/CMSMain_rightbottom.ss
  217. 21  templates/Includes/CMSRight.ss
  218. 41  templates/Includes/Editor_toolbar.ss
  219. 28  templates/Includes/GenericDataAdmin_left.ss
  220. 16  templates/Includes/GenericDataAdmin_right.ss
  221. 0  templates/Includes/LeftAndMain_rightbottom.ss
  222. 15  templates/Includes/MemberList_PageControls.ss
  223. 42  templates/Includes/MemberList_Table.ss
  224. 77  templates/Includes/MemberTableField.ss
  225. 24  templates/Includes/NewsletterAdmin_BouncedList.ss
  226. 30  templates/Includes/NewsletterAdmin_SiteTree.ss
  227. 20  templates/Includes/NewsletterAdmin_UnsubscribedList.ss
  228. 50  templates/Includes/NewsletterAdmin_left.ss
  229. 32  templates/Includes/NewsletterAdmin_right.ss
  230. 14  templates/Includes/ReportAdmin_SiteTree.ss
  231. 17  templates/Includes/ReportAdmin_left.ss
  232. 11  templates/Includes/ReportAdmin_right.ss
  233. 28  templates/Includes/SecurityAdmin_left.ss
  234. 14  templates/Includes/SecurityAdmin_right.ss
  235. 72  templates/LeftAndMain.ss
  236. 12  templates/LeftAndMain_printable.ss
  237. 17  templates/MemberList.ss
  238. 19  templates/NewsletterList.ss
  239. 48  templates/Newsletter_RecipientImportField.ss
  240. 46  templates/Newsletter_RecipientImportField_Table.ss
  241. 45  templates/PageCommentInterface.ss
  242. 16  templates/PageCommentInterface_singlecomment.ss
  243. 1  templates/ReceivedFormSubmission.ss
  244. 10  templates/TaskList.ss
  245. 3  templates/ThumbnailStripField.ss
  246. 10  templates/WaitingOn.ss
  247. 20  templates/email/SubmittedFormEmail.ss
  248. 3  templates/email/ViewArchivedEmail.ss
19  _config.php
... ...
@@ -0,0 +1,19 @@
  1
+<?php
  2
+
  3
+Director::addRules(50, array(
  4
+	'processes/$Action/$ID/$Batch' => 'BatchProcess_Controller',
  5
+	'silverstripe/' => '->admin/',
  6
+	'cms/' => '->admin/',
  7
+	'admin/security/$Action/$ID/$OtherID' => 'SecurityAdmin',
  8
+	'admin/help/$Action/$ID' => 'CMSHelp',
  9
+	'admin/newsletter/$Action/$ID' => 'NewsletterAdmin',
  10
+	'admin/reports/$Action/$ID' => 'ReportAdmin',
  11
+	'admin/assets/$Action/$ID' => 'AssetAdmin',
  12
+	'admin/ReportField/$Action/$ID/$Type/$OtherID' => 'ReportField_Controller',
  13
+	'admin/bulkload/$Action/$ID/$OtherID' => 'BulkLoaderAdmin',
  14
+	'admin/$Action/$ID/$OtherID' => 'CMSMain',
  15
+	'unsubscribe/$Email/$MailingList' => 'Unsubscribe_Controller',
  16
+	'membercontrolpanel/$Email' => 'MemberControlPanel'
  17
+));
  18
+
  19
+?>
660  code/AssetAdmin.php
... ...
@@ -0,0 +1,660 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * AssetAdmin is the 'file store' section of the CMS.
  5
+ * It provides an interface for maniupating the File and Folder objects in the system.
  6
+ */
  7
+class AssetAdmin extends LeftAndMain {
  8
+	static $tree_class = "File";
  9
+
  10
+	public function Link($action=null) {
  11
+		if(!$action) $action = "index";
  12
+		return "admin/assets/$action/" . $this->currentPageID();
  13
+	}
  14
+	
  15
+	/**
  16
+	 * Return fake-ID "root" if no ID is found (needed to upload files into the root-folder)
  17
+	 */
  18
+	public function currentPageID() {
  19
+		if(isset($_REQUEST['ID']) && is_numeric($_REQUEST['ID']))	{
  20
+			return $_REQUEST['ID'];
  21
+		} elseif (is_numeric($this->urlParams['ID'])) {
  22
+			return $this->urlParams['ID'];
  23
+		} elseif(is_numeric(Session::get("{$this->class}.currentPage"))) {
  24
+			return Session::get("{$this->class}.currentPage");
  25
+		} else {
  26
+			return "root";
  27
+		}
  28
+	}
  29
+
  30
+	/**
  31
+	 * Set up the controller, in particular, re-sync the File database with the assets folder./
  32
+	 */
  33
+	function init() {
  34
+		parent::init();
  35
+
  36
+		// needed for MemberTableField (Requirements not determined before Ajax-Call)
  37
+		Requirements::javascript("sapphire/javascript/ComplexTableField.js");
  38
+		Requirements::css("jsparty/greybox/greybox.css");
  39
+		Requirements::css("sapphire/css/ComplexTableField.css");
  40
+
  41
+		Requirements::javascript("cms/javascript/AssetAdmin.js");
  42
+		Requirements::javascript("cms/javascript/AssetAdmin_left.js");
  43
+		Requirements::javascript("cms/javascript/AssetAdmin_right.js");
  44
+		
  45
+		// Requirements::javascript('sapphire/javascript/TableListField.js');
  46
+
  47
+		// Include the right JS]
  48
+		// Hayden: This didn't appear to be used at all
  49
+		/*$fileList = new FileList("Form_EditForm_Files", null);
  50
+		$fileList->setClick_AjaxLoad('admin/assets/getfile/', 'Form_SubForm');
  51
+		$fileList->FieldHolder();*/
  52
+		
  53
+		Requirements::javascript("jsparty/greybox/AmiJS.js");
  54
+		Requirements::javascript("jsparty/greybox/greybox.js");
  55
+		Requirements::css("jsparty/greybox/greybox.css");
  56
+	}
  57
+	
  58
+	/**
  59
+	 * Display the upload form.  Returns an iframe tag that will show admin/assets/uploadiframe.
  60
+	 */
  61
+	function getUploadIframe() {
  62
+		return <<<HTML
  63
+		<iframe name="AssetAdmin_upload" src="admin/assets/uploadiframe/{$this->urlParams['ID']}" id="AssetAdmin_upload" border="0" style="border-style: none; width: 100%; height: 200px">
  64
+		</iframe>
  65
+HTML;
  66
+	}
  67
+
  68
+	function index() {
  69
+		File::sync();
  70
+		return array();		
  71
+	}
  72
+
  73
+	/**
  74
+	 * Show the content of the upload iframe.  The form is specified by a template.
  75
+	 */
  76
+	function uploadiframe() {
  77
+		Requirements::clear();
  78
+		
  79
+		Requirements::javascript("jsparty/prototype.js");
  80
+		Requirements::javascript("jsparty/loader.js");
  81
+		Requirements::javascript("jsparty/behaviour.js");
  82
+		Requirements::javascript("jsparty/prototype_improvements.js");
  83
+		Requirements::javascript("jsparty/layout_helpers.js");
  84
+		Requirements::javascript("cms/javascript/LeftAndMain.js");
  85
+		Requirements::javascript("jsparty/multifile/multifile.js");
  86
+		Requirements::css("jsparty/multifile/multifile.css");
  87
+		Requirements::css("cms/css/typography.css");
  88
+		Requirements::css("cms/css/layout.css");
  89
+		Requirements::css("cms/css/cms_left.css");
  90
+		Requirements::css("cms/css/cms_right.css");
  91
+		
  92
+		if(isset($data['ID']) && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
  93
+		else $folder = singleton('Folder');
  94
+		
  95
+		$canUpload = $folder->userCanEdit();
  96
+		
  97
+		return array( 'CanUpload' => $canUpload );
  98
+	}
  99
+	
  100
+	/**
  101
+	 * Return the form object shown in the uploadiframe.
  102
+	 */
  103
+	function UploadForm() {
  104
+
  105
+		return new Form($this,'UploadForm', new FieldSet(
  106
+			new HiddenField("ID", "", $this->currentPageID()),
  107
+			// needed because the button-action is triggered outside the iframe
  108
+			new HiddenField("action_doUpload", "", "1"), 
  109
+			new FileField("Files[0]" , "Choose file "),
  110
+			new LiteralField('UploadButton',"
  111
+				<input type='submit' value='Upload Files Listed Below' name='action_upload' id='Form_UploadForm_action_upload' class='action' />
  112
+			"),
  113
+			new LiteralField('MultifileCode',"
  114
+				<p>Files ready to upload:</p>
  115
+				<div id='Form_UploadForm_FilesList'></div>
  116
+				<script>
  117
+					var multi_selector = new MultiSelector($('Form_UploadForm_FilesList'), null, $('Form_UploadForm_action_upload'));
  118
+					multi_selector.addElement($('Form_UploadForm_Files-0'));
  119
+				</script>
  120
+			")
  121
+		), new FieldSet(
  122
+		));
  123
+
  124
+	}
  125
+	
  126
+	/**
  127
+	 * This method processes the results of the UploadForm.
  128
+	 * It will save the uploaded files to /assets/ and create new File objects as required.
  129
+	 */
  130
+	function doUpload($data, $form) {
  131
+		foreach($data['Files'] as $param => $files) {
  132
+			foreach($files as $key => $value) {
  133
+				$processedFiles[$key][$param] = $value;
  134
+			}
  135
+		}
  136
+		
  137
+		if($data['ID'] && $data['ID'] != 'root') $folder = DataObject::get_by_id("Folder", $data['ID']);
  138
+		else $folder = singleton('Folder');
  139
+
  140
+		$warnFiles = array();
  141
+		$fileSizeWarnings = '';
  142
+
  143
+		foreach($processedFiles as $file) {
  144
+			if($file['tmp_name']) {
  145
+				// check that the file can be uploaded and isn't too large
  146
+				
  147
+				$extensionIndex = strripos( $file['name'], '.' );
  148
+				$extension = strtolower( substr( $file['name'], $extensionIndex + 1 ) );
  149
+				
  150
+				if( $extensionIndex !== FALSE )
  151
+					list( $maxSize, $warnSize ) = File::getMaxFileSize( $extension );
  152
+				else
  153
+					list( $maxSize, $warnSize ) = File::getMaxFileSize();
  154
+				
  155
+				// check that the file is not too large or that the current user is an administrator
  156
+				if( $this->can('AdminCMS') || ( File::allowedFileType( $extension ) && (!isset($maxsize) || $file['size'] < $maxSize)))
  157
+					$newFiles[] = $folder->addUploadToFolder($file);
  158
+				elseif( !File::allowedFileType( $extension ) ) {
  159
+					$fileSizeWarnings .= "alert( 'Only administrators can upload $extension files.' );";
  160
+				} else {
  161
+					if( $file['size'] > 1048576 )
  162
+						$fileSize = "" . ceil( $file['size'] / 1048576 ) . "MB";
  163
+					elseif( $file['size'] > 1024 )
  164
+						$fileSize = "" . ceil( $file['size'] / 1024 ) . "KB";
  165
+					else
  166
+						$fileSize = "" . ceil( $file['size'] ) . "B";
  167
+											
  168
+								
  169
+					$fileSizeWarnings .= "alert( '\\'" . $file['name'] . "\\' is too large ($fileSize). Files of this type cannot be larger than $warnSize ' );";
  170
+				}
  171
+			}
  172
+		}
  173
+		
  174
+		if($newFiles) {
  175
+			$numFiles = sizeof($newFiles);
  176
+			$statusMessage = "Uploaded $numFiles files";
  177
+			$status = "good";
  178
+		} else {
  179
+			$statusMessage = "There was nothing to upload";
  180
+			$status = "";
  181
+		}
  182
+		echo <<<HTML
  183
+			<script type="text/javascript">
  184
+			var form = parent.document.getElementById('Form_EditForm');
  185
+			form.getPageFromServer(form.elements.ID.value);
  186
+			parent.statusMessage("{$statusMessage}","{$status}");
  187
+			$fileSizeWarnings
  188
+			parent.document.getElementById('sitetree').getTreeNodeByIdx( "{$folder->ID}" ).getElementsByTagName('a')[0].className += ' contents';
  189
+			</script>
  190
+HTML;
  191
+	}
  192
+	
  193
+	/**
  194
+	 * Needs to be overridden to make sure an ID with value "0" is still valid (rootfolder)
  195
+	 */
  196
+	
  197
+	
  198
+	/**
  199
+	 * Return the form that displays the details of a folder, including a file list and fields for editing the folder name.
  200
+	 */
  201
+	function getEditForm($id) {
  202
+		if($id && $id != "root") {
  203
+			$record = DataObject::get_by_id("File", $id);
  204
+		} else {
  205
+			$record = singleton("Folder");
  206
+		}
  207
+		
  208
+		$fileList = new AssetTableField(
  209
+			$this,
  210
+			"Files",
  211
+			"File", 
  212
+			array("Title" => "Title", "LinkedURL" => "Filename"), 
  213
+			""
  214
+		);
  215
+		$fileList->setFolder($record);
  216
+		$fileList->setPopupCaption("View/Edit Asset");
  217
+
  218
+		if($record) {
  219
+			$nameField = ($id != "root") ? new TextField("Name") : new HiddenField("Name");
  220
+			$fields = new FieldSet(
  221
+				new HiddenField("Title"),
  222
+				$nameField,
  223
+				new TabSet("Root", 
  224
+					new Tab("Files",
  225
+						$fileList
  226
+					),
  227
+					new Tab("Details", 
  228
+						new ReadonlyField("URL"),
  229
+						new ReadonlyField("ClassName", "Type"),
  230
+						new ReadonlyField("Created", "First Uploaded"),
  231
+						new ReadonlyField("LastEdited", "Last Updated")
  232
+					),
  233
+					new Tab("Upload",
  234
+						new LiteralField("UploadIframe",
  235
+							$this->getUploadIframe()
  236
+						)
  237
+					)
  238
+				),
  239
+				new HiddenField("ID")
  240
+			);
  241
+			
  242
+			$actions = new FieldSet();
  243
+			
  244
+			if( $record->userCanEdit() ) {
  245
+				$actions = new FieldSet(
  246
+					new FormAction('deletemarked',"Delete files"),
  247
+					new FormAction('movemarked',"Move files..."),
  248
+					new FormAction('save',"Save")
  249
+				);
  250
+			}
  251
+			
  252
+			$form = new Form($this, "EditForm", $fields, $actions);
  253
+			if($record->ID) {
  254
+				$form->loadDataFrom($record);
  255
+			} else {
  256
+				$form->loadDataFrom(array(
  257
+					"ID" => "root",
  258
+					"URL" => Director::absoluteBaseURL() . 'assets/',
  259
+				));
  260
+			}
  261
+			
  262
+			// @todo: These workflow features aren't really appropriate for all projects
  263
+			if( Member::currentUser()->_isAdmin() && project() == 'mot' ) {
  264
+				$fields->addFieldsToTab( 'Root.Workflow', new DropdownField("Owner", "Owner", Member::map() ) );
  265
+				$fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanUse", "Content usable by") );
  266
+				$fields->addFieldsToTab( 'Root.Workflow', new TreeMultiselectField("CanEdit", "Content modifiable by") );
  267
+			}
  268
+
  269
+			if( !$record->userCanEdit() )
  270
+				$form->makeReadonly();
  271
+
  272
+			return $form;
  273
+
  274
+		}
  275
+	}
  276
+	
  277
+	/**
  278
+	 * Returns the form used to specify options for the "move marked" action.
  279
+	 */
  280
+	public function MoveMarkedOptionsForm() {
  281
+		$folderDropdown = new TreeDropdownField("DestFolderID", "Move files to", "Folder");
  282
+		$folderDropdown->setFilterFunction(create_function('$obj', 'return $obj->class == "Folder";'));
  283
+		
  284
+		return new CMSActionOptionsForm($this, "MoveMarkedOptionsForm", new FieldSet(
  285
+			new HiddenField("ID"),
  286
+			new HiddenField("FileIDs"),
  287
+			$folderDropdown
  288
+		),
  289
+		new FieldSet(
  290
+			new FormAction("movemarked", "Move marked files")
  291
+		));
  292
+	}
  293
+	
  294
+	/**
  295
+	 * Perform the "move marked" action.
  296
+	 * Called by ajax, with a JavaScript return.
  297
+	 */
  298
+	public function movemarked() {
  299
+		if($_REQUEST['DestFolderID'] && is_numeric($_REQUEST['DestFolderID'])) {
  300
+			$destFolderID = $_REQUEST['DestFolderID'];
  301
+			$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
  302
+			$numFiles = 0;
  303
+	
  304
+			if($fileList != "''") {
  305
+				$files = DataObject::get("File", "`File`.ID IN ($fileList)");
  306
+				if($files) {
  307
+					foreach($files as $file) {
  308
+						$file->ParentID = $destFolderID;
  309
+						$file->write();
  310
+						$numFiles++;
  311
+					}
  312
+				} else {
  313
+					user_error("No files in $fileList could be found!", E_USER_ERROR);
  314
+				}
  315
+			}
  316
+		
  317
+			echo <<<JS
  318
+				statusMessage("Moved $numFiles files");
  319
+JS;
  320
+		} else {
  321
+			user_error("Bad data: $_REQUEST[DestFolderID]", E_USER_ERROR);
  322
+		}
  323
+	}
  324
+
  325
+	/**
  326
+	 * Returns the form used to specify options for the "delete marked" action.
  327
+	 * In actual fact, this form only has hidden fields and the button is auto-clickd without the
  328
+	 * form being displayed; it's just the most consistent way of providing this information to the
  329
+	 * CMS.
  330
+	 */
  331
+	public function DeleteMarkedOptionsForm() {
  332
+		return new CMSActionOptionsForm($this, "DeleteMarkedOptionsForm", new FieldSet(
  333
+			new HiddenField("ID"),
  334
+			new HiddenField("FileIDs")
  335
+		),
  336
+		new FieldSet(
  337
+			new FormAction("deletemarked", "Delete marked files")
  338
+		));
  339
+	}
  340
+	
  341
+	/**
  342
+	 * Perform the "delete marked" action.
  343
+	 * Called by ajax, with a JavaScript return.
  344
+	 */
  345
+	public function deletemarked() {
  346
+			$fileList = "'" . ereg_replace(' *, *',"','",trim(addslashes($_REQUEST['FileIDs']))) . "'";
  347
+			$numFiles = 0;
  348
+			$folderID = 0;
  349
+			$deleteList = '';
  350
+			$brokenPageList = '';
  351
+	
  352
+			if($fileList != "''") {
  353
+				$files = DataObject::get("File", "`File`.ID IN ($fileList)");
  354
+				if($files) {
  355
+					foreach($files as $file) {
  356
+						if( !$folderID )
  357
+							$folderID = $file->ParentID;
  358
+						
  359
+						// $deleteList .= "\$('Form_EditForm_Files').removeById($file->ID);\n";
  360
+						$file->delete();
  361
+						$numFiles++;
  362
+					}
  363
+					if($brokenPages = Notifications::getItems("BrokenLink")) {
  364
+						$brokenPageList = "  These pages now have broken links:</ul>";
  365
+						foreach($brokenPages as $brokenPage) {
  366
+							$brokenPageList .= "<li style=&quot;font-size: 65%&quot;>" . $brokenPage->Breadcrumbs(3, true) . "</li>";
  367
+						}
  368
+						$brokenPageList .= "</ul>";
  369
+						Notifications::notifyByEmail("BrokenLink", "Page_BrokenLinkEmail");
  370
+					}
  371
+					
  372
+					if( $folderID ) {
  373
+						$remaining = DB::query("SELECT COUNT(*) FROM `File` WHERE `ParentID`=$folderID")->value();
  374
+						
  375
+						if( !$remaining )
  376
+							$deleteList = "Element.removeClassName(\$('sitetree').getTreeNodeByIdx( '$folderID' ).getElementsByTagName('a')[0],'contents');";
  377
+					}
  378
+					
  379
+				} else {
  380
+					user_error("No files in $fileList could be found!", E_USER_ERROR);
  381
+				}
  382
+			}
  383
+		
  384
+			echo <<<JS
  385
+				$deleteList
  386
+				$('Form_EditForm').getPageFromServer($('Form_EditForm_ID').value);
  387
+				statusMessage("Deleted $numFiles files.$brokenPageList");
  388
+JS;
  389
+	}
  390
+	
  391
+	
  392
+	/**
  393
+	 * Returns the content to be placed in Form_SubForm when editing a file.
  394
+	 * Called using ajax.
  395
+	 */
  396
+	public function getfile() {
  397
+		SSViewer::setOption('rewriteHashlinks', false);
  398
+
  399
+		// bdc: only try to return something if user clicked on an object
  400
+		if (is_object($this->getSubForm($this->urlParams['ID']))) {
  401
+			return $this->getSubForm($this->urlParams['ID'])->formHtmlContent();
  402
+		}
  403
+		else return null;
  404
+	}
  405
+	
  406
+	/**
  407
+	 * Action handler for the save button on the file subform.
  408
+	 * Saves the file
  409
+	 */
  410
+	public function savefile($data, $form) {
  411
+		$record = DataObject::get_by_id("File", $data['ID']);
  412
+		$form->saveInto($record);
  413
+		$record->write();
  414
+		$title = Convert::raw2js($record->Title);
  415
+		$name = Convert::raw2js($record->Name);
  416
+		echo <<<JS
  417
+			statusMessage('Saved file #$data[ID]');
  418
+			$('record-$data[ID]').getElementsByTagName('td')[1].innerHTML = "$title";
  419
+			$('record-$data[ID]').getElementsByTagName('td')[2].innerHTML = "$name";
  420
+JS;
  421
+	}
  422
+	
  423
+	/**
  424
+	 * Return the entire site tree as a nested set of ULs
  425
+	
  426
+*/
  427
+	public function SiteTreeAsUL() {
  428
+		$obj = singleton('Folder');
  429
+		$obj->setMarkingFilter("ClassName", "Folder");
  430
+		$obj->markPartialTree();
  431
+
  432
+		if($p = $this->currentPage()) $obj->markToExpose($p);
  433
+
  434
+		// getChildrenAsUL is a flexible and complex way of traversing the tree
  435
+
  436
+		$siteTree = $obj->getChildrenAsUL("",
  437
+
  438
+					' "<li id=\"record-$child->ID\" class=\"$child->class" . $child->markingClasses() .  ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
  439
+
  440
+					' "<a href=\"" . Director::link(substr($extraArg->Link(),0,-1), "show", $child->ID) . "\" class=\"" . ($child->hasChildren() ? " contents" : "") . "\" >" . $child->Title . "</a>" ',
  441
+
  442
+					$this, true);
  443
+					
  444
+
  445
+		// Wrap the root if needs be.
  446
+
  447
+		$rootLink = $this->Link() . 'show/root';
  448
+
  449
+		if(!isset($rootID)) $siteTree = "<ul id=\"sitetree\" class=\"tree unformatted\"><li id=\"record-root\" class=\"Root\"><a href=\"$rootLink\">http://www.yoursite.com/assets</a>"
  450
+
  451
+					. $siteTree . "</li></ul>";
  452
+
  453
+
  454
+		return $siteTree;
  455
+
  456
+	}
  457
+
  458
+	/**
  459
+	 * Returns a subtree of items underneat the given folder.
  460
+	 */
  461
+	public function getsubtree() {
  462
+		$obj = DataObject::get_by_id("Folder", $_REQUEST['ID']);
  463
+		$obj->setMarkingFilter("ClassName", "Folder");
  464
+		$obj->markPartialTree();
  465
+
  466
+		$results = $obj->getChildrenAsUL("",
  467
+
  468
+					' "<li id=\"record-$child->ID\" class=\"$child->class" . $child->markingClasses() .  ($extraArg->isCurrentPage($child) ? " current" : "") . "\">" . ' .
  469
+
  470
+					' "<a href=\"" . Director::link(substr($extraArg->Link(),0,-1), "show", $child->ID) . "\" >" . $child->Title . "</a>" ',
  471
+
  472
+					$this, true);
  473
+
  474
+		return substr(trim($results), 4,-5);
  475
+
  476
+	}
  477
+	
  478
+
  479
+	//------------------------------------------------------------------------------------------//
  480
+
  481
+	// Data saving handlers
  482
+
  483
+	/**
  484
+	 * Add a new folder and return its details suitable for ajax.
  485
+	 */
  486
+	public function addfolder() {
  487
+		$parent = ($_REQUEST['ParentID'] && is_numeric($_REQUEST['ParentID'])) ? $_REQUEST['ParentID'] : 0;
  488
+		
  489
+		if($parent) {
  490
+			$parentObj = DataObject::get_by_id("File", $parent);
  491
+			if(!$parentObj || !$parentObj->ID) $parent = 0;
  492
+		}
  493
+		
  494
+		$p = new Folder();
  495
+		$p->ParentID = $parent;
  496
+		$p->Title = "NewFolder";
  497
+
  498
+		$p->Name = "NewFolder";
  499
+
  500
+		// Get the folder to be created		
  501
+		if(isset($parentObj->ID)) $filename = $parentObj->FullPath . $p->Name;
  502
+		else $filename = '../assets/' . $p->Name;
  503
+		
  504
+		// Ensure uniqueness		
  505
+		$i = 2;
  506
+		$baseFilename = $filename . '-';
  507
+		while(file_exists($filename)) {
  508
+			$filename = $baseFilename . $i;
  509
+			$p->Name = $p->Title = basename($filename);
  510
+			$i++;
  511
+		}
  512
+		
  513
+		// Actually create
  514
+		mkdir($filename);
  515
+		chmod($filename, 02775);
  516
+
  517
+		$p->write();
  518
+	
  519
+	
  520
+		return $this->returnItemToUser($p);
  521
+
  522
+	}
  523
+	
  524
+	/**
  525
+	 * Return the given tree item to the client.
  526
+	 * If called by ajax, this will be some javascript commands.
  527
+	 * Otherwise, it will redirect back.
  528
+	 */
  529
+	public function returnItemToUser($p) {
  530
+		if($_REQUEST['ajax']) {
  531
+			$parentID = (int)$p->ParentID;
  532
+			return <<<JS
  533
+				tree = $('sitetree');
  534
+
  535
+				var newNode = tree.createTreeNode($p->ID, "$p->Title", "$p->class");
  536
+
  537
+				tree.getTreeNodeByIdx($parentID).appendTreeNode(newNode);
  538
+
  539
+				newNode.selectTreeNode();
  540
+JS;
  541
+
  542
+		} else {
  543
+
  544
+			Director::redirectBack();
  545
+
  546
+		}
  547
+	}
  548
+	
  549
+	/**
  550
+	 * Delete a folder
  551
+	 */
  552
+	public function deletefolder() {
  553
+		$script = '';
  554
+		$ids = split(' *, *', $_REQUEST['csvIDs']);
  555
+
  556
+		foreach($ids as $id) {
  557
+
  558
+			if(is_numeric($id)) {
  559
+
  560
+				$record = DataObject::get_by_id($this->stat('tree_class'), $id);
  561
+				
  562
+if(!$record)
  563
+
  564
+					Debug::message( "Record appears to be null" );
  565
+
  566
+				
  567
+
  568
+				/*if($record->hasMethod('BackLinkTracking')) {
  569
+					$brokenPages = $record->BackLinkTracking();
  570
+
  571
+					foreach($brokenPages as $brokenPage) {
  572
+
  573
+						$brokenPageList .= "<li style=\"font-size: 65%\">" . $brokenPage->Breadcrumbs(3, true) . "</li>";
  574
+
  575
+						$brokenPage->HasBrokenLink = true;
  576
+
  577
+						$notifications[$brokenPage->OwnerID][] = $brokenPage;
  578
+
  579
+						$brokenPage->write();
  580
+
  581
+					}
  582
+
  583
+				}*/
  584
+				
  585
+				$record->delete();
  586
+				$record->destroy();
  587
+
  588
+
  589
+
  590
+				// DataObject::delete_by_id($this->stat('tree_class'), $id);
  591
+
  592
+				$script .= $this->deleteTreeNodeJS($record);
  593
+
  594
+			}
  595
+
  596
+		}
  597
+
  598
+
  599
+		
  600
+/*if($notifications) foreach($notifications as $memberID => $pages) {
  601
+
  602
+			$email = new Page_BrokenLinkEmail();
  603
+
  604
+			$email->populateTemplate(new ArrayData(array(
  605
+
  606
+				"Recipient" => DataObject::get_by_id("Member", $memberID),
  607
+
  608
+				"BrokenPages" => new DataObjectSet($pages),
  609
+
  610
+			)));
  611
+
  612
+			$email->debug();
  613
+
  614
+			$email->send();
  615
+
  616
+		}*/
  617
+
  618
+		
  619
+
  620
+		$s = (sizeof($ids) > 1) ? "s" :"";
  621
+		
  622
+		$message = sizeof($ids) . " folder$s deleted.";
  623
+		//
  624
+		if(isset($brokenPageList)) $message .= "  The following pages now have broken links:<ul>" . addslashes($brokenPageList) . "</ul>Their owners have been emailed and they will fix up those pages.";
  625
+		$script .= "statusMessage('$message');";
  626
+		echo $script;
  627
+	}
  628
+	
  629
+	public function removefile(){
  630
+		if($fileID = $this->urlParams['ID']){
  631
+			$file = DataObject::get_by_id('File', $fileID);
  632
+			$file->delete();
  633
+			$file->destroy();
  634
+			
  635
+			if(Director::is_ajax()) {
  636
+				echo <<<JS
  637
+				$('Form_EditForm_Files').removeFile($fileID);
  638
+				statusMessage('removed file', 'good');
  639
+JS;
  640
+			}else{
  641
+				Director::redirectBack();
  642
+			}
  643
+		}else{
  644
+			user_error("AssetAdmin::removefile: Bad parameters: File=$fileID", E_USER_ERROR);
  645
+		}
  646
+	}
  647
+	
  648
+	public function save($urlParams, $form) {
  649
+		// Don't save the root folder - there's no database record
  650
+		if($_REQUEST['ID'] == 'root') {
  651
+			FormResponse::status_message("Saved", "good");
  652
+			return FormResponse::respond();
  653
+		}
  654
+		
  655
+		
  656
+		$form->dataFieldByName('Title')->value = $form->dataFieldByName('Name')->value;
  657
+		
  658
+		return parent::save($urlParams, $form);
  659
+	}
  660
+}
130  code/AssetTableField.php
... ...
@@ -0,0 +1,130 @@
  1
+<?php
  2
+class AssetTableField extends ComplexTableField {
  3
+	
  4
+	protected $folder;
  5
+	
  6
+	protected $template = "AssetTableField";
  7
+	
  8
+	function __construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter = "", $sourceSort = "", $sourceJoin = "") {
  9
+		
  10
+		parent::__construct($controller, $name, $sourceClass, $fieldList, $detailFormFields, $sourceFilter, $sourceSort, $sourceJoin);
  11
+
  12
+		$this->sourceSort = "Title";		
  13
+		$this->Markable = true;
  14
+	}
  15
+	
  16
+	function setFolder($folder) {
  17
+		$this->folder = $folder;
  18
+		$this->sourceFilter .= ($this->sourceFilter) ? " AND " : "";
  19
+		$this->sourceFilter .= " ParentID = '" . $folder->ID . "' AND ClassName <> 'Folder'";
  20
+	}
  21
+	
  22
+	function Folder() {
  23
+		return $this->folder;
  24
+	}
  25
+	
  26
+	function sourceID() {
  27
+		return $this->folder->ID;
  28
+	}
  29
+	
  30
+	function DetailForm() {
  31
+		$ID = (isset($_REQUEST['ctf']['ID'])) ? Convert::raw2xml($_REQUEST['ctf']['ID']) : null;
  32
+		$childID = (isset($_REQUEST['ctf']['childID'])) ? Convert::raw2xml($_REQUEST['ctf']['childID']) : null;
  33
+		$childClass = (isset($_REQUEST['fieldName'])) ? Convert::raw2xml($_REQUEST['fieldName']) : null;
  34
+		$methodName = (isset($_REQUEST['methodName'])) ? $_REQUEST['methodName'] : null;
  35
+		
  36
+		if(!$childID) {
  37
+			user_error("AssetTableField::DetailForm Please specify a valid ID");
  38
+			return null;
  39
+		}
  40
+		
  41
+		if($childID) {
  42
+			$childData = DataObject::get_by_id("File", $childID);
  43
+		}
  44
+		
  45
+		if(!$childData) {
  46
+			user_error("AssetTableField::DetailForm No record found");
  47
+			return null;
  48
+		}
  49
+		
  50
+		if($childData->ParentID) {
  51
+			$folder = DataObject::get_by_id('File', $childData->ParentID );
  52
+		} else {
  53
+			$folder = singleton('Folder');
  54
+		}
  55
+		
  56
+		$urlLink = "<div class='field readonly'>";
  57
+		$urlLink .= "<label class='left'>URL</label>";
  58
+		$urlLink .= "<span class='readonly'><a href='{$childData->Link()}'>{$childData->RelativeLink()}</a></span>";
  59
+		$urlLink .= "</div>";
  60
+		
  61
+		$detailFormFields = new FieldSet(
  62
+			new TabSet("BottomRoot",
  63
+				new Tab("Main",
  64
+					new TextField("Title"),
  65
+					new TextField("Name", "Filename"),
  66
+					new LiteralField("AbsoluteURL", $urlLink),
  67
+					new ReadonlyField("FileType", "Type"),
  68
+					new ReadonlyField("Size", "Size", $childData->getSize()),
  69
+					new DropdownField("OwnerID", "Owner", Member::mapInCMSGroups( $folder->CanEdit() ) ),
  70
+					new DateField_Disabled("Created", "First uploaded"),
  71
+					new DateField_Disabled("LastEdited", "Last changed")
  72
+				)
  73
+			)
  74
+		);
  75
+		
  76
+		if(is_a($childData,'Image')) {
  77
+			$big = $childData->URL;
  78
+			$thumbnail = $childData->getFormattedImage('AssetLibraryPreview')->URL;
  79
+			
  80
+			$detailFormFields->addFieldToTab("BottomRoot.Main", 
  81
+				new ReadonlyField("Dimensions"),
  82
+				"Created"
  83
+			);
  84
+
  85
+			$detailFormFields->addFieldToTab("BottomRoot", 
  86
+				new Tab("Image",
  87
+					new LiteralField("ImageFull",
  88
+						"<img src='{$thumbnail}' alt='{$childData->Name}' />"
  89
+					)
  90
+				),
  91
+				'Main'
  92
+			);
  93
+		}
  94
+		
  95
+		if($childData && $childData->hasMethod('BackLinkTracking')) {
  96
+			$links = $childData->BackLinkTracking();
  97
+			if($links->exists()) {
  98
+				foreach($links as $link) {
  99
+					$backlinks[] = "<li><a href=\"admin/show/$link->ID\">" . $link->Breadcrumbs(null,true). "</a></li>";
  100
+				}
  101
+				$backlinks = "<div style=\"clear:left\">The following pages link to this file:<ul>" . implode("",$backlinks) . "</ul>";
  102
+			}
  103
+			if(!isset($backlinks)) $backlinks = "<p>This file hasn't been linked to from any pages.</p>";
  104
+			$detailFormFields->addFieldToTab("BottomRoot.Links", new LiteralField("Backlinks", $backlinks));
  105
+		}
  106
+		
  107
+		// the ID field confuses the Controller-logic in finding the right view for ReferencedField
  108
+		$detailFormFields->removeByName('ID');
  109
+		// add a namespaced ID instead thats "converted" by saveComplexTableField()
  110
+		$detailFormFields->push(new HiddenField("ctf[childID]","",$childID));
  111
+		$detailFormFields->push(new HiddenField("ctf[ClassName]","",$this->sourceClass));
  112
+			
  113
+		$readonly = ($this->methodName == "show");
  114
+		$form = new ComplexTableField_Popup($this, "DetailForm", $detailFormFields, $this->sourceClass, $readonly);
  115
+			
  116
+		if (is_numeric($childID)) {
  117
+			if ($methodName == "show" || $methodName == "edit") {
  118
+				$form->loadDataFrom($childData);
  119
+			}
  120
+		}
  121
+		
  122
+		if( !$folder->userCanEdit() || $methodName == "show") {
  123
+			$form->makeReadonly();
  124
+		}
  125
+		
  126
+		return $form;
  127
+	}
  128
+	
  129
+}
  130
+?>
67  code/BulkLoader.php
... ...
@@ -0,0 +1,67 @@
  1
+<?php
  2
+
  3
+abstract class BulkLoader extends ViewableData {
  4
+	/**
  5
+	 * Override this on subclasses to give the specific functions names
  6
+	 */
  7
+	static $title = null;
  8
+
  9
+	/**
  10
+	 * Return a human-readable name for this object.
  11
+	 * It defaults to the class name can be overridden by setting the static variable $title
  12
+	 */
  13
+	function Title() {
  14
+		if($title = $this->stat('title')) return $title;
  15
+		else return $this->class;
  16
+	}
  17
+	
  18
+	/**
  19
+	 * Process every record in the file
  20
+	 * @param filename The name of the CSV file to process
  21
+	 * @param preview If true, we'll just output a summary of changes but not actually do anything
  22
+	 *
  23
+	 * @returns A DataObjectSet containing a list of all the reuslst
  24
+	 */
  25
+	function processAll($filename, $preview = false) {
  26
+		// TODO
  27
+		// Get the first record out of the CSV and store it as headers
  28
+		// Get each record out of the CSV
  29
+		//		Remap the record so that it's keyed by headers
  30
+		//		Pass it to $this->processRecord, and get the results
  31
+		//		Put the results inside an ArrayData and push that onto a DataObjectSet for returning
  32
+	}
  33
+	
  34
+
  35
+	/*----------------------------------------------------------------------------------------
  36
+	 * Next, we have some abstract functions that let subclasses say what kind of batch operation they're 
  37
+	 * going to do
  38
+	 *----------------------------------------------------------------------------------------
  39
+	 */
  40
+	
  41
+	
  42
+	/**
  43
+	 * Return a FieldSet containing all the options for this form; this
  44
+	 * doesn't include the actual upload field itself
  45
+	 */
  46
+	abstract function getOptionFields();
  47
+	
  48
+	/**
  49
+	 * Process a single record from the CSV file.
  50
+	 * @param record An map of the CSV data, keyed by the header field
  51
+	 * @param preview 
  52
+	 * 
  53
+	 * @returns A 2 value array.  
  54
+	 *   - The first element should be "add", "edit" or "", depending on the operation performed in response to this record
  55
+	 *   - The second element is a free-text string that can optionally provide some more information about what changes have
  56
+	 *     been made	 
  57
+	 */
  58
+	abstract function processRecord($record, $preview = false);
  59
+	
  60
+	/*----------------------------------------------------------------------------------------
  61
+	 * Next, we have a library of helper functions (Brian to build as necessary)
  62
+	 *----------------------------------------------------------------------------------------
  63
+	 */
  64
+
  65
+}
  66
+
  67
+?>
123  code/BulkLoaderAdmin.php
... ...
@@ -0,0 +1,123 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * Class to provide batch-update facilities to CMS users.
  5
+ * The BulkLoaderAdmin class provides an interface for accessing all of the subclasses of BulkLoader,
  6
+ * each of which defines a particular bulk loading operation.
  7
+ * 
  8
+ * This code was originally developed for Per Week in collaboration with Brian Calhoun.
  9
+ */
  10
+class BulkLoaderAdmin extends LeftAndMain {
  11
+
  12
+	/**
  13
+	 * Initialisation method called before accessing any functionality that BulkLoaderAdmin has to offer
  14
+	 */
  15
+	public function init() {
  16
+		Requirements::javascript('cms/javascript/BulkLoaderAdmin.js');
  17
+		
  18
+		parent::init();
  19
+	}
  20
+	
  21
+	/**
  22
+	 * Link function to tell us how to get back to this controller.
  23
+	 */
  24
+	public function Link($action = null) {
  25
+		return "admin/bulkload/$action";
  26
+	}
  27
+	
  28
+	public function BulkLoaders() {
  29
+			$items = ClassInfo::subclassesFor("BulkLoader");
  30
+			array_shift($items);
  31
+			
  32
+			foreach($items as $item) {
  33
+				$itemObjects[] = new $item();
  34
+			}
  35
+			
  36
+			return new DataObjectSet($itemObjects);
  37
+	}
  38
+	
  39
+	/**
  40
+	 * Return the form shown when we first click on a loader on the left.
  41
+	 * Provides all the options, a file upload, and an option to proceed
  42
+	 */	 
  43
+	public function getEditForm($className = null) {
  44
+		if(is_subclass_of($className, 'BulkLoader')) {
  45
+			$loader = new $className();
  46
+			
  47
+			$fields = $loader->getOptionFields();
  48
+			if(!$fields) $fields = new FieldSet();
  49
+			
  50
+			$fields->push(new FileField("File", "CSV File"));
  51
+			$fields->push(new HiddenField('LoaderClass', '', $loader->class));
  52
+			
  53
+			return new Form($this, "EditForm",
  54
+				$fields,
  55
+				new FieldSet(
  56
+					new FormAction('preview', "Preview")
  57
+				)
  58
+			);
  59
+			
  60
+		}
  61
+	}
  62
+	
  63
+	public function preview() {
  64
+		$className = $_REQUEST['LoaderClass'];
  65
+		if(is_subclass_of($className, 'BulkLoader')) {
  66
+			$loader = new $className();
  67
+			
  68
+			$results = $loader->processAll($_FILES['File']['tmp_name'], false);
  69
+			
  70
+			return $this->customise(array(
  71
+				"Message" => "Press continue to load this data in",
  72
+				"Results" => $results,
  73
+				"ConfirmForm" => $this->getConfirmFormFor($loader, $file),
  74
+			))->renderWith("BulkLoaderAdmin_preview");
  75
+		}
  76
+	}
  77
+	
  78
+	/**
  79
+	 * Generate a confirmation form for the given file/class
  80
+	 * Will copy the file to a suitable temporary location
  81
+	 * @param loader A BulkLoader object
  82
+	 * @param file The name of the temp file
  83
+	 */
  84
+	public function getConfirmFormFor($loader, $file) {
  85
+		$tmpFile = tempnam(TEMP_FOLDER,'batch-upload-');
  86
+		copy($file,$tmpFile);
  87
+		
  88
+		return new Form($this, "ConfirmForm", new FieldSet(
  89
+			new HiddenField("File", "", $tmpFile),
  90
+			new HiddenField("LoaderClass", "", $loader->class)
  91
+		), new FieldSet(
  92
+			new FormAction('process', 'Confirm bulk load')
  93
+		));		
  94
+	}
  95
+	/**
  96
+	 * Stub to return the form back after pressing the button.
  97
+	 */
  98
+	public function ConfirmForm() {
  99
+		$className = $_REQUEST['LoaderClass'];
  100
+		return $this->getConfirmFormFor(new $className(), $_REQUEST['File']);
  101
+	}
  102
+	
  103
+	/**
  104
+	 * Process the data and display the final "finished" message
  105
+	 */	
  106
+	public function process() {
  107
+		$className = $_REQUEST['LoaderClass'];
  108
+		if(is_subclass_of($className, 'BulkLoader')) {
  109
+			$loader = new $className();
  110
+			
  111
+			$results = $loader->processAll($_REQUEST['Filename'], true);
  112
+			
  113
+			return $this->customise(array(
  114
+				"Message" => "This data has been loaded in",
  115
+				"Results" => $results,
  116
+				"ConfirmForm" => " ",
  117
+			))->renderWith("BulkLoaderAdmin_preview");
  118
+		}
  119
+	}
  120
+
  121
+}
  122
+
  123
+?>
17  code/CMSActionOptionsForm.php
... ...
@@ -0,0 +1,17 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * A special kind of form used to make the action dialogs that appear just underneath the top-right
  5
+ * buttons in the CMS
  6
+ */
  7
+class CMSActionOptionsForm extends Form {
  8
+	function FormAttributes() {
  9
+		return "class=\"actionparams\" style=\"display:none\" " . parent::FormAttributes();
  10
+	}
  11
+	function FormName() {
  12
+		$action = $this->actions->First()->Name();
  13
+		return "{$action}_options";
  14
+	}
  15
+}
  16
+
  17
+?>
8  code/CMSHelp.php
... ...
@@ -0,0 +1,8 @@
  1
+<?php
  2
+
  3
+class CMSHelp extends Controller {
  4
+
  5
+
  6
+}
  7
+
  8
+?>
1,132  code/CMSMain.php
... ...
@@ -0,0 +1,1132 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * The main "content" area of the CMS.
  5
+ * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
  6
+ * admin menu.
  7
+ * @todo Create some base classes to contain the generic functionality that will be replicated.
  8
+ */
  9
+class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider {
  10
+	static $tree_class = "SiteTree";
  11
+	static $subitem_class = "Member";
  12
+	
  13
+	public function init() {
  14
+		parent::init();
  15
+
  16
+		Requirements::javascript(MCE_ROOT . "tiny_mce_src.js");
  17
+		Requirements::javascript("jsparty/tiny_mce_improvements.js");
  18
+		Requirements::javascript("jsparty/hover.js");
  19
+		Requirements::javascript("