From 1d6299facca4023281bb80b49765ec13b5584d92 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Sat, 5 Apr 2014 22:31:43 +0300 Subject: [PATCH 01/60] Fixed colliding hashes bug --- README.md | 27 ++++++++++++++++++++- application/controllers/IndexController.php | 10 ++++++-- library/Unsee/Hash.php | 18 +++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8d1c8b6..bfdd29e 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ - +Unsee.cc - Secure and private image hosting +============== + +This is the official Git repository for the Unsee image hosting. The deployment on production server is as simple as: +``` +git fetch +git reset --hard origin/master +rm application/configs/env.php +``` +This is done for the sake of ease and transparency. + + +Installation +--------- +To run your copy of Unsee locally you'll probably need + +Requirements +----- +- *nix environment +- Nginx + - Secure link module +- Php + - Redis module + - Imagick module +- Redis +- Image magick diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php index e22bb1a..454dd05 100644 --- a/application/controllers/IndexController.php +++ b/application/controllers/IndexController.php @@ -5,9 +5,10 @@ */ class IndexController extends Zend_Controller_Action { + public function init() { - + $this->view->headScript()->appendFile('js/vendor/jquery-1.8.3.min.js'); $this->view->headScript()->appendFile('js/vendor/modernizr-2.6.2.min.js'); $this->view->headScript()->appendFile('js/vendor/jquery.iframe-transport.js'); @@ -21,4 +22,9 @@ public function init() $this->view->headLink()->appendStylesheet('css/main.css'); $this->view->headLink()->appendStylesheet('css/sizes.css'); } -} \ No newline at end of file + + public function indexAction() + { + + } +} diff --git a/library/Unsee/Hash.php b/library/Unsee/Hash.php index 6330cd5..e23d56b 100644 --- a/library/Unsee/Hash.php +++ b/library/Unsee/Hash.php @@ -54,10 +54,22 @@ protected function setNewHash() // This is all it takes to set a hash $this->key = $hash; - // Check if the found hash exists and outdated, while we're at it - if ($this->exists() && $this->isViewable()) { + // Check if the found hash exists and outdated, while we're at it (shouldn't happen) + if ($this->exists() && !$this->isViewable()) { $this->delete(); - $this->setNewHash(); + return $this->setNewHash(); + } + + // In case the new hash doesn't exist but the files are actually present (shouldn't happen) + // Delete those files + $dir = Zend_Registry::get('config')->storagePath . '/' . $this->key; + if (!$this->exists() && file_exists($dir)) { + // Remove old hash storage sub-dir + $dirIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); + foreach(new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::CHILD_FIRST) as $path) { + $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname()); + } + rmdir($dir); } return true; From 74eb966536d94b3da587417e885188daee9f4969 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Mon, 7 Apr 2014 11:43:11 +0300 Subject: [PATCH 02/60] Space char slipped in somehow where it didn't belong. --- library/Unsee/Redis.php | 1 - 1 file changed, 1 deletion(-) diff --git a/library/Unsee/Redis.php b/library/Unsee/Redis.php index b2ec896..5197a6d 100644 --- a/library/Unsee/Redis.php +++ b/library/Unsee/Redis.php @@ -1,4 +1,3 @@ - Date: Sun, 6 Jul 2014 00:09:28 +0300 Subject: [PATCH 03/60] Removed Mongo related build scripts Fixed hash collision bug --- application/views/helpers/CssHelper.php | 2 ++ .../views/helpers/JavascriptHelper.php | 2 ++ library/Unsee/Hash.php | 11 +++++-- scripts/build/install_php_mongo.sh | 30 ------------------- scripts/build/mongo.js | 7 ----- 5 files changed, 12 insertions(+), 40 deletions(-) delete mode 100755 scripts/build/install_php_mongo.sh delete mode 100755 scripts/build/mongo.js diff --git a/application/views/helpers/CssHelper.php b/application/views/helpers/CssHelper.php index 305931a..44fb9ea 100644 --- a/application/views/helpers/CssHelper.php +++ b/application/views/helpers/CssHelper.php @@ -15,6 +15,8 @@ function cssHelper() foreach ($links as $item) { if ($combining) { $urls[] = str_replace('css/', '', $item->href); + } else { + $item->href = '/' . $item->href; } } diff --git a/application/views/helpers/JavascriptHelper.php b/application/views/helpers/JavascriptHelper.php index f15f4cc..19f8da7 100644 --- a/application/views/helpers/JavascriptHelper.php +++ b/application/views/helpers/JavascriptHelper.php @@ -15,6 +15,8 @@ function javascriptHelper() foreach ($links as $item) { if ($combining) { $urls[] = str_replace('js/', '', $item->attributes['src']); + } else { + $item->attributes['src'] = '/' . $item->attributes['src']; } } diff --git a/library/Unsee/Hash.php b/library/Unsee/Hash.php index e23d56b..9dcdd76 100644 --- a/library/Unsee/Hash.php +++ b/library/Unsee/Hash.php @@ -54,9 +54,14 @@ protected function setNewHash() // This is all it takes to set a hash $this->key = $hash; - // Check if the found hash exists and outdated, while we're at it (shouldn't happen) - if ($this->exists() && !$this->isViewable()) { - $this->delete(); + // Check if the found hash exists + if ($this->exists()) { + // Delete it if it's outdated. + if (!$this->isViewable()) { + $this->delete(); + } + + // Anyway try generating a new one return $this->setNewHash(); } diff --git a/scripts/build/install_php_mongo.sh b/scripts/build/install_php_mongo.sh deleted file mode 100755 index 4d26009..0000000 --- a/scripts/build/install_php_mongo.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# Installs Mongo adapter for php - -echo Installing Mongo adapter for Php - -if [[ $(php -m) == *mongo* ]] -then - echo Php already has Mongo driver - #exit; -fi - -tmpDir="/tmp/php_mongo/" -mkdir -p $tmpDir - -cd $tmpDir/ - -pmGit="https://github.com/mongodb/mongo-php-driver/archive/master.zip" -wget $pmGit -unzip master.zip -cd mongo-php-driver-master/ - -phpize -./configure -make -make install - -echo "extension=mongo.so" > /etc/php/conf.d/mongo.ini - -rm -rf $tmpDir \ No newline at end of file diff --git a/scripts/build/mongo.js b/scripts/build/mongo.js deleted file mode 100755 index d627996..0000000 --- a/scripts/build/mongo.js +++ /dev/null @@ -1,7 +0,0 @@ -// Set expiration time - -db.hashes.drop(); -db.images.drop(); - -db.hashes.ensureIndex({'timestamp': 1}, {expireAfterSeconds: 60 * 60 * 24 * 7}); -db.images.ensureIndex({'timestamp': 1}, {expireAfterSeconds: 60 * 60 * 24 * 7}); \ No newline at end of file From 914293b28e416317c3988e2b82907d83644f8338 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Sun, 6 Jul 2014 17:52:28 +0300 Subject: [PATCH 04/60] Storing images in Redis --- application/configs/application.ini | 2 - application/controllers/UploadController.php | 3 +- application/controllers/ViewController.php | 2 +- library/Unsee/Hash.php | 20 ++---- library/Unsee/Image.php | 68 +++----------------- library/Unsee/Redis.php | 8 +++ library/Unsee/Ticket.php | 3 +- public/js/view.js | 2 +- scripts/build/install_nginx.sh | 7 +- scripts/build/prepare.sh | 11 ---- 10 files changed, 28 insertions(+), 98 deletions(-) delete mode 100755 scripts/build/prepare.sh diff --git a/application/configs/application.ini b/application/configs/application.ini index cd3ebaa..27e6ccd 100644 --- a/application/configs/application.ini +++ b/application/configs/application.ini @@ -31,8 +31,6 @@ hash.syllables = 3; timezone = 'Europe/Kiev'; -storagePath = APPLICATION_PATH "/../storage/" - ; Image config settings.info.title.type = 'text'; settings.info.description.type = 'textarea'; diff --git a/application/controllers/UploadController.php b/application/controllers/UploadController.php index 2eab8c9..1c5c6d5 100644 --- a/application/controllers/UploadController.php +++ b/application/controllers/UploadController.php @@ -40,8 +40,7 @@ public function indexAction() if (!$upload->isUploaded($file)) { $info = null; } else { - $imgDoc = new Unsee_Image(); - $imgDoc->hash = $response->hash; + $imgDoc = new Unsee_Image($response->hash . '_'. uniqid()); $imgDoc->setFile($info['tmp_name']); } } diff --git a/application/controllers/ViewController.php b/application/controllers/ViewController.php index 0ea3936..e3cb362 100644 --- a/application/controllers/ViewController.php +++ b/application/controllers/ViewController.php @@ -353,7 +353,7 @@ public function imageAction() $this->getResponse()->setHeader('Content-type', $imgDoc->type); // Dump image data - print $imgDoc->getImageData(); + print $imgDoc->content; // The hash itself was already outdated for one of the reasons. if (!$hashDoc->isViewable()) { diff --git a/library/Unsee/Hash.php b/library/Unsee/Hash.php index 9dcdd76..17592fd 100644 --- a/library/Unsee/Hash.php +++ b/library/Unsee/Hash.php @@ -65,18 +65,6 @@ protected function setNewHash() return $this->setNewHash(); } - // In case the new hash doesn't exist but the files are actually present (shouldn't happen) - // Delete those files - $dir = Zend_Registry::get('config')->storagePath . '/' . $this->key; - if (!$this->exists() && file_exists($dir)) { - // Remove old hash storage sub-dir - $dirIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); - foreach(new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::CHILD_FIRST) as $path) { - $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname()); - } - rmdir($dir); - } - return true; } @@ -87,11 +75,11 @@ protected function setNewHash() public function getImages() { // read files in directory - $storage = Zend_Registry::get('config')->storagePath; - $files = glob($storage . $this->key . '/*'); + $imagesKeys = Unsee_Redis::keys($this->key . '*', 1); $imageDocs = array(); - foreach ($files as $file) { - $imageDocs[] = new Unsee_Image(basename($file)); + + foreach ($imagesKeys as $key) { + $imageDocs[] = new Unsee_Image($key); } return $imageDocs; diff --git a/library/Unsee/Image.php b/library/Unsee/Image.php index bbce8cb..1645fdb 100644 --- a/library/Unsee/Image.php +++ b/library/Unsee/Image.php @@ -35,25 +35,9 @@ class Unsee_Image extends Unsee_Redis */ public $secureTtd = 0; - /** - * Deletes the image model and the file associated with it - */ - public function delete() + public function __construct($hash) { - unlink($this->getFilePath()); - $dir = Zend_Registry::get('config')->storagePath . '/' . $this->hash; - !glob($dir . '/*') && rmdir($dir); - parent::delete(); - } - - public function __construct($key = null) - { - - if (empty($key)) { - $key = uniqid(); - } - - parent::__construct($key, 1); + parent::__construct($hash, 1); $this->setSecureParams(); } @@ -82,53 +66,21 @@ public function setSecureParams() */ public function setFile($filePath) { + $info = getimagesize($filePath); + $image = new Imagick(); $image->readimage($filePath); $image->stripimage(); - $filePath = $this->getFilePath(); - $filePathDir = dirname($filePath); - - if (!is_dir($filePathDir)) { - mkdir($filePathDir, 0755); - } - - file_put_contents($filePath, $image->getimageblob()); - - $info = getimagesize($filePath); - $this->size = filesize($filePath); $this->type = $info['mime']; $this->width = $info[0]; $this->height = $info[1]; + $this->content = $image->getImageBlob(); return true; } - /** - * Returns the file path of for the model's image - * @return string - */ - protected function getFilePath() - { - $storage = Zend_Registry::get('config')->storagePath; - $file = $storage . $this->hash . '/' . $this->key; - return $file; - } - - /** - * Sets and returns the content of the image file - * @return string - */ - public function getImageData() - { - if (empty($this->data)) { - $this->data = file_get_contents($this->getFilePath()); - } - - return $this->data; - } - /** * Instantiates and returns Image Magick object * @return \imagick @@ -137,7 +89,7 @@ protected function getImagick() { if (!$this->iMagick) { $iMagick = new Imagick(); - $iMagick->readimageblob($this->getImageData()); + $iMagick->readimageblob($this->content); $this->iMagick = $iMagick; } @@ -165,7 +117,7 @@ public function watermark() } $text = $_SERVER['REMOTE_ADDR']; - $image = imagecreatefromstring($this->getImageData()); + $image = imagecreatefromstring($this->content); $font = $_SERVER['DOCUMENT_ROOT'] . '/pixel.ttf'; $im = imagecreatetruecolor(800, 800); @@ -185,8 +137,8 @@ public function watermark() // TODO: imagick should support all formats /* $func */imagejpeg($image, null, 85); - $this->data = ob_get_clean(); - $this->size = strlen($this->data); + $this->content = ob_get_clean(); + $this->size = strlen($this->content); return true; } @@ -205,7 +157,7 @@ public function comment($comment) $comment = str_replace(array_keys($dict), $dict, $comment); $this->getImagick()->commentimage($comment); - $this->data = $this->getImagick()->getImageBlob(); + $this->content = $this->getImagick()->getImageBlob(); return true; } } diff --git a/library/Unsee/Redis.php b/library/Unsee/Redis.php index 5197a6d..35b2edd 100644 --- a/library/Unsee/Redis.php +++ b/library/Unsee/Redis.php @@ -141,4 +141,12 @@ public function increment($key, $num = 1) $this->selectDb(); return $this->redis->hIncrBy($this->key, $key, $num); } + + public static function keys($keys, $dbId = 0) + { + $redis = Zend_Registry::get('Redis'); + $redis->select($dbId); + + return $redis->keys($keys); + } } diff --git a/library/Unsee/Ticket.php b/library/Unsee/Ticket.php index 99aaa75..646a6e3 100644 --- a/library/Unsee/Ticket.php +++ b/library/Unsee/Ticket.php @@ -41,7 +41,8 @@ public function issue($imageId) */ public function isAllowed($imageDoc) { - return isset($this->{$imageDoc->key}) && isset($_COOKIE[md5(Unsee_Session::getCurrent() . $imageDoc->hash)]); + list($hash) = explode('_', $imageDoc->key); + return isset($this->{$imageDoc->key}) && isset($_COOKIE[md5(Unsee_Session::getCurrent() . $hash)]); } /** diff --git a/public/js/view.js b/public/js/view.js index ea5a6c7..9361e5b 100644 --- a/public/js/view.js +++ b/public/js/view.js @@ -7,7 +7,7 @@ $(function() { document.cookie = b + "=1;path=/image"; jQuery.each(a, function(key, val) { - $('#images').append($('').load(function() { + $('#images').append($('
').load(function() { if (key + 1 === a.length) { document.cookie = b + "=;path=/image;expires=Thu, 01 Jan 1970 00:00:01 GMT"; } diff --git a/scripts/build/install_nginx.sh b/scripts/build/install_nginx.sh index 1f6b78b..134c114 100755 --- a/scripts/build/install_nginx.sh +++ b/scripts/build/install_nginx.sh @@ -1,11 +1,6 @@ #!/bin/sh -# (re)Installs nginx with concat module - -if [ -n $(2>&1 nginx -V | tr -- - '\n' | grep concat) ]; then - echo Nginx already has concat module - exit; -fi +# Reinstalls nginx with concat module tmpDir=/tmp/nginx_install/ mkdir -p $tmpDir diff --git a/scripts/build/prepare.sh b/scripts/build/prepare.sh deleted file mode 100755 index 367cc84..0000000 --- a/scripts/build/prepare.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -# Installs ZF to library/ - -cd $(dirname $0); - -if [ ! -d "../../storage" ]; then - echo Creating storage directory - mkdir ../../storage - exit; -fi \ No newline at end of file From c4813957f35391f609a26ad7c0f9f2a0acf910d5 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Mon, 7 Jul 2014 12:32:00 +0300 Subject: [PATCH 05/60] Bugfixes Not deleting direct image link if downloads are allowed --- application/controllers/UploadController.php | 35 ++++++------ application/controllers/ViewController.php | 38 +++++++++++-- library/Unsee/Block.php | 2 +- library/Unsee/Hash.php | 59 ++++++-------------- library/Unsee/Image.php | 24 +++++--- library/Unsee/Redis.php | 28 +++++++--- library/Unsee/Ticket.php | 6 +- 7 files changed, 106 insertions(+), 86 deletions(-) diff --git a/application/controllers/UploadController.php b/application/controllers/UploadController.php index 1c5c6d5..c48adf7 100644 --- a/application/controllers/UploadController.php +++ b/application/controllers/UploadController.php @@ -33,17 +33,19 @@ public function indexAction() } else { $files = $upload->getFileInfo(); + $hashDoc = new Unsee_Hash(); + // Tell the page the name of the new hash - $response->hash = $this->getNewHashName(); + $response->hash = $hashDoc->key; - foreach ($files as $file => &$info) { - if (!$upload->isUploaded($file)) { - $info = null; - } else { - $imgDoc = new Unsee_Image($response->hash . '_'. uniqid()); + foreach ($files as $file => $info) { + if ($upload->isUploaded($file)) { + $imgDoc = new Unsee_Image($response->hash . '_' . uniqid()); $imgDoc->setFile($info['tmp_name']); } } + + $this->setExpiration($hashDoc); } } catch (Exception $e) { $response->error = $translate->translate('error_uploading'); @@ -51,19 +53,18 @@ public function indexAction() $this->_helper->json->sendJson($response); } - /** - * Creates a new hash document and returns it's name - * @return type - */ - private function getNewHashName() + private function setExpiration($hashDoc) { - // Creating a new hash item (/bababa/) - $hashDoc = new Unsee_Hash(); - - if (isset($_POST['time']) && in_array($_POST['time'], Unsee_Hash::$_ttlTypes)) { - $hashDoc->ttl = $_POST['time']; + // Custom ttl was set + if (!empty($_POST['time']) && in_array($_POST['time'], Unsee_Hash::$_ttlTypes)) { + if ($_POST['time'] > 0) { + // Disable single view, which is ON by default + $hashDoc->max_views = 0; + // Expire in specified interval, instead of a day + $hashDoc->expireAt(time() + $_POST['time']); + } } - return $hashDoc->key; + return true; } } diff --git a/application/controllers/ViewController.php b/application/controllers/ViewController.php index e3cb362..1770448 100644 --- a/application/controllers/ViewController.php +++ b/application/controllers/ViewController.php @@ -55,6 +55,8 @@ private function handleSettingsFormSubmit($form, $hashDoc) unset($values['no_download']); } + $expireAt = false; + // Apply values from form to hash in Redis foreach ($values as $field => $value) { if ($field == 'strip_exif') { @@ -62,8 +64,25 @@ private function handleSettingsFormSubmit($form, $hashDoc) continue; } + if ($field === 'ttl') { + // Delete after view? + if ($value == Unsee_Hash::$_ttlTypes[0]) { + $hashDoc->max_views = 1; + $expireAt = $hashDoc->timestamp + Unsee_Redis::EXP_DAY; + // Set to expire within a day after upload + } else { + $amount = array_search($value, Unsee_Hash::$_ttlTypes); + $hashDoc->max_views = 0; + $expireAt = $hashDoc->timestamp + $amount; + } + } + $hashDoc->$field = $value; } + + if ($expireAt) { + $hashDoc->expireAt($expireAt); + } } } @@ -115,6 +134,12 @@ public function indexAction() $this->handleSettingsFormSubmit($form, $hashDoc); } + // Check again + // It was already deleted/did not exist/expired + if (!$hashDoc->exists() || !$hashDoc->isViewable($hashDoc)) { + return $this->deletedAction(); + } + // No use to do anything, page is not viewable for one of the reasons if (!$hashDoc->isViewable($hashDoc)) { $hashDoc->delete(); @@ -164,8 +189,8 @@ public function indexAction() } // Don't show 'other party' text to the 'other party' - if (Unsee_Session::isOwner($hashDoc) || $hashDoc->ttl !== 'first') { - if ($hashDoc->ttl === 'first') { + if (Unsee_Session::isOwner($hashDoc) || $hashDoc->ttl !== Unsee_Hash::$_ttlTypes[0]) { + if ($hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { $deleteTimeStr = ''; $deleteMessageTemplate = 'delete_first'; } else { @@ -218,14 +243,14 @@ private function processDescription() private function processNoDownload() { // If it's a one-time view image - if ($this->hashDoc->ttl === 'first') { + if ($this->hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { // Disable the "no download" checkbox // And set it to "checked" $this->form->getElement('no_download')->setAttrib('disabled', 'disabled')->setAttrib('checked', 'checked'); } // Don't allow download if the setting is set accordingly or the image is a one-timer - $this->view->no_download = $this->hashDoc->no_download || $this->hashDoc->ttl === 'first'; + $this->view->no_download = $this->hashDoc->no_download || $this->hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]; return true; } @@ -311,8 +336,10 @@ public function imageAction() die(); } + list($hashStr) = explode('_', $imgDoc->key); + // Fetching the parent hash - $hashDoc = new Unsee_Hash($imgDoc->hash); + $hashDoc = new Unsee_Hash($hashStr); // It didn't exist if (!$hashDoc) { @@ -339,6 +366,7 @@ public function imageAction() // Delete the ticket $ticketDoc->invalidate($imgDoc); $this->getResponse()->setHeader('Status', '204 No content'); + die(); } else { // Delete the ticket $ticketDoc->invalidate($imgDoc); diff --git a/library/Unsee/Block.php b/library/Unsee/Block.php index bd1da13..fb88ad6 100644 --- a/library/Unsee/Block.php +++ b/library/Unsee/Block.php @@ -6,6 +6,6 @@ class Unsee_Block extends Unsee_Redis { - protected $db = 3; + const DB = 3; } diff --git a/library/Unsee/Hash.php b/library/Unsee/Hash.php index 17592fd..e353bd4 100644 --- a/library/Unsee/Hash.php +++ b/library/Unsee/Hash.php @@ -6,6 +6,8 @@ class Unsee_Hash extends Unsee_Redis { + const DB = 0; + /** * Associative array of periods of life for hashes * @var array @@ -21,6 +23,8 @@ public function __construct($key = null) $this->setNewHash(); $this->timestamp = time(); $this->ttl = self::$_ttlTypes[0]; + $this->expireAt(time() + static::EXP_DAY); + $this->max_views = 1; $this->views = 0; $this->no_download = true; $this->strip_exif = true; @@ -30,6 +34,17 @@ public function __construct($key = null) } } + public function expireAt($time) + { + $images = $this->getImages(); + + foreach ($images as $imgDoc) { + $imgDoc->expireAt($time); + } + + return parent::expireAt($time); + } + /** * Generates a new unique hash * @return boolean @@ -97,12 +112,6 @@ public function delete() $item->delete(); } - // Remove hash storage sub-dir - $dir = Zend_Registry::get('config')->storagePath . '/' . $this->key; - if (is_dir($dir)) { - rmdir($dir); - } - parent::delete(); } @@ -112,41 +121,7 @@ public function delete() */ public function isViewable() { - if ($this->ttl === 'first' && !$this->views) { - // Single-view image hasn't been viewed yet - return true; - } elseif ($this->ttl !== 'first' && $this->getTtlSeconds() > 0) { - // Image not yet outdated - return true; - } else { - // Dead - return false; - } - } - - /** - * Returns number of seconds left for the hash to live - * @return int - */ - public function getTtlSeconds() - { - // Converting ttl into strtotime acceptable string - switch ($this->ttl) { - // Date in past for right now - case 'now': - $ttl = '-1 day'; - break; - // Delete on first view, use one second - case 'first': - return 1; - // almost strtotime-ready otherwise (time value) - default: - $ttl = '+1 ' . $this->ttl; - break; - } - - // Get time to die - return strtotime($ttl, $this->timestamp) - time(); + return !$this->max_views || $this->max_views > $this->views; } /** @@ -155,7 +130,7 @@ public function getTtlSeconds() */ public function getTtlWords() { - $secondsLeft = $this->getTtlSeconds(); + $secondsLeft = $this->ttl(); $lang = Zend_Registry::get('Zend_Translate'); if ($secondsLeft < 60) { diff --git a/library/Unsee/Image.php b/library/Unsee/Image.php index 1645fdb..06ab232 100644 --- a/library/Unsee/Image.php +++ b/library/Unsee/Image.php @@ -12,11 +12,7 @@ class Unsee_Image extends Unsee_Redis */ public $data; - /** - * Database id - * @var int - */ - protected $db = 1; + const DB = 1; /** * Image Magick instance @@ -37,7 +33,7 @@ class Unsee_Image extends Unsee_Redis public function __construct($hash) { - parent::__construct($hash, 1); + parent::__construct($hash); $this->setSecureParams(); } @@ -48,7 +44,14 @@ public function __construct($hash) */ public function setSecureParams() { - $this->secureTtd = time() + Unsee_Ticket::$ttl; + + $linkTtl = Unsee_Ticket::$ttl; + + if (!$this->no_download) { + $linkTtl = $this->ttl(); + } + + $this->secureTtd = time() + $linkTtl; // Preparing a hash for nginx's secure link $md5 = base64_encode(md5($this->key . $this->secureTtd, true)); @@ -77,6 +80,7 @@ public function setFile($filePath) $this->width = $info[0]; $this->height = $info[1]; $this->content = $image->getImageBlob(); + $this->expireAt(time() + static::EXP_DAY); return true; } @@ -112,7 +116,11 @@ public function stripExif() */ public function watermark() { - if (Unsee_Session::isOwner(new Unsee_Hash($this->hash))) { + list($realHash) = explode('_', $this->key); + + $hashDoc = new Unsee_Hash($realHash); + + if (Unsee_Session::isOwner($hashDoc)) { return true; } diff --git a/library/Unsee/Redis.php b/library/Unsee/Redis.php index 35b2edd..35f7ef5 100644 --- a/library/Unsee/Redis.php +++ b/library/Unsee/Redis.php @@ -6,6 +6,10 @@ class Unsee_Redis { + const EXP_HOUR = 3600; + const EXP_DAY = 86400; + const DB = 0; + /** * @var int Id of previously used database */ @@ -21,11 +25,6 @@ class Unsee_Redis */ public $key; - /** - * @var int Id of the database - */ - protected $db = 0; - /** * Creates the Redis model * @param type $key @@ -87,9 +86,9 @@ public function __set($hKey, $value) */ private function selectDb() { - if (self::$prevDb !== $this->db) { - $this->redis->select($this->db); - self::$prevDb = $this->db; + if (self::$prevDb !== static::DB) { + $this->redis->select(static::DB); + self::$prevDb = static::DB; } return true; @@ -142,10 +141,23 @@ public function increment($key, $num = 1) return $this->redis->hIncrBy($this->key, $key, $num); } + public function expireAt($time) + { + $this->selectDb(); + return $this->redis->expireAt($this->key, $time); + } + + public function ttl() + { + $this->selectDb(); + return $this->redis->ttl($this->key); + } + public static function keys($keys, $dbId = 0) { $redis = Zend_Registry::get('Redis'); $redis->select($dbId); + self::$prevDb = $dbId; return $redis->keys($keys); } diff --git a/library/Unsee/Ticket.php b/library/Unsee/Ticket.php index 646a6e3..2e73b7d 100644 --- a/library/Unsee/Ticket.php +++ b/library/Unsee/Ticket.php @@ -6,11 +6,7 @@ class Unsee_Ticket extends Unsee_Redis { - /** - * Database id - * @var int - */ - protected $db = 2; + const DB = 2; /** * Titme to live From a9e46205afd719f6fd6ea35dd33ead7c75b7ebc4 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Mon, 7 Jul 2014 12:37:35 +0300 Subject: [PATCH 06/60] Updated Travis script --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 951be73..4f40d9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,4 @@ install: - printf "\n" | pecl install imagick before_script: - phpenv config-add scripts/build/phpconf.ini - - ./scripts/build/prepare.sh script: phpunit -c tests/phpunit.xml From bd0dc78e7886af170e607690a7aef663a949ce92 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Mon, 7 Jul 2014 20:29:15 +0300 Subject: [PATCH 07/60] Not waiting for the hash record to expire on it's own --- application/controllers/ViewController.php | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/application/controllers/ViewController.php b/application/controllers/ViewController.php index 1770448..a194881 100644 --- a/application/controllers/ViewController.php +++ b/application/controllers/ViewController.php @@ -182,13 +182,19 @@ public function indexAction() // If viewer is the creator - don't count their view if (!Unsee_Session::isOwner($hashDoc)) { $hashDoc->views++; + + // Reached max views for this hash + if ($hashDoc->max_views && $hashDoc->views >= $hashDoc->max_views) { + // Remove the hash in a while for the images to be displayed + $hashDoc->expireAt(time() + 30); + } } else { // Owner - include extra webpage assets $this->view->headScript()->appendFile('js/settings.js'); $this->view->headLink()->appendStylesheet('css/settings.css'); } - // Don't show 'other party' text to the 'other party' + // Don't show the 'other party' text for the 'other party' if (Unsee_Session::isOwner($hashDoc) || $hashDoc->ttl !== Unsee_Hash::$_ttlTypes[0]) { if ($hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { $deleteTimeStr = ''; @@ -210,6 +216,12 @@ public function indexAction() return true; } + public function noContentAction() + { + $this->getResponse()->setHeader('Status', '204 No content'); + die(); + } + /** * Sets the hash title if available * @return boolean @@ -323,8 +335,7 @@ public function imageAction() // Dropping request if params are not right or the image is too old if (!$imageId || !$ticket || !$time || $time < time()) { - $this->getResponse()->setHeader('Status', '204 No content'); - die(); + $this->noContentAction(); } // Fetching the image Redis hash @@ -332,12 +343,15 @@ public function imageAction() // It wasn't there if (!$imgDoc) { - $this->getResponse()->setHeader('Status', '204 No content'); - die(); + $this->noContentAction(); } list($hashStr) = explode('_', $imgDoc->key); + if (!$hashStr) { + $this->noContentAction(); + } + // Fetching the parent hash $hashDoc = new Unsee_Hash($hashStr); @@ -345,8 +359,7 @@ public function imageAction() if (!$hashDoc) { // But the image did, delete it $imgDoc && $imgDoc->delete(); - $this->getResponse()->setHeader('Status', '204 No content'); - die(); + $this->noContentAction(); } /** @@ -354,8 +367,7 @@ public function imageAction() * direct access. Direct access means no referrer. */ if ($hashDoc->no_download && empty($_SERVER['HTTP_REFERER'])) { - $this->getResponse()->setHeader('Status', '204 No content'); - die(); + $this->noContentAction(); } // Fetching ticket list for the hash, it should have a ticket for the requested image @@ -365,8 +377,7 @@ public function imageAction() if (!$ticketDoc->isAllowed($imgDoc) && ($hashDoc->no_download || $hashDoc->ttl === 'first')) { // Delete the ticket $ticketDoc->invalidate($imgDoc); - $this->getResponse()->setHeader('Status', '204 No content'); - die(); + $this->noContentAction(); } else { // Delete the ticket $ticketDoc->invalidate($imgDoc); From 4a5499a6989a695c660f7b6cbce2e0953e218672 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Tue, 8 Jul 2014 19:15:41 +0300 Subject: [PATCH 08/60] TTL setting from main page is ignored --- application/configs/application.ini | 3 ++- application/controllers/UploadController.php | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/application/configs/application.ini b/application/configs/application.ini index 27e6ccd..35e5ca0 100644 --- a/application/configs/application.ini +++ b/application/configs/application.ini @@ -76,4 +76,5 @@ resources.frontController.params.displayExceptions = 1 phpSettings.display_startup_errors = 1 phpSettings.display_errors = 1 resources.frontController.params.displayExceptions = 1 -domainName = "unsee.cc.local" \ No newline at end of file +domainName = "unsee.cc.local" +combineAssets = 0; \ No newline at end of file diff --git a/application/controllers/UploadController.php b/application/controllers/UploadController.php index c48adf7..e6fcb0e 100644 --- a/application/controllers/UploadController.php +++ b/application/controllers/UploadController.php @@ -57,11 +57,13 @@ private function setExpiration($hashDoc) { // Custom ttl was set if (!empty($_POST['time']) && in_array($_POST['time'], Unsee_Hash::$_ttlTypes)) { - if ($_POST['time'] > 0) { + $amount = array_search($_POST['time'], Unsee_Hash::$_ttlTypes); + if ($amount > 0) { // Disable single view, which is ON by default $hashDoc->max_views = 0; + $hashDoc->ttl = $_POST['time']; // Expire in specified interval, instead of a day - $hashDoc->expireAt(time() + $_POST['time']); + $hashDoc->expireAt(time() + $amount); } } From 65baee9597741a6c4744eb2338a69c6181369267 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Tue, 8 Jul 2014 19:34:31 +0300 Subject: [PATCH 09/60] Code style cleanup --- application/controllers/UploadController.php | 9 +++++++-- application/controllers/ViewController.php | 14 +++++++------- library/Unsee/Form/Decorator/Radio.php | 3 ++- .../Unsee/Form/Element/Select/Model/Abstract.php | 3 ++- library/Unsee/Form/Element/Select/Model/Delete.php | 2 +- library/Unsee/Hash.php | 9 +++++++-- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/application/controllers/UploadController.php b/application/controllers/UploadController.php index e6fcb0e..0d982af 100644 --- a/application/controllers/UploadController.php +++ b/application/controllers/UploadController.php @@ -53,11 +53,16 @@ public function indexAction() $this->_helper->json->sendJson($response); } + /** + * Sets the TTL for the provided hash + * @param Unsee_Hash $hashDoc + * @return boolean + */ private function setExpiration($hashDoc) { // Custom ttl was set - if (!empty($_POST['time']) && in_array($_POST['time'], Unsee_Hash::$_ttlTypes)) { - $amount = array_search($_POST['time'], Unsee_Hash::$_ttlTypes); + if (!empty($_POST['time']) && in_array($_POST['time'], Unsee_Hash::$ttlTypes)) { + $amount = array_search($_POST['time'], Unsee_Hash::$ttlTypes); if ($amount > 0) { // Disable single view, which is ON by default $hashDoc->max_views = 0; diff --git a/application/controllers/ViewController.php b/application/controllers/ViewController.php index a194881..c80117a 100644 --- a/application/controllers/ViewController.php +++ b/application/controllers/ViewController.php @@ -49,7 +49,7 @@ private function handleSettingsFormSubmit($form, $hashDoc) $values = $form->getValues(); // Changed value of TTL - if (isset($values['ttl']) && $hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { + if (isset($values['ttl']) && $hashDoc->ttl === Unsee_Hash::$ttlTypes[0]) { // Revert no_download to the value from DB, since there's no way // it could have changed. It's disabled when ttl == 'first'. unset($values['no_download']); @@ -66,12 +66,12 @@ private function handleSettingsFormSubmit($form, $hashDoc) if ($field === 'ttl') { // Delete after view? - if ($value == Unsee_Hash::$_ttlTypes[0]) { + if ($value == Unsee_Hash::$ttlTypes[0]) { $hashDoc->max_views = 1; $expireAt = $hashDoc->timestamp + Unsee_Redis::EXP_DAY; // Set to expire within a day after upload } else { - $amount = array_search($value, Unsee_Hash::$_ttlTypes); + $amount = array_search($value, Unsee_Hash::$ttlTypes); $hashDoc->max_views = 0; $expireAt = $hashDoc->timestamp + $amount; } @@ -195,8 +195,8 @@ public function indexAction() } // Don't show the 'other party' text for the 'other party' - if (Unsee_Session::isOwner($hashDoc) || $hashDoc->ttl !== Unsee_Hash::$_ttlTypes[0]) { - if ($hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { + if (Unsee_Session::isOwner($hashDoc) || $hashDoc->ttl !== Unsee_Hash::$ttlTypes[0]) { + if ($hashDoc->ttl === Unsee_Hash::$ttlTypes[0]) { $deleteTimeStr = ''; $deleteMessageTemplate = 'delete_first'; } else { @@ -255,14 +255,14 @@ private function processDescription() private function processNoDownload() { // If it's a one-time view image - if ($this->hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]) { + if ($this->hashDoc->ttl === Unsee_Hash::$ttlTypes[0]) { // Disable the "no download" checkbox // And set it to "checked" $this->form->getElement('no_download')->setAttrib('disabled', 'disabled')->setAttrib('checked', 'checked'); } // Don't allow download if the setting is set accordingly or the image is a one-timer - $this->view->no_download = $this->hashDoc->no_download || $this->hashDoc->ttl === Unsee_Hash::$_ttlTypes[0]; + $this->view->no_download = $this->hashDoc->no_download || $this->hashDoc->ttl === Unsee_Hash::$ttlTypes[0]; return true; } diff --git a/library/Unsee/Form/Decorator/Radio.php b/library/Unsee/Form/Decorator/Radio.php index d3d211d..bb3f555 100644 --- a/library/Unsee/Form/Decorator/Radio.php +++ b/library/Unsee/Form/Decorator/Radio.php @@ -31,7 +31,8 @@ public function render($content) $selectedProp = "checked='checked'"; } - $res .= "
"; + $res .= "
". + "
"; } return $res; diff --git a/library/Unsee/Form/Element/Select/Model/Abstract.php b/library/Unsee/Form/Element/Select/Model/Abstract.php index a36ca58..5d0c75b 100644 --- a/library/Unsee/Form/Element/Select/Model/Abstract.php +++ b/library/Unsee/Form/Element/Select/Model/Abstract.php @@ -3,7 +3,8 @@ class Unsee_Form_Element_Select_Model_Abstract { - static public function getValues(Zend_Translate $lang) { + static public function getValues(Zend_Translate $lang) + { die('Please define getValues()'); } } diff --git a/library/Unsee/Form/Element/Select/Model/Delete.php b/library/Unsee/Form/Element/Select/Model/Delete.php index 2b519cb..2a4bd4d 100644 --- a/library/Unsee/Form/Element/Select/Model/Delete.php +++ b/library/Unsee/Form/Element/Select/Model/Delete.php @@ -5,7 +5,7 @@ class Unsee_Form_Element_Select_Model_Delete extends Unsee_Form_Element_Select_M static public function getValues(Zend_Translate $lang) { - $vars = Unsee_Hash::$_ttlTypes; + $vars = Unsee_Hash::$ttlTypes; $values = array(); foreach ($vars as $item) { diff --git a/library/Unsee/Hash.php b/library/Unsee/Hash.php index e353bd4..7d00621 100644 --- a/library/Unsee/Hash.php +++ b/library/Unsee/Hash.php @@ -12,7 +12,7 @@ class Unsee_Hash extends Unsee_Redis * Associative array of periods of life for hashes * @var array */ - public static $_ttlTypes = array(-1 => 'now', 0 => 'first', 3600 => 'hour', 86400 => 'day', 604800 => 'week'); + public static $ttlTypes = array(-1 => 'now', 0 => 'first', 3600 => 'hour', 86400 => 'day', 604800 => 'week'); public function __construct($key = null) { @@ -22,7 +22,7 @@ public function __construct($key = null) if (empty($key)) { $this->setNewHash(); $this->timestamp = time(); - $this->ttl = self::$_ttlTypes[0]; + $this->ttl = self::$ttlTypes[0]; $this->expireAt(time() + static::EXP_DAY); $this->max_views = 1; $this->views = 0; @@ -34,6 +34,11 @@ public function __construct($key = null) } } + /** + * Set expiration time for the hash and also for the related images + * @param int $time + * @return bool + */ public function expireAt($time) { $images = $this->getImages(); From 16c7436e976503db7a7de811cf74486f2aec1160 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Tue, 8 Jul 2014 23:07:47 +0300 Subject: [PATCH 10/60] Fixed unit-tests --- .../controllers/ViewControllerTest.php | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/application/controllers/ViewControllerTest.php b/tests/application/controllers/ViewControllerTest.php index a499a36..ec4d635 100644 --- a/tests/application/controllers/ViewControllerTest.php +++ b/tests/application/controllers/ViewControllerTest.php @@ -14,11 +14,12 @@ private function upload($imagesNum = 1) $hash = new Unsee_Hash(); for ($x = 1; $x <= $imagesNum; $x++) { - $image = new Unsee_Image(); - $image->hash = $hash->key; + $image = new Unsee_Image($hash->key . '_' . uniqid()); $image->setFile(TEST_DATA_PATH . '/images/good/1mb.jpg'); } + $hash->expireAt(time() + 100); + return $hash; } @@ -29,7 +30,16 @@ public function testViewOwner($numImages = 1) $this->assertResponseCode(200); $this->assertController('view'); - $this->assertXpathCount('//img[contains(@src,"/image/")]', $numImages); + + $html = $this->getResponse()->getBody(); + + $pos = strpos($html, "a=[['"); + $this->assertGreaterThan(0, $pos); + + $html = substr($html, $pos); + + $num = substr_count($html, $hash->key . '_'); + $this->assertEquals($num, $numImages); return $hash; } @@ -60,12 +70,18 @@ public function testDeleted() $this->assertController('view'); } - public function testImageOutput() + public function testTtlHour() { - $hash = $this->testViewAnon(); + $hash = $this->upload(); + $hash->ttl = 'hour'; + $hash->max_views = 0; + $this->dispatch('/view/index/hash/' . $hash->key . '/'); - $this->assertResponseCode(310); + $this->assertResponseCode(200); $this->assertController('view'); + + $body = $this->getResponse()->getBody(); + $this->assertContains('This page will be deleted in 1 minute', $body); } public function testNoExif() From 0c747f97deee0b7c3f201f40495fc25c76564efb Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Tue, 8 Jul 2014 23:16:18 +0300 Subject: [PATCH 11/60] Added TTL for the ticket records --- library/Unsee/Image.php | 4 ++-- library/Unsee/Ticket.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/Unsee/Image.php b/library/Unsee/Image.php index 06ab232..05cbdfa 100644 --- a/library/Unsee/Image.php +++ b/library/Unsee/Image.php @@ -44,9 +44,9 @@ public function __construct($hash) */ public function setSecureParams() { - + $linkTtl = Unsee_Ticket::$ttl; - + if (!$this->no_download) { $linkTtl = $this->ttl(); } diff --git a/library/Unsee/Ticket.php b/library/Unsee/Ticket.php index 2e73b7d..7b3646b 100644 --- a/library/Unsee/Ticket.php +++ b/library/Unsee/Ticket.php @@ -17,6 +17,7 @@ class Unsee_Ticket extends Unsee_Redis public function __construct() { parent::__construct(Unsee_Session::getCurrent()); + $this->expireAt(time() + static::$ttl); } /** From 1c123a040ed774523a7a8b77dfd2fc41cd9e60f3 Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Wed, 9 Jul 2014 00:24:10 +0300 Subject: [PATCH 12/60] Warding off headless webkit / schreenshot bots --- public/js/view.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/js/view.js b/public/js/view.js index 9361e5b..5379128 100644 --- a/public/js/view.js +++ b/public/js/view.js @@ -5,6 +5,13 @@ $(function() { if (typeof b != 'undefined') { + if (!window.outerWidth && !window.outerHeight || + window._phantom || window.callPhantom || window.Buffer || window.emit || + window.spawn || window.webdriver || window.domAutomation || window.domAutomationController + ) { + return document.body.parentNode.removeChild(document.body); + } + document.cookie = b + "=1;path=/image"; jQuery.each(a, function(key, val) { $('#images').append($('
').load(function() { From d25b1cbc3d233bfeea831f2ca6961773b08e07ee Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Wed, 9 Jul 2014 00:29:10 +0300 Subject: [PATCH 13/60] Response code fore deleted pages should be 410, not 310 --- application/controllers/ViewController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/controllers/ViewController.php b/application/controllers/ViewController.php index c80117a..a9eb526 100644 --- a/application/controllers/ViewController.php +++ b/application/controllers/ViewController.php @@ -316,7 +316,7 @@ private function processAllowDomain() public function deletedAction() { $this->render('deleted'); - return $this->getResponse()->setHttpResponseCode(310); + return $this->getResponse()->setHttpResponseCode(410); } /** From 662074c4ca8f34c21993f8b1466caca23c79300a Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Wed, 9 Jul 2014 00:32:31 +0300 Subject: [PATCH 14/60] Updated unit-tests --- tests/application/controllers/ViewControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/application/controllers/ViewControllerTest.php b/tests/application/controllers/ViewControllerTest.php index ec4d635..63797ed 100644 --- a/tests/application/controllers/ViewControllerTest.php +++ b/tests/application/controllers/ViewControllerTest.php @@ -66,7 +66,7 @@ public function testDeleted() { $hash = $this->testViewAnon(); $this->dispatch('/view/index/hash/' . $hash->key . '/'); - $this->assertResponseCode(310); + $this->assertResponseCode(410); $this->assertController('view'); } From ebcf4b043fbb9dad872860c47811bc0f72f80a4c Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Wed, 9 Jul 2014 16:28:47 +0300 Subject: [PATCH 15/60] Forcing the removal of uploaded image --- application/controllers/UploadController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/application/controllers/UploadController.php b/application/controllers/UploadController.php index 0d982af..31daf28 100644 --- a/application/controllers/UploadController.php +++ b/application/controllers/UploadController.php @@ -42,6 +42,11 @@ public function indexAction() if ($upload->isUploaded($file)) { $imgDoc = new Unsee_Image($response->hash . '_' . uniqid()); $imgDoc->setFile($info['tmp_name']); + + // Remove uploaded file from temporary dir if it wasn't removed + if (file_exists($info['tmp_name'])) { + @unlink($info['tmp_name']); + } } } From 9db3c8afae8ab35eaf9495d7952a0da473bad6ae Mon Sep 17 00:00:00 2001 From: Michael Gorianskii Date: Thu, 10 Jul 2014 14:38:22 +0300 Subject: [PATCH 16/60] Changed the layout of image sharing settings on the viewing page --- application/views/scripts/view/index.phtml | 120 ++++++++++++--------- public/css/settings.css | 19 ++-- public/css/view.css | 9 +- 3 files changed, 87 insertions(+), 61 deletions(-) diff --git a/application/views/scripts/view/index.phtml b/application/views/scripts/view/index.phtml index c1a59f4..32c1b10 100644 --- a/application/views/scripts/view/index.phtml +++ b/application/views/scripts/view/index.phtml @@ -1,12 +1,26 @@ headMeta()->appendName('robots', 'noindex'); -?> -