Java tutorial
/********************************************************************************** * $URL: $ * $Id: $ *********************************************************************************** * * Author: Eric Jeney, jeney@rutgers.edu * The original author was Joshua Ryan josh@asu.edu. However little of that code is actually left * * Copyright (c) 2010 Rutgers, the State University of New Jersey * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.lessonbuildertool.tool.beans; import java.text.SimpleDateFormat; import java.text.Format; import java.math.BigDecimal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroup; import org.sakaiproject.authz.api.Member; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.api.SecurityService; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.component.cover.ServerConfigurationService; import org.sakaiproject.content.api.*; import org.sakaiproject.content.api.GroupAwareEntity.AccessMode; import org.sakaiproject.db.cover.SqlService; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.api.ResourceProperties; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.event.cover.EventTrackingService; import org.sakaiproject.event.cover.NotificationService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.exception.TypeException; import org.sakaiproject.id.cover.IdManager; import org.sakaiproject.lessonbuildertool.*; import org.sakaiproject.lessonbuildertool.cc.CartridgeLoader; import org.sakaiproject.lessonbuildertool.cc.Parser; import org.sakaiproject.lessonbuildertool.cc.PrintHandler; import org.sakaiproject.lessonbuildertool.cc.ZipLoader; import org.sakaiproject.lessonbuildertool.model.SimplePageToolDao; import org.sakaiproject.lessonbuildertool.service.*; import org.sakaiproject.lessonbuildertool.tool.producers.ShowItemProducer; import org.sakaiproject.lessonbuildertool.tool.producers.ShowPageProducer; import org.sakaiproject.lessonbuildertool.tool.producers.PagePickerProducer; import org.sakaiproject.lessonbuildertool.tool.view.GeneralViewParameters; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.memory.api.SimpleConfiguration; import org.sakaiproject.site.api.*; import org.sakaiproject.time.cover.TimeService; import org.sakaiproject.tool.api.Placement; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.user.api.User; import org.sakaiproject.user.cover.UserDirectoryService; import org.sakaiproject.util.FormattedText; import org.sakaiproject.util.ResourceLoader; import org.sakaiproject.util.Validator; import org.springframework.web.multipart.MultipartFile; import uk.org.ponder.messageutil.MessageLocator; import uk.org.ponder.rsf.components.UIContainer; import uk.org.ponder.rsf.components.UIInternalLink; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.lang.reflect.Method; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.text.DateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.sakaiproject.lessonbuildertool.tool.beans.helpers.ResourceHelper; import au.com.bytecode.opencsv.CSVParser; import org.sakaiproject.portal.util.ToolUtils; import org.sakaiproject.lti.api.LTIService; import org.sakaiproject.basiclti.util.SakaiBLTIUtil; import org.imsglobal.lti2.ContentItem; /** * Backing bean for Simple pages * * @author Eric Jeney <jeney@rutgers.edu> * @author Joshua Ryan josh@asu.edu alt^I */ // This bean has two related but somewhat separate uses: // 1) It keeps common data for the producers and other code. In that use the lifetime of the bean is while // generating a single page. The bean has common application logic. The producers are pretty much just UI. // The DAO is low-level data access. This is everything else. The producers call this bean, and not the // DAO directly. This layer sticks caches on top of the data access, and provides more complex logic. Security // is primarily in the DAO, but the DAO only checks permissions. We have to make sure we only access pages // and items in our site // Most of the caches are local. Since this bean is request-scope they are recreated for each request. // Thus we don't have to worry about timing out the entries. // 2) It is used by RSF to access data. Normally the bean is associated with a specific page. However the // UI often has to update attributes of a specific item. For that use, there are some item-specific variables // in the bean. They are only meaningful during item operations, when itemId will show which item is involved. // While the bean is used by all the producers, the caching was designed specifically for ShowPageProducer. // That's because it is used a lot more often than the others. ShowPageProducer should do all data access through // the methods here that cache. There is also caching by hibernate. However this code is cheaper, partly because // it doesn't have to do synchronization (since it applies just to processing one transaction). public class SimplePageBean { public static final int CACHE_MAX_ENTRIES = 5000; public static final int CACHE_TIME_TO_LIVE_SECONDS = 600; public static final int CACHE_TIME_TO_IDLE_SECONDS = 360; private static Log log = LogFactory.getLog(SimplePageBean.class); public enum Status { NOT_REQUIRED, REQUIRED, DISABLED, COMPLETED, FAILED, NEEDSGRADING } // from ResourceProperites. This isn't in 2.7.1, so define it here. Let's hope it doesn't change... public static final String PROP_ALLOW_INLINE = "SAKAI:allow_inline"; public static final Pattern YOUTUBE_PATTERN = Pattern.compile("v[=/_]([\\w-]{11}([\\?\\&][\\w\\.\\=\\&]*)?)"); public static final Pattern YOUTUBE2_PATTERN = Pattern.compile("embed/([\\w-]{11}([\\?\\&][\\w\\.\\=\\&]*)?)"); public static final Pattern SHORT_YOUTUBE_PATTERN = Pattern.compile("([\\w-]{11}([\\?\\&][\\w\\.\\=\\&]*)?)"); public static final String GRADES[] = { "A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "E", "F" }; public static final String FILTERHTML = "lessonbuilder.filterhtml"; public static final String LESSONBUILDER_ITEMID = "lessonbuilder.itemid"; public static final String LESSONBUILDER_ADDBEFORE = "sakai.addbefore"; public static final String LESSONBUILDER_PATH = "lessonbuilder.path"; public static final String LESSONBUILDER_BACKPATH = "lessonbuilder.backpath"; public static final String LESSONBUILDER_ID = "sakai.lessonbuildertool"; private static String PAGE = "simplepage.page"; private static String SITE_UPD = "site.upd"; private String contents = null; private String pageTitle = null; private String newPageTitle = null; private String subpageTitle = null; private boolean subpageNext = false; private boolean subpageButton = false; private String csrfToken = null; private List<Long> currentPath = null; private Set<Long> allowedPages = null; private Site currentSite = null; // cache, can be null; used by getCurrentSite private List<GroupEntry> currentGroups = null; private Set<String> myGroups = null; private String filterHtml = ServerConfigurationService.getString(FILTERHTML); public String selectedAssignment = null; public String selectedBlti = null; // generic entity stuff. selectedEntity is the string // coming from the picker. We'll use the same variable for any entity type public String selectedEntity = null; public String[] selectedEntities = new String[] {}; public String[] selectedGroups = new String[] {}; public String[] studentSelectedGroups = new String[] {}; public String selectedQuiz = null; public long removeId = 0; private SimplePage currentPage; private Long currentPageId = null; private Long currentPageItemId = null; private String currentUserId = null; private long previousPageId = -1; // Item-specific variables. These are set by setters which are called // by the various edit dialogs. So they're basically inputs to the // methods used to make changes to items. The way it works is that // when the user submits the form, RSF takes all the form variables, // calls setters for each field, and then calls the method specified // by the form. The setters set these variables public Long itemId = null; public boolean isMultimedia = false; public int multimediaDisplayType = 0; public String multimediaMimeType = null; public String commentsId; public boolean anonymous; public String comment; public String formattedComment; public String editId; public boolean graded, sGraded; public String gradebookTitle; public String maxPoints, sMaxPoints; public boolean comments; public boolean forcedAnon; public boolean groupOwned; public String questionType; public String questionText, questionCorrectText, questionIncorrectText; public String questionAnswer; public Boolean questionShowPoll; private HashMap<Integer, String> questionAnswers = null; public Long questionId; public String questionResponse; public boolean isWebsite = false; public boolean isCaption = false; private String linkUrl; private String height, width; private String description; private String name; private boolean required; private boolean replacefile; private boolean subrequirement; private boolean prerequisite; private boolean newWindow; private String dropDown; private String points; private String mimetype; // for BLTI, values window, inline, and null for in a new page with navigation // but sameWindow should also be set properly, based on the format private String format; private String numberOfPages; private boolean copyPage; private String alt = null; private String order = null; private String youtubeURL; private String mmUrl; private long youtubeId; private boolean hidePage; private Date releaseDate; private boolean hasReleaseDate; private boolean nodownloads; private String addBefore; // add new item before this item private String redirectSendingPage = null; private String redirectViewId = null; private String quiztool = null; private String topictool = null; private String assigntool = null; private boolean importtop = false; private Integer editPrivs = null; private String currentSiteId = null; public Map<String, MultipartFile> multipartMap; public String rubricSelections; public boolean peerEval; public String rubricTitle; public String rubricRow; private HashMap<Integer, String> rubricRows = null; private Date peerEvalDueDate; private Date peerEvalOpenDate; private boolean peerEvalAllowSelfGrade; // almost ISO format. real thing can't be done until Java 7. uses -0400 rather than -04:00 // SimpleDateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); SimpleDateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); public void setPeerEval(boolean peerEval) { this.peerEval = peerEval; } public void setRubricTitle(String rubricTitle) { this.rubricTitle = rubricTitle; } public void setRubricRow(String rubricRow) { this.rubricRow = rubricRow; if (rubricRows == null) { rubricRows = new HashMap<Integer, String>(); } rubricRows.put(rubricRows.size(), rubricRow); } public Date getPeerEvalDueDate() { return peerEvalDueDate; } // format comes back as 2014-05-27T16:15:00-04:00 // if user's computer is on a different time zone, we want the UI to match // Sakai. Hence we really want to handle everything as local time. // That means we want to ignore the time zone on input public void setPeerEvalDueDate(String date) { try { date = date.substring(0, 19); this.peerEvalDueDate = isoDateFormat.parse(date); } catch (Exception e) { System.out.println(e + "bad format duedate " + date); } } public Date getPeerEvalOpenDate() { return peerEvalOpenDate; } public void setPeerEvalOpenDate(String date) { try { date = date.substring(0, 19); this.peerEvalOpenDate = isoDateFormat.parse(date); } catch (Exception e) { System.out.println(e + "bad format duedate " + date); } } public boolean getPeerEvalAllowSelfGrade() { return peerEvalAllowSelfGrade; } public void setPeerEvalAllowSelfGrade(boolean self) { this.peerEvalAllowSelfGrade = self; } ArrayList<String> rubricPeerGrades, rubricPeerCategories; public String rubricPeerGrade; public void setRubricPeerGrade(String rubricPeerGrade) { this.rubricPeerGrade = rubricPeerGrade; if (rubricPeerGrades == null) { rubricPeerGrades = new ArrayList<String>(); rubricPeerCategories = new ArrayList<String>(); } int theColon = rubricPeerGrade.lastIndexOf(":"); rubricPeerGrades.add(rubricPeerGrade.substring(theColon + 1)); rubricPeerCategories.add(rubricPeerGrade.substring(0, theColon)); } // Caches // The following caches are used only during a single display of the page. I believe they // are so transient that we don't have to worry about synchronizing them or keeping them up to date. // Because the producer code tends to deal with items and even item ID's, it doesn't keep objects such // as Assignment or PublishedAssessment around. It calls functions here to worry about those. If we // don't cache, we'll be doing database lookups a lot. The worst is the code to see whether an item // is available. Because it checks all items above, we'd end up order N**2 in the number of items on the // page in database queries. It doesn't appear that assignments and assessments do any caching of their // own, but hibernate as we use it does. // Normal code shouldn't use the caches directly, but should call something like getAssignment here, // which checks the cache and if necessary calls the real getAssignment. I've chosen to do caching on // this level, and let the DAO be actual database access. I've really only optimized what is used by // ShowPageProducer, as that is used every time a page is shown. Things used when you add or change // an item aren't as critical. // If anyone is doing serious work on the code, I recommend creating an Item class that encapsulates // all the stuff associated with items. Then the producer would manipulate items. Thus the things in // these caches would be held in the Items. private Map<Long, SimplePageItem> itemCache = new HashMap<Long, SimplePageItem>(); private Map<Long, SimplePage> pageCache = new HashMap<Long, SimplePage>(); private Map<Long, List<SimplePageItem>> itemsCache = new HashMap<Long, List<SimplePageItem>>(); private Map<String, SimplePageLogEntry> logCache = new HashMap<String, SimplePageLogEntry>(); private Map<Long, Boolean> completeCache = new HashMap<Long, Boolean>(); private Map<Long, Boolean> visibleCache = new HashMap<Long, Boolean>(); // this one needs to be global private static Cache groupCache = null; // itemId => grouplist private static Cache resourceCache = null; protected static final int DEFAULT_EXPIRATION = 10 * 60; public static class PathEntry { public Long pageId; public Long pageItemId; public String title; } public static class UrlItem { public String Url; public String label; public String fa_icon = null; public UrlItem(String Url, String label) { this.Url = Url; this.label = label; } public UrlItem(String Url, String label, String fa_icon) { this.Url = Url; this.label = label; this.fa_icon = fa_icon; } } public static class GroupEntry { public String name; public String id; } public static class BltiTool { public int id; public String title; public String description; // can be null public String addText; public String addInstructions; // can be null } public static Map<Integer, BltiTool> bltiTools = initBltiTools(); public static Map<Integer, BltiTool> initBltiTools() { String[] bltiToolLines = ServerConfigurationService.getStrings("lessonbuilder.blti_tools"); if (bltiToolLines == null || bltiToolLines.length == 0) return null; CSVParser csvParser = new CSVParser(); Map<Integer, BltiTool> ret = new HashMap<Integer, BltiTool>(); for (int i = 0; i < bltiToolLines.length; i++) { String[] items = null; try { items = csvParser.parseLine(bltiToolLines[i]); } catch (Exception e) { System.out.println("bad blti tool spec in lessonbuilder.blti_tools " + i + " " + bltiToolLines[i]); continue; } if (items.length < 5) { System.out.println("bad blti tool spec in lessonbuilder.blti_tools " + i + " " + bltiToolLines[i]); continue; } BltiTool bltiTool = new BltiTool(); try { bltiTool.id = Integer.parseInt(items[0]); } catch (Exception e) { System.out.println( "first item in line not integer in lessonbuilder.blti_tools " + i + " " + bltiToolLines[i]); continue; } if (items[1] == null || items[1].length() == 0) { System.out.println( "second item in line missing in lessonbuilder.blti_tools " + i + " " + bltiToolLines[i]); continue; } bltiTool.title = items[1]; // allow null but not zero length if (items[2] == null || items[2].length() == 0) bltiTool.description = null; else bltiTool.description = items[2]; if (items[3] == null || items[3].length() == 0) { System.out.println( "third item in line missing in lessonbuilder.blti_tools " + i + " " + bltiToolLines[i]); continue; } bltiTool.addText = items[3]; // allow null but not zero length if (items[4] == null || items[4].length() == 0) bltiTool.addInstructions = null; else bltiTool.addInstructions = items[4]; ret.put(bltiTool.id, bltiTool); } for (BltiTool tool : ret.values()) { System.out.println(tool.id + " " + tool.title + " " + tool.description + " " + tool.addText); } return ret; } public BltiTool getBltiTool(int i) { if (bltiTools == null) return null; return bltiTools.get(i); } public Collection<BltiTool> getBltiTools() { if (bltiTools == null) return null; return bltiTools.values(); } // Image types public static ArrayList<String> imageTypes; static { imageTypes = new ArrayList<String>(); imageTypes.add("bmp"); imageTypes.add("gif"); imageTypes.add("icns"); imageTypes.add("ico"); imageTypes.add("jpg"); imageTypes.add("jpeg"); imageTypes.add("png"); imageTypes.add("tiff"); imageTypes.add("tif"); } // Spring Injection private SessionManager sessionManager; public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } private ContentHostingService contentHostingService; public void setContentHostingService(ContentHostingService contentHostingService) { this.contentHostingService = contentHostingService; } private GradebookIfc gradebookIfc = null; public void setGradebookIfc(GradebookIfc g) { gradebookIfc = g; } private LessonEntity forumEntity = null; public void setForumEntity(Object e) { forumEntity = (LessonEntity) e; } private LessonEntity quizEntity = null; public void setQuizEntity(Object e) { quizEntity = (LessonEntity) e; } private LessonEntity assignmentEntity = null; public void setAssignmentEntity(Object e) { assignmentEntity = (LessonEntity) e; } private LessonEntity bltiEntity = null; public void setBltiEntity(Object e) { bltiEntity = (LessonEntity) e; } private ToolManager toolManager; private LTIService ltiService; private SecurityService securityService; private SiteService siteService; private AuthzGroupService authzGroupService; private SimplePageToolDao simplePageToolDao; private LessonsAccess lessonsAccess; private LessonBuilderAccessService lessonBuilderAccessService; private MessageLocator messageLocator; public void setMessageLocator(MessageLocator x) { messageLocator = x; } public MessageLocator getMessageLocator() { return messageLocator; } static MemoryService memoryService = null; public void setMemoryService(MemoryService m) { memoryService = m; } private HttpServletResponse httpServletResponse; public void setHttpServletResponse(HttpServletResponse httpServletResponse) { this.httpServletResponse = httpServletResponse; } private LessonBuilderEntityProducer lessonBuilderEntityProducer; public void setLessonBuilderEntityProducer(LessonBuilderEntityProducer p) { lessonBuilderEntityProducer = p; } // End Injection static Class levelClass = null; static Object[] levels = null; static Class ftClass = null; static Method ftMethod = null; static Object ftInstance = setupFtStuff(); static Object setupFtStuff() { Object ret = null; try { levelClass = Class.forName("org.sakaiproject.util.api.FormattedText$Level"); levels = levelClass.getEnumConstants(); ftClass = Class.forName("org.sakaiproject.util.api.FormattedText"); ftMethod = ftClass.getMethod("processFormattedText", new Class[] { String.class, StringBuilder.class, levelClass }); ret = org.sakaiproject.component.cover.ComponentManager.get("org.sakaiproject.util.api.FormattedText"); return ret; } catch (Exception e) { log.error("Formatted Text with levels not available: " + e); return null; } } public void init() { TimeZone tz = TimeService.getLocalTimeZone(); isoDateFormat.setTimeZone(tz); if (groupCache == null) { groupCache = memoryService.createCache( "org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean.groupCache", new SimpleConfiguration<>(CACHE_MAX_ENTRIES, CACHE_TIME_TO_LIVE_SECONDS, CACHE_TIME_TO_IDLE_SECONDS)); } if (resourceCache == null) { resourceCache = memoryService .getCache("org.sakaiproject.lessonbuildertool.tool.beans.SimplePageBean.resourceCache"); } } static PagePickerProducer pagePickerProducer = null; // need the bean, because findallpages uses a global that's in the class */ public PagePickerProducer pagePickerProducer() { if (pagePickerProducer == null) { pagePickerProducer = new PagePickerProducer(); pagePickerProducer.setSimplePageBean(this); pagePickerProducer.setSimplePageToolDao(simplePageToolDao); } return pagePickerProducer; } // no destroy. We want to leave the cache intact when we exit, because there's one of us // per request. public SimplePageItem findItem(long itId) { Long itemId = itId; SimplePageItem ret = itemCache.get(itemId); if (ret != null) return ret; ret = simplePageToolDao.findItem(itemId); if (ret != null) itemCache.put(itemId, ret); return ret; } public SimplePage getPage(Long pageId) { SimplePage ret = pageCache.get(pageId); if (ret != null) return ret; ret = simplePageToolDao.getPage(pageId); if (ret != null) pageCache.put(pageId, ret); return ret; } // findStudentPage for current user // Calls appropriate Dao code depending upon whether it's controlled by group or individual owner. // by putting it here rather than in the Dao we can use caching for all the objects. // This is used student-side so optimiztion is important. public SimpleStudentPage findStudentPage(SimplePageItem item) { if (item.isGroupOwned()) { Set<String> myGroups = getMyGroups(); return simplePageToolDao.findStudentPage(item.getId(), myGroups); } else { return simplePageToolDao.findStudentPage(item.getId(), getCurrentUserId()); } } public List<String> errMessages() { ToolSession toolSession = sessionManager.getCurrentToolSession(); List<String> errors = (List<String>) toolSession.getAttribute("lessonbuilder.errors"); if (errors != null) toolSession.removeAttribute("lessonbuilder.errors"); return errors; } public void setErrMessage(String s) { ToolSession toolSession = sessionManager.getCurrentToolSession(); if (toolSession == null) { System.out.println("Lesson Builder error not in tool: " + s); return; } List<String> errors = (List<String>) toolSession.getAttribute("lessonbuilder.errors"); if (errors == null) errors = new ArrayList<String>(); errors.add(s); toolSession.setAttribute("lessonbuilder.errors", errors); } public void setErrKey(String key, String text) { if (text == null) text = ""; setErrMessage(messageLocator.getMessage(key).replace("{}", text)); } public void setTopRefresh() { ToolSession toolSession = sessionManager.getCurrentToolSession(); if (toolSession == null) return; toolSession.setAttribute("lessonbuilder.topRefresh", true); } public boolean getTopRefresh() { ToolSession toolSession = sessionManager.getCurrentToolSession(); if (toolSession.getAttribute("lessonbuilder.topRefresh") != null) { toolSession.removeAttribute("lessonbuilder.topRefresh"); return true; } return false; } // a lot of these are setters and getters used for the form process, as // described above public void setAlt(String alt) { this.alt = alt; } public String getDescription() { if (itemId != null && itemId != -1) { return findItem(itemId).getDescription(); } else { return null; } } public void setDescription(String description) { this.description = description; } public void setHidePage(boolean hide) { hidePage = hide; } // argument is in ISO8601 format, which has -04:00 time zone. // if user's computer is on a different time zone, we want the UI to match // Sakai. Hence we really want to handle everything as local time. // That means we want to ignore the time zone on input public void setReleaseDate(String date) { if (date.equals("")) this.releaseDate = null; else try { // if (date.substring(22,23).equals(":")) // date = date.substring(0,22) + date.substring(23,25); date = date.substring(0, 19); this.releaseDate = isoDateFormat.parse(date); } catch (Exception e) { System.out.println(e + "bad format releasedate " + date); } } public Date getReleaseDate() { return this.releaseDate; } public void setHasReleaseDate(boolean hasReleaseDate) { this.hasReleaseDate = hasReleaseDate; } public void setNodownloads(boolean n) { this.nodownloads = n; } public void setAddBefore(String n) { this.addBefore = n; } public void setImporttop(boolean i) { this.importtop = i; } // gets called for non-checked boxes also, but q will be null public void setQuiztool(String q) { if (q != null) quiztool = q; } public void setAssigntool(String q) { if (q != null) assigntool = q; } public void setTopictool(String q) { if (q != null) topictool = q; } public String getName() { if (itemId != null && itemId != -1) { return findItem(itemId).getName(); } else { return null; } } public void setName(String name) { this.name = name; } public void setRequired(boolean required) { this.required = required; } public void setSubrequirement(boolean subrequirement) { this.subrequirement = subrequirement; } public void setPrerequisite(boolean prerequisite) { this.prerequisite = prerequisite; } public void setReplacefile(boolean replacefile) { this.replacefile = replacefile; } public void setNewWindow(boolean newWindow) { this.newWindow = newWindow; } public void setDropDown(String dropDown) { this.dropDown = dropDown; } public void setPoints(String points) { this.points = points; } public void setFormat(String format) { this.format = format; } public void setMimetype(String mimetype) { if (mimetype != null) mimetype = mimetype.toLowerCase().trim(); this.mimetype = mimetype; } public String getPageTitle() { return getCurrentPage().getTitle(); } public void setPageTitle(String title) { pageTitle = title; } public void setNewPageTitle(String title) { newPageTitle = title; } public void setNumberOfPages(String n) { numberOfPages = n; } public void setCopyPage(boolean c) { this.copyPage = c; } public String getContents() { return (itemId != null && itemId != -1 ? findItem(itemId).getHtml() : ""); } public void setContents(String contents) { this.contents = contents; } public void setItemId(Long id) { itemId = id; } public Long getItemId() { return itemId; } public void setMultimedia(boolean isMm) { isMultimedia = isMm; } public void setMultimediaDisplayType(String type) { if (type != null && !type.trim().equals("")) { try { multimediaDisplayType = Integer.valueOf(type); } catch (Exception e) { } } } public void setMultimediaMimeType(String type) { multimediaMimeType = type; } public void setWebsite(boolean isWebsite) { this.isWebsite = isWebsite; } public void setCaption(boolean isCaption) { this.isCaption = isCaption; } // hibernate interposes something between us and saveItem, and that proxy gets an // error after saveItem does. Thus we never see any value that saveItem might // return. Hence we pass saveItem a list to which it adds the error message. If // there is a message from saveItem take precedence over the message we detect here, // since it's the root cause. public boolean saveItem(Object i, boolean requiresEditPermission) { String err = null; List<String> elist = new ArrayList<String>(); try { simplePageToolDao.saveItem(i, elist, messageLocator.getMessage("simplepage.nowrite"), requiresEditPermission); } catch (Throwable t) { // this is probably a bogus error, but find its root cause while (t.getCause() != null) { t = t.getCause(); } err = t.toString(); } // if we got an error from saveItem use it instead if (elist.size() > 0) err = elist.get(0); if (err != null) { setErrMessage(messageLocator.getMessage("simplepage.savefailed") + err); return false; } return true; } public boolean saveItem(Object i) { return saveItem(i, true); } public boolean update(Object i) { return update(i, true); } // see notes for saveupdate // requiresEditPermission determines whether simplePageToolDao should confirm // edit permissions before making the update boolean update(Object i, boolean requiresEditPermission) { String err = null; List<String> elist = new ArrayList<String>(); try { simplePageToolDao.update(i, elist, messageLocator.getMessage("simplepage.nowrite"), requiresEditPermission); } catch (Throwable t) { // this is probably a bogus error, but find its root cause while (t.getCause() != null) { t = t.getCause(); } err = t.toString(); } // if we got an error from saveItem use it instead if (elist.size() > 0) err = elist.get(0); if (err != null) { setErrMessage(messageLocator.getMessage("simplepage.savefailed") + err); return false; } return true; } // The permissions model assumes that all code operates on the current // page. When the current page is set, the set code verifies that the // page is in the current site. However when operating on items, we // have to make sure they are in the current page, or we could end up // hacking on an item in a completely different site. This method checks // that an item is OK to hack on, given the current page. private boolean itemOk(Long itemId) { // not specified, we'll add a new one if (itemId == null || itemId == -1) return true; SimplePageItem item = findItem(itemId); if (item.getPageId() != getCurrentPageId()) { return false; } return true; } // called by the producer that uses FCK to update a text block public String submit() { String rv = "success"; if (!itemOk(itemId)) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (canEditPage()) { Placement placement = toolManager.getCurrentPlacement(); // WARNING: keep in sync with code in AjaxFilter.java StringBuilder error = new StringBuilder(); // there's an issue with HTML security in the Sakai community. // a lot of people feel users shouldn't be able to add javascript, etc // to their HTML. I think enforcing that makes Sakai less than useful. // So check config options to see whether to do that check final Integer FILTER_DEFAULT = 0; final Integer FILTER_HIGH = 1; final Integer FILTER_LOW = 2; final Integer FILTER_NONE = 3; String html = contents; // figure out how to filter Integer filter = FILTER_DEFAULT; if (getCurrentPage().getOwner() != null) { filter = FILTER_DEFAULT; // always filter student content } else { // this is instructor content. // see if specified String filterSpec = placement.getPlacementConfig().getProperty("filterHtml"); if (filterSpec == null) filterSpec = filterHtml; // no, default to LOW. That will allow embedding but not Javascript if (filterSpec == null) // should never be null. unspeciifed should give "" filter = FILTER_DEFAULT; // old specifications else if (filterSpec.equalsIgnoreCase("true")) filter = FILTER_HIGH; // old value of true produced the same result as missing else if (filterSpec.equalsIgnoreCase("false")) filter = FILTER_NONE; // new ones else if (filterSpec.equalsIgnoreCase("default")) filter = FILTER_DEFAULT; else if (filterSpec.equalsIgnoreCase("high")) filter = FILTER_HIGH; else if (filterSpec.equalsIgnoreCase("low")) filter = FILTER_LOW; else if (filterSpec.equalsIgnoreCase("none")) filter = FILTER_NONE; // unspecified else filter = FILTER_DEFAULT; } if (filter.equals(FILTER_NONE)) { html = FormattedText.processHtmlDocument(contents, error); } else if (filter.equals(FILTER_DEFAULT)) { html = FormattedText.processFormattedText(contents, error); } else if (ftInstance != null) { try { // now filter is set. Implement it. Depends upon whether we have the anti-samy code Object level = null; if (filter.equals(FILTER_HIGH)) level = levels[1]; else level = levels[2]; html = (String) ftMethod.invoke(ftInstance, new Object[] { contents, error, level }); } catch (Exception e) { // this should never happen. If it does, emulate what the anti-samy // code does if antisamy is disabled. It always filters html = FormattedText.processFormattedText(contents, error); } } else { // don't have antisamy. For LOW, use old instructor behavior, since // LOW is the default. For high, it makes sense to filter if (filter.equals(FILTER_HIGH)) html = FormattedText.processFormattedText(contents, error); else html = FormattedText.processHtmlDocument(contents, error); } // WARNING: keep in sync with code in AjaxFilter.java // if (getCurrentPage().getOwner() != null || filterHtml // && !"false".equals(placement.getPlacementConfig().getProperty("filterHtml")) || // "true".equals(placement.getPlacementConfig().getProperty("filterHtml"))) { // html = FormattedText.processFormattedText(contents, error); //} else { // html = FormattedText.processHtmlDocument(contents, error); if (html != null) { SimplePageItem item; // itemid -1 means we're adding a new item to the page, // specified itemid means we're updating an existing one if (itemId != null && itemId != -1) { item = findItem(itemId); } else { item = appendItem("", "", SimplePageItem.TEXT); } item.setHtml(html); item.setPrerequisite(this.prerequisite); setItemGroups(item, selectedGroups); update(item); } else { rv = "cancel"; } placement.save(); String errString = error.toString(); if (errString != null && errString.length() > 0) setErrMessage(errString); } else { rv = "cancel"; } return rv; } public String cancel() { return "cancel"; } public String processMultimedia() { return processResource(SimplePageItem.MULTIMEDIA, false, false); } public String processResource() { return processResource(SimplePageItem.RESOURCE, false, false); } public String processWebSite() { return processResource(SimplePageItem.RESOURCE, true, false); } public String processCaption() { return processResource(SimplePageItem.RESOURCE, false, true); } // get mime type for a URL. connect to the server hosting // it and ask them. Sorry, but I don't think there's a better way public String getTypeOfUrl(String url) { String mimeType = "text/html"; // try to find the mime type of the remote resource // this is only likely to be a problem if someone is pointing to // a url within Sakai. We think in realistic cases those that are // files will be handled as files, so anything that comes where // will be HTML. That's the default if this fails. URLConnection conn = null; try { conn = new URL(new URL(ServerConfigurationService.getServerUrl()), url).openConnection(); conn.setConnectTimeout(10000); conn.setReadTimeout(10000); // generate cookie based on code in RequestFilter.java //String suffix = System.getProperty("sakai.serverId"); //if (suffix == null || suffix.equals("")) // suffix = "sakai"; //Session s = sessionManager.getCurrentSession(); //conn.setRequestProperty("Cookie", "JSESSIONID=" + s.getId() + "." + suffix); conn.connect(); String t = conn.getContentType(); if (t != null && !t.equals("")) { int i = t.indexOf(";"); if (i >= 0) t = t.substring(0, i); t = t.trim(); mimeType = t; } } catch (Exception e) { log.error("getTypeOfUrl connection error " + e); } finally { if (conn != null) { try { conn.getInputStream().close(); } catch (Exception e) { log.error("getTypeOfUrl unable to close " + e); } } } return mimeType; } // return call from the file picker, used by add resource // the picker communicates with us by session variables public String processResource(int type, boolean isWebSite, boolean isCaption) { if (!canEditPage()) return "permission-failed"; ToolSession toolSession = sessionManager.getCurrentToolSession(); Long itemId = (Long) toolSession.getAttribute(LESSONBUILDER_ITEMID); addBefore = (String) toolSession.getAttribute(LESSONBUILDER_ADDBEFORE); toolSession.removeAttribute(LESSONBUILDER_ITEMID); toolSession.removeAttribute(LESSONBUILDER_ADDBEFORE); if (!itemOk(itemId)) return "permission-failed"; // if itemId specified, better only be one resource, since we replacing an existing one List<Reference> refs = null; String returnMesssage = null; if (toolSession.getAttribute(FilePickerHelper.FILE_PICKER_CANCEL) == null && toolSession.getAttribute(FilePickerHelper.FILE_PICKER_ATTACHMENTS) != null) { refs = (List) toolSession.getAttribute(FilePickerHelper.FILE_PICKER_ATTACHMENTS); //Changed 'refs.size != 1' to refs.isEmpty() as there can be multiple Resources // more than one is an error if replacing an existing one if (refs == null || refs.isEmpty()) return "no-reference"; // if item id specified, use first item only. Can't really return an error because of the way // the UI works if (itemId != null && itemId != -1) returnMesssage = processSingleResource(refs.get(0), type, isWebSite, isCaption, itemId); else { for (Reference reference : refs) { returnMesssage = processSingleResource(reference, type, isWebSite, isCaption, itemId); } } toolSession.removeAttribute(FilePickerHelper.FILE_PICKER_ATTACHMENTS); toolSession.removeAttribute(FilePickerHelper.FILE_PICKER_CANCEL); } else { toolSession.removeAttribute(FilePickerHelper.FILE_PICKER_ATTACHMENTS); toolSession.removeAttribute(FilePickerHelper.FILE_PICKER_CANCEL); return "cancel"; } return returnMesssage; } //This method is written to enable user to select multiple Resources from the tool private String processSingleResource(Reference reference, int type, boolean isWebSite, boolean isCaption, Long itemId) { ToolSession toolSession = sessionManager.getCurrentToolSession(); String id = reference.getId(); String description = reference.getProperties().getProperty(ResourceProperties.PROP_DESCRIPTION); String name = reference.getProperties().getProperty("DAV:displayname"); // URLs are complex. There are two issues: // 1) The stupid helper treats a URL as a file upload. Have to make it a URL type. // I suspect we're intended to upload a file from the URL, but I don't think // any part of Sakai actually does that. So we reset Sakai's file type to URL // 2) Lesson builder needs to know the mime type, to know how to set up the // OBJECT or IFRAME. We send that out of band in the "html" field of the // lesson builder item entry. I see no way to do that other than to talk // to the server at the other end and see what MIME type it claims. String mimeType = reference.getProperties().getProperty("DAV:getcontenttype"); if (mimeType.equals("text/url")) { mimeType = null; // use default rules if we can't find it String url = null; // part 1, fix up the type fields boolean pushed = false; try { pushed = pushAdvisor(); ContentResourceEdit res = contentHostingService.editResource(id); res.setContentType("text/url"); res.setResourceType("org.sakaiproject.content.types.urlResource"); url = new String(res.getContent()); contentHostingService.commitResource(res, NotificationService.NOTI_NONE); } catch (Exception ignore) { return "no-reference"; } finally { if (pushed) popAdvisor(); } // part 2, find the actual data type. if (url != null) mimeType = getTypeOfUrl(url); } else if (isCaption) { // sakai probably sees it as a normal text file. // some browsers require the mime type to be right boolean pushed = false; try { pushed = pushAdvisor(); ContentResourceEdit res = contentHostingService.editResource(id); res.setContentType("text/vtt"); contentHostingService.commitResource(res, NotificationService.NOTI_NONE); } catch (Exception ignore) { return "no-reference"; } finally { if (pushed) popAdvisor(); } } boolean pushed = false; try { // I don't think we want the user adding anything he doesn't have access to // accessservice depends upon that // pushed = pushAdvisor(); contentHostingService.checkResource(id); } catch (PermissionException e) { return "permission-exception"; } catch (IdUnusedException e) { // Typically Means Cancel return "cancel"; } catch (TypeException e) { return "type-exception"; } // }finally { // if(pushed) popAdvisor(); //} String[] split = id.split("/"); if ("application/zip".equals(mimeType) && isWebSite) { // We need to set the sakaiId to the resource id of the index file id = expandZippedResource(id); if (id == null) return "failed"; // We set this special type for the html field in the db. This allows us to // map an icon onto website links in applicationContext.xml // originally it was a special type. The problem is that this is actually // an HTML file, and we may have trouble if we don't show it that way mimeType = "LBWEBSITE"; // strip .ZIP off the name if (name == null) { name = split[split.length - 1]; } if (name.lastIndexOf(".") > 0) name = name.substring(0, name.lastIndexOf(".")); } SimplePageItem i; if (itemId != null && itemId != -1 && isCaption) { // existing item, add or change caption i = findItem(itemId); i.setAttribute("captionfile", id); } else if (itemId != null && itemId != -1) { // updating existing item i = findItem(itemId); // editing an existing item which might have customized properties // retrieve the original resource and check for customizations ResourceHelper resHelp = new ResourceHelper(getContentResource(i.getSakaiId())); boolean hasCustomName = !isWebsite && resHelp.isNameCustom(i.getName()); // ignore website names for now boolean hasCustomDesc = resHelp.isDescCustom(i.getDescription()); i.setSakaiId(id); if (mimeType != null) i.setHtml(mimeType); if (!hasCustomName) { i.setName(name != null ? name : split[split.length - 1]); } if (!hasCustomDesc) { i.setDescription(description); } clearImageSize(i); // with a new underlying file, it's hard to see how an old caption file // could still be valid i.removeAttribute("captionfile"); } else { // adding new item i = appendItem(id, (name != null ? name : split[split.length - 1]), type); if (mimeType != null) { i.setHtml(mimeType); } i.setDescription(description); i.setSameWindow(false); } i.setAttribute("addedby", getCurrentUserId()); update(i); return "importing"; } private ContentResource getContentResource(String id) { ContentResource res = null; boolean pushed = false; try { pushed = pushAdvisor(); res = contentHostingService.getResource(id); } catch (PermissionException pe) { // ignore } catch (IdUnusedException iue) { // ignore } catch (TypeException te) { // ignore } finally { if (pushed) { popAdvisor(); } } return res; } // set default for image size for new objects private void clearImageSize(SimplePageItem i) { // defaults to a fixed width and height, appropriate for some things, but for an // image, leave it blank, since browser will then use the native size if (i.getType() == SimplePageItem.MULTIMEDIA) { if (isImageType(i)) { i.setHeight(""); i.setWidth(""); } } } // main code for adding a new item to a page private SimplePageItem appendItem(String id, String name, int type) { // add at the end of the page List<SimplePageItem> items = getItemsOnPage(getCurrentPageId()); // ideally the following should be the same, but there can be odd cases. So be safe long before = 0; boolean addAfter = false; if (addBefore != null && addBefore.startsWith("-")) { addAfter = true; addBefore = addBefore.substring(1); } if (addBefore != null && !addBefore.equals("")) { try { before = Long.parseLong(addBefore); } catch (Exception e) { // nothing. ignore bad arg } } // we have an item id. insert before it int nseq = 0; // sequence number of new item boolean after = false; // we found the item to insert before if (before > 0) { // have an item number specified, look for the item to insert before for (SimplePageItem item : items) { if (item.getId() == before) { // found item to insert before // use its sequence and bump up it and all after nseq = item.getSequence(); after = true; if (addAfter) { nseq++; continue; } } if (after) { item.setSequence(item.getSequence() + 1); simplePageToolDao.quickUpdate(item); } } } // if after not set, we didn't find the item; either no item specified or it // isn't on the page if (!after) { nseq = items.size(); if (nseq > 0) { int seq = items.get(nseq - 1).getSequence(); if (seq > nseq) nseq = seq; } nseq++; } SimplePageItem i = simplePageToolDao.makeItem(getCurrentPageId(), nseq, type, id, name); // defaults to a fixed width and height, appropriate for some things, but for an // image, leave it blank, since browser will then use the native size clearImageSize(i); saveItem(i); ToolSession toolSession = sessionManager.getCurrentToolSession(); toolSession.setAttribute("lessonbuilder.newitem", "" + i.getId()); return i; } /** * isPageOwner(page) * * if it's a student page and currernt user is owner or in owning group * **/ public boolean isPageOwner(SimplePage page) { String owner = page.getOwner(); String group = page.getGroup(); if (group != null) group = "/site/" + page.getSiteId() + "/group/" + group; if (owner == null) return false; if (group == null) return owner.equals(getCurrentUserId()); else return authzGroupService.getUserRole(getCurrentUserId(), group) != null; } public boolean isPageOwner(SimpleStudentPage page) { String owner = page.getOwner(); String group = page.getGroup(); if (group != null) group = "/site/" + getCurrentSiteId() + "/group/" + group; if (owner == null) return false; if (group == null) return owner.equals(getCurrentUserId()); else return authzGroupService.getUserRole(getCurrentUserId(), group) != null; } /** * Returns 0 if user has site.upd or simplepage.upd. * Returns 1 if user is page owner * Returns 2 otherwise * @return */ public int getEditPrivs() { if (editPrivs != null) { return editPrivs; } editPrivs = 2; String ref = "/site/" + getCurrentSiteId(); boolean ok = securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_UPDATE, ref); if (ok) editPrivs = 0; SimplePage page = getCurrentPage(); if (editPrivs != 0 && page != null && isPageOwner(page)) { editPrivs = 1; } return editPrivs; } /** * Returns true if user has site.upd, simplepage.upd, or is page owner. * False otherwise. * @return */ public boolean canEditPage() { if (getEditPrivs() <= 1) { return true; } else { return false; } } public boolean canReadPage() { String ref = "/site/" + getCurrentSiteId(); return securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_READ, ref); } public boolean canEditSite() { String ref = "/site/" + getCurrentSiteId(); return securityService.unlock("site.upd", ref); } public boolean canSeeAll() { if (canEditPage()) return true; String ref = "/site/" + getCurrentSiteId(); return securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_SEE_ALL, ref); } public void setLtiService(LTIService service) { ltiService = service; } public void setToolManager(ToolManager toolManager) { this.toolManager = toolManager; } public void setSecurityService(SecurityService service) { securityService = service; } public void setSiteService(SiteService service) { siteService = service; } public void setAuthzGroupService(AuthzGroupService authzGroupService) { this.authzGroupService = authzGroupService; } public void setSimplePageToolDao(Object dao) { simplePageToolDao = (SimplePageToolDao) dao; } public void setLessonsAccess(LessonsAccess a) { lessonsAccess = a; } public void setLessonBuilderAccessService(LessonBuilderAccessService a) { lessonBuilderAccessService = a; } public List<SimplePageItem> getItemsOnPage(long pageid) { List<SimplePageItem> items = itemsCache.get(pageid); if (items != null) return items; items = simplePageToolDao.findItemsOnPage(pageid); // This code adds a global comments tool to the bottom of each // student page, but only if there's something else on the page // already and the instructor has enabled the option. // For some reason these are added to the beginning. In ShowPageProducer // they are moved to the end. Beacuse that reverses the order, put peer first // here in order to get it last. We need to check whether we can't just put // them at the end in the first place. if (items.size() > 0) { SimplePage page = getPage(pageid); if (page.getOwner() != null) { SimpleStudentPage student = simplePageToolDao.findStudentPage(page.getTopParent()); if (student != null && student.getCommentsSection() != null) { SimplePageItem item = simplePageToolDao.findItem(student.getItemId()); if (item != null && item.getShowPeerEval() != null && item.getShowPeerEval()) { String peerEval = item.getAttributeString(); SimplePageItem studItem = new SimplePageItemImpl(); studItem.setSakaiId(page.getTopParent().toString()); studItem.setAttributeString(peerEval); studItem.setName("peerEval"); studItem.setPageId(-10L); studItem.setType(SimplePageItem.PEEREVAL); // peer eval defined in SimplePageItem.java items.add(0, studItem); } if (item != null && item.getShowComments() != null && item.getShowComments()) { //copy the attribute string from the top student section page to each student page items.add(0, simplePageToolDao.findItem(student.getCommentsSection())); } } } } for (SimplePageItem item : items) { itemCache.put(item.getId(), item); } itemsCache.put(pageid, items); return items; } public String deleteItem() { if (!itemOk(itemId) || !canEditPage()) { return "permission-failed"; } if (!checkCsrf()) return "permission-failed"; SimplePageItem i = findItem(itemId); if (i == null) { log.warn("deleteItem: null item. id: " + itemId); return "failure"; } return deleteItem(i); } public String deleteItem(SimplePageItem i) { int seq = i.getSequence(); boolean b = false; // if access controlled, clear it before deleting item if (i.isPrerequisite()) { i.setPrerequisite(false); checkControlGroup(i, false); } // Also delete gradebook entries if (i.getGradebookId() != null) { gradebookIfc.removeExternalAssessment(getCurrentSiteId(), i.getGradebookId()); } if (i.getAltGradebook() != null) { gradebookIfc.removeExternalAssessment(getCurrentSiteId(), i.getAltGradebook()); } b = simplePageToolDao.deleteItem(i); if (b) { List<SimplePageItem> list = getItemsOnPage(getCurrentPageId()); for (SimplePageItem item : list) { if (item.getSequence() > seq) { item.setSequence(item.getSequence() - 1); update(item); } } return "successDelete"; } else { log.warn("deleteItem error deleting Item: " + itemId); return "failure"; } } // not clear whether it's worth caching this. The first time it's called for a site // the pages are fetched. Beyond that it's a linear search of pages that are in memory // ids are sakai.assignment.grades, sakai.samigo, sakai.mneme, sakai.forums, sakai.jforum.tool public String getCurrentTool(String commonToolId) { Site site = getCurrentSite(); ToolConfiguration tool = site.getToolForCommonId(commonToolId); if (tool == null) return null; return tool.getId(); } public String getCurrentToolTitle(String commonToolId) { Site site = getCurrentSite(); ToolConfiguration tool = site.getToolForCommonId(commonToolId); if (tool == null) return null; return tool.getTitle(); } public Site getCurrentSite() { if (currentSite != null) // cached value return currentSite; try { currentSite = siteService.getSite(getCurrentSiteId()); } catch (Exception impossible) { impossible.printStackTrace(); } return currentSite; } // after someone else hacks on the site public void clearCurrentSite() { currentSite = null; } // find page to show in next link // If the current page is a LB page, and it has a single "next" link on it, use that // If the current page is a LB page, and it has more than one // "next" link on it, show no next. If there's more than one // next, this is probably a page with a branching question, in // which case there really isn't a single next. // If the current page is a LB page, and it is not finished (i.e. // there are required items not done), there is no next, or next // is grayed out. // Otherwise look at the page above in the breadcrumbs. If the // next item on the page is not an inline item, and the item is // available, next should be the next item on that page. (If // it's an inline item we need to go back to the page above so // they can see the inline item next.) // If the current page is something like a test, there is an // issue. What if the next item is not available when the page is // displayed, because it requires that you get a passing score on // the current test? For the moment, if the current item is required // but the next is not available, show the link but test it when it's // clicked. // TODO: showpage and showitem, implement next. Should not pass a // path argument. That gives next. If there's a pop we do it. // in showitem, check if it's available, if not, show an error // with a link to the page above. // return: new item on same level, null if none, the item arg if need to go up a level // java really needs to be able to return more than one thing, item == item is being // used as a flag to return up a level public SimplePageItem findNextPage(SimplePageItem item) { if (item.getType() == SimplePageItem.PAGE) { Long pageId = Long.valueOf(item.getSakaiId()); List<SimplePageItem> items = getItemsOnPage(pageId); int nexts = 0; SimplePageItem nextPage = null; for (SimplePageItem i : items) { if (i.getType() == SimplePageItem.PAGE && i.getNextPage()) { nextPage = i; nexts++; } } // if next, use it; no next if not ready if (nexts == 1) { if (isItemAvailable(nextPage, pageId)) return nextPage; return null; } // more than one, presumably you're intended to pick one of them, and // there is no generic next if (nexts > 1) { return null; } // if this is a next page, if there's no explicIt next it's // not clear that it makes sense to go anywhere. it's kind of // detached from its parent if (item.getNextPage()) return null; // here for a page with no explicit next. Treat like any other item // except that we need to compute path op. Page must be complete or we // would have returned null. } else if (item.getType() == SimplePageItem.STUDENT_CONTENT) { return null; } // this should be a top level page. We're not currently doing next for that. // we have to trap it because now and then we have items with bogus 0 page ID, so we // could get a spurious next item if (item.getPageId() == 0L) return null; // see if there's an actual next we can go to, otherwise calling page SimplePageItem nextItem = simplePageToolDao.findNextItemOnPage(item.getPageId(), item.getSequence()); // skip items which won't show because user isn't in the group while (nextItem != null && !isItemVisible(nextItem)) { nextItem = simplePageToolDao.findNextItemOnPage(nextItem.getPageId(), nextItem.getSequence()); } boolean available = false; if (nextItem != null) { int itemType = nextItem.getType(); if (itemType == SimplePageItem.ASSIGNMENT || itemType == SimplePageItem.ASSESSMENT || itemType == SimplePageItem.FORUM || itemType == SimplePageItem.PAGE || itemType == SimplePageItem.BLTI || itemType == SimplePageItem.RESOURCE && nextItem.isSameWindow()) { // it's easy if the next item is available. If it's not, then // we need to see if everything other than this item is done and // this one is required. In that case the issue must be that this // one isn't finished yet. Let's assume the user is going to finish // this one. We'll verify that when he actually does the next; if (isItemAvailable(nextItem, item.getPageId()) || item.isRequired() && wouldItemBeAvailable(item, item.getPageId())) return nextItem; } } // otherwise return to calling page return item; // special flag } // corresponding code for outputting the link // perhaps I should adjust the definition of path so that normal items show on it and not just pages // but at the moment path is just the pages. So when we're in a normal item, it doesn't show. // that means that as we do Next between items and pages, when we go to a page it gets pushed // on and when we go from a page to an item, the page has to be popped off. public void addNextLink(UIContainer tofill, SimplePageItem item) { SimplePageItem nextItem = findNextPage(item); if (nextItem == item) { // that we need to go up a level List<PathEntry> path = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_PATH); int top; if (path == null) top = -1; else top = path.size() - 1; // if we're on a page, have to pop it off first // for a normal item the end of the path already is the page above if (item.getType() == SimplePageItem.PAGE) top--; if (top >= 0) { PathEntry e = path.get(top); GeneralViewParameters view = new GeneralViewParameters(ShowPageProducer.VIEW_ID); view.setSendingPage(e.pageId); view.setItemId(e.pageItemId); view.setPath(Integer.toString(top)); UIInternalLink.make(tofill, "next", messageLocator.getMessage("simplepage.next"), view); UIInternalLink.make(tofill, "next1", messageLocator.getMessage("simplepage.next"), view); } } else if (nextItem != null) { GeneralViewParameters view = new GeneralViewParameters(); int itemType = nextItem.getType(); if (itemType == SimplePageItem.PAGE) { view.setSendingPage(Long.valueOf(nextItem.getSakaiId())); view.viewID = ShowPageProducer.VIEW_ID; if (item.getType() == SimplePageItem.PAGE) view.setPath("next"); // page to page, just a next else view.setPath("push"); // item to page, have to push the page } else if (itemType == SimplePageItem.RESOURCE) { /// must be a same page resource view.setSendingPage(Long.valueOf(item.getPageId())); // to the check. We need the check to set access control appropriately // if the user has passed. if (!isItemAvailable(nextItem, nextItem.getPageId())) view.setRecheck("true"); String URL = nextItem.getItemURL(getCurrentSiteId(), getCurrentPage().getOwner()); if (lessonBuilderAccessService.needsCopyright(nextItem.getSakaiId())) URL = "/access/require?ref=" + URLEncoder.encode("/content" + nextItem.getSakaiId()) + "&url=" + URLEncoder.encode(URL.substring(7)); view.setSource(URL); view.viewID = ShowItemProducer.VIEW_ID; } else { view.setSendingPage(Long.valueOf(item.getPageId())); LessonEntity lessonEntity = null; switch (nextItem.getType()) { case SimplePageItem.ASSIGNMENT: lessonEntity = assignmentEntity.getEntity(nextItem.getSakaiId()); break; case SimplePageItem.ASSESSMENT: view.setClearAttr("LESSONBUILDER_RETURNURL_SAMIGO"); lessonEntity = quizEntity.getEntity(nextItem.getSakaiId(), this); break; case SimplePageItem.FORUM: lessonEntity = forumEntity.getEntity(nextItem.getSakaiId()); break; case SimplePageItem.BLTI: if (bltiEntity != null) lessonEntity = bltiEntity.getEntity(nextItem.getSakaiId()); break; } // normally we won't send someone to an item that // isn't available. But if the current item is a test, etc, we can't // know whether the user will pass it, so we have to ask ShowItem to // to the check. We need the check to set access control appropriately // if the user has passed. if (!isItemAvailable(nextItem, nextItem.getPageId())) view.setRecheck("true"); view.setSource((lessonEntity == null) ? "dummy" : lessonEntity.getUrl()); if (item.getType() == SimplePageItem.PAGE) view.setPath("pop"); // now on a have, have to pop it off view.viewID = ShowItemProducer.VIEW_ID; } view.setItemId(nextItem.getId()); view.setBackPath("push"); UIInternalLink.make(tofill, "next", messageLocator.getMessage("simplepage.next"), view); UIInternalLink.make(tofill, "next1", messageLocator.getMessage("simplepage.next"), view); } } // Because of the existence of chains of "next" pages, there's no static approach that will find // back links. Thus we keep track of the actual path the user has followed. However we have to // prune both path and back path when we return to an item that's already on them to avoid // loops of various kinds. public void addPrevLink(UIContainer tofill, SimplePageItem item) { List<PathEntry> backPath = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_BACKPATH); List<PathEntry> path = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_PATH); // current item is last on path, so need one before that if (backPath == null || backPath.size() < 2) return; PathEntry prevEntry = backPath.get(backPath.size() - 2); SimplePageItem prevItem = findItem(prevEntry.pageItemId); if (prevItem == null) return; GeneralViewParameters view = new GeneralViewParameters(); int itemType = prevItem.getType(); if (itemType == SimplePageItem.PAGE) { view.setSendingPage(Long.valueOf(prevItem.getSakaiId())); view.viewID = ShowPageProducer.VIEW_ID; // are we returning to a page? If so use existing path entry int lastEntry = -1; int i = 0; long prevItemId = prevEntry.pageItemId; for (PathEntry entry : path) { if (entry.pageItemId == prevItemId) lastEntry = i; i++; } if (lastEntry >= 0) view.setPath(Integer.toString(lastEntry)); else if (item.getType() == SimplePageItem.PAGE) view.setPath("next"); // page to page, just a next else view.setPath("push"); // item to page, have to push the page } else if (itemType == SimplePageItem.RESOURCE) { // must be a samepage resource view.setSendingPage(Long.valueOf(item.getPageId())); String URL = prevItem.getItemURL(getCurrentSiteId(), getCurrentPage().getOwner()); // this is unlikely but possible. If you don't accept the copyright, go on and // then go back, this will trigger if (lessonBuilderAccessService.needsCopyright(prevItem.getSakaiId())) URL = "/access/require?ref=" + URLEncoder.encode("/content" + prevItem.getSakaiId()) + "&url=" + URLEncoder.encode(URL.substring(7)); view.setSource(URL); view.viewID = ShowItemProducer.VIEW_ID; } else if (itemType == SimplePageItem.STUDENT_CONTENT) { view.setSendingPage(prevEntry.pageId); view.setItemId(prevEntry.pageItemId); view.viewID = ShowPageProducer.VIEW_ID; if (item.getType() == SimplePageItem.PAGE) { view.setPath("pop"); } else { view.setPath("next"); } } else { view.setSendingPage(Long.valueOf(item.getPageId())); LessonEntity lessonEntity = null; switch (prevItem.getType()) { case SimplePageItem.ASSIGNMENT: lessonEntity = assignmentEntity.getEntity(prevItem.getSakaiId()); break; case SimplePageItem.ASSESSMENT: view.setClearAttr("LESSONBUILDER_RETURNURL_SAMIGO"); lessonEntity = quizEntity.getEntity(prevItem.getSakaiId(), this); break; case SimplePageItem.FORUM: lessonEntity = forumEntity.getEntity(prevItem.getSakaiId()); break; case SimplePageItem.BLTI: if (bltiEntity != null) lessonEntity = bltiEntity.getEntity(prevItem.getSakaiId()); break; } view.setSource((lessonEntity == null) ? "dummy" : lessonEntity.getUrl()); if (item.getType() == SimplePageItem.PAGE) view.setPath("pop"); // now on a page, have to pop it off view.viewID = ShowItemProducer.VIEW_ID; } view.setItemId(prevItem.getId()); view.setBackPath("pop"); UIInternalLink.make(tofill, "prev", messageLocator.getMessage("simplepage.back"), view); UIInternalLink.make(tofill, "prev1", messageLocator.getMessage("simplepage.back"), view); } public String getCurrentSiteId() { if (currentSiteId != null) return currentSiteId; try { currentSiteId = toolManager.getCurrentPlacement().getContext(); return currentSiteId; } catch (Exception impossible) { return null; } } // so access can inject the siteid public void setCurrentSiteId(String siteId) { currentSiteId = siteId; } // recall that code typically operates on a "current page." See below for // the code that sets a new current page. We also have a current item, which // is the item defining the page. I.e. if the page is a subpage of another // one, this is the item on the parent page pointing to this page. If it's // a top-level page, it's a dummy item. The item is needed in order to do // access checks. Whether an item is required, etc, is stored in the item. // in theory a page could be called from several other pages, with different // access control parameters. So we need to know the actual item on the page // page from which this page was called. // we need to track the pageitem because references to the same page can appear // in several places. In theory each one could have different status of availability // so we need to know which in order to check availability public void updatePageItem(long item) throws PermissionException { SimplePageItem i = findItem(item); if (i != null) { if (i.getType() != SimplePageItem.STUDENT_CONTENT && (long) currentPageId != (long) Long.valueOf(i.getSakaiId())) { log.warn("updatePageItem permission failure " + i + " " + Long.valueOf(i.getSakaiId()) + " " + currentPageId); throw new PermissionException(getCurrentUserId(), "set item", Long.toString(item)); } } currentPageItemId = item; sessionManager.getCurrentToolSession().setAttribute("current-pagetool-item", item); } // update our concept of the current page. it is imperative to make sure the page is in // the current site, or people could hack on other people's pages // if you call updatePageObject, consider whether you need to call updatePageItem as well // this combines two functions, so maybe not, but any time you're going to a new page // you should do both. Make sure all Producers set the page to the one they will work on public void updatePageObject(long l, boolean save) throws PermissionException { if (l != previousPageId) { currentPage = getPage(l); String siteId = getCurrentSiteId(); // get a rare error here, trying to debug it if (currentPage == null || currentPage.getSiteId() == null) { throw new PermissionException(getCurrentUserId(), "set page", Long.toString(l)); } // page should always be in this site, or someone is gaming us if (!currentPage.getSiteId().equals(siteId)) throw new PermissionException(getCurrentUserId(), "set page", Long.toString(l)); previousPageId = l; if (save) { sessionManager.getCurrentToolSession().setAttribute("current-pagetool-page", l); } currentPageId = (Long) l; } } public void updatePageObject(long l) throws PermissionException { updatePageObject(l, true); } // if tool was reset, return last page from previous session, so we can give the user // a chance to go back public SimplePageToolDao.PageData toolWasReset() { if (sessionManager.getCurrentToolSession().getAttribute("current-pagetool-page") == null) { // no page in session, which means it was reset String toolId = ((ToolConfiguration) toolManager.getCurrentPlacement()).getPageId(); return simplePageToolDao.findMostRecentlyVisitedPage(getCurrentUserId(), toolId); } else return null; } // ought to be simple, but this is typically called at the beginning of a producer, when // the current page isn't set yet. So if there isn't one, we use the session variable // to tell us what the current page is. Note that a user can add our tool using Site // Info. Site info knows nothing about us, so it will make an entry for the page without // creating it. When the user then tries to go to the page, this code will be the firsst // to notice it. Hence we have to create pages that don't exist private long getCurrentPageId() { // return ((ToolConfiguration)toolManager.getCurrentPlacement()).getPageId(); if (currentPageId != null) return (long) currentPageId; Placement placement = toolManager.getCurrentPlacement(); // See whether the tool is disabled in Sakai site information // you can either hide or disable a tool. Our page hidden is // really a disable, so we sync Sakai's disabled with our hidden // we're only checking when you first go into a tool Properties roleConfig = placement.getPlacementConfig(); String roleList = roleConfig.getProperty("functions.require"); boolean siteHidden = (roleList != null && roleList.indexOf(SITE_UPD) > -1); // Let's go back to where we were last time. Long l = (Long) sessionManager.getCurrentToolSession().getAttribute("current-pagetool-page"); if (l != null && l != 0) { try { updatePageObject(l); Long i = (Long) sessionManager.getCurrentToolSession().getAttribute("current-pagetool-item"); if (i != null && i != 0) updatePageItem(i); } catch (PermissionException e) { e.printStackTrace(); log.warn("getCurrentPageId Permission failed setting to item in toolsession"); return 0; } // currentPage should now be set syncHidden(currentPage, siteHidden); return l; } else { // No recent activity. Let's go to the top level page. l = simplePageToolDao.getTopLevelPageId(((ToolConfiguration) placement).getPageId()); ; // l = simplePageToolDao.getTopLevelPageId(((ToolConfiguration) toolManager.getCurrentPlacement()).getPageId()); if (l != null) { try { updatePageObject(l); // this should exist except if the page was created by old code SimplePageItem i = simplePageToolDao.findTopLevelPageItemBySakaiId(String.valueOf(l)); if (i == null) { // and dummy item, the site is the notional top level page i = simplePageToolDao.makeItem(0, 0, SimplePageItem.PAGE, l.toString(), currentPage.getTitle()); saveItem(i); } updatePageItem(i.getId()); } catch (PermissionException e) { log.warn("getCurrentPageId Permission failed setting to page in toolsession"); return 0; } // currentPage should now be set syncHidden(currentPage, siteHidden); return l; } else { // No page found. Let's make a new one. String toolId = ((ToolConfiguration) toolManager.getCurrentPlacement()).getPageId(); String title = getCurrentSite().getPage(toolId).getTitle(); // Use title supplied // during creation SimplePage page = simplePageToolDao.makePage(toolId, getCurrentSiteId(), title, null, null); if (!saveItem(page)) { currentPage = null; return 0; } try { updatePageObject(page.getPageId()); l = page.getPageId(); // and dummy item, the site is the notional top level page SimplePageItem i = simplePageToolDao.makeItem(0, 0, SimplePageItem.PAGE, l.toString(), title); saveItem(i); updatePageItem(i.getId()); } catch (PermissionException e) { log.warn("getCurrentPageId Permission failed setting to new page"); return 0; } // currentPage should now be set syncHidden(currentPage, siteHidden); return l; } } } private void syncHidden(SimplePage page, boolean siteHidden) { // only do it for top level pages if (page != null && page.getParent() == null) { // hidden in site if (siteHidden != page.isHidden()) { page.setHidden(siteHidden); // use quick, as we don't want permission check. even normal users can do this simplePageToolDao.quickUpdate(page); } } } public void setCurrentPageId(long p) { currentPageId = p; } // current page must be set. public SimplePageItem getCurrentPageItem(Long itemId) { // if itemId is known, this is easy. but check to make sure it's // actually this page, to prevent the user gaming us if (itemId == null || itemId == -1) itemId = currentPageItemId; if (itemId != null && itemId != -1) { SimplePageItem ret = findItem(itemId); if (ret != null && (ret.getSakaiId().equals(Long.toString(getCurrentPageId())) || ret.getType() == SimplePageItem.STUDENT_CONTENT)) { try { updatePageItem(ret.getId()); } catch (PermissionException e) { log.warn("getCurrentPageItem Permission failed setting to specified item"); return null; } return ret; } else { return null; } } // else must be a top level item SimplePage page = getPage(getCurrentPageId()); SimplePageItem ret = simplePageToolDao.findTopLevelPageItemBySakaiId(Long.toString(getCurrentPageId())); if (ret == null && page.getOwner() != null) { ret = simplePageToolDao.findItemFromStudentPage(page.getPageId()); } if (ret == null) return null; try { updatePageItem(ret.getId()); } catch (PermissionException e) { log.warn("getCurrentPageItem Permission failed setting to top level page in tool session"); return null; } return ret; } // called at the start of showpageproducer, with page info for the page about to be displayed // updates the breadcrumbs, which are kept in session variables. // returns string version of the new path public String adjustPath(String op, Long pageId, Long pageItemId, String title) { List<PathEntry> path = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_PATH); // if no current path, op doesn't matter. we can just do the current page if (path == null || path.size() == 0) { PathEntry entry = new PathEntry(); entry.pageId = pageId; entry.pageItemId = pageItemId; entry.title = title; path = new ArrayList<PathEntry>(); path.add(entry); } else if (path.get(path.size() - 1).pageId.equals(pageId)) { // nothing. we're already there. this is to prevent // oddities if we refresh the page } else if (op == null || op.equals("") || op.equals("next")) { PathEntry entry = path.get(path.size() - 1); // overwrite last item entry.pageId = pageId; entry.pageItemId = pageItemId; entry.title = title; } else if (op.equals("push")) { // a subpage PathEntry entry = new PathEntry(); entry.pageId = pageId; entry.pageItemId = pageItemId; entry.title = title; path.add(entry); // put it on the end } else if (op.equals("pop")) { // a subpage path.remove(path.size() - 1); } else if (op.startsWith("log")) { // set path to what was saved in the last log entry for this item // this is used for users who go directly to a page from the // main list of pages. path = new ArrayList<PathEntry>(); SimplePageLogEntry logEntry = getLogEntry(pageItemId); if (logEntry != null) { String items[] = null; if (logEntry.getPath() != null) items = split(logEntry.getPath(), ","); if (items != null) { for (String s : items) { // don't see how this could happen, but it did if (s.trim().equals("")) { log.warn("adjustPath attempt to set invalid path: invalid item: " + op + ":" + logEntry.getPath()); return null; } SimplePageItem i = findItem(Long.valueOf(s)); if (i == null || i.getType() != SimplePageItem.PAGE) { log.warn("adjustPath attempt to set invalid path: invalid item: " + op); return null; } SimplePage p = getPage(Long.valueOf(i.getSakaiId())); if (p == null || !currentPage.getSiteId().equals(p.getSiteId())) { log.warn("adjustPath attempt to set invalid path: invalid page: " + op); return null; } PathEntry entry = new PathEntry(); entry.pageId = p.getPageId(); entry.pageItemId = i.getId(); entry.title = i.getName(); path.add(entry); } } } } else { int index = Integer.valueOf(op); // better be number if (index < path.size()) { // if we're going back, this should actually // be redundant PathEntry entry = path.get(index); // back to specified item entry.pageId = pageId; entry.pageItemId = pageItemId; entry.title = title; if (index < (path.size() - 1)) path.subList(index + 1, path.size()).clear(); } } // have new path; set it in session variable sessionManager.getCurrentToolSession().setAttribute(LESSONBUILDER_PATH, path); // and make string representation to return String ret = null; for (PathEntry entry : path) { String itemString = Long.toString(entry.pageItemId); if (ret == null) ret = itemString; else ret = ret + "," + itemString; } if (ret == null) ret = ""; return ret; } public void adjustBackPath(String op, Long pageId, Long pageItemId, String title) { List<PathEntry> backPath = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_BACKPATH); if (backPath == null) backPath = new ArrayList<PathEntry>(); // default case going directly to something. // normally we want to push it, but if it's already there, // we're going back to it, use the old one if (op == null || op.equals("")) { // is it there already? Some would argue that we should use the first occurrence int lastEntry = -1; int i = 0; long itemId = pageItemId; // to avoid having to use equals for (PathEntry entry : backPath) { if (entry.pageItemId == itemId) lastEntry = i; i++; } if (lastEntry >= 0) { // yes, back up to that entry if (lastEntry < (backPath.size() - 1)) backPath.subList(lastEntry + 1, backPath.size()).clear(); return; } // no fall through and push the new item } if (op.equals("pop")) { if (backPath.size() > 0) backPath.remove(backPath.size() - 1); } else { // push or no operation PathEntry entry = new PathEntry(); entry.pageId = pageId; entry.pageItemId = pageItemId; entry.title = title; backPath.add(entry); } // have new path; set it in session variable sessionManager.getCurrentToolSession().setAttribute(LESSONBUILDER_BACKPATH, backPath); } public void setSubpageTitle(String st) { subpageTitle = st; } public void setSubpageNext(boolean s) { subpageNext = s; } public void setSubpageButton(boolean s) { subpageButton = s; } public void setCsrfToken(String s) { csrfToken = s; } // called from "select page" dialog in Reorder to insert items from anoher page public String selectPage() { if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; ToolSession toolSession = sessionManager.getCurrentToolSession(); toolSession.setAttribute("lessonbuilder.selectedpage", selectedEntity); // doesn't do anything but call back reorder // the submit sets selectedEntity, which is passed to Reorder by addResultingViewBinding return "selectpage"; } // called from "add subpage" dialog // create if itemId == null or -1, else update existing public String createSubpage() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; String title = subpageTitle; boolean makeNewPage = (selectedEntity == null || selectedEntity.length() == 0); boolean makeNewItem = (itemId == null || itemId == -1); // make sure the page is legit if (!makeNewPage) { SimplePage p = getPage(Long.valueOf(selectedEntity)); if (p == null || !getCurrentSiteId().equals(p.getSiteId())) { log.warn("addpage tried to add invalid page: " + selectedEntity); return "invalidpage"; } } if ((title == null || title.length() == 0) && (selectedEntity == null || selectedEntity.length() == 0)) { return "notitle"; } SimplePage page = getCurrentPage(); Long parent = page.getPageId(); Long topParent = page.getTopParent(); // Allows students to make subpages of Student Content pages String owner = page.getOwner(); String group = page.getGroup(); if (topParent == null) { topParent = parent; } String toolId = ((ToolConfiguration) toolManager.getCurrentPlacement()).getPageId(); SimplePage subpage = null; if (makeNewPage) { subpage = simplePageToolDao.makePage(toolId, getCurrentSiteId(), title, parent, topParent); subpage.setOwner(owner); subpage.setGroup(group); saveItem(subpage); selectedEntity = String.valueOf(subpage.getPageId()); } else { subpage = getPage(Long.valueOf(selectedEntity)); } SimplePageItem i = null; if (makeNewItem) i = appendItem(selectedEntity, subpage.getTitle(), SimplePageItem.PAGE); else { i = findItem(itemId); } if (i == null) return "failure"; if (makeNewItem) { i.setNextPage(subpageNext); if (subpageButton) i.setFormat("button"); else i.setFormat(""); } else { // when itemid is specified, we're changing pages for existing entry i.setSakaiId(selectedEntity); i.setName(subpage.getTitle()); } update(i); if (makeNewPage) { // if creating new entry, go to it try { updatePageObject(subpage.getPageId()); updatePageItem(i.getId()); } catch (PermissionException e) { log.warn("createSubpage permission failed going to new page"); return "failed"; } adjustPath((subpageNext ? "next" : "push"), subpage.getPageId(), i.getId(), i.getName()); submit(); } return "success"; } public String deleteOrphanPages() { if (getEditPrivs() != 0) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; // code is mostly from PagePickerProducer // list we're going to display List<PagePickerProducer.PageEntry> entries = new ArrayList<PagePickerProducer.PageEntry>(); // build map of all pages, so we can see if any are left over Map<Long, SimplePage> pageMap = new HashMap<Long, SimplePage>(); Set<Long> sharedPages = new HashSet<Long>(); // all pages List<SimplePage> pages = simplePageToolDao.getSitePages(getCurrentSiteId()); for (SimplePage p : pages) pageMap.put(p.getPageId(), p); List<SimplePageItem> sitePages = simplePageToolDao.findItemsInSite(getCurrentSiteId()); Set<Long> topLevelPages = new HashSet<Long>(); for (SimplePageItem i : sitePages) topLevelPages.add(Long.valueOf(i.getSakaiId())); // this adds everything you can find from top level pages to entries for (SimplePageItem sitePageItem : sitePages) { // System.out.println("findallpages " + sitePageItem.getName() + " " + true); pagePickerProducer().findAllPages(sitePageItem, entries, pageMap, topLevelPages, sharedPages, 0, true, true); } // everything we didn't find should be deleted. It's items remaining in pagemap List<String> orphans = new ArrayList<String>(); if (pageMap.size() > 0) { for (SimplePage p : pageMap.values()) { // non-null owner are student pages if (p.getOwner() == null) { orphans.add(Long.toString(p.getPageId())); } } // do the deletetion // selectedEntities is the argument for deletePages selectedEntities = orphans.toArray(selectedEntities); deletePages(); } return "success"; } public String deletePages() { if (getEditPrivs() != 0) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; String siteId = getCurrentSiteId(); for (int i = 0; i < selectedEntities.length; i++) { deletePage(siteId, Long.valueOf(selectedEntities[i])); if ((i % 10) == 0) { // we've seen situations with a million orphan pages // we don't want to leave those all in cache simplePageToolDao.flush(); simplePageToolDao.clear(); } } return "success"; } public void deletePage(String siteId, Long pageId) { SimplePage target = simplePageToolDao.getPage(pageId); if (target != null) { if (!target.getSiteId().equals(siteId)) { return; } // delete all the items on the page List<SimplePageItem> items = simplePageToolDao.findItemsOnPage(target.getPageId()); for (SimplePageItem item : items) { // if access controlled, clear it before deleting item if (item.isPrerequisite()) { item.setPrerequisite(false); // doesn't seem to use any internal data checkControlGroup(item, false); } // delete gradebook entries if (item.getGradebookId() != null) { gradebookIfc.removeExternalAssessment(siteId, item.getGradebookId()); } if (item.getAltGradebook() != null) { gradebookIfc.removeExternalAssessment(siteId, item.getAltGradebook()); } //actually delete item simplePageToolDao.deleteItem(item); } // remove from gradebook Double currentPoints = target.getGradebookPoints(); if (currentPoints != null && currentPoints != 0.0) gradebookIfc.removeExternalAssessment(siteId, "lesson-builder:" + pageId); // remove fake item if it's top level. We won't see it if it's still active // so this means the user has removed it in site info SimplePageItem item = simplePageToolDao.findTopLevelPageItemBySakaiId(pageId + ""); if (item != null) simplePageToolDao.deleteItem(item); // currently the UI doesn't allow you to kill top level pages until they have been // removed by site info, so we don't have to hack on the site pages // remove page simplePageToolDao.deleteItem(target); } } // remove a top-level page from the left margin. Does not actually delete it. // this and addpages checks only edit page permission. should it check site.upd? public String removePage() { if (getEditPrivs() != 0) { return "permission-failed"; } if (!checkCsrf()) return "permission-failed"; // if (removeId == 0) // removeId = getCurrentPageId(); SimplePage page = getPage(removeId); if (page == null) return "no-such-page"; if (page.getOwner() == null) { // this code should never be called return "failure"; } else { SimpleStudentPage studentPage = simplePageToolDao.findStudentPageByPageId(page.getPageId()); if (studentPage != null) { studentPage.setDeleted(true); update(studentPage, false); String[] path = split(adjustPath("pop", null, null, null), ","); Long itemId = Long.valueOf(path[path.length - 1]); try { SimplePageItem item = simplePageToolDao.findItem(itemId); updatePageObject(Long.valueOf(item.getSakaiId())); updatePageItem(itemId); } catch (PermissionException e) { return "failure"; } return "success"; } else { return "failure"; } } } // called from "save" in main edit item dialog public String editItem() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (name.length() < 1) { return "Notitle"; } SimplePageItem i = findItem(itemId); if (i == null) { return "failure"; } else { i.setName(name); i.setDescription(description); i.setRequired(required); i.setPrerequisite(prerequisite); i.setSubrequirement(subrequirement); i.setNextPage(subpageNext); if (subpageButton) i.setFormat("button"); else i.setFormat(""); if (points != "") { i.setRequirementText(points); } else { i.setRequirementText(dropDown); } // currently we only display HTML in the same page if (i.getType() == SimplePageItem.RESOURCE) i.setSameWindow(!newWindow); else i.setSameWindow(false); if (i.getType() == SimplePageItem.BLTI) { if (format == null || format.trim().equals("")) i.setFormat(""); else i.setFormat(format); // this is redundant, but the display code uses it if ("window".equals(format)) i.setSameWindow(false); else i.setSameWindow(true); i.setHeight(height); } update(i); if (i.getType() == SimplePageItem.PAGE) { SimplePage page = getPage(Long.valueOf(i.getSakaiId())); if (page != null) { page.setTitle(name); update(page); } } else { checkControlGroup(i, i.isPrerequisite()); } setItemGroups(i, selectedGroups); return "successEdit"; // Shouldn't reload page } } // Set access control for an item to the state requested by i.isPrerequisite(). // This code should depend only upon isPrerequisite() in the item object, not the database, // because we call it when deleting or updating items, before saving them to the database. // The caller will update the item in the database, typically after this call // correct is correct value, i.e whether it hsould be there or not // WARNING: with argument false is called by the service layer. Can't depend upon data in the bean public void checkControlGroup(SimplePageItem i, boolean correct) { if (i.getType() == SimplePageItem.RESOURCE) { checkControlResource(i, correct); return; } if (i.getType() != SimplePageItem.ASSESSMENT && i.getType() != SimplePageItem.ASSIGNMENT && i.getType() != SimplePageItem.FORUM) { // We only do this for assignments and assessments // currently we can't actually set it for forum topics return; } String sakaiId = i.getSakaiId(); // /sam_core/nnn isn't published. It can't have groups. When it is published // we do a fixup which will call this again to create the real groups if (sakaiId.equals(SimplePageItem.DUMMY) || sakaiId.startsWith("/sam_core/")) return; SimplePageGroup group = simplePageToolDao.findGroup(i.getSakaiId()); String ourGroupName = null; try { // correct is the correct setting, i.e. if there is supposed to be // a group or not. We only change if reality disagrees with it. if (correct) { if (group == null) { // create our a new access control group, and save the current tool group list with it. LessonEntity lessonEntity = null; switch (i.getType()) { case SimplePageItem.ASSIGNMENT: lessonEntity = assignmentEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.ASSESSMENT: lessonEntity = quizEntity.getEntity(i.getSakaiId(), this); break; case SimplePageItem.FORUM: lessonEntity = forumEntity.getEntity(i.getSakaiId()); break; } if (lessonEntity != null) { String groups = getItemGroupString(i, lessonEntity, true); ourGroupName = messageLocator.getMessage("simplepage.access-group").replace("{}", getNameOfSakaiItem(i)); // backup in case group was created by old code String oldGroupName = "Access: " + getNameOfSakaiItem(i); // this can produce duplicate names. Searches are actually done based // on entity reference, not title, so this is acceptable though confusing // to users. But using object ID's for the name would be just as confusing. if (!SqlService.getVendor().equals("mysql")) ourGroupName = utf8truncate(ourGroupName, 99); else if (ourGroupName.length() > 99) ourGroupName = ourGroupName.substring(0, 99); String groupId = GroupPermissionsService.makeGroup(getCurrentPage().getSiteId(), ourGroupName, oldGroupName, i.getSakaiId(), this); saveItem(simplePageToolDao.makeGroup(i.getSakaiId(), groupId, groups, getCurrentPage().getSiteId())); // update the tool access control to point to our access control group String[] newGroups = { groupId }; lessonEntity.setGroups(Arrays.asList(newGroups)); } } } else { if (group != null) { // shouldn't be under control. Delete our access control and put the // groups back into the tool's list LessonEntity lessonEntity = null; switch (i.getType()) { case SimplePageItem.ASSIGNMENT: lessonEntity = assignmentEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.ASSESSMENT: lessonEntity = quizEntity.getEntity(i.getSakaiId(), this); break; case SimplePageItem.FORUM: lessonEntity = forumEntity.getEntity(i.getSakaiId()); break; } if (lessonEntity != null) { String groups = group.getGroups(); List<String> groupList = null; if (groups != null && !groups.equals("")) groupList = Arrays.asList(groups.split(",")); lessonEntity.setGroups(groupList); simplePageToolDao.deleteItem(group); } } } } catch (Exception ex) { ex.printStackTrace(); } } // to control a resource, set hidden. /access/lessonbuilder does the actual control private void checkControlResource(SimplePageItem i, boolean correct) { String resourceId = i.getSakaiId(); if (resourceId != null) { try { ContentResource res = contentHostingService.getResource(resourceId); if (res.isHidden() != correct) { ContentResourceEdit resEdit = contentHostingService.editResource(resourceId); resEdit.setAvailability(correct, resEdit.getReleaseDate(), resEdit.getRetractDate()); contentHostingService.commitResource(resEdit, NotificationService.NOTI_NONE); } } catch (Exception ignore) { } } } public SimplePage getCurrentPage() { getCurrentPageId(); return currentPage; } public void setCurrentPage(SimplePage p) { currentPage = p; } public String getToolId(String tool) { try { ToolConfiguration tc = siteService.getSite(currentPage.getSiteId()).getToolForCommonId(tool); return tc.getId(); } catch (IdUnusedException e) { // This really shouldn't happen. log.warn("getToolId 1 attempt to get tool config for " + tool + " failed. Tool missing from site?"); return null; } catch (java.lang.NullPointerException e) { log.warn("getToolId 2 attempt to get tool config for " + tool + " failed. Tool missing from site?"); return null; } } public void updateCurrentPage() { update(currentPage); } public List<PathEntry> getHierarchy() { List<PathEntry> path = (List<PathEntry>) sessionManager.getCurrentToolSession() .getAttribute(LESSONBUILDER_PATH); if (path == null) return new ArrayList<PathEntry>(); return path; } public void setSelectedAssignment(String selectedAssignment) { this.selectedAssignment = selectedAssignment; } public void setSelectedEntity(String selectedEntity) { this.selectedEntity = selectedEntity; } public void setSelectedQuiz(String selectedQuiz) { this.selectedQuiz = selectedQuiz; } public void setSelectedBlti(String selectedBlti) { this.selectedBlti = selectedBlti; } public String assignmentRef(String id) { return "/assignment/a/" + getCurrentSiteId() + "/" + id; } public boolean checkCsrf() { Object sessionToken = sessionManager.getCurrentSession().getAttribute("sakai.csrf.token"); if (sessionToken != null && sessionToken.toString().equals(csrfToken)) { return true; } else return false; } // called by add forum dialog. Create a new item that points to a forum or // update an existing item, depending upon whether itemid is set public String addForum() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (selectedEntity == null) { return "failure"; } else { try { LessonEntity selectedObject = forumEntity.getEntity(selectedEntity); if (selectedObject == null) { return "failure"; } SimplePageItem i; // editing existing item? if (itemId != null && itemId != -1) { i = findItem(itemId); // if no change, don't worry if (!i.getSakaiId().equals(selectedEntity)) { // if access controlled, clear restriction from old assignment and add to new if (i.isPrerequisite()) { i.setPrerequisite(false); checkControlGroup(i, false); // sakaiid and name are used in setting control i.setSakaiId(selectedEntity); i.setName(selectedObject.getTitle()); i.setPrerequisite(true); checkControlGroup(i, true); } else { i.setSakaiId(selectedEntity); i.setName(selectedObject.getTitle()); } // reset assignment-specific stuff i.setDescription(""); update(i); } } else { // no, add new item i = appendItem(selectedEntity, selectedObject.getTitle(), SimplePageItem.FORUM); i.setDescription(""); update(i); } return "success"; } catch (Exception ex) { ex.printStackTrace(); return "failure"; } finally { selectedEntity = null; } } } // called by add assignment dialog. Create a new item that points to an assigment // or update an existing item, depending upon whether itemid is set public String addAssignment() { DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, new ResourceLoader().getLocale()); df.setTimeZone(TimeService.getLocalTimeZone()); if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (selectedAssignment == null) { return "failure"; } else { try { LessonEntity selectedObject = assignmentEntity.getEntity(selectedAssignment); if (selectedObject == null) return "failure"; SimplePageItem i; // editing existing item? if (itemId != null && itemId != -1) { i = findItem(itemId); // if no change, don't worry LessonEntity existing = assignmentEntity.getEntity(i.getSakaiId()); String ref = null; if (existing != null) ref = existing.getReference(); // if same quiz, nothing to do if ((existing == null) || !ref.equals(selectedAssignment)) { // if access controlled, clear restriction from old assignment and add to new if (i.isPrerequisite()) { if (existing != null) { i.setPrerequisite(false); checkControlGroup(i, false); } // sakaiid and name are used in setting control i.setSakaiId(selectedAssignment); i.setName(selectedObject.getTitle()); i.setPrerequisite(true); checkControlGroup(i, true); } else { i.setSakaiId(selectedAssignment); i.setName(selectedObject.getTitle()); } // reset assignment-specific stuff // Because we don't update the due date when it changes, this raises more // problems than it fixes. It's also done only for assignments and not tests // if (selectedObject.getDueDate() != null) // i.setDescription("(" + messageLocator.getMessage("simplepage.due") + " " + df.format(selectedObject.getDueDate()) + ")"); // else // i.setDescription(null); update(i); } } else { // no, add new item i = appendItem(selectedAssignment, selectedObject.getTitle(), SimplePageItem.ASSIGNMENT); //if (selectedObject.getDueDate() != null) // i.setDescription("(" + messageLocator.getMessage("simplepage.due") + " " + df.format(selectedObject.getDueDate()) + ")"); //else i.setDescription(null); update(i); } return "success"; } catch (Exception ex) { ex.printStackTrace(); return "failure"; } finally { selectedAssignment = null; } } } // called by add blti picker. Create a new item that points to an assigment // or update an existing item, depending upon whether itemid is set public String addBlti() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (selectedBlti == null || bltiEntity == null) { return "failure"; } else { try { LessonEntity selectedObject = bltiEntity.getEntity(selectedBlti); if (selectedObject == null) return "failure"; SimplePageItem i; // editing existing item? if (itemId != null && itemId != -1) { i = findItem(itemId); // if no change, don't worry LessonEntity existing = bltiEntity.getEntity(i.getSakaiId()); String ref = null; if (existing != null) ref = existing.getReference(); // if same item, nothing to do if ((existing == null) || !ref.equals(selectedBlti)) { // if access controlled, clear restriction from old assignment and add to new // group access not used for BLTI items, so don't need the setcontrolgroup // logic from other item types i.setSakaiId(selectedBlti); i.setName(selectedObject.getTitle()); if (format == null || format.trim().equals("")) i.setFormat(""); else i.setFormat(format); // this is redundant, but the display code uses it if ("window".equals(format)) i.setSameWindow(false); else i.setSameWindow(true); i.setHeight(height); setItemGroups(i, selectedGroups); update(i); } } else { // no, add new item i = appendItem(selectedBlti, selectedObject.getTitle(), SimplePageItem.BLTI); BltiInterface blti = (BltiInterface) bltiEntity.getEntity(selectedBlti); if (blti != null) { int height = blti.frameSize(); if (height > 0) i.setHeight(Integer.toString(height)); else i.setHeight(""); if (format == null || format.trim().equals("")) i.setFormat(""); else i.setFormat(format); } update(i); } return "success"; } catch (Exception ex) { ex.printStackTrace(); return "failure"; } finally { selectedBlti = null; } } } /// ShowPageProducers needs the item ID list anyway. So to avoid calling the underlying // code twice, we take that list and translate to titles, rather than calling // getItemGroups again public String getItemGroupTitles(String itemGroups, SimplePageItem item) { String ret = ""; if (itemGroups == null || itemGroups.equals("")) ret = ""; else { List<String> groupNames = new ArrayList<String>(); Site site = getCurrentSite(); String[] groupIds = split(itemGroups, ","); for (int i = 0; i < groupIds.length; i++) { Group group = site.getGroup(groupIds[i]); if (group != null) { String title = group.getTitle(); if (title != null && !title.equals("")) groupNames.add(title); else groupNames.add(messageLocator.getMessage("simplepage.deleted-group")); } else groupNames.add(messageLocator.getMessage("simplepage.deleted-group")); } Collections.sort(groupNames); for (String name : groupNames) { if (ret.equals("")) ret = name; else ret = ret + "," + name; } } if (item.isPrerequisite()) { if (ret.equals("")) ret = messageLocator.getMessage("simplepage.prerequisites_tag"); else ret = messageLocator.getMessage("simplepage.prerequisites_tag") + "; " + ret; } if (ret.equals("")) return null; return ret; } // too much existing code to convert to throw at the moment public String getItemGroupString(SimplePageItem i, LessonEntity entity, boolean nocache) { String groups = null; try { groups = getItemGroupStringOrErr(i, entity, nocache); } catch (IdUnusedException exp) { // unfortunately some uses aren't user-visible, so it's this or // add error handling to all callers return ""; } return groups; } // use this one in the future public String getItemGroupStringOrErr(SimplePageItem i, LessonEntity entity, boolean nocache) throws IdUnusedException { StringBuilder ret = new StringBuilder(""); Collection<String> groups = null; // may throw IdUnUsed groups = getItemGroups(i, entity, nocache); if (groups == null) return ""; for (String g : groups) { ret.append(","); ret.append(g); } if (ret.length() == 0) return ""; return ret.substring(1); } public String getItemOwnerGroupString(SimplePageItem i) { String ret = i.getOwnerGroups(); if (ret == null) ret = ""; return ret; } public String getReleaseString(SimplePageItem i) { if (i.getType() == SimplePageItem.PAGE) { SimplePage page = getPage(Long.valueOf(i.getSakaiId())); if (page == null) return null; if (page.isHidden()) return messageLocator.getMessage("simplepage.hiddenpage"); if (page.getReleaseDate() != null && page.getReleaseDate().after(new Date())) return messageLocator.getMessage("simplepage.pagenotreleased"); } return null; } // return GroupEntrys for all groups associated with item // need group entries so we can display labels to user // entity is optional. pass it if you have it, to avoid requiring // us to get it a second time // idunusedexception if underlying object doesn't exist public Collection<String> getItemGroups(SimplePageItem i, LessonEntity entity, boolean nocache) throws IdUnusedException { Collection<String> ret = new ArrayList<String>(); if (!nocache && i.getType() != SimplePageItem.PAGE && i.getType() != SimplePageItem.TEXT && i.getType() != SimplePageItem.BLTI && i.getType() != SimplePageItem.COMMENTS && i.getType() != SimplePageItem.QUESTION && i.getType() != SimplePageItem.BREAK && i.getType() != SimplePageItem.STUDENT_CONTENT) { Object cached = groupCache.get(i.getSakaiId()); if (cached != null) { if (cached instanceof String) return null; return (List<String>) cached; } } if (entity == null) { switch (i.getType()) { case SimplePageItem.ASSIGNMENT: entity = assignmentEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.ASSESSMENT: entity = quizEntity.getEntity(i.getSakaiId(), this); break; case SimplePageItem.FORUM: entity = forumEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.MULTIMEDIA: String displayType = i.getAttribute("multimediaDisplayType"); if ("1".equals(displayType) || "3".equals(displayType)) return getLBItemGroups(i); // for all native LB objects else return getResourceGroups(i, nocache); // responsible for caching the result case SimplePageItem.RESOURCE: return getResourceGroups(i, nocache); // responsible for caching the result // throws IdUnusedException if necessary case SimplePageItem.BLTI: entity = bltiEntity.getEntity(i.getSakaiId()); if (entity == null || !entity.objectExists()) throw new IdUnusedException(i.toString()); // fall through: groups controlled by LB // for the following items we don't have non-LB items so don't need itemunused case SimplePageItem.TEXT: case SimplePageItem.PAGE: case SimplePageItem.COMMENTS: case SimplePageItem.QUESTION: case SimplePageItem.STUDENT_CONTENT: return getLBItemGroups(i); // for all native LB objects default: return null; } } // only here for object types with underlying entities boolean exists = false; try { pushAdvisorAlways(); // assignments won't let the student look if (entity != null) exists = entity.objectExists(); } finally { popAdvisor(); } if (!exists) { throw new IdUnusedException(i.toString()); } // in principle the groups are stored in a SimplePageGroup if we // are doing access control, and in the tool if not. We can // check that with i.isPrerequisite. However I'm concerned // that if multiple items point to the same object, and some // are set with prerequisite and some are not, that things // could get out of kilter. So I'm going to use the // SimplePageGroup if it exists, and the tool if not. // this can be needed if the call to getEntity causes us to recognize // a recently published test and replace the /sam_core with a /sam_pub // in that case the entity is up to date but the sakaiid is not if (i.getSakaiId().startsWith("/sam_core") && entity != null) { i.setSakaiId(entity.getReference()); } SimplePageGroup simplePageGroup = simplePageToolDao.findGroup(i.getSakaiId()); if (simplePageGroup != null) { String groups = simplePageGroup.getGroups(); if (groups != null && !groups.equals("")) ret = Arrays.asList(groups.split(",")); else ; // leave ret as an empty list } else { // not under our control, use list from tool try { pushAdvisorAlways(); ret = entity.getGroups(nocache); // assignments won't let a student see } finally { popAdvisor(); } } if (ret == null) groupCache.put(i.getSakaiId(), "*"); else groupCache.put(i.getSakaiId(), ret); return ret; } // obviously this function must be called right after getResourceGroups private boolean inherited = false; public boolean getInherited() { return inherited; } // getItemGroups version for resources, since we don't have // an interface object. IdUnusedException if the underlying resource doesn't exist public Collection<String> getResourceGroups(SimplePageItem i, boolean nocache) throws IdUnusedException { SecurityAdvisor advisor = null; try { // do this before getting privs. It is implemented by seeing whether anon can access, // so the advisor will cause the wrong answer boolean inheritingPubView = contentHostingService.isInheritingPubView(i.getSakaiId()); // for isItemVisible to work, users need to be able to get this all the time //if(getCurrentPage().getOwner() != null) { advisor = new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }; securityService.pushAdvisor(advisor); // } Collection<String> ret = null; ContentResource resource = null; try { resource = contentHostingService.getResource(i.getSakaiId()); } catch (Exception ignore) { throw new IdUnusedException(i.toString()); } Collection<String> groups = null; AccessMode access = resource.getAccess(); if (AccessMode.INHERITED.equals(access) || inheritingPubView) { access = resource.getInheritedAccess(); // inherited means that we can't set it locally // an inherited value of site is OK // anything else can't be changed, so we set inherited if (AccessMode.SITE.equals(access) && !inheritingPubView) inherited = false; else inherited = true; if (AccessMode.GROUPED.equals(access)) groups = resource.getInheritedGroups(); } else { // we can always change local modes, even if they are public inherited = false; if (AccessMode.GROUPED.equals(access)) groups = resource.getGroups(); } if (groups != null) { ret = new ArrayList<String>(); for (String group : groups) { int n = group.indexOf("/group/"); ret.add(group.substring(n + 7)); } } if (!nocache) { if (ret == null) groupCache.put(i.getSakaiId(), "*"); else groupCache.put(i.getSakaiId(), ret); } return ret; } finally { if (advisor != null) securityService.popAdvisor(); } } // no obvious need to cache public Collection<String> getLBItemGroups(SimplePageItem i) { List<String> ret = null; String groupString = i.getGroups(); if (groupString == null || groupString.equals("")) { return null; } String[] groupsArray = split(groupString, ","); return Arrays.asList(groupsArray); } // set group list in tool. We'll have an array of group ids // returns old list, sorted, or null if entity not found. // WARNING: you must check whether isprerequisite. If so, we maintain // the group list, so you need to do i.setGroups(). public List<String> setItemGroups(SimplePageItem i, String[] groups) { // can't allow groups on student pages if (getCurrentPage().getOwner() != null) return null; LessonEntity lessonEntity = null; switch (i.getType()) { case SimplePageItem.ASSIGNMENT: lessonEntity = assignmentEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.ASSESSMENT: lessonEntity = quizEntity.getEntity(i.getSakaiId(), this); break; case SimplePageItem.FORUM: lessonEntity = forumEntity.getEntity(i.getSakaiId()); break; case SimplePageItem.MULTIMEDIA: String displayType = i.getAttribute("multimediaDisplayType"); if ("1".equals(displayType) || "3".equals(displayType)) return setLBItemGroups(i, groups); else return setResourceGroups(i, groups); case SimplePageItem.RESOURCE: return setResourceGroups(i, groups); case SimplePageItem.TEXT: case SimplePageItem.PAGE: case SimplePageItem.BLTI: case SimplePageItem.COMMENTS: case SimplePageItem.QUESTION: case SimplePageItem.STUDENT_CONTENT: return setLBItemGroups(i, groups); case SimplePageItem.BREAK: return null; // better not actually happen } if (lessonEntity != null) { // need a list to sort it. Collection oldGroupCollection = null; try { oldGroupCollection = getItemGroups(i, lessonEntity, true); } catch (IdUnusedException exc) { return null; // no such entity } List<String> oldGroups = null; if (oldGroupCollection == null) oldGroups = new ArrayList<String>(); else oldGroups = new ArrayList<String>(oldGroupCollection); Collections.sort(oldGroups); List<String> newGroups = Arrays.asList(groups); Collections.sort(newGroups); boolean difference = false; if (oldGroups.size() == newGroups.size()) { for (int n = 0; n < oldGroups.size(); n++) if (!oldGroups.get(n).equals(newGroups.get(n))) { difference = true; break; } } else difference = true; if (difference) { if (i.isPrerequisite()) { String groupString = ""; for (String groupId : newGroups) { if (groupString.equals("")) groupString = groupId; else groupString = groupString + "," + groupId; } SimplePageGroup simplePageGroup = simplePageToolDao.findGroup(i.getSakaiId()); simplePageGroup.setGroups(groupString); update(simplePageGroup); } else lessonEntity.setGroups(Arrays.asList(groups)); } return oldGroups; } return null; } public List<String> setResourceGroups(SimplePageItem i, String[] groups) { ContentResourceEdit resource = null; List<String> ret = null; boolean pushed = false; try { pushed = pushAdvisor(); resource = contentHostingService.editResource(i.getSakaiId()); if (AccessMode.GROUPED.equals(resource.getInheritedAccess())) { Collection<String> oldGroups = resource.getInheritedGroups(); if (oldGroups instanceof List) ret = (List<String>) oldGroups; else if (oldGroups != null) ret = new ArrayList<String>(oldGroups); } // else null if (groups == null || groups.length == 0) { if (AccessMode.GROUPED.equals(resource.getAccess())) resource.clearGroupAccess(); // else must be public or site already, leave it } else { Site site = getCurrentSite(); for (int n = 0; n < groups.length; n++) { Group group = site.getGroup(groups[n]); groups[n] = group.getReference(); } resource.setGroupAccess(Arrays.asList(groups)); } contentHostingService.commitResource(resource, NotificationService.NOTI_NONE); resource = null; } catch (java.lang.NullPointerException e) { // KNL-714 gives spurious null pointer setErrMessage(messageLocator.getMessage("simplepage.resourcepossibleerror")); } catch (Exception e) { setErrMessage(e.toString()); return null; } finally { // this will generate a traceback in the case of KNL-714, but there's no way // to trap it. Sorry. The log entry will say // org.sakaiproject.content.impl.BaseContentService - cancelResource(): closed ContentResourceEdit // the user will also get a warning if (resource != null) { contentHostingService.cancelResource(resource); } if (pushed) popAdvisor(); } return ret; } public List<String> setLBItemGroups(SimplePageItem i, String[] groups) { List<String> ret = null; // old value String groupString = i.getGroups(); if (groupString != null && !groupString.equals("")) { ret = Arrays.asList(groupString.split(",")); } groupString = null; if (groups != null) { for (int n = 0; n < groups.length; n++) { if (groupString == null) groupString = groups[n]; else groupString = groupString + "," + groups[n]; } } i.setGroups(groupString); update(i); return ret; // old value } public Set<String> getMyGroups() { if (myGroups != null) return myGroups; String userId = getCurrentUserId(); Collection<Group> groups = getCurrentSite().getGroupsWithMember(userId); Set<String> ret = new HashSet<String>(); if (groups == null) return ret; for (Group group : groups) ret.add(group.getId()); myGroups = ret; return ret; } // sort the list, since it will typically be presented // to the user. This skips our access groups public List<GroupEntry> getCurrentGroups() { if (currentGroups != null) return currentGroups; Site site = getCurrentSite(); Collection<Group> groups = site.getGroups(); List<GroupEntry> groupEntries = new ArrayList<GroupEntry>(); for (Group g : groups) { if (g.getProperties().getProperty("lessonbuilder_ref") != null || g.getTitle().startsWith("Access: ")) continue; GroupEntry e = new GroupEntry(); e.name = g.getTitle(); e.id = g.getId(); groupEntries.add(e); } Collections.sort(groupEntries, new Comparator() { public int compare(Object o1, Object o2) { GroupEntry e1 = (GroupEntry) o1; GroupEntry e2 = (GroupEntry) o2; return e1.name.compareTo(e2.name); } }); currentGroups = groupEntries; return groupEntries; } // called by add quiz dialog. Create a new item that points to a quiz // or update an existing item, depending upon whether itemid is set public String addQuiz() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; if (selectedQuiz == null) { return "failure"; } else { try { LessonEntity selectedObject = quizEntity.getEntity(selectedQuiz, this); if (selectedObject == null) return "failure"; // editing existing item? SimplePageItem i; if (itemId != null && itemId != -1) { i = findItem(itemId); // do getEntity/getreference to normalize, in case sakaiid is old format LessonEntity existing = quizEntity.getEntity(i.getSakaiId(), this); String ref = null; if (existing != null) ref = existing.getReference(); // if same quiz, nothing to do if ((existing == null) || !ref.equals(selectedQuiz)) { // if access controlled, clear restriction from old quiz and add to new if (i.isPrerequisite()) { if (existing != null) { i.setPrerequisite(false); checkControlGroup(i, false); } // sakaiid and name are used in setting control i.setSakaiId(selectedQuiz); i.setName(selectedObject.getTitle()); i.setPrerequisite(true); checkControlGroup(i, true); } else { i.setSakaiId(selectedQuiz); i.setName(selectedObject.getTitle()); } // reset quiz-specific stuff i.setDescription(""); update(i); } } else // no, add new item appendItem(selectedQuiz, selectedObject.getTitle(), SimplePageItem.ASSESSMENT); return "success"; } catch (Exception ex) { ex.printStackTrace(); return "failure"; } finally { selectedQuiz = null; } } } public void setLinkUrl(String url) { linkUrl = url; } // doesn't seem to be used at the moment public String createLink() { if (linkUrl == null || linkUrl.equals("")) { return "cancel"; } String url = linkUrl; url = url.trim(); // the intent is to handle something like www.cnn.com or www.cnn.com/foo // Note that the result has no protocol. That means it will use the protocol // of the page it's displayed from, which should be right. if (!url.startsWith("http:") && !url.startsWith("https:") && !url.startsWith("/")) { String atom = url; int i = atom.indexOf("/"); if (i >= 0) atom = atom.substring(0, i); // first atom is hostname if (atom.indexOf(".") >= 0) { String server = ServerConfigurationService.getServerUrl(); if (server.startsWith("https:")) url = "https://" + url; else url = "http://" + url; } } appendItem(url, url, SimplePageItem.URL); return "success"; } public void setPage(long pageId) { sessionManager.getCurrentToolSession().setAttribute("current-pagetool-page", pageId); currentPageId = null; } // more setters and getters used by forms public void setHeight(String height) { this.height = height; } public String getHeight() { String r = ""; if (itemId != null && itemId > 0) { r = findItem(itemId).getHeight(); } return (r == null ? "" : r); } public void setWidth(String width) { this.width = width; } public String getWidth() { String r = ""; if (itemId != null && itemId > 0) { r = findItem(itemId).getWidth(); } return (r == null ? "" : r); } public String getAlt() { String r = ""; if (itemId != null && itemId > 0) { r = findItem(itemId).getAlt(); } return (r == null ? "" : r); } // called by edit multimedia dialog to change parameters in a multimedia item public String editMultimedia() { if (!itemOk(itemId)) return "permission-failed"; if (!canEditPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; SimplePageItem i = findItem(itemId); if (i != null && i.getType() == SimplePageItem.MULTIMEDIA) { i.setHeight(height); i.setWidth(width); i.setAlt(alt); i.setDescription(description); i.setHtml(mimetype); i.setPrerequisite(this.prerequisite); update(i); setItemGroups(i, selectedGroups); return "success"; } else { log.warn("editMultimedia Could not find multimedia object: " + itemId); return "cancel"; } } // called by edit title dialog to change attributes of the page such as the title public String editTitle() { if (pageTitle == null || pageTitle.equals("")) { return "notitle"; } // because we're using a security advisor, need to make sure it's OK ourselves if (!canEditPage()) { return "permission-failed"; } if (!checkCsrf()) return "permission-failed"; Placement placement = toolManager.getCurrentPlacement(); SimplePage page = getCurrentPage(); SimplePageItem pageItem = getCurrentPageItem(null); Site site = getCurrentSite(); boolean needRecompute = false; if (page.getOwner() == null && getEditPrivs() == 0) { // update gradebook link if necessary Double currentPoints = page.getGradebookPoints(); Double newPoints = null; if (points != null) { try { newPoints = Double.parseDouble(points); if (newPoints == 0.0) newPoints = null; } catch (Exception ignore) { newPoints = null; } } // adjust gradebook entry boolean add = false; if (newPoints == null && currentPoints != null) { add = gradebookIfc.removeExternalAssessment(site.getId(), "lesson-builder:" + page.getPageId()); } else if (newPoints != null && currentPoints == null) { add = gradebookIfc.addExternalAssessment(site.getId(), "lesson-builder:" + page.getPageId(), null, pageTitle, newPoints, null, "Lesson Builder"); if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else needRecompute = true; } else if (currentPoints != null && (!currentPoints.equals(newPoints) || !pageTitle.equals(page.getTitle()))) { add = gradebookIfc.updateExternalAssessment(site.getId(), "lesson-builder:" + page.getPageId(), null, pageTitle, newPoints, null); if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else if (!currentPoints.equals(newPoints)) needRecompute = true; } if (add) page.setGradebookPoints(newPoints); boolean oldDownloads = site.getProperties().getProperty("lessonbuilder-nodownloadlinks") != null; if (oldDownloads != nodownloads) { if (oldDownloads) site.getPropertiesEdit().removeProperty("lessonbuilder-nodownloadlinks"); else if (nodownloads) site.getPropertiesEdit().addProperty("lessonbuilder-nodownloadlinks", "true"); try { siteService.save(site); } catch (Exception e) { log.error("editTitle unable to save site " + e); } } } if (pageTitle != null && pageItem.getPageId() == 0) { try { // we need a security advisor because we're allowing users to edit the page if they // have // simplepage.upd privileges, but site.save requires site.upd. securityService.pushAdvisor(new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { if (function.equals(SITE_UPD) && reference.equals("/site/" + getCurrentSiteId())) { return SecurityAdvice.ALLOWED; } else { return SecurityAdvice.PASS; } } }); SitePage sitePage = site.getPage(page.getToolId()); for (ToolConfiguration t : sitePage.getTools()) { if (t.getId().equals(placement.getId())) t.setTitle(pageTitle); } sitePage.setTitle(pageTitle); siteService.save(site); page.setTitle(pageTitle); page.setHidden(hidePage); if (hasReleaseDate) page.setReleaseDate(releaseDate); else page.setReleaseDate(null); update(page); updateCurrentPage(); placement.setTitle(pageTitle); placement.save(); pageVisibilityHelper(site, page.getToolId(), !hidePage); pageItem.setPrerequisite(prerequisite); pageItem.setRequired(required); pageItem.setName(pageTitle); update(pageItem); } catch (Exception e) { e.printStackTrace(); } finally { securityService.popAdvisor(); } } else if (pageTitle != null) { page.setTitle(pageTitle); page.setHidden(hidePage); if (hasReleaseDate) page.setReleaseDate(releaseDate); else page.setReleaseDate(null); update(page); } if (pageTitle != null) { if (pageItem.getType() == SimplePageItem.STUDENT_CONTENT) { SimpleStudentPage student = simplePageToolDao.findStudentPageByPageId(page.getPageId()); student.setTitle(pageTitle); update(student, false); } else { pageItem.setName(pageTitle); update(pageItem); } adjustPath("", pageItem.getPageId(), pageItem.getId(), pageTitle); } String collectionId = contentHostingService.getSiteCollection(getCurrentSiteId()) + "LB-CSS/"; String uploadId = uploadFile(collectionId); if (uploadId != null) { page.setCssSheet(uploadId); // Make sure the relevant caches are wiped clean. resourceCache.remove(collectionId); resourceCache.remove(uploadId); } else { page.setCssSheet(dropDown); } update(page); // have to do this after the page itself is updated if (needRecompute) recomputeGradebookEntries(page.getPageId(), points); // points, not newPoints because API wants a string if (pageItem.getPageId() == 0) { return "reload"; } else { return "success"; } } private boolean uploadSizeOk(MultipartFile file) { long uploadedFileSize = file.getSize(); return uploadSizeOk(uploadedFileSize); } private boolean uploadSizeOk(long uploadedFileSize) { if (uploadedFileSize == 0) { setErrMessage(messageLocator.getMessage("simplepage.filezero")); return false; } // implement precedence rules: ceiling if set, else max, else 20 String max = ServerConfigurationService.getString("content.upload.max", null); String ceiling = ServerConfigurationService.getString("content.upload.ceiling", null); String effective = ceiling; if (effective == null) effective = max; if (effective == null) effective = "20"; long maxFileSizeInBytes = 20 * 1024 * 1024; try { maxFileSizeInBytes = Long.parseLong(effective) * 1024 * 1024; } catch (NumberFormatException e) { log.warn("Unable to parse content.upload.max retrieved from properties file during upload"); } if (uploadedFileSize > maxFileSizeInBytes) { String limit = Long.toString(maxFileSizeInBytes / (1024 * 1024)); setErrMessage(messageLocator.getMessage("simplepage.filetoobig").replace("{}", limit)); return false; } return true; } private String uploadFile(String collectionId) { String name = null; String mimeType = null; MultipartFile file = null; if (multipartMap.size() > 0) { // user specified a file, create it file = multipartMap.values().iterator().next(); } if (file != null) { // uploadsizeok would otherwise complain about 0 length file. For // this case it's valid. Means no file. if (file.getSize() == 0) return null; if (!uploadSizeOk(file)) return null; try { contentHostingService.checkCollection(collectionId); } catch (Exception ex) { try { ContentCollectionEdit edit = contentHostingService.addCollection(collectionId); edit.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, "LB-CSS"); contentHostingService.commitCollection(edit); } catch (Exception e) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return null; } } //String collectionId = getCollectionIdfalse); // user specified a file, create it name = file.getOriginalFilename(); if (name == null || name.length() == 0) name = file.getName(); int i = name.lastIndexOf("/"); if (i >= 0) name = name.substring(i + 1); String base = name; String extension = ""; i = name.lastIndexOf("."); if (i > 0) { base = name.substring(0, i); extension = name.substring(i + 1); } mimeType = file.getContentType(); try { ContentResourceEdit res = contentHostingService .addResource(collectionId, fixFileName(collectionId, Validator.escapeResourceName(base), Validator.escapeResourceName(extension)), "", MAXIMUM_ATTEMPTS_FOR_UNIQUENESS); res.setContentType(mimeType); res.setContent(file.getInputStream()); try { contentHostingService.commitResource(res, NotificationService.NOTI_NONE); // there's a bug in the kernel that can cause // a null pointer if it can't determine the encoding // type. Since we want this code to work on old // systems, work around it. } catch (java.lang.NullPointerException e) { setErrMessage(messageLocator.getMessage("simplepage.resourcepossibleerror")); } return res.getId(); } catch (org.sakaiproject.exception.OverQuotaException ignore) { setErrMessage(messageLocator.getMessage("simplepage.overquota")); return null; } catch (Exception e) { setErrMessage(messageLocator.getMessage("simplepage.resourceerror").replace("{}", e.toString())); log.error("addMultimedia error 1 " + e); return null; } } else { return null; } } public String addPages() { if (!canEditPage()) return "permission-fail"; if (!checkCsrf()) return "permission-failed"; // javascript should have checked all this if (newPageTitle == null || newPageTitle.equals("")) return "fail"; int numPages = 1; if (numberOfPages != null && !numberOfPages.equals("")) numPages = Integer.valueOf(numberOfPages); String prefix = ""; String suffix = ""; int start = 1; if (numPages > 1) { Pattern pattern = Pattern.compile("(\\D*)(\\d+)(.*)"); Matcher matcher = pattern.matcher(newPageTitle); if (!matcher.matches()) return "fail"; prefix = matcher.group(1); suffix = matcher.group(3); start = Integer.parseInt(matcher.group(2)); } if (numPages == 1) { addPage(newPageTitle, copyPage); } else { // note sure what to do here. We have to have a maximum to prevent creating 1,000,000 pages due // to a typo. This allows one a week for a year. Surely that's enough. We can make it // configurable if necessary. if (numPages > 52) numPages = 52; while (numPages > 0) { String title = prefix + Integer.toString(start) + suffix; addPage(title, null, copyPage, (numPages == 1)); // only save the last time numPages--; start++; } } setTopRefresh(); return "success"; } // Adds an existing page as a top level page public String addOldPage() { if (getEditPrivs() != 0) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; SimplePage target = getPage(Long.valueOf(selectedEntity)); if (target != null) addPage(target.getTitle(), target.getPageId(), false, true); return "success"; } public SimplePage addPage(String title, boolean copyCurrent) { return addPage(title, null, copyCurrent, true); } public SimplePage addPage(String title, Long pageId, boolean copyCurrent, boolean doSave) { Site site = getCurrentSite(); SitePage sitePage = site.addPage(); ToolConfiguration tool = sitePage.addTool(LESSONBUILDER_ID); String toolId = tool.getPageId(); SimplePage page = null; if (pageId == null) { page = simplePageToolDao.makePage(toolId, getCurrentSiteId(), title, null, null); saveItem(page); } else { page = getPage(pageId); page.setToolId(toolId); page.setParent(null); page.setTopParent(null); update(page); title = page.getTitle(); } tool.setTitle(title); SimplePageItem item = simplePageToolDao.makeItem(0, 0, SimplePageItem.PAGE, Long.toString(page.getPageId()), title); saveItem(item); sitePage.setTitle(title); sitePage.setTitleCustom(true); if (doSave) { try { siteService.save(site); } catch (Exception e) { log.error("addPage unable to save site " + e); } currentSite = null; // force refetch, since we've changed it. note sure this is strictly needed } if (copyCurrent) { long oldPageId = getCurrentPageId(); long newPageId = page.getPageId(); for (SimplePageItem oldItem : simplePageToolDao.findItemsOnPage(oldPageId)) { // don't copy pages. It's not clear whether we want to deep copy or // not. If we do the wrong thing the user coudl end up editing the // wrong page and losing content. if (oldItem.getType() == SimplePageItem.PAGE) continue; SimplePageItem newItem = simplePageToolDao.copyItem(oldItem); newItem.setPageId(newPageId); saveItem(newItem); simplePageToolDao.copyItem2(oldItem, newItem); } } setTopRefresh(); return page; } // when a gradebook entry is added or point value for page changed, need to // add or update all student entries for the page // this only updates grades for users that are complete. Others should have 0 score, which won't change public void recomputeGradebookEntries(Long pageId, String newPoints) { Map<String, String> userMap = new HashMap<String, String>(); List<SimplePageItem> items = simplePageToolDao.findPageItemsBySakaiId(Long.toString(pageId)); if (items == null) return; for (SimplePageItem item : items) { List<String> users = simplePageToolDao.findUserWithCompletePages(item.getId()); for (String user : users) userMap.put(user, newPoints); } gradebookIfc.updateExternalAssessmentScores(getCurrentSiteId(), "lesson-builder:" + pageId, userMap); } public boolean isImageType(SimplePageItem item) { // if mime type is defined use it String mimeType = item.getHtml(); if (mimeType != null && (mimeType.startsWith("http") || mimeType.equals(""))) mimeType = null; if (mimeType != null && mimeType.length() > 0) { return mimeType.toLowerCase().startsWith("image/"); } // else use extension String name = item.getSakaiId(); // starts after last / int i = name.lastIndexOf("/"); if (i >= 0) name = name.substring(i + 1); String extension = null; i = name.lastIndexOf("."); if (i > 0) extension = name.substring(i + 1); if (extension == null) return false; extension = extension.trim(); extension = extension.toLowerCase(); if (imageTypes.contains(extension)) { return true; } else { return false; } } public void setOrder(String order) { this.order = order; } public void fixorder() { List<SimplePageItem> items = getItemsOnPage(getCurrentPageId()); for (int i = 0; i < items.size(); i++) { if (items.get(i).getSequence() <= 0) { items.remove(items.get(i)); i--; } } int i = 1; for (SimplePageItem item : items) { if (item.getSequence() != i) { item.setSequence(i); update(item); } i++; } } // called by reorder tool to do the reordering public String reorder() { if (!canEditPage()) return "permission-fail"; if (!checkCsrf()) return "permission-failed"; if (order == null) { return "cancel"; } fixorder(); // order has to be contiguous or things will break order = order.trim(); List<SimplePageItem> items = getItemsOnPage(getCurrentPageId()); // Remove items that weren't ordered due to having sequences too low. // Typically means they are tacked onto the end automatically. for (int i = 0; i < items.size(); i++) { if (items.get(i).getSequence() <= 0) { items.remove(items.get(i)); i--; } } List<SimplePageItem> secondItems = null; if (selectedEntity != null && !selectedEntity.equals("")) { // second page is involved Long secondPageId = Long.parseLong(selectedEntity); SimplePage secondPage = getPage(secondPageId); if (secondPage != null && secondPage.getSiteId().equals(getCurrentPage().getSiteId())) { secondItems = getItemsOnPage(secondPageId); if (secondItems.size() == 0) secondItems = null; else { for (int i = 0; i < secondItems.size(); i++) { if (secondItems.get(i).getSequence() <= 0) { secondItems.remove(secondItems.get(i)); i--; } } } } } String[] split = split(order, " "); // make sure nothing is duplicated. I know it shouldn't be, but // I saw the Fluid reorderer get confused once. Set<String> used = new HashSet<String>(); for (int i = 0; i < split.length; i++) { if (!used.add(split[i].trim())) { log.warn("reorder: duplicate value"); setErrMessage(messageLocator.getMessage("simplepage.reorder-duplicates")); return "failed"; // it was already there. Oops. } } // keep track of which old items are used so we can remove the ones that aren't. // items in set are indices into "items" Set<Integer> keep = new HashSet<Integer>(); // now do the reordering for (int i = 0; i < split.length; i++) { if (split[i].equals("---")) break; if (split[i].startsWith("*")) { // item from second page. add copy SimplePageItem oldItem = secondItems.get(Integer.valueOf(split[i].substring(1)) - 1); SimplePageItem newItem = simplePageToolDao.copyItem(oldItem); newItem.setPageId(getCurrentPageId()); newItem.setSequence(i + 1); saveItem(newItem); simplePageToolDao.copyItem2(oldItem, newItem); } else { // existing item. update its sequence and note that it's still used int old = items.get(Integer.valueOf(split[i]) - 1).getSequence(); keep.add(Integer.valueOf(split[i]) - 1); items.get(Integer.valueOf(split[i]) - 1).setSequence(i + 1); if (old != i + 1) { update(items.get(Integer.valueOf(split[i]) - 1)); } } } // now kill all items on the page we didn't see in the new order for (int i = 0; i < items.size(); i++) { if (!keep.contains((Integer) i)) deleteItem(items.get(i)); } itemsCache.remove(getCurrentPage().getPageId()); // removals left gaps in order. fix it. fixorder(); itemsCache.remove(getCurrentPage().getPageId()); return "success"; } // this is sort of sleasy. A simple redirect passes no data. Thus it takes // us back to the default page. So we advance the default page. It would probably // have been better to use a link rather than a command, then the link could // have passed the page and item. // public String next() { // getCurrentPageId(); // sets item id, which is what we want // SimplePageItem item = getCurrentPageItem(null); // // List<SimplePageItem> items = getItemsOnPage(item.getPageId()); // // item = items.get(item.getSequence()); // sequence start with 1, so this is the next item // updatePageObject(Long.valueOf(item.getSakaiId())); // updatePageItem(item.getId()); // // return "redirect"; // } public String getCurrentUserId() { if (currentUserId == null) currentUserId = UserDirectoryService.getCurrentUser().getId(); return currentUserId; } // page is complete, update gradebook entry if any // note that if the user has never gone to a page, the gradebook item will be missing. // if they gone to it but it's not complete, it will be 0. We can't explicitly set // a missing value, and this is the only way things will work if someone completes a page // and something changes so it is no longer complete. public void trackComplete(SimplePageItem item, boolean complete) { SimplePage page = getCurrentPage(); if (page.getGradebookPoints() != null) gradebookIfc.updateExternalAssessmentScore(getCurrentSiteId(), "lesson-builder:" + page.getPageId(), getCurrentUserId(), complete ? Double.toString(page.getGradebookPoints()) : "0.0"); } /** * * @param itemId * The ID in the <b>items</b> table. * @param path * breadcrumbs, only supplied it the item is a page * It is valid for code to check path == null to see * whether it is a page * Create or update a log entry when user accesses an item. */ public void track(long itemId, String path) { track(itemId, path, null); } public void track(long itemId, String path, Long studentPageId) { String userId = getCurrentUserId(); if (userId == null) userId = ".anon"; SimplePageLogEntry entry = getLogEntry(itemId, studentPageId); String toolId = ((ToolConfiguration) toolManager.getCurrentPlacement()).getPageId(); if (entry == null) { entry = simplePageToolDao.makeLogEntry(userId, itemId, studentPageId); if (path != null && studentPageId == null) { boolean complete = isPageComplete(itemId); entry.setComplete(complete); entry.setPath(path); entry.setToolId(toolId); SimplePageItem i = findItem(itemId); EventTrackingService.post(EventTrackingService.newEvent("lessonbuilder.read", "/lessonbuilder/page/" + i.getSakaiId(), complete)); trackComplete(i, complete); studentPageId = -1L; } else if (path != null) { entry.setPath(path); entry.setComplete(true); entry.setToolId(toolId); SimplePage page = getPage(studentPageId); EventTrackingService.post(EventTrackingService.newEvent("lessonbuilder.read", "/lessonbuilder/page/" + page.getPageId(), true)); } saveItem(entry); logCache.put(itemId + "-" + studentPageId, entry); } else { if (path != null && studentPageId == null) { boolean wasComplete = entry.isComplete(); boolean complete = isPageComplete(itemId); entry.setComplete(complete); entry.setPath(path); entry.setToolId(toolId); entry.setDummy(false); SimplePageItem i = findItem(itemId); EventTrackingService.post(EventTrackingService.newEvent("lessonbuilder.read", "/lessonbuilder/page/" + i.getSakaiId(), complete)); if (complete != wasComplete) trackComplete(i, complete); studentPageId = -1L; } else if (path != null) { entry.setComplete(true); entry.setPath(path); entry.setToolId(toolId); entry.setDummy(false); SimplePage page = getPage(studentPageId); EventTrackingService.post(EventTrackingService.newEvent("lessonbuilder.read", "/lessonbuilder/page/" + page.getPageId(), true)); } update(entry); } //SimplePageItem i = findItem(itemId); // todo // code can't work anymore. I'm not sure whether it's needed. // we don't update a page as complete if the user finishes a test, etc, until he // comes back to the page. I'm not sure I feel compelled to do this either. But // once we move to the new hiearchy, we'll see // top level doesn't have a next level, so avoid null pointer problem // if (i.getPageId() != 0) { // SimplePageItem nextLevelUp = simplePageToolDao.findPageItemBySakaiId(String.valueOf(i.getPageId())); // if (isItemComplete(findItem(itemId)) && nextLevelUp != null) { // track(nextLevelUp.getId(), true); // } // } } public SimplePageLogEntry getLogEntry(long itemId) { return getLogEntry(itemId, null); } public SimplePageLogEntry getLogEntry(long itemId, Long studentPageId) { if (studentPageId == null) { studentPageId = -1L; } String lookup = itemId + "-" + studentPageId; SimplePageLogEntry entry = logCache.get(lookup); if (entry != null) return entry; String userId = getCurrentUserId(); if (userId == null) userId = ".anon"; entry = simplePageToolDao.getLogEntry(userId, itemId, studentPageId); logCache.put(lookup, entry); return entry; } public boolean hasLogEntry(long itemId) { return (getLogEntry(itemId) != null); } public boolean isItemVisible(SimplePageItem item) { return isItemVisible(item, null); } public boolean isItemVisible(SimplePageItem item, SimplePage page) { return isItemVisible(item, page, true); } // if the item has a group requirement, are we in one of the groups. // this is called a lot and is fairly expensive, so results are cached // for student pages, if it's not the owner, use resources test for resources // for a student page, we don't bypass hidden and release date, so it's safest // just to call contentHosting. public boolean isItemVisible(SimplePageItem item, SimplePage page, boolean testpriv) { if (testpriv && canSeeAll()) { return true; } Boolean ret = visibleCache.get(item.getId()); if (ret != null) { return (boolean) ret; } // item is page, and it is hidden or not released if (item.getType() == SimplePageItem.BREAK) return true; // breaks are always visible to all users else if (item.getType() == SimplePageItem.PAGE) { SimplePage itemPage = getPage(Long.valueOf(item.getSakaiId())); if (itemPage.isHidden()) return false; if (itemPage.getReleaseDate() != null && itemPage.getReleaseDate().after(new Date())) return false; } else if (page != null && page.getOwner() != null && (item.getType() == SimplePageItem.RESOURCE || item.getType() == SimplePageItem.MULTIMEDIA)) { // This code is taken from LessonBuilderAccessService, mostly // for student pages, we give people access to files in the owner's worksite // get data we need to check that String id = item.getSakaiId(); String owner = page.getOwner(); // if student content String group = page.getGroup(); // if student content if (group != null) group = "/site/" + page.getSiteId() + "/group/" + group; String currentSiteId = page.getSiteId(); // if group owned, and /user/xxxx is the person who created the resource // this will be his uesrid. Note that xxxx is eid, so we need to translate String usersite = null; if (owner != null && group != null && id.startsWith("/user/")) { String username = id.substring(6); int slash = username.indexOf("/"); if (slash > 0) usersite = username.substring(0, slash); // normally it is /user/EID, so convert to userid try { usersite = UserDirectoryService.getUserId(usersite); } catch (Exception e) { } ; String itemcreator = item.getAttribute("addedby"); if (usersite != null && itemcreator != null && !usersite.equals(itemcreator)) usersite = null; } if (owner != null && usersite != null && authzGroupService.getUserRole(usersite, group) != null) { return true; } else if (owner != null && group == null && id.startsWith("/user/" + owner)) { return true; } else { try { contentHostingService.checkResource(id); return true; } catch (Exception e) { // I think we should hide the item no matter what the error is return false; } } } Collection<String> itemGroups = null; boolean pushed = false; try { pushAdvisorAlways(); pushed = true; LessonEntity entity = null; if (!canSeeAll()) { switch (item.getType()) { case SimplePageItem.ASSIGNMENT: entity = assignmentEntity.getEntity(item.getSakaiId()); if (entity == null || entity.notPublished()) return false; break; case SimplePageItem.ASSESSMENT: if (quizEntity.notPublished(item.getSakaiId())) return false; break; case SimplePageItem.FORUM: entity = forumEntity.getEntity(item.getSakaiId()); if (entity == null || entity.notPublished()) return false; break; case SimplePageItem.BLTI: if (bltiEntity != null) entity = bltiEntity.getEntity(item.getSakaiId()); if (entity == null || entity.notPublished()) return false; } } popAdvisor(); pushed = false; // entity can be null. passing the actual entity just avoids a second lookup itemGroups = getItemGroups(item, entity, false); } catch (IdUnusedException exc) { visibleCache.put(item.getId(), false); return false; // underlying entity missing, don't show it } finally { if (pushed) popAdvisor(); } if (itemGroups == null || itemGroups.size() == 0) { // this includes items for which for which visibility doesn't apply visibleCache.put(item.getId(), true); return true; } getMyGroups(); for (String group : itemGroups) { if (myGroups.contains(group)) { visibleCache.put(item.getId(), true); return true; } } visibleCache.put(item.getId(), false); return false; } // this is called in a loop to see whether items are available. Since computing it can require // database transactions, we cache the results public boolean isItemComplete(SimplePageItem item) { if (!item.isRequired()) { // We don't care if it has been completed if it isn't required. return true; } Long itemId = item.getId(); Boolean cached = completeCache.get(itemId); if (cached != null) return (boolean) cached; if (item.getType() == SimplePageItem.RESOURCE || item.getType() == SimplePageItem.URL || item.getType() == SimplePageItem.BLTI) { // Resource. Completed if viewed. if (hasLogEntry(item.getId())) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.PAGE) { SimplePageLogEntry entry = getLogEntry(item.getId()); if (entry == null || entry.getDummy()) { completeCache.put(itemId, false); return false; } else if (entry.isComplete()) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.ASSIGNMENT) { try { if (item.getSakaiId().equals(SimplePageItem.DUMMY)) { completeCache.put(itemId, false); return false; } LessonEntity assignment = assignmentEntity.getEntity(item.getSakaiId()); if (assignment == null) { completeCache.put(itemId, false); return false; } LessonSubmission submission = assignment.getSubmission(getCurrentUserId()); if (submission == null) { completeCache.put(itemId, false); return false; } int type = assignment.getTypeOfGrade(); if (!item.getSubrequirement()) { completeCache.put(itemId, true); return true; } else if (submission.getGradeString() != null) { // assume that assignments always use string grade. this may change boolean ret = isAssignmentComplete(type, submission, item.getRequirementText()); completeCache.put(itemId, ret); return ret; } else { completeCache.put(itemId, false); return false; } } catch (Exception e) { e.printStackTrace(); completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.FORUM) { try { if (item.getSakaiId().equals(SimplePageItem.DUMMY)) { completeCache.put(itemId, false); return false; } User user = UserDirectoryService.getUser(getCurrentUserId()); LessonEntity forum = forumEntity.getEntity(item.getSakaiId()); if (forum == null) return false; // for the moment don't find grade. just see if they submitted if (forum.getSubmissionCount(user.getId()) > 0) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } catch (Exception e) { e.printStackTrace(); completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.ASSESSMENT) { if (item.getSakaiId().equals(SimplePageItem.DUMMY)) { completeCache.put(itemId, false); return false; } LessonEntity quiz = quizEntity.getEntity(item.getSakaiId(), this); if (quiz == null) { completeCache.put(itemId, false); return false; } User user = null; try { user = UserDirectoryService.getUser(getCurrentUserId()); } catch (Exception ignore) { completeCache.put(itemId, false); return false; } LessonSubmission submission = quiz.getSubmission(user.getId()); if (submission == null) { completeCache.put(itemId, false); return false; } else if (!item.getSubrequirement()) { // All that was required was that the user submit the test completeCache.put(itemId, true); return true; } else { Double grade = submission.getGrade(); // 1.99999 should match 2, so do a bit of rounding up if ((grade + 0.0001d) >= Double.valueOf(item.getRequirementText())) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } } else if (item.getType() == SimplePageItem.COMMENTS) { List<SimplePageComment> comments = simplePageToolDao.findCommentsOnItemByAuthor((long) itemId, getCurrentUserId()); boolean found = false; if (comments != null) { for (SimplePageComment comment : comments) { if (comment.getComment() != null && !comment.getComment().equals("")) { found = true; break; } } } if (found) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.STUDENT_CONTENT) { // need option for also requiring the student to submit a comment on the content SimpleStudentPage student = findStudentPage(item); if (student != null && !student.isDeleted()) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.TEXT || item.getType() == SimplePageItem.MULTIMEDIA) { // In order to be considered "complete", these items // only have to be viewed. If this code is reached, // we know that that the page has already been viewed. completeCache.put(itemId, true); return true; } else if (item.getType() == SimplePageItem.QUESTION) { SimplePageQuestionResponse response = simplePageToolDao.findQuestionResponse(item.getId(), getCurrentUserId()); if (response != null) { completeCache.put(itemId, true); return true; } else { completeCache.put(itemId, false); return false; } } else if (item.getType() == SimplePageItem.PEEREVAL) { SimplePagePeerEval peerEval = simplePageToolDao.findPeerEval(item.getId()); boolean result = peerEval == null ? true : false; completeCache.put(itemId, result); return result; } else { completeCache.put(itemId, false); return false; } } private boolean isAssignmentComplete(int type, LessonSubmission submission, String requirementString) { String grade = submission.getGradeString(); if (type == SimplePageItem.ASSESSMENT) { if (grade.equals("Pass")) { return true; } else { return false; } } else if (type == SimplePageItem.TEXT) { if (grade.equals("Checked")) { return true; } else { return false; } } else if (type == SimplePageItem.PAGE) { if (grade.equals("ungraded")) { return false; } int requiredIndex = -1; int currentIndex = -1; for (int i = 0; i < GRADES.length; i++) { if (GRADES[i].equals(requirementString)) { requiredIndex = i; } if (GRADES[i].equals(grade)) { currentIndex = i; } } if (requiredIndex == -1 || currentIndex == -1) { return false; } else { if (requiredIndex >= currentIndex) { return true; } else { return false; } } } else if (type == SimplePageItem.ASSIGNMENT) { // assignment 2 uses gradebook, so we have a float value // use some fuzz so 1.9999 is the same as 2 if (submission.getGrade() != null) return (submission.getGrade() + 0.0001d) >= Double.valueOf(requirementString); // otherwise use the String. With two strings we can use exact decimal arithmetic if (new BigDecimal(grade) .compareTo(new BigDecimal(requirementString).multiply(new BigDecimal(10))) >= 0) { return true; } else { return false; } } else { return false; } } // note on completion: there's an issue with subpages. isItemComplete just looks to see if // the logEntry shows that the page is complete. But that's filled out when someone actually // visits the page. If an instructor has graded something or a student has submitted directly // through a tool, requirements in a subpage might have been completed without the student // actually visiting the page. // So for the first subpage that is both required and not completed, we want to recheck // the subpage to see if it's now complete. This will have to happen recursively. Of course // if it's OK then we need to check the next one, etc. // // This code will return false immediately when it gets the first thing that is required // and not completed. We just do // a recusrive check if the subpage is required, visited, and not already completed. The reason // for only checking if visited is to avoid false positives for page with no requirements. A // page with no required items is completed when it's visited. If there are no reqirements, the // recursive call will return true, but if the page hasn't been visited it's still not completed. // So we only want to do the recursive call if it's been visited. // I think it's reasonable not to start checking status of quizes etc until the page has been // visited. // // no changes are needed for isItemCompleted. isPageCompleted is called at the starrt of ShowPage // by track. That call will update the status of subpages. So when we're doing other operations on // the page we can use a simple isItemComplete, because status would have been updated at the start // of ShowPage. // // Note that we only check the first subpage that hasn't been completed. If there's more than one, // information about later ones could be out of date. I'm claim that's less important, because it can't // affect whether anything is allowed. // // the recursive call to isItemComplete for subpages has some issues. isItemComplete will // use the completeCache, which may be set by isitemcomplete without doing a full recursive // scan. We are again depending upon the fact that the first check is done here, which does // the necessary recursion. The cache is request-scope. If it were longer-lived we'd have // a problem. // alreadySeen is needed in case there's a loop in the page structure. This is uncommon but // possible public boolean isPageComplete(long itemId) { return isPageComplete(itemId, null); } /** * @param itemId * The ID of the page from the <b>items</b> table (not the page table). * @return */ public boolean isPageComplete(long itemId, Set<Long> alreadySeen) { // Make sure student content objects aren't treated like pages. // TODO: Put in requirements if (findItem(itemId).getType() == SimplePageItem.STUDENT_CONTENT) { return true; } List<SimplePageItem> items = getItemsOnPage(Long.valueOf(findItem(itemId).getSakaiId())); for (SimplePageItem item : items) { if (!isItemComplete(item) && isItemVisible(item)) { if (item.getType() == SimplePageItem.PAGE) { // If we get here, must be not completed or isItemComplete would be true SimplePageLogEntry entry = getLogEntry(item.getId()); // two possibilities in next check: // 1) hasn't seen page, can't be complete // 2) we've checked before; there's a loop; be safe and disallow it if (entry == null || entry.getDummy() || (alreadySeen != null && alreadySeen.contains(item.getId()))) { return false; } if (alreadySeen == null) alreadySeen = new HashSet<Long>(); alreadySeen.add(itemId); // recursive check to see whether page is complete boolean subOK = isPageComplete(item.getId(), alreadySeen); if (!subOK) { return false; // nope, that was our last hope } // was complete; fall through and return true } else return false; } } // All of them were complete. completeCache.put(itemId, true); return true; } // return list of pages needed for current page. This is the primary code // used by ShowPageProducer to see whether the user is allowed to the page // (given that they have read permission, of course). Use LessonsAccess // elsewhere, because there are additional checks in ShowPageProducer that // are not in this code. // Note that the same page can occur // multiple places, but we're passing the item, so we've got the right one public List<String> pagesNeeded(SimplePageItem item) { String currentPageId = Long.toString(getCurrentPageId()); List<String> needed = new ArrayList<String>(); // authorized or maybe user is gaming us, or maybe next page code // sent them to something that isn't available. // as an optimization check haslogentry first. That will be true if // they have been here before. Saves us the trouble of doing full // access checking. Otherwise do a real check. That should only happen // for next page in odd situations. The code in ShowPageProducer checks // visible and release for this page, so we really need // available for this item. But to be complete we do need to check // accessibility of the containing page. if (item.getPageId() > 0) { if (!hasLogEntry(item.getId()) && (!isItemAvailable(item, item.getPageId()) || !lessonsAccess .isPageAccessible(item.getPageId(), getCurrentSiteId(), getCurrentUserId(), this))) { SimplePage parent = getPage(item.getPageId()); if (parent != null) needed.add(parent.getTitle()); else needed.add("unknown page"); // not possible, it says } return needed; } // we've got a top level page. // There is no containing page, so this is just available (i.e. prerequesites). // We can't use the normal code because we need a list of prerequisite pages. if (!item.isPrerequisite()) { return needed; } // get dummy items for top level pages in site List<SimplePageItem> items = simplePageToolDao.findItemsInSite(getCurrentSite().getId()); // sorted by SQL for (SimplePageItem i : items) { if (i.getSakaiId().equals(currentPageId)) { return needed; // reached current page. we're done } if (i.isRequired() && !isItemComplete(i) && isItemVisible(i)) needed.add(i.getName()); } return needed; } // maybeUpdateLinks checks to see if this page was copied from another // site and needs an update // only works if you have lessons write permission. Caller shold check public void maybeUpdateLinks() { String needsFixup = getCurrentSite().getProperties().getProperty("lessonbuilder-needsfixup"); if (needsFixup != null && needsFixup.length() != 0) { // it's important for only one process to do the update. So instead of depending upon something // that can be cached and is not synced across sites, do this directly in the DB with somehting // atomic. Also site save is veyr heavy weight, and not well interlocked. Much better just // to remove the property. This should only be needed for 10 min (cache lifetime), after which // the test above will show that it's not needed // Permission note: this should work for a student. A full site save won't. However this // code only gets called for people with lessons.write. Normally lessons.write is also people // with site.upd, but maybe not always. It should be OK for anyone to clear this flag in this code int updated = 0; try { updated = simplePageToolDao.clearNeedsFixup(getCurrentSiteId()); } catch (Exception e) { // should get here if the flag has been removed already by another process log.warn("clearneedsfixup " + e); } // only do this if there was a flag to delete if (updated != 0) { lessonBuilderEntityProducer.updateEntityReferences(getCurrentSiteId()); currentSite = null; // force refetch next time } } int fixupType = simplePageToolDao.clearNeedsGroupFixup(getCurrentSiteId()); if (fixupType != 0) lessonBuilderEntityProducer.fixupGroupRefs(getCurrentSiteId(), this, fixupType); } public boolean isItemAvailable(SimplePageItem item) { return isItemAvailable(item, getCurrentPageId()); } public boolean isItemAvailable(SimplePageItem item, long pageId) { if (item.isPrerequisite()) { List<SimplePageItem> items = getItemsOnPage(pageId); for (SimplePageItem i : items) { // System.out.println(i.getSequence() + " " + i.isRequired() + " " + isItemVisible(i) + " " + isItemComplete(i)); if (i.getSequence() >= item.getSequence()) { break; } else if (i.isRequired() && isItemVisible(i)) { if (!isItemComplete(i)) { return false; } } } } return true; } // weird variant that works even if current item doesn't have prereq. public boolean wouldItemBeAvailable(SimplePageItem item, long pageId) { List<SimplePageItem> items = getItemsOnPage(pageId); for (SimplePageItem i : items) { if (i.getSequence() >= item.getSequence()) { break; } else if (i.isRequired() && isItemVisible(i)) { if (!isItemComplete(i)) return false; } } return true; } public String getNameOfSakaiItem(SimplePageItem i) { String SakaiId = i.getSakaiId(); if (SakaiId == null || SakaiId.equals(SimplePageItem.DUMMY)) return null; if (i.getType() == SimplePageItem.ASSIGNMENT) { LessonEntity assignment = assignmentEntity.getEntity(i.getSakaiId()); if (assignment == null) return null; return assignment.getTitle(); } else if (i.getType() == SimplePageItem.FORUM) { LessonEntity forum = forumEntity.getEntity(i.getSakaiId()); if (forum == null) return null; return forum.getTitle(); } else if (i.getType() == SimplePageItem.ASSESSMENT) { LessonEntity quiz = quizEntity.getEntity(i.getSakaiId(), this); if (quiz == null) return null; return quiz.getTitle(); } else if (i.getType() == SimplePageItem.BLTI) { if (bltiEntity == null) return null; LessonEntity blti = bltiEntity.getEntity(i.getSakaiId()); if (blti == null) return null; return blti.getTitle(); } else return null; } // we allow both ? and &. The key may be the value of something like ?v=, so we don't know // whether the next thing is & or ?. To be safe, use & except for the first param, which // uses ?. Note that RSF will turn & into & in the src= attribute. THis appears to be correct, // as HTML is an SGML dialect. // If you run into trouble with &, you can use ; in the following. Google seems to // process it correctly. ; is a little-known alterantive to & that the RFCs do permit private static String normalizeParams(String URL) { URL = URL.replaceAll("[\\?\\&\\;]", "&"); return URL.replaceFirst("\\&", "?"); } public static String getYoutubeKeyFromUrl(String URL) { // see if it has a Youtube ID int offset = 0; if (URL.startsWith("http:")) offset = 5; else if (URL.startsWith("https:")) offset = 6; if (URL.startsWith("//www.youtube.com/", offset) || URL.startsWith("//youtube.com/", offset)) { Matcher match = YOUTUBE_PATTERN.matcher(URL); if (match.find()) { return normalizeParams(match.group(1)); } match = YOUTUBE2_PATTERN.matcher(URL); if (match.find()) { return normalizeParams(match.group(1)); } } else if (URL.startsWith("//youtu.be/", offset)) { Matcher match = SHORT_YOUTUBE_PATTERN.matcher(URL); if (match.find()) { return normalizeParams(match.group(1)); } } return null; } /* * return 11-char youtube ID for a URL, or null if it doesn't match * we store URLs as content objects, so we have to retrieve the object * in order to check. The actual URL is stored as the contents * of the entity */ public String getYoutubeKey(SimplePageItem i) { String sakaiId = i.getSakaiId(); // this is called only from contexts where we know it's OK to get the data. // indeed if I were doing it over I'd put it in the item, not resources SecurityAdvisor advisor = null; try { // if(getCurrentPage().getOwner() != null) { // Need to allow access into owner's home directory advisor = new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { if ("content.read".equals(function) || "content.hidden".equals(function)) { return SecurityAdvice.ALLOWED; } else { return SecurityAdvice.PASS; } } }; securityService.pushAdvisor(advisor); // } // find the resource ContentResource resource = null; try { resource = contentHostingService.getResource(sakaiId); } catch (Exception ignore) { return null; } // make sure it's a URL if (resource == null || // need to check both. Sakai 10 sets only resource type, but earlier releases don't // copy that when doing site copy, so for them have to check contenttype. (!resource.getResourceType().equals("org.sakaiproject.content.types.urlResource") && !resource.getContentType().equals("text/url"))) { return null; } // get the actual URL String URL = null; try { URL = new String(resource.getContent()); } catch (Exception ignore) { return null; } if (URL == null) { return null; } return getYoutubeKeyFromUrl(URL); } catch (Exception ex) { ex.printStackTrace(); } finally { if (advisor != null) securityService.popAdvisor(); } // no return null; } // current recommended best URL for youtube. Put here because the same code is // used a couple of different places public static String getYoutubeUrlFromKey(String key) { return "https://www.youtube.com/embed/" + key + "?wmode=opaque"; } public String[] split(String s, String p) { if (s == null || s.equals("")) return new String[0]; else return s.split(p); } /** * Meant to guarantee that the permissions are set correctly on an assessment for a user. * * @param item * @param shouldHaveAccess */ public void checkItemPermissions(SimplePageItem item, boolean shouldHaveAccess) { checkItemPermissions(item, shouldHaveAccess, true); } /** * * @param item * @param shouldHaveAccess * @param canRecurse * Is it allowed to delete the row in the table for the group and recurse to try * again. true for normal calls; false if called inside this code to avoid infinite loop */ // only called if the item should be under control. Also only called if the item is displayed // so if it's limited to a group, we'll never add people who aren't in the group, since the // item isn't shown to them. private void checkItemPermissions(SimplePageItem item, boolean shouldHaveAccess, boolean canRecurse) { if (SimplePageItem.DUMMY.equals(item.getSakaiId())) return; // for pages, presence of log entry is it if (item.getType() == SimplePageItem.PAGE) { Long itemId = item.getId(); if (getLogEntry(itemId) != null) return; // already ok // if no log entry, create a dummy entry if (shouldHaveAccess) { String userId = getCurrentUserId(); if (userId == null) userId = ".anon"; SimplePageLogEntry entry = simplePageToolDao.makeLogEntry(userId, itemId, null); entry.setDummy(true); saveItem(entry); logCache.put(itemId + "--1", entry); } return; } SimplePageGroup group = simplePageToolDao.findGroup(item.getSakaiId()); if (group == null) { // For some reason, the group doesn't exist. Let's re-add it. checkControlGroup(item, true); group = simplePageToolDao.findGroup(item.getSakaiId()); if (group == null) { return; } } boolean success = true; String groupId = group.getGroupId(); try { if (shouldHaveAccess) { success = GroupPermissionsService.addCurrentUser(getCurrentPage().getSiteId(), getCurrentUserId(), groupId); } else { success = GroupPermissionsService.removeUser(getCurrentPage().getSiteId(), getCurrentUserId(), groupId); } } catch (Exception ex) { ex.printStackTrace(); return; } // hmmm.... couldn't add or remove from group. Most likely the Sakai-level group // doesn't exist, although our database entry says it was created. Presumably // the user deleted the group for Site Info. Make very sure that's the cause, // or we'll create a duplicate group. I've seen failures for other reasons, such // as a weird permissions problem with the only maintain users trying to unjoin // a group. if (!success && canRecurse) { try { authzGroupService.getAuthzGroup(groupId); // group exists, it was something else. Who knows what return; } catch (org.sakaiproject.authz.api.GroupNotDefinedException ee) { } catch (Exception e) { // some other failure from getAuthzGroup, shouldn't be possible log.warn("checkItemPermissions unable to join or unjoin group " + groupId); } log.warn("checkItemPermissions: User seems to have deleted group " + groupId + ". We'll recreate it."); // OK, group doesn't exist. When we recreate it, it's going to have a // different groupId, so we have to back out of everything and reset it checkControlGroup(item, false); // huh? checkcontrolgroup just deleted it //simplePageToolDao.deleteItem(group); // We've undone it; call ourselves again, since the code at the // start will recreate the group checkItemPermissions(item, shouldHaveAccess, false); } } public void setYoutubeURL(String url) { youtubeURL = url; } public void setYoutubeId(long id) { youtubeId = id; } public void deleteYoutubeItem() { itemId = findItem(youtubeId).getId(); deleteItem(); } public void setMmUrl(String url) { mmUrl = url; } public void setMultipartMap(Map<String, MultipartFile> multipartMap) { this.multipartMap = multipartMap; } public String fixFileName(String collectionId, String name, String extension) { if (extension.equals("") || extension.startsWith(".")) { // do nothing } else { extension = "." + extension; } name = Validator.escapeResourceName(name.trim()) + Validator.escapeResourceName(extension); // allow for possible addition of -NN for uniqueness int maxname = 250 - collectionId.length() - 3; if (name.length() > maxname) name = org.apache.commons.lang.StringUtils.abbreviateMiddle(name, "_", maxname); return name; } // for group-owned student pages, put it in the worksite of the current user public String getCollectionId(boolean urls) { String siteId = getCurrentPage().getSiteId(); String baseDir = ServerConfigurationService.getString("lessonbuilder.basefolder", null); boolean hiddenDir = ServerConfigurationService.getBoolean("lessonbuilder.folder.hidden", false); String pageOwner = getCurrentPage().getOwner(); String collectionId; String folder = null; if (pageOwner == null) { collectionId = contentHostingService.getSiteCollection(siteId); if (baseDir != null) { if (!baseDir.endsWith("/")) baseDir = baseDir + "/"; collectionId = collectionId + baseDir; // basedir which is hidden; have to create it if it doesn't exist, so we can make hidden if (hiddenDir) { hiddenDir = false; // hiding base, done hide actual folder try { try { contentHostingService.checkCollection(collectionId); } catch (IdUnusedException idex) { ContentCollectionEdit edit = contentHostingService.addCollection(collectionId); edit.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, Validator.escapeResourceName(baseDir.substring(0, baseDir.length() - 1))); edit.setHidden(); contentHostingService.commitCollection(edit); } } catch (Exception ignore) { // I've been ignoring errors. // that will cause failure at a later stage where we can // return an error message. This may not be optimal. } } } // actual folder. Use hierarchy of files SimplePage page = getCurrentPage(); String folderString = page.getFolder(); if (folderString != null) { folder = collectionId + folderString; } else { Path path = getPagePath(page, new HashSet<Long>()); String title = path.title; // there's a limit of 255 to resource names. Leave 30 chars for the file. // getPagePath limits folder names in the hiearchy to 30, but in weird situations // we could a very deep hierarchy and the whole thing could get too long. If that // happens, just use the page name. We assume the collection ID will be /group/UUID, // so it will always be reasonable. In theory we could have to do yet another test // on collection ID. // 33 is a name of length 30 and -NN for duplicates // actual length is 255, but I worry about weird characters I don't understand if (title.length() > (250 - collectionId.length() - 33)) { title = Validator.escapeResourceName( org.apache.commons.lang.StringUtils.abbreviateMiddle(getPageTitle(), "_", 30)) + "/"; } // make sure folder names are unique if (simplePageToolDao.doesPageFolderExist(getCurrentSiteId(), title)) { String base = title.substring(0, title.length() - 1); for (int suffix = 1; suffix < 100; suffix++) { String trial = base + "-" + suffix + "/"; if (!simplePageToolDao.doesPageFolderExist(getCurrentSiteId(), trial)) { title = trial; break; } } } folder = collectionId + title; page.setFolder(title); simplePageToolDao.quickUpdate(page); } // folder = collectionId + Validator.escapeResourceName(getPageTitle()) + "/"; } else { collectionId = "/user/" + getCurrentUserId() + "/stuff4/"; // actual folder -- just use page name for student content folder = collectionId + Validator.escapeResourceName(getPageTitle()) + "/"; } // folder we really want if (urls) folder = folder + "urls/"; // OK? try { contentHostingService.checkCollection(folder); // OK, let's use it return folder; } catch (Exception ignore) { } ; // no. create folders as needed // if url subdir, need an extra level if (urls) { // try creating the root. if it exists this will fail. That's OK. String root = collectionId + Validator.escapeResourceName(getPageTitle()) + "/"; try { ContentCollectionEdit edit = contentHostingService.addCollection(root); edit.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, Validator.escapeResourceName(getPageTitle())); if (hiddenDir) edit.setHidden(); contentHostingService.commitCollection(edit); // well, we got that far anyway collectionId = root; } catch (Exception ignore) { } } // now try creating what we want try { ContentCollectionEdit edit = contentHostingService.addCollection(folder); if (urls) edit.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, "urls"); else { edit.getPropertiesEdit().addProperty(ResourceProperties.PROP_DISPLAY_NAME, Validator.escapeResourceName(getPageTitle())); if (hiddenDir) edit.setHidden(); } contentHostingService.commitCollection(edit); return folder; // worked. use it } catch (Exception ignore) { } ; // didn't. do the best we can return collectionId; } public class Path { public int level; public String title; public Path(int level, String title) { this.level = level; this.title = title; } } // not implemented for student pages public Path getPagePath(SimplePage page, Set<Long> seen) { seen.add(page.getPageId()); List<SimplePageItem> items = simplePageToolDao.findPageItemsBySakaiId(Long.toString(page.getPageId())); if (items == null || items.size() == 0) { return new Path(0, Validator.escapeResourceName( org.apache.commons.lang.StringUtils.abbreviateMiddle(page.getTitle(), "_", 30)) + "/"); } else { int minlevel = 9999; String bestPath = ""; for (SimplePageItem i : items) { SimplePage p = simplePageToolDao.getPage(i.getPageId()); if (p == null) continue; if (p.getOwner() != null) // probably can't happen continue; if (seen.contains(p.getPageId())) // already seen this page, we're in a loop continue; Path path = getPagePath(p, seen); // there can be loops in the network if (path.level < minlevel) { minlevel = path.level; bestPath = path.title; } } if (bestPath.equals("")) return new Path(0, Validator.escapeResourceName( org.apache.commons.lang.StringUtils.abbreviateMiddle(page.getTitle(), "_", 30)) + "/"); return new Path( minlevel + 1, bestPath + Validator.escapeResourceName( org.apache.commons.lang.StringUtils.abbreviateMiddle(page.getTitle(), "_", 30)) + "/"); } } public boolean isHtml(SimplePageItem i) { StringTokenizer token = new StringTokenizer(i.getSakaiId(), "."); String extension = ""; while (token.hasMoreTokens()) { extension = token.nextToken().toLowerCase(); } // we are just starting to store the MIME type for resources now. So existing content // won't have them. String mimeType = i.getHtml(); if (mimeType != null && (mimeType.startsWith("http") || mimeType.equals(""))) mimeType = null; if (mimeType != null && (mimeType.equals("text/html") || mimeType.equals("application/xhtml+xml")) || mimeType == null && (extension.equals("html") || extension.equals("htm"))) { return true; } return false; } public static final int MAXIMUM_ATTEMPTS_FOR_UNIQUENESS = 100; // called by dialog to add inline multimedia item, or update existing // item if itemid is specified // NOTE: in a group-owned student page, the files are put in the home directory // of the current user. That's the only consistent approach I could come up with // this function uses a security advicor, so that will work. public void addMultimedia() { // This code must be read together with the SimplePageItem.MULTIMEDIA // display code in ShowPageProducer.java (To find it search for // multimediaDisplayType) and with the code in show-page.js that // handles the add multimedia dialog (look for #mm-add-item) // historically this code was to display files ,and urls leading to things // like MP4. as backup if we couldn't figure out what to do we'd put something // in an iframe. The one exception is youtube, which we supposed explicitly. // However we now support several ways to embed content. We use the // multimediaDisplayType code to indicate which. The codes are // 1 -- embed code, 2 -- av type, 3 -- oembed, 4 -- iframe // 2 is the original code: MP4, image, and as a special case youtube urls // For all practical purposes type 2 is the same as the old items that don't // have type codes (although iframes are also handled by the old code) // the old code creates ojbects in ContentHosting for both files and URLs. // The new code saves the embed code or URL itself as an atteibute of the item // If I were doing it again, I wouldn't create the ContebtHosting item // Note that IFRAME is only used for something where the far end claims the MIME // type is HTML. For weird stuff like MS Word files I use the file display code, which // ShowPageProducer figures out how to display type 2 (or default) items // on the fly, so we don't have to known here what they are. SecurityAdvisor advisor = null; try { if (getCurrentPage().getOwner() != null) { advisor = new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }; securityService.pushAdvisor(advisor); } if (!itemOk(itemId)) return; if (!canEditPage()) return; if (!checkCsrf()) return; if (multipartMap.size() > 0) { // user specified a file, create it for (MultipartFile file : multipartMap.values()) { if (file.isEmpty()) file = null; addMultimediaFile(file); } } } catch (Exception exception) { exception.printStackTrace(); } finally { if (advisor != null) securityService.popAdvisor(); } } public void addMultimediaFile(MultipartFile file) { try { String name = null; String sakaiId = null; String mimeType = null; if (file != null) { if (!uploadSizeOk(file)) return; String collectionId = getCollectionId(false); // user specified a file, create it name = file.getOriginalFilename(); if (name == null || name.length() == 0) name = file.getName(); int i = name.lastIndexOf("/"); if (i >= 0) name = name.substring(i + 1); String base = name; String extension = ""; i = name.lastIndexOf("."); if (i > 0) { base = name.substring(0, i); extension = name.substring(i + 1); } mimeType = file.getContentType(); try { ContentResourceEdit res = null; if (itemId != -1 && replacefile) { // upload new version -- get existing file SimplePageItem item = findItem(itemId); String resId = item.getSakaiId(); res = contentHostingService.editResource(resId); } else { // otherwise create a new file res = contentHostingService.addResource(collectionId, fixFileName(collectionId, Validator.escapeResourceName(base), Validator.escapeResourceName(extension)), "", MAXIMUM_ATTEMPTS_FOR_UNIQUENESS); } if (isCaption) res.setContentType("text/vtt"); else res.setContentType(mimeType); res.setContent(file.getInputStream()); try { contentHostingService.commitResource(res, NotificationService.NOTI_NONE); // there's a bug in the kernel that can cause // a null pointer if it can't determine the encoding // type. Since we want this code to work on old // systems, work around it. } catch (java.lang.NullPointerException e) { setErrMessage(messageLocator.getMessage("simplepage.resourcepossibleerror")); } sakaiId = res.getId(); if (("application/zip".equals(mimeType) || "application/x-zip-compressed".equals(mimeType)) && isWebsite) { // We need to set the sakaiId to the resource id of the index file sakaiId = expandZippedResource(sakaiId); if (sakaiId == null) return; // We set this special type for the html field in the db. This allows us to // map an icon onto website links in applicationContext.xml mimeType = "LBWEBSITE"; } } catch (org.sakaiproject.exception.OverQuotaException ignore) { setErrMessage(messageLocator.getMessage("simplepage.overquota")); return; } catch (Exception e) { setErrMessage( messageLocator.getMessage("simplepage.resourceerror").replace("{}", e.toString())); log.error("addMultimedia error 1 " + e); return; } ; } else if (mmUrl != null && !mmUrl.trim().equals("") && multimediaDisplayType != 1 && multimediaDisplayType != 3) { // user specified a URL, create the item String url = mmUrl.trim(); // if user gives a plain hostname, make it a URL. // ui add https if page is displayed with https. I'm reluctant to use protocol-relative // urls, because I don't know whether all the players understand it. if (!url.startsWith("http:") && !url.startsWith("https:") && !url.startsWith("/")) { String atom = url; int i = atom.indexOf("/"); if (i >= 0) atom = atom.substring(0, i); // first atom is hostname if (atom.indexOf(".") >= 0) { String server = ServerConfigurationService.getServerUrl(); if (server.startsWith("https:")) url = "https://" + url; else url = "http://" + url; } } name = url; String basename = url; // SAK-11816 method for creating resource ID String extension = ".url"; if (basename != null && basename.length() > 32) { // lose the http first if (basename.startsWith("http:")) { basename = basename.substring(7); } else if (basename.startsWith("https:")) { basename = basename.substring(8); } if (basename.length() > 32) { // max of 18 chars from the URL itself basename = basename.substring(0, 18); // add a timestamp to differentiate it (+14 chars) Format f = new SimpleDateFormat("yyyyMMddHHmmss"); basename += f.format(new Date()); // total new length of 32 chars } } String collectionId; SimplePage page = getCurrentPage(); collectionId = getCollectionId(true); try { // urls aren't something people normally think of as resources. Let's hide them ContentResourceEdit res = contentHostingService .addResource(collectionId, fixFileName(collectionId, Validator.escapeResourceName(basename), Validator.escapeResourceName(extension)), "", MAXIMUM_ATTEMPTS_FOR_UNIQUENESS); res.setContentType("text/url"); res.setResourceType("org.sakaiproject.content.types.urlResource"); res.setContent(url.getBytes()); contentHostingService.commitResource(res, NotificationService.NOTI_NONE); sakaiId = res.getId(); } catch (org.sakaiproject.exception.OverQuotaException ignore) { setErrMessage(messageLocator.getMessage("simplepage.overquota")); return; } catch (Exception e) { setErrMessage( messageLocator.getMessage("simplepage.resourceerror").replace("{}", e.toString())); log.error("addMultimedia error 2 " + e); return; } // connect to url and get mime type // new dialog passes the mime type if (multimediaMimeType != null && !"".equals(multimediaMimeType)) mimeType = multimediaMimeType; else mimeType = getTypeOfUrl(url); } else if (mmUrl != null && !mmUrl.trim().equals("") && (multimediaDisplayType == 1 || multimediaDisplayType == 3)) { // fall through. we have an embed code, don't need file } else // nothing to do return; // itemId tells us whether it's an existing item // isMultimedia tells us whether resource or multimedia // sameWindow is only passed for existing items of type HTML/XHTML // for new items it should be set true for HTML/XTML, false otherwise // for existing items it should be set to the passed value for HTML/XMTL, false otherwise // it is ignored for isMultimedia, as those are always displayed inline in the current page SimplePageItem item = null; if (itemId == -1 && isMultimedia) { item = appendItem(sakaiId, name, SimplePageItem.MULTIMEDIA); } else if (itemId == -1 && isWebsite) { String websiteName = name.substring(0, name.indexOf(".")); item = appendItem(sakaiId, websiteName, SimplePageItem.RESOURCE); } else if (itemId == -1) { item = appendItem(sakaiId, name, SimplePageItem.RESOURCE); } else if (isCaption) { item = findItem(itemId); if (item == null) return; item.setAttribute("captionfile", sakaiId); update(item); return; } else { item = findItem(itemId); if (item == null) return; // editing an existing item which might have customized properties // retrieve original resource and check for customizations ResourceHelper resHelp = new ResourceHelper(getContentResource(item.getSakaiId())); // if replacing file, keep existing name boolean hasCustomName = resHelp.isNameCustom(item.getName()) || replacefile; item.setSakaiId(sakaiId); if (!hasCustomName) { item.setName(name); } } // for new file, old captions don't make sense item.removeAttribute("captionfile"); // remember who added it, for permission checks item.setAttribute("addedby", getCurrentUserId()); item.setPrerequisite(this.prerequisite); if (mimeType != null) { item.setHtml(mimeType); } else { item.setHtml(null); } if (mmUrl != null && !mmUrl.trim().equals("") && isMultimedia) { if (multimediaDisplayType == 1) // the code is filtered by the UI, so the user can see the effect. // This protects against someone handcrafting a post. // The code is similar to that in submit, but currently doesn't // have folder-specific override (because there are no folders involved) item.setAttribute("multimediaEmbedCode", AjaxServer.filterHtml(mmUrl.trim())); else if (multimediaDisplayType == 3) item.setAttribute("multimediaUrl", mmUrl.trim()); item.setAttribute("multimediaDisplayType", Integer.toString(multimediaDisplayType)); } // if this is an existing item and a resource, leave it alone // otherwise initialize to false if (isMultimedia || itemId == -1) item.setSameWindow(false); clearImageSize(item); try { // if (itemId == -1) // saveItem(item); // else update(item); } catch (Exception e) { System.out.println("save error " + e); // saveItem and update produce the errors } } catch (Exception ex) { ex.printStackTrace(); } } public boolean deleteRecursive(File path) throws FileNotFoundException { if (!path.exists()) throw new FileNotFoundException(path.getAbsolutePath()); boolean ret = true; if (path.isDirectory()) { for (File f : path.listFiles()) { ret = ret && deleteRecursive(f); } } return ret && path.delete(); } public List<Map<String, Object>> getToolsFileItem() { return ltiService.getToolsFileItem(); } public void handleFileItem() { ToolSession toolSession = sessionManager.getCurrentToolSession(); if (toolSession != null) toolSession.setAttribute("lessonbuilder.fileImportDone", "true"); String returnedData = ToolUtils.getRequestParameter("data"); String contentItems = ToolUtils.getRequestParameter("content_items"); // Retrieve the tool associated with the content item String toolId = ToolUtils.getRequestParameter("toolId"); Long toolKey = SakaiBLTIUtil.getLongNull(toolId); if (toolKey == 0 || toolKey < 0) { setErrKey("simplepage.lti-import-error-id", toolId); return; } Map<String, Object> tool = ltiService.getTool(toolKey); if (tool == null) { setErrKey("simplepage.lti-import-error-id", toolId); return; } // Parse, validate and check OAuth signature for the incoming ContentItem ContentItem contentItem = null; try { contentItem = SakaiBLTIUtil.getContentItemFromRequest(tool); } catch (Exception e) { setErrKey("simplepage.lti-import-bad-content-item", e.getMessage()); e.printStackTrace(); return; } // System.out.println("contentItem="+contentItem); // Extract the content item data Map item = (Map) contentItem.getItemOfType(ContentItem.TYPE_FILEITEM); if (item == null) { setErrKey("simplepage.lti-import-missing-file-item", null); return; } String localUrl = (String) item.get("url"); // System.out.println("localUrl="+localUrl); InputStream fis = null; if (localUrl != null && localUrl.length() > 1) { try { URL parsedUrl = new URL(localUrl); URLConnection yc = parsedUrl.openConnection(); fis = yc.getInputStream(); } catch (Exception e) { setErrKey("simplepage.lti-import-error-reading-url", localUrl); e.printStackTrace(); return; } // System.out.println("Importing..."); long length = importCcFromStream(fis); if (length > 0 && toolSession != null) { String successMessage = messageLocator.getMessage("simplepage.lti-import-success-length") .replace("{}", length + ""); toolSession.setAttribute("lessonbuilder.fileImportDone", successMessage); } return; } else { setErrKey("simplepage.lti-import-missing-url", null); } } // Import a Common Cartridge public void importCc() { if (!canEditPage()) return; if (!checkCsrf()) return; // Import an uploaded file MultipartFile file = null; if (multipartMap.size() > 0) { // user specified a file, create it file = multipartMap.values().iterator().next(); } InputStream fis = null; if (file != null) { if (!uploadSizeOk(file)) return; try { fis = file.getInputStream(); } catch (IOException e) { setErrKey("simplepage.cc-error", ""); e.printStackTrace(); return; } long length = importCcFromStream(fis); setTopRefresh(); } } // Import a Common Cartridge form an InputStream private long importCcFromStream(InputStream fis) { File cc = null; File root = null; long length = 0; try { cc = File.createTempFile("ccloader", "file"); root = File.createTempFile("ccloader", "root"); if (root.exists()) { if (!root.delete()) { setErrMessage("unable to delete temp file for load"); return -1; } } if (!root.mkdir()) { setErrMessage("unable to create temp directory for load"); return -1; } BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(cc)); byte[] buffer = new byte[8096]; int n = 0; while ((n = bis.read(buffer, 0, 8096)) >= 0) { if (n > 0) { bos.write(buffer, 0, n); length += n; } } bis.close(); bos.close(); CartridgeLoader cartridgeLoader = ZipLoader.getUtilities(cc, root.getCanonicalPath()); Parser parser = Parser.createCartridgeParser(cartridgeLoader); LessonEntity quizobject = null; for (LessonEntity q = quizEntity; q != null; q = q.getNextEntity()) { if (q.getToolId().equals(quiztool)) quizobject = q; } LessonEntity assignobject = null; for (LessonEntity q = assignmentEntity; q != null; q = q.getNextEntity()) { if (q.getToolId().equals(assigntool)) assignobject = q; } LessonEntity topicobject = null; for (LessonEntity q = forumEntity; q != null; q = q.getNextEntity()) { if (q.getToolId().equals(topictool)) topicobject = q; } parser.parse(new PrintHandler(this, cartridgeLoader, simplePageToolDao, quizobject, topicobject, bltiEntity, assignobject, importtop)); } catch (Exception e) { setErrKey("simplepage.cc-error", ""); e.printStackTrace(); length = -1; } finally { if (cc != null) try { deleteRecursive(cc); } catch (Exception e) { } try { deleteRecursive(root); } catch (Exception e) { } } return length; } // called by edit dialog to update parameters of a Youtube item public void updateYoutube() { if (!itemOk(youtubeId)) return; if (!canEditPage()) return; if (!checkCsrf()) return; SimplePageItem item = findItem(youtubeId); // find the new key, if the new thing is a legit youtube url String key = getYoutubeKeyFromUrl(youtubeURL); if (key == null) { setErrMessage(messageLocator.getMessage("simplepage.must_be_youtube")); return; } // oldkey had better work, since the youtube edit woudln't // be displayed if it wasn't recognized String oldkey = getYoutubeKey(item); // if there's a new youtube URL, and it's different from // the old one, update the URL if they are different if (key != null && !key.equals(oldkey)) { String url = "http://www.youtube.com/watch#!v=" + key; String siteId = getCurrentPage().getSiteId(); String collectionId = getCollectionId(true); SecurityAdvisor advisor = null; try { if (getCurrentPage().getOwner() != null) { advisor = new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }; securityService.pushAdvisor(advisor); } ContentResourceEdit res = contentHostingService.addResource(collectionId, Validator.escapeResourceName("Youtube video " + key), Validator.escapeResourceName("swf"), MAXIMUM_ATTEMPTS_FOR_UNIQUENESS); res.setContentType("text/url"); res.setResourceType("org.sakaiproject.content.types.urlResource"); res.setContent(url.getBytes()); contentHostingService.commitResource(res, NotificationService.NOTI_NONE); item.setSakaiId(res.getId()); } catch (org.sakaiproject.exception.OverQuotaException ignore) { setErrMessage(messageLocator.getMessage("simplepage.overquota")); } catch (Exception e) { setErrMessage(messageLocator.getMessage("simplepage.resourceerror").replace("{}", e.toString())); log.error("addMultimedia error 3 " + e); } finally { if (advisor != null) securityService.popAdvisor(); } } // even if there's some oddity with URLs, we do these updates item.setHeight(height); item.setWidth(width); item.setDescription(description); item.setPrerequisite(this.prerequisite); update(item); setItemGroups(item, selectedGroups); } /** * Adds or removes the requirement to have site.upd in order to see a page * i.e. hide or unhide a page * @param pageId * The Id of the Page * @param visible * @return true for success, false for failure * @throws IdUnusedException * , PermissionException */ private boolean pageVisibilityHelper(Site site, String pageId, boolean visible) throws IdUnusedException, PermissionException { SitePage page = site.getPage(pageId); List<ToolConfiguration> tools = page.getTools(); Iterator<ToolConfiguration> iterator = tools.iterator(); // If all the tools on a page require site.upd then only users with site.upd will see // the page in the site nav of Charon... not sure about the other Sakai portals floating // about while (iterator.hasNext()) { ToolConfiguration placement = iterator.next(); Properties roleConfig = placement.getPlacementConfig(); String roleList = roleConfig.getProperty("functions.require"); String visibility = roleConfig.getProperty("sakai-portal:visible"); boolean saveChanges = false; if (roleList == null) { roleList = ""; } if (!(roleList.indexOf(SITE_UPD) > -1) && !visible) { if (roleList.length() > 0) { roleList += ","; } roleList += SITE_UPD; saveChanges = true; } else if ((roleList.indexOf(SITE_UPD) > -1) && visible) { roleList = roleList.replaceAll("," + SITE_UPD, ""); roleList = roleList.replaceAll(SITE_UPD, ""); saveChanges = true; } if (saveChanges) { roleConfig.setProperty("functions.require", roleList); if (visible) roleConfig.remove("sakai-portal:visible"); else roleConfig.setProperty("sakai-portal:visible", "false"); placement.save(); siteService.save(site); } } return true; } // used by edit dialog to update properties of a multimedia object public void updateMovie() { if (!itemOk(itemId)) return; if (!canEditPage()) return; if (!checkCsrf()) return; SimplePageItem item = findItem(itemId); item.setHeight(height); item.setWidth(width); item.setDescription(description); item.setPrerequisite(prerequisite); item.setHtml(mimetype); update(item); setItemGroups(item, selectedGroups); } public void addCommentsSection(String ab) { addBefore = ab; // used by appendItem if (canEditPage()) { SimplePageItem item = appendItem("", messageLocator.getMessage("simplepage.comments-section"), SimplePageItem.COMMENTS); item.setDescription(messageLocator.getMessage("simplepage.comments-section")); update(item); // Must clear the cache so that the new item appears on the page itemsCache.remove(getCurrentPage().getPageId()); } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); } } /** * Admins can always edit. Authors can edit for 30 minutes. * * The second parameter is only used to distinguish this method from * the one directly below it. Allowing CommentsProducer to cache whether * or not the current user can edit the page, without having to hit the * database each time. * */ public boolean canModifyComment(SimplePageComment c, boolean canEditPage) { if (canEditPage) return true; if (c.getAuthor().equals(UserDirectoryService.getCurrentUser().getId())) { // Author can edit for 30 minutes. if (System.currentTimeMillis() - c.getTimePosted().getTime() <= 1800000) { return true; } else { return false; } } else { return false; } } // See method above public boolean canModifyComment(SimplePageComment c) { return canModifyComment(c, canEditPage()); } // May add or edit comments public String addComment() { if (!checkCsrf()) return "permission-failed"; boolean html = false; // Patch in the fancy editor's comment, if it's been used if (formattedComment != null && !formattedComment.equals("")) { comment = formattedComment; html = true; } StringBuilder error = new StringBuilder(); comment = FormattedText.processFormattedText(comment, error); if (comment == null || comment.equals("")) { setErrMessage(messageLocator.getMessage("simplepage.empty-comment-error")); return "failure"; } if (editId == null || editId.equals("")) { String userId = UserDirectoryService.getCurrentUser().getId(); Double grade = null; if (findItem(itemId).getGradebookId() != null) { List<SimplePageComment> comments = simplePageToolDao.findCommentsOnItemByAuthor(itemId, userId); if (comments != null && comments.size() > 0) { grade = comments.get(0).getPoints(); } } SimplePageComment commentObject = simplePageToolDao.makeComment(itemId, getCurrentPage().getPageId(), userId, comment, IdManager.getInstance().createUuid(), html); commentObject.setPoints(grade); saveItem(commentObject, false); } else { SimplePageComment commentObject = simplePageToolDao.findCommentById(Long.valueOf(editId)); if (commentObject != null && canModifyComment(commentObject)) { commentObject.setComment(comment); update(commentObject, false); } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } } if (getCurrentPage().getOwner() != null) { SimpleStudentPage student = simplePageToolDao.findStudentPage(getCurrentPage().getTopParent()); student.setLastCommentChange(new Date()); update(student, false); } return "added-comment"; } public String updateComments() { if (!checkCsrf()) return "permission-failed"; if (canEditPage()) { SimplePageItem comment = findItem(itemId); comment.setAnonymous(anonymous); setItemGroups(comment, selectedGroups); comment.setRequired(required); comment.setPrerequisite(prerequisite); if (maxPoints == null || maxPoints.equals("")) { maxPoints = "1"; } if (graded) { int points; try { points = Integer.valueOf(maxPoints); } catch (Exception ex) { setErrMessage(messageLocator.getMessage("simplepage.integer-expected")); return "failure"; } if (comment.getGradebookId() == null || !comment.getGradebookPoints().equals(points)) { String pageTitle = ""; String gradebookId = ""; boolean add = true; if (comment.getPageId() >= 0) { pageTitle = getPage(comment.getPageId()).getTitle(); gradebookId = "lesson-builder:comment:" + comment.getId(); if (comment.getGradebookId() != null && !comment.getGradebookPoints().equals(points)) add = gradebookIfc.updateExternalAssessment(getCurrentSiteId(), "lesson-builder:comment:" + comment.getId(), null, pageTitle + " Comments (item:" + comment.getId() + ")", Integer.valueOf(maxPoints), null); else add = gradebookIfc.addExternalAssessment(getCurrentSiteId(), "lesson-builder:comment:" + comment.getId(), null, pageTitle + " Comments (item:" + comment.getId() + ")", Integer.valueOf(maxPoints), null, "Lesson Builder"); if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else { comment.setGradebookTitle(pageTitle + " Comments (item:" + comment.getId() + ")"); } } else { // Must be a student page comments tool. SimpleStudentPage studentPage = simplePageToolDao .findStudentPage(Long.valueOf(comment.getSakaiId())); SimplePageItem studentPageItem = simplePageToolDao.findItem(studentPage.getItemId()); //pageTitle = simplePageToolDao.findStudentPage(Long.valueOf(comment.getSakaiId())).getTitle(); gradebookId = "lesson-builder:page-comment:" + studentPageItem.getId(); } if (add) { comment.setGradebookId(gradebookId); comment.setGradebookPoints(points); regradeComments(comment); } } } else if (comment.getGradebookId() != null && comment.getPageId() >= 0) { gradebookIfc.removeExternalAssessment(getCurrentSiteId(), comment.getGradebookId()); comment.setGradebookId(null); comment.setGradebookPoints(null); } // for forced comments, the UI won't ever do this, but if // it does, update will fail with permissions update(comment); return "success"; } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } } private void regradeComments(SimplePageItem comment) { List<SimplePageComment> comments = simplePageToolDao.findComments(comment.getId()); for (SimplePageComment c : comments) { if (c.getPoints() != null) { gradebookIfc.updateExternalAssessmentScore(getCurrentSiteId(), comment.getGradebookId(), c.getAuthor(), String.valueOf(c.getPoints())); } } } /** * Comments aren't actually deleted. The comment field is set to empty. * This is so that the namings remain consistent when the comment section * is set to show names as anonymous. Otherwise, deleting a post could change * the numbering, which hinders discussion. */ public String deleteComment(String commentUUID) { SimplePageComment comment = simplePageToolDao.findCommentByUUID(commentUUID); if (comment != null && comment.getPageId() == getCurrentPage().getPageId()) { if (canModifyComment(comment)) { comment.setComment(""); update(comment, false); return "success"; } } setErrMessage(messageLocator.getMessage("simplepage.comment-permissions-error")); return "failure"; } public void addStudentContentSection(String ab) { addBefore = ab; // used by appebdItem if (getCurrentPage().getOwner() == null && canEditPage()) { SimplePageItem item = appendItem("", messageLocator.getMessage("simplepage.student-content"), SimplePageItem.STUDENT_CONTENT); item.setDescription(messageLocator.getMessage("simplepage.student-content")); update(item); // Must clear the cache so that the new item appears on the page itemsCache.remove(getCurrentPage().getPageId()); } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); } } public boolean myStudentPageGroupsOk(SimplePageItem item) { Group group = null; String groupId = null; if (item.isGroupOwned()) { // all groups we are a member of Collection<Group> groups = getCurrentSite().getGroupsWithMember(getCurrentUserId()); String allowedString = item.getOwnerGroups(); // if no group list specified, we're OK if user is in any groups if (allowedString == null || allowedString.length() == 0) return groups != null && groups.size() > 0; // otherwise have to check HashSet<String> allowedIds = new HashSet<String>(Arrays.asList(allowedString.split(","))); HashSet<String> inIds = new HashSet<String>(); for (Group g : groups) inIds.add(g.getId()); // see if overlap between allowed and in inIds.retainAll(allowedIds); return inIds.size() > 0; } // if not group owned, always OK return true; } public boolean createStudentPage(long itemId) { SimplePage curr = getCurrentPage(); User user = UserDirectoryService.getCurrentUser(); // Need to make sure the section exists SimplePageItem containerItem = simplePageToolDao.findItem(itemId); // We want to make sure each student only has one top level page per section. SimpleStudentPage page = findStudentPage(containerItem); if (page == null && containerItem != null && containerItem.getType() == SimplePageItem.STUDENT_CONTENT && canReadPage()) { // First create object in lesson_builder_pages. String title = user.getDisplayName(); if (containerItem.isAnonymous()) { List<SimpleStudentPage> otherPages = simplePageToolDao.findStudentPages(itemId); int serial = 1; if (otherPages != null) serial = otherPages.size() + 1; title = messageLocator.getMessage("simplepage.anonymous") + " " + serial; } Group group = null; String groupId = null; if (containerItem.isGroupOwned()) { String allowedString = containerItem.getOwnerGroups(); HashSet<String> allowedGroups = null; if (allowedString != null && allowedString.length() > 0) allowedGroups = new HashSet<String>(Arrays.asList(allowedString.split(","))); Collection<Group> groups = getCurrentSite().getGroupsWithMember(user.getId()); if (groups.size() == 0) { setErrMessage(messageLocator.getMessage("simplepage.owner-groups-nogroup")); return false; } // ideally just one matches. But if more than one does, let's be deterministic // about which one we use. List<GroupEntry> groupEntries = new ArrayList<GroupEntry>(); for (Group g : groups) { if (allowedGroups != null && !allowedGroups.contains(g.getId())) continue; if (allowedGroups == null && (g.getProperties().getProperty("lessonbuilder_ref") != null || g.getTitle().startsWith("Access: "))) continue; GroupEntry e = new GroupEntry(); e.name = g.getTitle(); e.id = g.getId(); groupEntries.add(e); } if (groupEntries.size() == 0) { setErrMessage(messageLocator.getMessage("simplepage.owner-groups-nogroup")); return false; } Collections.sort(groupEntries, new Comparator() { public int compare(Object o1, Object o2) { GroupEntry e1 = (GroupEntry) o1; GroupEntry e2 = (GroupEntry) o2; return e1.name.compareTo(e2.name); } }); GroupEntry groupEntry = groupEntries.get(0); if (!containerItem.isAnonymous()) title = groupEntry.name; groupId = groupEntry.id; } SimplePage newPage = simplePageToolDao.makePage(curr.getToolId(), curr.getSiteId(), title, curr.getPageId(), null); newPage.setOwner(user.getId()); newPage.setGroup(groupId); saveItem(newPage, false); // Then attach the lesson_builder_student_pages item. // Then attach the lesson_builder_student_pages item. page = simplePageToolDao.makeStudentPage(itemId, newPage.getPageId(), title, user.getId(), groupId); SimplePageItem commentsItem = simplePageToolDao.makeItem(-1, -1, SimplePageItem.COMMENTS, null, messageLocator.getMessage("simplepage.comments-section")); saveItem(commentsItem, false); page.setCommentsSection(commentsItem.getId()); saveItem(page, false); commentsItem.setAnonymous(containerItem.getForcedCommentsAnonymous()); commentsItem.setSakaiId(String.valueOf(page.getId())); update(commentsItem, false); newPage.setTopParent(page.getId()); update(newPage, false); try { updatePageItem(containerItem.getId()); updatePageObject(newPage.getPageId()); adjustPath("push", newPage.getPageId(), containerItem.getId(), newPage.getTitle()); } catch (Exception ex) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return false; } // Reset the edit cache so that they can actually edit their page. String ref = "/site/" + getCurrentSiteId(); boolean ok = securityService.unlock(SimplePage.PERMISSION_LESSONBUILDER_UPDATE, ref); if (ok) editPrivs = 0; else editPrivs = 1; return true; } else if (page != null) { setErrMessage(messageLocator.getMessage("simplepage.page-exists")); return false; } else { return false; } } public HashMap<Long, SimplePageLogEntry> cacheStudentPageLogEntries(long itemId) { List<SimplePageLogEntry> entries = simplePageToolDao.getStudentPageLogEntries(itemId, UserDirectoryService.getCurrentUser().getId()); HashMap<Long, SimplePageLogEntry> map = new HashMap<Long, SimplePageLogEntry>(); for (SimplePageLogEntry entry : entries) { logCache.put(entry.getItemId() + "-" + entry.getStudentPageId(), entry); map.put(entry.getStudentPageId(), entry); } return map; } private void pushAdvisorAlways() { securityService.pushAdvisor(new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }); } private boolean pushAdvisor() { if (getCurrentPage().getOwner() != null) { securityService.pushAdvisor(new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }); return true; } else { return false; } } private void popAdvisor() { securityService.popAdvisor(); } public void setAddAnswerData(String data) { if (data == null || data.equals("")) { return; } int separator = data.indexOf(":"); String indexString = data.substring(0, separator); Integer index = Integer.valueOf(indexString); data = data.substring(separator + 1); // I think this method should only be called from one thread // so this should be safe. if (questionAnswers == null) { questionAnswers = new HashMap<Integer, String>(); log.info("setAddAnswer: it was null"); } // We store with the index so that we can maintain the order // in which the instructor inputted the answers questionAnswers.put(index, data); } /** Used for both adding and updating questions on a page. */ public String updateQuestion() { if (!itemOk(itemId)) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "permission-failed"; } if (!canEditPage()) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } if (!checkCsrf()) return "permission-failed"; if (questionType == null) { setErrMessage(messageLocator.getMessage("simplepage.no-question-type")); log.warn("No question type provided for question."); return "failure"; } SimplePageItem item; if (itemId != null && itemId != -1) { item = findItem(Long.valueOf(itemId)); } else { // Adding a question to the page item = appendItem("", messageLocator.getMessage("simplepage.questionName"), SimplePageItem.QUESTION); item.setAttribute("questionType", "shortanswer"); } item.setAttribute("questionText", questionText); item.setAttribute("questionCorrectText", questionCorrectText); item.setAttribute("questionIncorrectText", questionIncorrectText); item.setAttribute("questionType", questionType); if (questionType.equals("shortanswer")) { String shortAnswers[] = questionAnswer.split("\n"); questionAnswer = ""; for (int i = 0; i < shortAnswers.length; i++) { String a = shortAnswers[i].trim(); if (!a.equals("")) questionAnswer = questionAnswer + a + "\n"; } item.setAttribute("questionAnswer", questionAnswer); } else if (questionType.equals("multipleChoice")) { Long max = simplePageToolDao.maxQuestionAnswer(item); simplePageToolDao.clearQuestionAnswers(item); for (int i = 0; questionAnswers.get(i) != null; i++) { // get data sent from post operation for this answer String data = questionAnswers.get(i); // split the data into the actual fields String[] fields = data.split(":", 3); Long answerId; if (fields[0].equals("")) answerId = -1L; else answerId = Long.valueOf(fields[0]); if (answerId <= 0L) answerId = ++max; Boolean correct = fields[1].equals("true"); String text = fields[2]; if (text != null && !text.trim().equals("")) simplePageToolDao.addQuestionAnswer(item, answerId, text, correct); } item.setAttribute("questionShowPoll", String.valueOf(questionShowPoll)); simplePageToolDao.syncQRTotals(item); } int pointsInt = 10; if (maxPoints != null && !maxPoints.equals("")) { try { pointsInt = Integer.valueOf(maxPoints); } catch (Exception ex) { setErrMessage(messageLocator.getMessage("simplepage.integer-expected")); return "failure"; } } if (!graded || (gradebookTitle != null && gradebookTitle.trim().equals(""))) gradebookTitle = null; if (gradebookTitle != null && (item.getGradebookId() == null || item.getGradebookId().equals(""))) { // Creating new gradebook entry String gradebookId = "lesson-builder:question:" + item.getId(); String title = gradebookTitle; if (title == null || title.equals("")) { title = questionText; } boolean add = gradebookIfc.addExternalAssessment(getCurrentSiteId(), gradebookId, null, title, pointsInt, null, "Lesson Builder"); if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else { item.setGradebookId(gradebookId); item.setGradebookTitle(title); } } else if (gradebookTitle != null) { // Updating an old gradebook entry gradebookIfc.updateExternalAssessment(getCurrentSiteId(), item.getGradebookId(), null, gradebookTitle, pointsInt, null); item.setGradebookTitle(gradebookTitle); } else if (gradebookTitle == null && (item.getGradebookId() != null && !item.getGradebookId().equals(""))) { // Removing an existing gradebook entry gradebookIfc.removeExternalAssessment(getCurrentSiteId(), item.getGradebookId()); item.setGradebookId(null); item.setGradebookTitle(null); } item.setAttribute("questionGraded", String.valueOf(graded)); item.setRequired(required); if (graded) item.setGradebookPoints(pointsInt); else item.setGradebookPoints(null); item.setPrerequisite(prerequisite); update(item); setItemGroups(item, selectedGroups); regradeAllQuestionResponses(item.getId()); return "success"; } private void regradeAllQuestionResponses(long questionId) { List<SimplePageQuestionResponse> responses = simplePageToolDao.findQuestionResponses(questionId); for (SimplePageQuestionResponse response : responses) { gradeQuestionResponse(response); update(response); } } private boolean gradeQuestionResponse(SimplePageQuestionResponse response) { SimplePageItem question = findItem(response.getQuestionId()); if (question == null) { log.warn("Invalid question for QuestionResponse " + response.getId()); return false; } Double gradebookPoints = null; if (question.getGradebookPoints() != null) gradebookPoints = (double) question.getGradebookPoints(); boolean correct = true; if (response.isOverridden()) { // The teacher set this score manually, so we'd rather not mess with it. correct = response.isCorrect(); gradebookPoints = response.getPoints(); } else if (question.getAttribute("questionType") != null && question.getAttribute("questionType").equals("multipleChoice")) { SimplePageQuestionAnswer answer = simplePageToolDao.findAnswerChoice(question, response.getMultipleChoiceId()); if (answer != null && answer.isCorrect()) { correct = true; } else if (answer != null && !answer.isCorrect()) { correct = false; gradebookPoints = 0.0; } else { // The answer no longer exists, so we'll just leave everything the way it was last time it was graded. correct = response.isCorrect(); gradebookPoints = response.getPoints(); } } else if (question.getAttribute("questionType") != null && question.getAttribute("questionType").equals("shortanswer")) { String correctAnswer = question.getAttribute("questionAnswer"); StringTokenizer correctAnswerTokenizer = new StringTokenizer(correctAnswer, "\n"); String theirResponse = response.getShortanswer().trim().toLowerCase(); int totalTokens = correctAnswerTokenizer.countTokens(); boolean foundAnswer = false; for (int i = 0; i < totalTokens; i++) { String token = correctAnswerTokenizer.nextToken().replaceAll("\n", "").trim().toLowerCase(); if (theirResponse.equals(token)) { foundAnswer = true; break; } } if (foundAnswer) { correct = true; } else { correct = false; gradebookPoints = 0.0; } } else { log.warn("Invalid question type for question " + question.getId()); correct = false; } response.setCorrect(correct); if ("true".equals(question.getAttribute("questionGraded"))) response.setPoints(gradebookPoints); if (question.getGradebookId() != null && !question.getGradebookId().equals("")) { gradebookIfc.updateExternalAssessmentScore(getCurrentSiteId(), question.getGradebookId(), response.getUserId(), String.valueOf(gradebookPoints)); } return correct; } public String answerMultipleChoiceQuestion() { String userId = getCurrentUserId(); if (!itemOk(questionId) || !canReadPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; SimplePageItem question = findItem(questionId); SimplePageQuestionResponse response = simplePageToolDao.findQuestionResponse(questionId, userId); if (response != null) { if (!canEditPage()) { // Don't let students re-answer questions. setErrMessage(messageLocator.getMessage("simplepage.permissions-question")); return "failure"; } } else { response = simplePageToolDao.makeQuestionResponse(userId, questionId); } long responseId = Long.valueOf(questionResponse); response.setMultipleChoiceId(responseId); simplePageToolDao.incrementQRCount(questionId, responseId); SimplePageQuestionAnswer answer = simplePageToolDao.findAnswerChoice(question, response.getMultipleChoiceId()); response.setOriginalText(answer.getText()); gradeQuestionResponse(response); saveItem(response); return "success"; } public String answerShortanswerQuestion() { String userId = getCurrentUserId(); if (!itemOk(questionId) || !canReadPage()) return "permission-failed"; if (!checkCsrf()) return "permission-failed"; SimplePageQuestionResponse response = simplePageToolDao.findQuestionResponse(questionId, userId); if (response != null) { if (!canEditPage()) { // Don't let students re-answer questions. setErrMessage(messageLocator.getMessage("simplepage.permissions-question")); return "failure"; } } else { response = simplePageToolDao.makeQuestionResponse(userId, questionId); } SimplePageItem question = findItem(response.getQuestionId()); if (questionResponse != null) questionResponse = questionResponse.trim(); response.setShortanswer(questionResponse); gradeQuestionResponse(response); saveItem(response); return "success"; } public String updateStudent() { if (!checkCsrf()) return "permission-failed"; if (canEditPage()) { SimplePageItem page = findItem(itemId); page.setAnonymous(anonymous); page.setShowComments(comments); page.setForcedCommentsAnonymous(forcedAnon); page.setRequired(required); page.setPrerequisite(prerequisite); page.setGroupOwned(groupOwned); page.setShowPeerEval(peerEval); setItemGroups(page, selectedGroups); if (studentSelectedGroups == null || studentSelectedGroups.length == 0) page.setOwnerGroups(""); else { StringBuilder ownerGroups = new StringBuilder(); for (int i = 0; i < studentSelectedGroups.length; i++) { if (i > 0) ownerGroups.append(","); ownerGroups.append(studentSelectedGroups[i]); } page.setOwnerGroups(ownerGroups.toString()); } // Update the comments tools to reflect any changes if (comments) { List<SimpleStudentPage> pages = simplePageToolDao.findStudentPages(itemId); for (SimpleStudentPage p : pages) { if (p.getCommentsSection() != null) { SimplePageItem item = simplePageToolDao.findItem(p.getCommentsSection()); //if(item.isAnonymous() != forcedAnon) { //item.setAnonymous(forcedAnon); //update(item); //} } } } // RU Rubrics // This function is called last. By the time this function is called, rubricRows has been created. //the peerEval should not be in here if (rubricRows == null) log.info("rubricRows is null"); else log.info("rubricRows is not null"); if (peerEval) { String result = addPeerEval(); log.info("peerEval" + result); } if (maxPoints == null || maxPoints.equals("")) { maxPoints = "1"; } if (sMaxPoints == null || sMaxPoints.equals("")) { sMaxPoints = "1"; } // Handle the grading of pages if (graded) { int points; try { points = Integer.valueOf(maxPoints); } catch (Exception ex) { setErrMessage(messageLocator.getMessage("simplepage.integer-expected")); return "failure"; } if (page.getGradebookId() == null || !page.getGradebookPoints().equals(points)) { boolean add = false; if (page.getGradebookId() != null && !page.getGradebookPoints().equals(points)) add = gradebookIfc.updateExternalAssessment(getCurrentSiteId(), "lesson-builder:page:" + page.getId(), null, getPage(page.getPageId()).getTitle() + " Student Pages (item:" + page.getId() + ")", Integer.valueOf(maxPoints), null); else add = gradebookIfc.addExternalAssessment(getCurrentSiteId(), "lesson-builder:page:" + page.getId(), null, getPage(page.getPageId()).getTitle() + " Student Pages (item:" + page.getId() + ")", Integer.valueOf(maxPoints), null, "Lesson Builder"); if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else { page.setGradebookId("lesson-builder:page:" + page.getId()); page.setGradebookTitle(getPage(page.getPageId()).getTitle() + " Student Pages (item:" + page.getId() + ")"); page.setGradebookPoints(points); regradeStudentPages(page); } } } else if (page.getGradebookId() != null) { gradebookIfc.removeExternalAssessment(getCurrentSiteId(), page.getGradebookId()); page.setGradebookId(null); page.setGradebookPoints(null); } // Handling the grading of comments on pages if (sGraded) { int points; try { points = Integer.valueOf(sMaxPoints); } catch (Exception ex) { setErrMessage(messageLocator.getMessage("simplepage.integer-expected")); return "failure"; } if (page.getAltGradebook() == null || !page.getAltPoints().equals(points)) { String title = getPage(page.getPageId()).getTitle() + " Student Page Comments (item:" + page.getId() + ")"; boolean add = false; if (page.getAltGradebook() != null && !page.getAltPoints().equals(points)) add = gradebookIfc.updateExternalAssessment(getCurrentSiteId(), "lesson-builder:page-comment:" + page.getId(), null, title, points, null); else add = gradebookIfc.addExternalAssessment(getCurrentSiteId(), "lesson-builder:page-comment:" + page.getId(), null, title, points, null, "Lesson Builder"); // The assessment couldn't be added if (!add) { setErrMessage(messageLocator.getMessage("simplepage.no-gradebook")); } else { page.setAltGradebook("lesson-builder:page-comment:" + page.getId()); page.setAltGradebookTitle(title); page.setAltPoints(points); regradeStudentPageComments(page); } } } else if (page.getAltGradebook() != null) { gradebookIfc.removeExternalAssessment(getCurrentSiteId(), page.getAltGradebook()); page.setAltGradebook(null); page.setAltPoints(null); ungradeStudentPageComments(page); } update(page); return "success"; } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } } private void regradeStudentPageComments(SimplePageItem pageItem) { List<SimpleStudentPage> pages = simplePageToolDao.findStudentPages(pageItem.getId()); for (SimpleStudentPage c : pages) { SimplePageItem comments = findItem(c.getCommentsSection()); comments.setGradebookId(pageItem.getAltGradebook()); comments.setGradebookPoints(pageItem.getAltPoints()); update(comments); regradeComments(comments); } } private void ungradeStudentPageComments(SimplePageItem pageItem) { List<SimpleStudentPage> pages = simplePageToolDao.findStudentPages(pageItem.getId()); for (SimpleStudentPage c : pages) { SimplePageItem comments = findItem(c.getCommentsSection()); comments.setGradebookId(null); comments.setGradebookPoints(null); update(comments); } } private void regradeStudentPages(SimplePageItem pageItem) { List<SimpleStudentPage> pages = simplePageToolDao.findStudentPages(pageItem.getId()); for (SimpleStudentPage c : pages) { if (c.getPoints() != null) { if (c.getGroup() == null) gradebookIfc.updateExternalAssessmentScore(getCurrentSiteId(), pageItem.getGradebookId(), c.getOwner(), String.valueOf(c.getPoints())); else { String group = c.getGroup(); if (group != null) group = "/site/" + getCurrentSiteId() + "/group/" + group; try { AuthzGroup g = authzGroupService.getAuthzGroup(group); Set<Member> members = g.getMembers(); for (Member m : members) { gradebookIfc.updateExternalAssessmentScore(getCurrentSiteId(), pageItem.getGradebookId(), m.getUserId(), String.valueOf(c.getPoints())); } } catch (Exception e) { System.out.println("unable to get members of group " + group); } } } } } private String expandZippedResource(String resourceId) { String contentCollectionId = resourceId.substring(0, resourceId.lastIndexOf(".")) + "/"; try { contentHostingService.removeCollection(contentCollectionId); } catch (Exception e) { log.info("Failed to delete expanded collection"); } // Q: Are we running a kernel with KNL-273? Class contentHostingInterface = ContentHostingService.class; try { Method expandMethod = contentHostingInterface.getMethod("expandZippedResource", new Class[] { String.class }); // Expand the website expandMethod.invoke(contentHostingService, new Object[] { resourceId }); } catch (NoSuchMethodException nsme) { // A: No; should be impossible, UI already tested return null; } catch (Exception e) { // This is very strange. The kernel code will normally trap exceptions // and print a backtrace, robbing us of any ability to see that something // has gone wrong. log.error("Exception thrown by expandZippedResource", e); setErrKey("simplepage.website.cantexpand", null); return null; } // Now set the html ok flag try { ContentCollectionEdit cce = contentHostingService.editCollection(contentCollectionId); ResourcePropertiesEdit props = cce.getPropertiesEdit(); props.addProperty(PROP_ALLOW_INLINE, "true"); List<String> children = cce.getMembers(); for (int j = 0; j < children.size(); j++) { String resId = children.get(j); if (resId.endsWith("/")) { setPropertyOnFolderRecursively(resId, PROP_ALLOW_INLINE, "true"); } } contentHostingService.commitCollection(cce); // when you tell someone to create a zip file with index.html at the // top level, it's unclear whether they do "zip directory" or "cd; zip *" // make both work ContentCollection cc = cce; // a directory with just a subdirectory. Use the subdirectory if (children.size() == 1 && children.get(0).endsWith("/")) { contentCollectionId = children.get(0); cc = contentHostingService.getCollection(contentCollectionId); } // With MacOS we might have __MACOSX and a subdirectory. __MACOSX should be ignored, // so treat it just like the case above if (children.size() == 2 && children.get(0).endsWith("/") && children.get(1).endsWith("/")) { String dataChild = null; if (children.get(0).endsWith("__MACOSX/")) { dataChild = children.get(1); } else if (children.get(1).endsWith("__MACOSX/")) { dataChild = children.get(0); } if (dataChild != null) { contentCollectionId = dataChild; cc = contentHostingService.getCollection(contentCollectionId); } } // Now lets work out what type it is and return the appropriate // index url String index = null; String name = contentCollectionId.substring(0, contentCollectionId.lastIndexOf("/")); name = name.substring(name.lastIndexOf("/") + 1); if (name.endsWith("_HTML")) { // This is probably Wimba Create as wc adds this suffix to the // zips it creates name = name.substring(0, name.indexOf("_HTML")); } ContentEntity ce = cc.getMember(contentCollectionId + name + ".xml"); if (ce != null) { index = "index.htm"; } // Test for Camtasia < 9 ce = cc.getMember(contentCollectionId + "ProductionInfo.xml"); if (ce != null) { index = name + ".html"; } // Test for Camtasia 9 ce = cc.getMember(contentCollectionId + name + "_player.html"); if (ce != null) { index = name + ".html"; } // Test for Articulate ce = cc.getMember(contentCollectionId + "player.html"); if (ce != null) { index = "player.html"; } // Test for generic web site ce = cc.getMember(contentCollectionId + "index.html"); if (ce != null) { index = "index.html"; } ce = cc.getMember(contentCollectionId + "index.htm"); if (ce != null) { index = "index.htm"; } if (index == null) { // /content/group/nnnn/folder int i = contentCollectionId.indexOf("/", 1); i = contentCollectionId.indexOf("/", i + 1); setErrKey("simplepage.website.noindex", contentCollectionId.substring(i)); return null; } //String relativeUrl = contentCollectionId.substring(contentCollectionId.indexOf("/Lesson Builder")) + index; // collections end in / already String relativeUrl = contentCollectionId + index; return relativeUrl; } catch (Exception e) { log.error(e); setErrKey("simplepage.website.cantexpand", null); return null; } } // see if there is a folder in which images, etc, are likely to be // stored for this resource. This only applies to HTML files // for index.html, etc, it's the containing folder // otherwise, if it's an HTML file, look for a folder with the same name public static String associatedFolder(String resourceId) { int i = resourceId.lastIndexOf("/"); String folder = null; String name = null; if (i >= 0) { folder = resourceId.substring(0, i + 1); // include trailing name = resourceId.substring(i + 1); } else return null; String folderName = resourceId.substring(0, i); i = folderName.lastIndexOf("/"); if (i >= 0) folderName = folderName.substring(i + 1); else return null; if (folderName.endsWith("_HTML")) // wimba create folderName = folderName.substring(0, folderName.indexOf("_HTML")); // folder is whole folder // folderName is last atom of folder name // name is last atom of resource id if (name.equals("index.html") || name.equals("index.htm") || name.equals(folderName + ".html")) return folder; if (resourceId.endsWith(".html") || resourceId.endsWith(".htm")) { i = resourceId.lastIndexOf("."); resourceId = resourceId.substring(0, i) + "/"; // no need to check whether it actually exists return resourceId; } return null; } private void setPropertyOnFolderRecursively(String resourceId, String property, String value) { try { if (contentHostingService.isCollection(resourceId)) { // collection ContentCollectionEdit col = contentHostingService.editCollection(resourceId); ResourcePropertiesEdit resourceProperties = col.getPropertiesEdit(); resourceProperties.addProperty(property, Boolean.valueOf(value).toString()); contentHostingService.commitCollection(col); List<String> children = col.getMembers(); for (int i = 0; i < children.size(); i++) { String resId = children.get(i); if (resId.endsWith("/")) { setPropertyOnFolderRecursively(resId, property, value); } } } else { // resource ContentResourceEdit res = contentHostingService.editResource(resourceId); ResourcePropertiesEdit resourceProperties = res.getPropertiesEdit(); resourceProperties.addProperty(property, Boolean.valueOf(value).toString()); contentHostingService.commitResource(res, NotificationService.NOTI_NONE); } } catch (Exception pe) { pe.printStackTrace(); } } /** * Returns an ArrayList containing all of the system-wide and site-wide CSS files. * * One entry may be null, to separate system-wide from site-wide. * * Caches lookups, to prevent extra database hits. */ public ArrayList<ContentResource> getAvailableCss() { ArrayList<ContentResource> list = new ArrayList<ContentResource>(); String collectionId = contentHostingService.getSiteCollection(getCurrentSiteId()) + "LB-CSS/"; List<ContentResource> resources = (List<ContentResource>) resourceCache.get(collectionId); if (resources == null) { resources = contentHostingService.getAllResources(collectionId); if (resources == null) resources = new ArrayList<ContentResource>(); resourceCache.put(collectionId, resources); } for (ContentResource r : resources) { if (r.getUrl().endsWith(".css")) { list.add(r); } } collectionId = "/public/LB-CSS/"; resources = null; resources = (List<ContentResource>) resourceCache.get(collectionId); if (resources == null) { resources = contentHostingService.getAllResources(collectionId); if (resources == null) resources = new ArrayList<ContentResource>(); resourceCache.put(collectionId, resources); } // Insert separator if (list.size() > 0 && resources.size() > 0) { list.add(null); } for (ContentResource r : resources) { if (r.getUrl().endsWith(".css")) { list.add(r); } } return list; } /** * First checks if a sheet has been explicitly set. Then checks for a default * at the site level. It then finally checks to see if there is a default on the * system level. * * Caches lookups to prevent too many lookups in the database. */ public ContentResource getCssForCurrentPage() { ContentResource resource = null; // I'm always using ArrayList for the resourceCache so that I can distinguish // between never having looked up the resource, and the resource not being there. // Otherwise, if I just check for null, if a resource isn't there, it will still check // every time. String collectionId = getCurrentPage().getCssSheet(); if (getCurrentPage().getCssSheet() != null) { try { ArrayList<ContentResource> resources = (ArrayList<ContentResource>) resourceCache.get(collectionId); if (resources == null) { resource = contentHostingService.getResource(collectionId); resources = new ArrayList<ContentResource>(); resources.add(resource); resourceCache.put(collectionId, resources); } if (resources.size() > 0) { return resources.get(0); } else { throw new Exception(); } } catch (Exception ex) { resourceCache.put(collectionId, new ArrayList<ContentResource>()); } } collectionId = contentHostingService.getSiteCollection(getCurrentSiteId()) + "LB-CSS/" + ServerConfigurationService.getString("lessonbuilder.default.css", "default.css"); try { ArrayList<ContentResource> resources = (ArrayList<ContentResource>) resourceCache.get(collectionId); if (resources == null) { resource = contentHostingService.getResource(collectionId); resources = new ArrayList<ContentResource>(); resources.add(resource); resourceCache.put(collectionId, resources); } if (resources.size() > 0) { return resources.get(0); } } catch (Exception ignore) { resourceCache.put(collectionId, new ArrayList<ContentResource>()); } collectionId = "/public/LB-CSS/" + ServerConfigurationService.getString("lessonbuilder.default.css", "default.css"); try { ArrayList<ContentResource> resources = (ArrayList<ContentResource>) resourceCache.get(collectionId); if (resources == null) { resource = contentHostingService.getResource(collectionId); resources = new ArrayList<ContentResource>(); resources.add(resource); resourceCache.put(collectionId, resources); } if (resources.size() > 0) { return resources.get(0); } } catch (Exception ignore) { resourceCache.put(collectionId, new ArrayList<ContentResource>()); } return null; } /** Used for both adding and updating peer evaluation on a page. */ public String addPeerEval() { if (!itemOk(itemId)) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "permission-failed"; } if (!canEditPage()) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } SimplePageItem item; if (itemId != null && itemId != -1) { item = findItem(Long.valueOf(itemId)); } else { item = appendItem("", messageLocator.getMessage("simplepage.peerEval"), SimplePageItem.PEEREVAL); } Long max = simplePageToolDao.maxPeerEvalRow(item); simplePageToolDao.clearPeerEvalRows(item); if (rubricRows == null) { return "failure"; } item.setAttribute("rubricTitle", rubricTitle); Calendar peerevalcal = Calendar.getInstance(); peerevalcal.setTime(peerEvalOpenDate); long peerEvalDate = peerevalcal.getTimeInMillis(); item.setAttribute("rubricOpenDate", String.valueOf(peerEvalDate)); Date openDate = peerevalcal.getTime(); peerevalcal.setTime(peerEvalDueDate); peerEvalDate = peerevalcal.getTimeInMillis(); item.setAttribute("rubricDueDate", String.valueOf(peerEvalDate)); Date dueDate = peerevalcal.getTime(); if (openDate.after(dueDate)) { //error message for dueDate before openDate setErrMessage(messageLocator.getMessage("simplepage.dueDatebeforopenDate")); return "failure"; } item.setAttribute("rubricAllowSelfGrade", String.valueOf(peerEvalAllowSelfGrade)); for (int i = 0; rubricRows.get(i) != null; i++) { // get data sent from post operation for this answer String data = rubricRows.get(i); // split the data into the actual fields String[] fields = data.split(":", 2); Long rowId = 0L; if (("").equals(fields[0])) rowId = -1L; else rowId = Long.valueOf(fields[0]); if (rowId <= 0L) rowId = ++max; String text = fields[1]; simplePageToolDao.addPeerEvalRow(item, rowId, text); } update(item); return "success"; } public String savePeerEvalResult() { String userId = getCurrentUserId(); String gradeeId = getCurrentPage().getOwner(); if (!itemOk(itemId)) { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "permission-failed"; } if (!checkCsrf()) return "permission-failed"; List<SimplePagePeerEvalResult> result = simplePageToolDao.findPeerEvalResult(getCurrentPage().getPageId(), userId, gradeeId); if (result != null) { //set the existing deleted flag=true; for (int i = 0; i < result.size(); i++) { SimplePagePeerEvalResult rst = result.get(i); rst.setSelected(false); update(rst, false); } } //rubricPeerGrades, rubricPeerCategories for (int i = 0; i < rubricPeerGrades.size(); i++) { String rowText = rubricPeerCategories.get(i); int columnValue = Integer.parseInt(rubricPeerGrades.get(i)); SimplePagePeerEvalResult ret = simplePageToolDao.makePeerEvalResult(getCurrentPage().getPageId(), getCurrentPage().getOwner(), userId, rowText, columnValue); saveItem(ret, false); } return "success"; } // May add or edit comments public String addComment1() { boolean html = false; // Patch in the fancy editor's comment, if it's been used if (formattedComment != null && !formattedComment.equals("")) { comment = formattedComment; html = true; } StringBuilder error = new StringBuilder(); comment = FormattedText.processFormattedText(comment, error); if (comment == null || comment.equals("")) { setErrMessage(messageLocator.getMessage("simplepage.empty-comment-error")); return "failure"; } if (editId == null || editId.equals("")) { String userId = UserDirectoryService.getCurrentUser().getId(); Double grade = null; if (findItem(itemId).getGradebookId() != null) { List<SimplePageComment> comments = simplePageToolDao.findCommentsOnItemByAuthor(itemId, userId); if (comments != null && comments.size() > 0) { grade = comments.get(0).getPoints(); } } SimplePageComment commentObject = simplePageToolDao.makeComment(itemId, getCurrentPage().getPageId(), userId, comment, IdManager.getInstance().createUuid(), html); commentObject.setPoints(grade); saveItem(commentObject, false); } else { SimplePageComment commentObject = simplePageToolDao.findCommentById(Long.valueOf(editId)); if (commentObject != null && canModifyComment(commentObject)) { commentObject.setComment(comment); update(commentObject, false); } else { setErrMessage(messageLocator.getMessage("simplepage.permissions-general")); return "failure"; } } if (getCurrentPage().getOwner() != null) { SimpleStudentPage student = simplePageToolDao.findStudentPage(getCurrentPage().getTopParent()); student.setLastCommentChange(new Date()); update(student, false); } return "added-comment"; } /** * Truncate a Java string so that its UTF-8 representation will not * exceed the specified number of bytes. * * For discussion of why you might want to do this, see * http://lpar.ath0.com/2011/06/07/unicode-alchemy-with-db2/ */ public static String utf8truncate(String input, int length) { StringBuffer result = new StringBuffer(length); int resultlen = 0; for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); int charlen = 0; if (c <= 0x7f) { charlen = 1; } else if (c <= 0x7ff) { charlen = 2; } else if (c <= 0xd7ff) { charlen = 3; } else if (c <= 0xdbff) { charlen = 4; } else if (c <= 0xdfff) { charlen = 0; } else if (c <= 0xffff) { charlen = 3; } if (resultlen + charlen > length) { break; } result.append(c); resultlen += charlen; } return result.toString(); } }