diff --git a/.gitignore b/.gitignore index e6b6e69..133a96a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /javascript/dev/ /javascript/build/ /node_modules/ +package-lock.json +/nbproject/ +/assets.json \ No newline at end of file diff --git a/Module.php b/Module.php index ce680df..9a335ab 100644 --- a/Module.php +++ b/Module.php @@ -7,6 +7,7 @@ namespace slideshow; use slideshow\Factory\NavBar; +use slideshow\Factory\Home; use Canopy\Request; use Canopy\Response; use Canopy\Server; @@ -21,7 +22,7 @@ public function __construct() parent::__construct(); $this->loadDefines(); $this->setTitle('slideshow'); - $this->setProperName('SlideShow'); + $this->setProperName('Slideshow'); spl_autoload_register('\slideshow\Module::autoloader', true, true); } @@ -39,7 +40,7 @@ public function getController(Request $request) } catch (\Exception $e) { if (SS_FRIENDLY_ERROR) { \phpws2\Error::log($e); - echo \Layout::wrap('
An error occurred with SlideShow.
An error occurred with Slideshow.
Content...
'; - $currentSort = $this->getCurrentSort($slide->sectionId); - if ($currentSort === false) { - $nextSort = 0; - } else { - $nextSort = $currentSort + 1; - } - $slide->sorting = $nextSort; - return $slide; + // Remove all slides comes from Showlist + $vars = $request->getRequestVars(); + $showId = intval($vars['Slide']); + + if (!$this->deleteAllImages($request)) + echo("an error has occured\n"); + + $sql = 'DELETE from ss_slide WHERE showId=:showId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + return $q->execute(array('showId' => $showId)); } - public function save(Resource $slide) + /** + * Deletes an image given a deletion request with slideId + * @return boolean true if deletion was successful + */ + public function deleteImage(Request $request) { - self::saveResource($slide); - return $slide->id; + $resourceId = $request->pullDeleteVar('slideId'); + $resource = $this->load($resourceId); + $media = json_decode($resource->media); + if ($this->removeUpload($resourceId, $media->imgUrl)) { + $resource->media = ""; + $this->saveResource($resource); + return true; + } + return false; } - public function delete($slideId) + /** + * Deletes all images within a specific slideshow given a deletion request with slideId + * @return boolean true if deletion was successful + */ + public function deleteAllImages(Request $request) { - $slide = $this->load($slideId); - self::deleteResource($slide); - $sortable = new \phpws2\Sortable('ss_slide', 'sorting'); - $sortable->startAtZero(); - $sortable->setAnchor('sectionId', $slide->sectionId); - $sortable->reorder(); - $this->deleteImageDirectory($slide); - } + $vars = $request->getRequestVars(); + $showId = intval($vars['Slide']); - public function deleteImageDirectory($slide) - { - $path = $slide->getImagePath(); - \phpws\PHPWS_File::rmdir($path); + $sql = 'SELECT id, media from ss_slide WHERE showId=:showId;'; + $db = Database::getDB(); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array('showId' => $showId)); + if (!$q) + return false; + $res = $q->fetchAll(); + $flag = false; + foreach ($res as $r) { + $media = json_decode($r['media']); + if ($media != null && !empty($media->imgUrl)) { + $flag = $this->removeUpload($r['id'], $media->imgUrl); + if (!$flag) + echo("an error has occured"); // An error occured + } + $this->deleteSlideDir($r['id']); + } + return true; } - public function createImageDirectory($slide) + private function deleteSlideDir($resourceId) { - $path = $slide->getImagePath(); - if (!is_dir($path)) { - mkdir($path); + $dir = SLIDESHOW_MEDIA_DIRECTORY . $resourceId; + if (is_dir($dir)) { + // If directory exists then we dump it + system('rm -rf ' . escapeshellarg($dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + } else { + echo("directory already removed"); } } - public function clearBackgroundImage($slideId) + /** + * Returns the slide of the show corresponding to the showId + * @var integer showId + * @return array of slides + */ + private function getSlides(int $showId, $includeInactive = false) { - /* @var $slide \slideshow\Resource\SlideResource */ - $slide = $this->load($slideId); - if (is_file($slide->backgroundImage)) { - unlink($slide->backgroundImage); + $sql = 'SELECT * FROM ss_slide WHERE showId=:showId ORDER BY ss_slide.slideIndex;'; + $db = Database::getDB(); + $tbl = $db->addTable('ss_slide'); + $tbl->addFieldConditional('showId', $showId); + if (!$includeInactive) { + $tbl2 = $db->addTable('ss_show', null, false); + $tbl2->addFieldConditional('active', 1); + $tbl->addFieldConditional('showId', $tbl2->getField('id')); } - $slide->backgroundImage = null; - $this->save($slide); + $pdo = $db->getPDO(); + $q = $pdo->prepare($sql); + $q->execute(array('showId' => $showId)); + $slides = $q->fetchAll(); + return $slides; } - public function patch($id, $param, $value) + private function Oldupload($file, $path, $slideId) { - static $allowed_params = array('delay', 'sorting', 'title', 'content', 'backgroundImage'); + # TODO: handle upload of background + # My idea is that I leave the upload process to the path + # and depending on the path, I will upload respectivly + $target = SLIDESHOW_MEDIA_DIRECTORY . $path; + $dir = PHPWS_HOME_DIR . $target; - if (!in_array($param, $allowed_params)) { - throw new \Exception('Parameter may not be patched'); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); } - $slide = $this->load($id); - $slide->$param = $value; - $this->save($slide); - return true; + + if (gettype($file) === 'array') { + $dest = $dir . basename($file['name']); + if (move_uploaded_file($file['tmp_name'], $dest)) { + return './' . $target . basename($file['name']); + } else { + echo("not uploaded and error occured"); + var_dump($file); + var_dump($target); + } + return null; + } else if (gettype($file) === 'string') { // file is a thumbnail or a background img + $dir .= 'thumb/'; + if (is_dir($dir)) { + // If directory exists then we dump it + system('rm -rf ' . escapeshellarg($dir), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + } + mkdir($dir, 0755, true); + // image will be named based on timestamp + $time = time(); + $filename = $time . '.png'; + $dest = $dir . $filename; + $status = file_put_contents($dest, $file); + if (!$status) { + // error has occured + return null; + } + return './' . $target . 'thumb/' . $filename; + } + return null; } - public function getDecisions(Resource $slide) + /** + * Uploads a file to a given path + * @var mixed - of type array or file string data + * @var string - if path doesn't exist it will recursivly created also it will get dumped if it does exist. + * @return string - filepath of new file + */ + private function upload($file, $path) { - $dFactory = new DecisionFactory; - return $dFactory->listing($slide->id); + $slideshow_path = SLIDESHOW_MEDIA_DIRECTORY . $path; + $canopy_path = PHPWS_HOME_DIR . $slideshow_path; + + if (!is_dir($canopy_path)) { + mkdir($canopy_path, 0755, true); + } else { + system('rm -rf ' . escapeshellarg($canopy_path), $ret); + if ($ret != 0) + throw new Exception('Directory Removal Error: ' . $ret); + mkdir($canopy_path, 0755, true); + } + + if (gettype($file) === 'array') { + $dest = $canopy_path . basename($file['name']); + if (move_uploaded_file($file['tmp_name'], $dest)) { + return './' . $slideshow_path . basename($file['name']); + } // If returns false then error occurred. + echo("not uploaded and error occured"); + //var_dump($file); + var_dump($target); + return null; + } else if (gettype($file) === 'string') { + // We will name the file based on timestamp + $time = time(); + $filename = $time . '.png'; + $dest = $canopy_path . $filename; + //var_dump($dest); + $status = file_put_contents($dest, $file); + if (!$status) { + // error has occured + throw new Exception('Slideshow: File failed to upload'); + return null; + } + return './' . $slideshow_path . $filename; + } } - - public function sort($slide, $new_position) + + private function removeUpload($slideId, $path) { - $sortable = new \phpws2\Sortable('ss_slide', 'sorting'); - $sortable->startAtZero(); - $sortable->setAnchor('sectionId', $slide->sectionId); - $sortable->moveTo($slide->getId(), $new_position); + if (empty($path)) { + return false; + } + try { + unlink($path); + $dir = SLIDESHOW_MEDIA_DIRECTORY . $slideId . '/'; + return true; + } catch (\Exception $e) { + echo("A fatal error has occured: " . $e); + // uncomment to show stacktrace as an array + // var_dump($e); + } + return false; } } diff --git a/class/Resource/BaseResource.php b/class/Resource/BaseAbstract.php similarity index 94% rename from class/Resource/BaseResource.php rename to class/Resource/BaseAbstract.php index 0369574..1c25153 100644 --- a/class/Resource/BaseResource.php +++ b/class/Resource/BaseAbstract.php @@ -9,7 +9,7 @@ use \phpws2\Database; -class BaseResource extends \phpws2\Resource +class BaseAbstract extends \phpws2\Resource { public function __set($name, $value) diff --git a/class/Resource/DecisionResource.php b/class/Resource/DecisionResource.php deleted file mode 100644 index 00c34d4..0000000 --- a/class/Resource/DecisionResource.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * @license http://opensource.org/licenses/lgpl-3.0.html - */ - -namespace slideshow\Resource; - -class DecisionResource extends BaseResource -{ - static $allowedTags = array( - 'strong', 's', 'b', 'a', 'i', 'u', 'ul', 'ol', 'li', 'table', 'tr', - 'td', 'tbody', 'dd', 'dt', 'dl', 'p', 'br', 'div', 'span', 'blockquote', - 'th', 'tt', 'img', 'pre', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'fieldset', 'legend', 'code', 'em', 'iframe', 'embed', 'audio', 'video', - 'source', 'object', 'sup', 'sub', 'param', 'strike', 'del', 'abbr', - 'small'); - - /** - * Option listed on the button - * @var \phpws2\Variable\StringVar - */ - protected $title; - - /** - * Message shown when decision made - * @var \phpws2\Variable\StringVar - */ - protected $message; - - /** - * User can continue after picking this decision - * @var \phpws2\Variable\BooleanVar - */ - protected $next; - - /** - * Slide this decision is associated with - * @var \phpws2\Variable\IntegerVar - */ - protected $slideId; - - /** - * Display order of slide - * @var \phpws2\Variable\SmallInteger - */ - protected $sorting; - protected $table = 'ss_decision'; - - public function __construct() - { - parent::__construct(); - $this->title = new \phpws2\Variable\StringVar(null, 'title'); - $this->title->setLimit(50); - $this->message = new \phpws2\Variable\StringVar(null, 'message'); - $this->message->allowEmpty(); - $this->message->addAllowedTags(self::$allowedTags); - $this->next = new \phpws2\Variable\BooleanVar(true, 'next'); - $this->slideId = new \phpws2\Variable\IntegerVar(null, 'slideId'); - $this->sorting = new \phpws2\Variable\SmallInteger(0, 'sorting'); - } - -} diff --git a/class/Resource/QuizResource.php b/class/Resource/QuizResource.php new file mode 100644 index 0000000..9798acd --- /dev/null +++ b/class/Resource/QuizResource.php @@ -0,0 +1,74 @@ +. + * Connor plunkettMultiple choice options with one possible correct answer
+ +Open answer field for user's open response
+Multiple choice options with one or more possible correct answer(s)
+ +Question
+ ++ {check} {question} +
+This slide may have been corrupted. We apolgize for the error.
+Change Slide
+Return to
+This show is not available.
+This quiz slide has not been filled with data
+If you are a student, contact the one responsible for making you take this
+If you are an admin, fill out this slide with data
+{title}
+Content here...
' - this.backgroundImage = '' - this.decisions = [] - } -} diff --git a/javascript/Resources/dom-to-image.js b/javascript/Resources/dom-to-image.js new file mode 100644 index 0000000..a2e6f8d --- /dev/null +++ b/javascript/Resources/dom-to-image.js @@ -0,0 +1,770 @@ +(function (global) { + 'use strict'; + + var util = newUtil(); + var inliner = newInliner(); + var fontFaces = newFontFaces(); + var images = newImages(); + + // Default impl options + var defaultOptions = { + // Default is to fail on error, no placeholder + imagePlaceholder: undefined, + // Default cache bust is false, it will use the cache + cacheBust: false + }; + + var domtoimage = { + toSvg: toSvg, + toPng: toPng, + toJpeg: toJpeg, + toBlob: toBlob, + toPixelData: toPixelData, + impl: { + fontFaces: fontFaces, + images: images, + util: util, + inliner: inliner, + options: {} + } + }; + + if (typeof module !== 'undefined') + module.exports = domtoimage; + else + global.domtoimage = domtoimage; + + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options + * @param {Function} options.filter - Should return true if passed node should be included in the output + * (excluding node means excluding it's children as well). Not called on the root node. + * @param {String} options.bgcolor - color for the background, any valid CSS color value. + * @param {Number} options.width - width to be applied to node before rendering. + * @param {Number} options.height - height to be applied to node before rendering. + * @param {Object} options.style - an object whose properties to be copied to node's style before rendering. + * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only), + defaults to 1.0. + * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch + * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url + * @return {Promise} - A promise that is fulfilled with a SVG image data URL + * */ + function toSvg(node, options) { + options = options || {}; + copyOptions(options); + return Promise.resolve(node) + .then(function (node) { + return cloneNode(node, options.filter, true); + }) + .then(embedFonts) + .then(inlineImages) + .then(applyOptions) + .then(function (clone) { + return makeSvgDataUri(clone, + options.width || util.width(node), + options.height || util.height(node) + ); + }); + + function applyOptions(clone) { + if (options.bgcolor) clone.style.backgroundColor = options.bgcolor; + + if (options.width) clone.style.width = options.width + 'px'; + if (options.height) clone.style.height = options.height + 'px'; + + if (options.style) + Object.keys(options.style).forEach(function (property) { + clone.style[property] = options.style[property]; + }); + + return clone; + } + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data. + * */ + function toPixelData(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.getContext('2d').getImageData( + 0, + 0, + util.width(node), + util.height(node) + ).data; + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image data URL + * */ + function toPng(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.toDataURL(); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a JPEG image data URL + * */ + function toJpeg(node, options) { + options = options || {}; + return draw(node, options) + .then(function (canvas) { + return canvas.toDataURL('image/jpeg', options.quality || 1.0); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image blob + * */ + function toBlob(node, options) { + return draw(node, options || {}) + .then(util.canvasToBlob); + } + + function copyOptions(options) { + // Copy options to impl options for use in impl + if(typeof(options.imagePlaceholder) === 'undefined') { + domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder; + } else { + domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder; + } + + if(typeof(options.cacheBust) === 'undefined') { + domtoimage.impl.options.cacheBust = defaultOptions.cacheBust; + } else { + domtoimage.impl.options.cacheBust = options.cacheBust; + } + } + + function draw(domNode, options) { + return toSvg(domNode, options) + .then(util.makeImage) + .then(util.delay(100)) + .then(function (image) { + var canvas = newCanvas(domNode); + canvas.getContext('2d').drawImage(image, 0, 0); + return canvas; + }); + + function newCanvas(domNode) { + var canvas = document.createElement('canvas'); + canvas.width = options.width || util.width(domNode); + canvas.height = options.height || util.height(domNode); + + if (options.bgcolor) { + var ctx = canvas.getContext('2d'); + ctx.fillStyle = options.bgcolor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return canvas; + } + } + + function cloneNode(node, filter, root) { + if (!root && filter && !filter(node)) return Promise.resolve(); + + return Promise.resolve(node) + .then(makeNodeCopy) + .then(function (clone) { + return cloneChildren(node, clone, filter); + }) + .then(function (clone) { + return processClone(node, clone); + }); + + function makeNodeCopy(node) { + if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL()); + return node.cloneNode(false); + } + + function cloneChildren(original, clone, filter) { + var children = original.childNodes; + if (children.length === 0) return Promise.resolve(clone); + + return cloneChildrenInOrder(clone, util.asArray(children), filter) + .then(function () { + return clone; + }); + + function cloneChildrenInOrder(parent, children, filter) { + var done = Promise.resolve(); + children.forEach(function (child) { + done = done + .then(function () { + return cloneNode(child, filter); + }) + .then(function (childClone) { + if (childClone) parent.appendChild(childClone); + }); + }); + return done; + } + } + + function processClone(original, clone) { + if (!(clone instanceof Element)) return clone; + + return Promise.resolve() + .then(cloneStyle) + .then(clonePseudoElements) + .then(copyUserInput) + .then(fixSvg) + .then(function () { + return clone; + }); + + function cloneStyle() { + copyStyle(window.getComputedStyle(original), clone.style); + + function copyStyle(source, target) { + if (source.cssText) target.cssText = source.cssText; + else copyProperties(source, target); + + function copyProperties(source, target) { + util.asArray(source).forEach(function (name) { + target.setProperty( + name, + source.getPropertyValue(name), + source.getPropertyPriority(name) + ); + }); + } + } + } + + function clonePseudoElements() { + [':before', ':after'].forEach(function (element) { + clonePseudoElement(element); + }); + + function clonePseudoElement(element) { + var style = window.getComputedStyle(original, element); + var content = style.getPropertyValue('content'); + + if (content === '' || content === 'none') return; + + var className = util.uid(); + clone.className = clone.className + ' ' + className; + var styleElement = document.createElement('style'); + styleElement.appendChild(formatPseudoElementStyle(className, element, style)); + clone.appendChild(styleElement); + + function formatPseudoElementStyle(className, element, style) { + var selector = '.' + className + ':' + element; + var cssText = style.cssText ? formatCssText(style) : formatCssProperties(style); + return document.createTextNode(selector + '{' + cssText + '}'); + + function formatCssText(style) { + var content = style.getPropertyValue('content'); + return style.cssText + ' content: ' + content + ';'; + } + + function formatCssProperties(style) { + + return util.asArray(style) + .map(formatProperty) + .join('; ') + ';'; + + function formatProperty(name) { + return name + ': ' + + style.getPropertyValue(name) + + (style.getPropertyPriority(name) ? ' !important' : ''); + } + } + } + } + } + + function copyUserInput() { + if (original instanceof HTMLTextAreaElement) clone.innerHTML = original.value; + if (original instanceof HTMLInputElement) clone.setAttribute("value", original.value); + } + + function fixSvg() { + if (!(clone instanceof SVGElement)) return; + clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + + if (!(clone instanceof SVGRectElement)) return; + ['width', 'height'].forEach(function (attribute) { + var value = clone.getAttribute(attribute); + if (!value) return; + + clone.style.setProperty(attribute, value); + }); + } + } + } + + function embedFonts(node) { + return fontFaces.resolveAll() + .then(function (cssText) { + var styleNode = document.createElement('style'); + node.appendChild(styleNode); + styleNode.appendChild(document.createTextNode(cssText)); + return node; + }); + } + + function inlineImages(node) { + return images.inlineAll(node) + .then(function () { + return node; + }); + } + + function makeSvgDataUri(node, width, height) { + return Promise.resolve(node) + .then(function (node) { + node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + return new XMLSerializer().serializeToString(node); + }) + .then(util.escapeXhtml) + .then(function (xhtml) { + return '|
+
+ Username
+
+ |
+
+
+ Status
+
+ |
+
|---|
- The slide below is an approximation of the final - result. -
-