if (!isset($value['id']) || empty($value['url'])) {
die('Internal Error: A link should always have an id and URL.');
}
- if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) {
+ if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
die('You must specify an integer as a key.');
}
- if (! empty($offset) && $offset !== $value['id']) {
+ if ($offset !== null && $offset !== $value['id']) {
die('Array offset and link ID must be equal.');
}
* otherwise default format '%c' will be returned.
*
* @param DateTime $date to format.
+ * @param bool $time Displays time if true.
* @param bool $intl Use international format if true.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
-function format_date($date, $intl = true)
+function format_date($date, $time = true, $intl = true)
{
if (! $date instanceof DateTime) {
return false;
}
if (! $intl || ! class_exists('IntlDateFormatter')) {
- return strftime('%c', $date->getTimestamp());
+ $format = $time ? '%c' : '%x';
+ return strftime($format, $date->getTimestamp());
}
$formatter = new IntlDateFormatter(
setlocale(LC_TIME, 0),
IntlDateFormatter::LONG,
- IntlDateFormatter::LONG
+ $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
);
return $formatter->format($date);
}
+
+/**
+ * Check if the input is an integer, no matter its real type.
+ *
+ * PHP is a bit messy regarding this:
+ * - is_int returns false if the input is a string
+ * - ctype_digit returns false if the input is an integer or negative
+ *
+ * @param mixed $input value
+ *
+ * @return bool true if the input is an integer, false otherwise
+ */
+function is_integer_mixed($input)
+{
+ if (is_array($input) || is_bool($input) || is_object($input)) {
+ return false;
+ }
+ $input = strval($input);
+ return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1)));
+}
+
+/**
+ * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
+ *
+ * @param string $val Size expressed in string.
+ *
+ * @return int Size expressed in bytes.
+ */
+function return_bytes($val)
+{
+ if (is_integer_mixed($val) || $val === '0' || empty($val)) {
+ return $val;
+ }
+ $val = trim($val);
+ $last = strtolower($val[strlen($val)-1]);
+ $val = intval(substr($val, 0, -1));
+ switch($last) {
+ case 'g': $val *= 1024;
+ case 'm': $val *= 1024;
+ case 'k': $val *= 1024;
+ }
+ return $val;
+}
+
+/**
+ * Return a human readable size from bytes.
+ *
+ * @param int $bytes value
+ *
+ * @return string Human readable size
+ */
+function human_bytes($bytes)
+{
+ if ($bytes === '') {
+ return t('Setting not set');
+ }
+ if (! is_integer_mixed($bytes)) {
+ return $bytes;
+ }
+ $bytes = intval($bytes);
+ if ($bytes === 0) {
+ return t('Unlimited');
+ }
+
+ $units = [t('B'), t('kiB'), t('MiB'), t('GiB')];
+ for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) {
+ $bytes /= 1024;
+ }
+
+ return round($bytes) . $units[$i];
+}
+
+/**
+ * Try to determine max file size for uploads (POST).
+ * Returns an integer (in bytes) or formatted depending on $format.
+ *
+ * @param mixed $limitPost post_max_size PHP setting
+ * @param mixed $limitUpload upload_max_filesize PHP setting
+ * @param bool $format Format max upload size to human readable size
+ *
+ * @return int|string max upload file size
+ */
+function get_max_upload_size($limitPost, $limitUpload, $format = true)
+{
+ $size1 = return_bytes($limitPost);
+ $size2 = return_bytes($limitUpload);
+ // Return the smaller of two:
+ $maxsize = min($size1, $size2);
+ return $format ? human_bytes($maxsize) : $maxsize;
+}
/**
* Validates a JWT token authenticity.
*
- * @param string $token JWT token extracted from the headers.
+ * @param string $token JWT token extracted from the headers.
* @param string $secret API secret set in the settings.
*
* @throws ApiAuthorizationException the token is not valid.
/**
* Format a Link for the REST API.
*
- * @param array $link Link data read from the datastore.
+ * @param array $link Link data read from the datastore.
* @param string $indexUrl Shaarli's index URL (used for relative URL).
*
* @return array Link data formatted for the REST API.
}
return $out;
}
+
+ /**
+ * Convert a link given through a request, to a valid link for LinkDB.
+ *
+ * If no URL is provided, it will generate a local note URL.
+ * If no title is provided, it will use the URL as title.
+ *
+ * @param array $input Request Link.
+ * @param bool $defaultPrivate Request Link.
+ *
+ * @return array Formatted link.
+ */
+ public static function buildLinkFromRequest($input, $defaultPrivate)
+ {
+ $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
+ if (isset($input['private'])) {
+ $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
+ } else {
+ $private = $defaultPrivate;
+ }
+
+ $link = [
+ 'title' => ! empty($input['title']) ? $input['title'] : $input['url'],
+ 'url' => $input['url'],
+ 'description' => ! empty($input['description']) ? $input['description'] : '',
+ 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
+ 'private' => $private,
+ 'created' => new \DateTime(),
+ ];
+ return $link;
+ }
}
$this->jsonStyle = null;
}
}
+
+ /**
+ * Get the container.
+ *
+ * @return Container
+ */
+ public function getCi()
+ {
+ return $this->ci;
+ }
}
*/
public function getLink($request, $response, $args)
{
- if (! isset($this->linkDb[$args['id']])) {
+ if (!isset($this->linkDb[$args['id']])) {
throw new ApiLinkNotFoundException();
}
$index = index_url($this->ci['environment']);
$out = ApiUtils::formatLink($this->linkDb[$args['id']], $index);
+
return $response->withJson($out, 200, $this->jsonStyle);
}
+
+ /**
+ * Creates a new link from posted request body.
+ *
+ * @param Request $request Slim request.
+ * @param Response $response Slim response.
+ *
+ * @return Response response.
+ */
+ public function postLink($request, $response)
+ {
+ $data = $request->getParsedBody();
+ $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
+ // duplicate by URL, return 409 Conflict
+ if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) {
+ return $response->withJson(
+ ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
+ 409,
+ $this->jsonStyle
+ );
+ }
+
+ $link['id'] = $this->linkDb->getNextId();
+ $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+
+ // note: general relative URL
+ if (empty($link['url'])) {
+ $link['url'] = '?' . $link['shorturl'];
+ }
+
+ if (empty($link['title'])) {
+ $link['title'] = $link['url'];
+ }
+
+ $this->linkDb[$link['id']] = $link;
+ $this->linkDb->save($this->conf->get('resource.page_cache'));
+ $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
+ $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
+ return $response->withAddedHeader('Location', $redirect)
+ ->withJson($out, 201, $this->jsonStyle);
+ }
}
// Optional redirect after login:
if (isset($_GET['post'])) {
$uri = '?post='. urlencode($_GET['post']);
- foreach (array('description', 'source', 'title') as $param) {
+ foreach (array('description', 'source', 'title', 'tags') as $param) {
if (!empty($_GET[$param])) {
$uri .= '&'.$param.'='.urlencode($_GET[$param]);
}
$redir = '&username='. $_POST['login'];
if (isset($_GET['post'])) {
$redir .= '&post=' . urlencode($_GET['post']);
- foreach (array('description', 'source', 'title') as $param) {
+ foreach (array('description', 'source', 'title', 'tags') as $param) {
if (!empty($_GET[$param])) {
$redir .= '&' . $param . '=' . urlencode($_GET[$param]);
}
}
}
-// ------------------------------------------------------------------------------------------
-// Misc utility functions:
-
-// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
-function return_bytes($val)
-{
- $val = trim($val); $last=strtolower($val[strlen($val)-1]);
- switch($last)
- {
- case 'g': $val *= 1024;
- case 'm': $val *= 1024;
- case 'k': $val *= 1024;
- }
- return $val;
-}
-
-// Try to determine max file size for uploads (POST).
-// Returns an integer (in bytes)
-function getMaxFileSize()
-{
- $size1 = return_bytes(ini_get('post_max_size'));
- $size2 = return_bytes(ini_get('upload_max_filesize'));
- // Return the smaller of two:
- $maxsize = min($size1,$size2);
- // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
- return $maxsize;
-}
-
// ------------------------------------------------------------------------------------------
// Token management for XSRF protection
// Token should be used in any form which acts on data (create,update,delete,import...).
$dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
$data = array(
+ 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
'linksToDisplay' => $linksToDisplay,
'cols' => $columns,
'day' => $dayDate->getTimestamp(),
+ 'dayDate' => $dayDate,
'previousday' => $previousday,
'nextday' => $nextday,
);
// Show login screen, then redirect to ?post=...
if (isset($_GET['post']))
{
- header('Location: ?do=login&post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link.
+ header( // Redirect to login page, then back to post link.
+ 'Location: ?do=login&post='.urlencode($_GET['post']).
+ (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
+ (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
+ (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
+ (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
+ );
exit;
}
$conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
$conf->set('updates.check_updates', !empty($_POST['updateCheck']));
$conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
- $conf->set('api.enabled', !empty($_POST['apiEnabled']));
+ $conf->set('api.enabled', !empty($_POST['enableApi']));
$conf->set('api.secret', escape($_POST['apiSecret']));
try {
$conf->write(isLoggedIn());
}
// lf_id should only be present if the link exists.
- $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
+ $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
// Linkdate is kept here to:
// - use the same permalink for notes as they're displayed when creating them
// - let users hack creation date of their posts
// -------- User clicked the "Cancel" button when editing a link.
if (isset($_POST['cancel_edit']))
{
+ $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
+ if (! isset($LINKSDB[$id])) {
+ header('Location: ?');
+ }
// If we are called from the bookmarklet, we must close the popup:
if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
- $link = $LINKSDB[(int) escape($_POST['lf_id'])];
+ $link = $LINKSDB[$id];
$returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
// Scroll to the link which has been edited.
$returnurl .= '#'. $link['shorturl'];
if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
// Show import dialog
- $PAGE->assign('maxfilesize', getMaxFileSize());
+ $PAGE->assign(
+ 'maxfilesize',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ false
+ )
+ );
+ $PAGE->assign(
+ 'maxfilesizeHuman',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ true
+ )
+ );
$PAGE->renderPage('import');
exit;
}
// The file is too big or some form field may be missing.
echo '<script>alert("The file you are trying to upload is probably'
.' bigger than what this webserver can accept ('
- .getMaxFileSize().' bytes).'
+ .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
.' Please upload in smaller chunks.");document.location=\'?do='
.Router::$PAGE_IMPORT .'\';</script>';
exit;
// REST API routes
$app->group('/api/v1', function() {
- $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo');
- $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks');
- $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink');
+ $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
+ $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
+ $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
+ $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
})->add('\Shaarli\Api\ApiMiddleware');
$response = $app->run(true);
+++ /dev/null
-<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" alt="readityourself" /></a></span>
+++ /dev/null
-description="For each link, add a ReadItYourself icon to save the shaared URL."
-parameters=READITYOUSELF_URL;
\ No newline at end of file
+++ /dev/null
-<?php
-
-/**
- * Plugin readityourself
- */
-
-// If we're talking about https://github.com/memiks/readityourself
-// it seems kinda dead.
-// Not tested.
-
-/**
- * Init function, return an error if the server is not set.
- *
- * @param $conf ConfigManager instance.
- *
- * @return array Eventual error.
- */
-function readityourself_init($conf)
-{
- $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
- if (empty($riyUrl)) {
- $error = 'Readityourself plugin error: '.
- 'Please define the "READITYOUSELF_URL" setting in the plugin administration page.';
- return array($error);
- }
-}
-
-/**
- * Add readityourself icon to link_plugin when rendering linklist.
- *
- * @param mixed $data Linklist data.
- * @param ConfigManager $conf Configuration Manager instance.
- *
- * @return mixed - linklist data with readityourself plugin.
- */
-function hook_readityourself_render_linklist($data, $conf)
-{
- $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
- if (empty($riyUrl)) {
- return $data;
- }
-
- $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html');
-
- foreach ($data['links'] as &$value) {
- $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH);
- $value['link_plugin'][] = $readityourself;
- }
-
- return $data;
-}
*/
require_once 'application/Utils.php';
+require_once 'application/Languages.php';
require_once 'tests/utils/ReferenceSessionIdHashes.php';
// Initialize reference data before PHPUnit starts a session
$this->assertFalse(format_date([]));
$this->assertFalse(format_date(null));
}
+
+ /**
+ * Test is_integer_mixed with valid values
+ */
+ public function testIsIntegerMixedValid()
+ {
+ $this->assertTrue(is_integer_mixed(12));
+ $this->assertTrue(is_integer_mixed('12'));
+ $this->assertTrue(is_integer_mixed(-12));
+ $this->assertTrue(is_integer_mixed('-12'));
+ $this->assertTrue(is_integer_mixed(0));
+ $this->assertTrue(is_integer_mixed('0'));
+ $this->assertTrue(is_integer_mixed(0x0a));
+ }
+
+ /**
+ * Test is_integer_mixed with invalid values
+ */
+ public function testIsIntegerMixedInvalid()
+ {
+ $this->assertFalse(is_integer_mixed(true));
+ $this->assertFalse(is_integer_mixed(false));
+ $this->assertFalse(is_integer_mixed([]));
+ $this->assertFalse(is_integer_mixed(['test']));
+ $this->assertFalse(is_integer_mixed([12]));
+ $this->assertFalse(is_integer_mixed(new DateTime()));
+ $this->assertFalse(is_integer_mixed('0x0a'));
+ $this->assertFalse(is_integer_mixed('12k'));
+ $this->assertFalse(is_integer_mixed('k12'));
+ $this->assertFalse(is_integer_mixed(''));
+ }
+
+ /**
+ * Test return_bytes
+ */
+ public function testReturnBytes()
+ {
+ $this->assertEquals(2 * 1024, return_bytes('2k'));
+ $this->assertEquals(2 * 1024, return_bytes('2K'));
+ $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m'));
+ $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M'));
+ $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g'));
+ $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G'));
+ $this->assertEquals(374, return_bytes('374'));
+ $this->assertEquals(374, return_bytes(374));
+ $this->assertEquals(0, return_bytes('0'));
+ $this->assertEquals(0, return_bytes(0));
+ $this->assertEquals(-1, return_bytes('-1'));
+ $this->assertEquals(-1, return_bytes(-1));
+ $this->assertEquals('', return_bytes(''));
+ }
+
+ /**
+ * Test human_bytes
+ */
+ public function testHumanBytes()
+ {
+ $this->assertEquals('2kiB', human_bytes(2 * 1024));
+ $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
+ $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
+ $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
+ $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
+ $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
+ $this->assertEquals('374B', human_bytes(374));
+ $this->assertEquals('374B', human_bytes('374'));
+ $this->assertEquals('232kiB', human_bytes(237481));
+ $this->assertEquals('Unlimited', human_bytes('0'));
+ $this->assertEquals('Unlimited', human_bytes(0));
+ $this->assertEquals('Setting not set', human_bytes(''));
+ }
+
+ /**
+ * Test get_max_upload_size with formatting
+ */
+ public function testGetMaxUploadSize()
+ {
+ $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
+ $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
+ $this->assertEquals('100B', get_max_upload_size(100, 100));
+ }
+
+ /**
+ * Test get_max_upload_size without formatting
+ */
+ public function testGetMaxUploadSizeRaw()
+ {
+ $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false));
+ $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
+ $this->assertEquals('100', get_max_upload_size(100, 100, false));
+ }
}
--- /dev/null
+<?php
+
+namespace Shaarli\Api\Controllers;
+
+
+use Shaarli\Config\ConfigManager;
+use Slim\Container;
+use Slim\Http\Environment;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Class PostLinkTest
+ *
+ * Test POST Link REST API service.
+ *
+ * @package Shaarli\Api\Controllers
+ */
+class PostLinkTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var string datastore to test write operations
+ */
+ protected static $testDatastore = 'sandbox/datastore.php';
+
+ /**
+ * @var ConfigManager instance
+ */
+ protected $conf;
+
+ /**
+ * @var \ReferenceLinkDB instance.
+ */
+ protected $refDB = null;
+
+ /**
+ * @var Container instance.
+ */
+ protected $container;
+
+ /**
+ * @var Links controller instance.
+ */
+ protected $controller;
+
+ /**
+ * Number of JSON field per link.
+ */
+ const NB_FIELDS_LINK = 9;
+
+ /**
+ * Before every test, instantiate a new Api with its config, plugins and links.
+ */
+ public function setUp()
+ {
+ $this->conf = new ConfigManager('tests/utils/config/configJson.json.php');
+ $this->refDB = new \ReferenceLinkDB();
+ $this->refDB->write(self::$testDatastore);
+
+ $this->container = new Container();
+ $this->container['conf'] = $this->conf;
+ $this->container['db'] = new \LinkDB(self::$testDatastore, true, false);
+
+ $this->controller = new Links($this->container);
+
+ $mock = $this->getMock('\Slim\Router', ['relativePathFor']);
+ $mock->expects($this->any())
+ ->method('relativePathFor')
+ ->willReturn('api/v1/links/1');
+
+ // affect @property-read... seems to work
+ $this->controller->getCi()->router = $mock;
+
+ // Used by index_url().
+ $this->controller->getCi()['environment'] = [
+ 'SERVER_NAME' => 'domain.tld',
+ 'SERVER_PORT' => 80,
+ 'SCRIPT_NAME' => '/',
+ ];
+ }
+
+ /**
+ * After every test, remove the test datastore.
+ */
+ public function tearDown()
+ {
+ @unlink(self::$testDatastore);
+ }
+
+ /**
+ * Test link creation without any field: creates a blank note.
+ */
+ public function testPostLinkMinimal()
+ {
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'POST',
+ ]);
+
+ $request = Request::createFromEnvironment($env);
+
+ $response = $this->controller->postLink($request, new Response());
+ $this->assertEquals(201, $response->getStatusCode());
+ $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(self::NB_FIELDS_LINK, count($data));
+ $this->assertEquals(43, $data['id']);
+ $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
+ $this->assertEquals('http://domain.tld/?' . $data['shorturl'], $data['url']);
+ $this->assertEquals('?' . $data['shorturl'], $data['title']);
+ $this->assertEquals('', $data['description']);
+ $this->assertEquals([], $data['tags']);
+ $this->assertEquals(false, $data['private']);
+ $this->assertTrue(new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
+ $this->assertEquals('', $data['updated']);
+ }
+
+ /**
+ * Test link creation with all available fields.
+ */
+ public function testPostLinkFull()
+ {
+ $link = [
+ 'url' => 'website.tld/test?foo=bar',
+ 'title' => 'new entry',
+ 'description' => 'shaare description',
+ 'tags' => ['one', 'two'],
+ 'private' => true,
+ ];
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_TYPE' => 'application/json'
+ ]);
+
+ $request = Request::createFromEnvironment($env);
+ $request = $request->withParsedBody($link);
+ $response = $this->controller->postLink($request, new Response());
+
+ $this->assertEquals(201, $response->getStatusCode());
+ $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]);
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(self::NB_FIELDS_LINK, count($data));
+ $this->assertEquals(43, $data['id']);
+ $this->assertRegExp('/[\w-_]{6}/', $data['shorturl']);
+ $this->assertEquals('http://' . $link['url'], $data['url']);
+ $this->assertEquals($link['title'], $data['title']);
+ $this->assertEquals($link['description'], $data['description']);
+ $this->assertEquals($link['tags'], $data['tags']);
+ $this->assertEquals(true, $data['private']);
+ $this->assertTrue(new \DateTime('2 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']));
+ $this->assertEquals('', $data['updated']);
+ }
+
+ /**
+ * Test link creation with an existing link (duplicate URL). Should return a 409 HTTP error and the existing link.
+ */
+ public function testPostLinkDuplicate()
+ {
+ $link = [
+ 'url' => 'mediagoblin.org/',
+ 'title' => 'new entry',
+ 'description' => 'shaare description',
+ 'tags' => ['one', 'two'],
+ 'private' => true,
+ ];
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_TYPE' => 'application/json'
+ ]);
+
+ $request = Request::createFromEnvironment($env);
+ $request = $request->withParsedBody($link);
+ $response = $this->controller->postLink($request, new Response());
+
+ $this->assertEquals(409, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(self::NB_FIELDS_LINK, count($data));
+ $this->assertEquals(7, $data['id']);
+ $this->assertEquals('IuWvgA', $data['shorturl']);
+ $this->assertEquals('http://mediagoblin.org/', $data['url']);
+ $this->assertEquals('MediaGoblin', $data['title']);
+ $this->assertEquals('A free software media publishing platform #hashtagOther', $data['description']);
+ $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
+ $this->assertEquals(false, $data['private']);
+ $this->assertEquals(
+ \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
+ \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
+ );
+ $this->assertEquals(
+ \DateTime::createFromFormat(\LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
+ \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
+ );
+ }
+}
public function testDateFormat()
{
$date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
- $this->assertRegExp('/1. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true));
+ $this->assertRegExp('/1\. Januar 2017 (um )?10:11:12 GMT\+0?3(:00)?/', format_date($date, true, true));
+ }
+
+ /**
+ * Test date_format() without time.
+ */
+ public function testDateFormatNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+ $this->assertRegExp('/1\. Januar 2017/', format_date($date, false,true));
}
/**
public function testDateFormatDefault()
{
$date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
- $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, false));
+ $this->assertEquals('So 01 Jan 2017 10:11:12 EAT', format_date($date, true, false));
+ }
+
+ /**
+ * Test date_format() using builtin PHP function strftime without time.
+ */
+ public function testDateFormatDefaultNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
+ $this->assertEquals('01.02.2017', format_date($date, false, false));
}
/**
public function testDateFormat()
{
$date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
- $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true));
+ $this->assertRegExp('/January 1, 2017 (at )?10:11:12 AM GMT\+0?3(:00)?/', format_date($date, true, true));
+ }
+
+ /**
+ * Test date_format() without time.
+ */
+ public function testDateFormatNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+ $this->assertRegExp('/January 1, 2017/', format_date($date, false, true));
}
/**
public function testDateFormatDefault()
{
$date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
- $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, false));
+ $this->assertEquals('Sun 01 Jan 2017 10:11:12 AM EAT', format_date($date, true, false));
+ }
+
+ /**
+ * Test date_format() using builtin PHP function strftime without time.
+ */
+ public function testDateFormatDefaultNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
+ $this->assertEquals('02/01/2017', format_date($date, false, false));
}
/**
$this->assertRegExp('/1 janvier 2017 (à )?10:11:12 UTC\+0?3(:00)?/', format_date($date));
}
+ /**
+ * Test date_format() without time.
+ */
+ public function testDateFormatNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
+ $this->assertRegExp('/1 janvier 2017/', format_date($date, false, true));
+ }
+
/**
* Test date_format() using builtin PHP function strftime.
*/
public function testDateFormatDefault()
{
$date = DateTime::createFromFormat('Ymd_His', '20170101_101112');
- $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, false));
+ $this->assertEquals('dim. 01 janv. 2017 10:11:12 EAT', format_date($date, true, false));
+ }
+
+ /**
+ * Test date_format() using builtin PHP function strftime without time.
+ */
+ public function testDateFormatDefaultNoTime()
+ {
+ $date = DateTime::createFromFormat('Ymd_His', '20170201_101112');
+ $this->assertEquals('01/02/2017', format_date($date, false, false));
}
/**
+++ /dev/null
-<?php
-use Shaarli\Config\ConfigManager;
-
-/**
- * PluginReadityourselfTest.php.php
- */
-
-require_once 'plugins/readityourself/readityourself.php';
-
-/**
- * Class PluginWallabagTest
- * Unit test for the Wallabag plugin
- */
-class PluginReadityourselfTest extends PHPUnit_Framework_TestCase
-{
- /**
- * Reset plugin path
- */
- public function setUp()
- {
- PluginManager::$PLUGINS_PATH = 'plugins';
- }
-
- /**
- * Test Readityourself init without errors.
- */
- public function testReadityourselfInitNoError()
- {
- $conf = new ConfigManager('');
- $conf->set('plugins.READITYOUSELF_URL', 'value');
- $errors = readityourself_init($conf);
- $this->assertEmpty($errors);
- }
-
- /**
- * Test Readityourself init with errors.
- */
- public function testReadityourselfInitError()
- {
- $conf = new ConfigManager('');
- $errors = readityourself_init($conf);
- $this->assertNotEmpty($errors);
- }
-
- /**
- * Test render_linklist hook.
- */
- public function testReadityourselfLinklist()
- {
- $conf = new ConfigManager('');
- $conf->set('plugins.READITYOUSELF_URL', 'value');
- $str = 'http://randomstr.com/test';
- $data = array(
- 'title' => $str,
- 'links' => array(
- array(
- 'url' => $str,
- )
- )
- );
-
- $data = hook_readityourself_render_linklist($data, $conf);
- $link = $data['links'][0];
- // data shouldn't be altered
- $this->assertEquals($str, $data['title']);
- $this->assertEquals($str, $link['url']);
-
- // plugin data
- $this->assertEquals(1, count($link['link_plugin']));
- $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
- }
-
- /**
- * Test without config: nothing should happened.
- */
- public function testReadityourselfLinklistWithoutConfig()
- {
- $conf = new ConfigManager('');
- $conf->set('plugins.READITYOUSELF_URL', null);
- $str = 'http://randomstr.com/test';
- $data = array(
- 'title' => $str,
- 'links' => array(
- array(
- 'url' => $str,
- )
- )
- );
-
- $data = hook_readityourself_render_linklist($data, $conf);
- $link = $data['links'][0];
- // data shouldn't be altered
- $this->assertEquals($str, $data['title']);
- $this->assertEquals($str, $link['url']);
-
- // plugin data
- $this->assertArrayNotHasKey('link_plugin', $link);
- }
-}
0,
DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
'gnu media web .hidden hashtag',
- null,
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'),
'IuWvgA'
);
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
<div class="form-label">
- <label for="apiEnabled">
+ <label for="enableApi">
<span class="label-name">{'Enable REST API'|t}</span><br>
<span class="label-desc">{'Allow third party software to use Shaarli such as mobile application'|t}</span>
</label>
</div>
<div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
<div class="form-input">
- <input type="checkbox" name="apiEnabled" id="apiEnabled"
+ <input type="checkbox" name="enableApi" id="enableApi"
{if="$api_enabled"}checked{/if}/>
</div>
</div>
}
@font-face {
- font-family: 'Roboto Slab';
+ font-family: 'Roboto';
font-weight: 400;
font-style: normal;
src:
- local('Fira Sans'),
- local('Fira-Sans-regular'),
- url('../fonts/Fira-Sans-regular.woff2') format('woff2'),
- url('../fonts/Fira-Sans-regular.woff') format('woff');
+ local('Roboto'),
+ local('Roboto-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
+}
+
+@font-face {
+ font-family: 'Roboto';
+ font-weight: 700;
+ font-style: normal;
+ src:
+ local('Roboto'),
+ local('Roboto-Bold'),
+ url('../fonts/Roboto-Bold.woff2') format('woff2'),
+ url('../fonts/Roboto-Bold.woff') format('woff');
+}
+
+body, .pure-g [class*="pure-u"] {
+ font-family: Roboto, Arial, sans-serif;
}
/**
.pure-u-xl-visible { display: inline-block !important; }
}
-.pure-g [class*="pure-u"]{
- font-family: Roboto Slab, Arial, sans-serif;
-}
-
/**
* Make pure-extras alert closable.
*/
color: #252525;
text-decoration: none;
vertical-align: middle;
- font-family: Roboto Slab, Arial, sans-serif;
}
.linklist-item-title .linklist-link {
.linklist-item-description {
position: relative;
padding: 10px;
- font-family: Roboto Slab, Arial, sans-serif;
word-wrap: break-word;
color: #252525;
line-height: 1.3em;
</div>
</div>
<div>
- <h3 class="window-subtitle">{function="strftime('%A %d, %B %Y', $day)"}</h3>
+ <h3 class="window-subtitle">{function="format_date($dayDate, false)"}</h3>
<div id="plugin_zone_about_daily" class="plugin_zone">
{loop="$daily_about_plugin"}
<div class="center" id="import-field">
<input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
<input type="file" name="filetoupload">
+ <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
</div>
<div class="pure-g">
{$ratioLabel='1-4'}
{$ratioInput='3-4'}
+{$ratioLabelMobile='7-8'}
+{$ratioInputMobile='1-8'}
<form method="POST" action="#" name="installform" id="installform">
<div class="pure-g">
</div>
</div>
+ <div class="pure-g">
+ <div class="pure-u-lg-{$ratioLabel} pure-u-{$ratioLabelMobile}">
+ <div class="form-label">
+ <label for="enableApi">
+ <span class="label-name">{'Enable REST API'|t}</span><br>
+ <span class="label-desc">{'Allow third party software to use Shaarli such as mobile application'|t}</span>
+ </label>
+ </div>
+ </div>
+ <div class="pure-u-lg-{$ratioInput} pure-u-{$ratioInputMobile}">
+ <div class="form-input">
+ <input type="checkbox" name="enableApi" id="enableApi" checked />
+ </div>
+ </div>
+ </div>
+
<div class="center">
<input type="submit" value="{'Install'|t}" name="Save">
</div>
* Remove CSS target padding (for fixed bar)
*/
if (location.hash != '') {
- var anchor = document.querySelector(location.hash);
+ var anchor = document.getElementById(location.hash.substr(1));
if (anchor != null) {
var padsize = anchor.clientHeight;
- console.log(document.querySelector(location.hash).clientHeight);
this.window.scroll(0, this.window.scrollY - padsize);
anchor.style.paddingTop = 0;
}
<tr>
<td valign="top"><b>Enable REST API</b></td>
<td>
- <input type="checkbox" name="apiEnabled" id="apiEnabled"
+ <input type="checkbox" name="enableApi" id="enableApi"
{if="$api_enabled"}checked{/if}/>
- <label for="apiEnabled"> Allow third party software to use Shaarli such as mobile application.</label>
+ <label for="enableApi"> Allow third party software to use Shaarli such as mobile application.</label>
</td>
</tr>
<tr>
<div id="pageheader">
{include="page.header"}
<div id="uploaddiv">
- Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes).
+ Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
<form method="POST" action="?do=import" enctype="multipart/form-data"
name="uploadform" id="uploadform">
<input type="hidden" name="token" value="{$token}">