Commit 9aab19fc authored by Karl Denninger's avatar Karl Denninger

ZP-1284 Stickynote backend initial code upload.

This is running here against a BlackBerry DTEK66 and Priv, but may be a bit
rough around the edges and/or contain (plenty of) bugs!  Comments and
other's experiences with this code are welcome; please have at it!  Make
sure you read the REQUIREMENTS file so you understand what has to be set up.

Released under the Affero GNU General Public License (AGPL) version 3.
parent 1b59bc7d
The author of this backend is tickerguy (Karl Denninger)
Copyright 2017 Karl Denninger
Karl Denninger released this code as AGPLv3 here: ZP-1284 on Jira.z-hub.io
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License, version 3,
as published by the Free Software Foundation.
REQUIREMENTS:
1. php-pgsql module
2. An active Postgres server v9.0 or above (developed on 9.6.2; v9.6+
recommended) either on the local machine (preferred) or accessible via
the network. Note that if the connection is remote performance may
be impaired and security implications come into play; see Postgres'
documentation.
WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
===============================================================
This backend does ZERO authentication of credentials. With Z-Push the
Logon process requires that ALL provided backends in the "combined" backend
succeed, so it is IMPERATIVE that AT LEAST one other back end be defined that
actually checks passwords. THIS IS NOT A BACKEND THAT CAN BE RUN STANDALONE
AS IT IMPLEMENTS NO SECURITY ON ITS OWN.
This is a design decision as not providing internal authentication removes
the need for a privileged (SUID root) "helper" application, OR hijacking
the IMAP server's authentication to check passwords. However, it thus relies
on at least one other backend (IMAP, CalDav or Carddav) to implement same.
===============================================================
WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
INSTALL:
Create the postgres role account you intend to use ("stickynote" is what's
in the config files) and grant it login permission.
Edit the create-sticky-tables.sql file to make sure the proper permissions
are set in the GRANT statements (the role account you create and permit
to sign in), editing as required.
You must make sure that the connection parameters you intend to use for
Postgres are in accord with what you set up in the config.php file for
the hostname (or IP), along with the role and password (if required) for
access to the database. Note that if a password is not required setting
one will not hurt (it's ignored if not required for the given role and
connection.)
Once you have edited the create-sticky-tables.sql file, create the
database and schemas with the following command as the Postgres superuser
(usually psql):
createdb stickynote
psql stickynote <create-sticky-tables.sql
Then edit config.php (REQUIRED; it will NOT run without modification)
as necessary to fit and activate it in the combined backend.
<?php
/***********************************************
* File : config.php
* Project : Z-Push
* Descr : Stickynote backend configuration file
*
* Created : 8/29/2017
*
* Copyright 2017 Karl Denninger
*
* Karl Denninger releases this code under AGPLv3.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult LICENSE file for details
************************************************/
// ************************
// BackendStickyNote settings
// NOTE that StickyNote does NOT perform any actual login verification.
//
// YOU ARE WARNED THAT YOU MUST HAVE AT LEAST ONE OTHER BACK END DEFINED THAT
// DOES ACTUALLY CHECK PASSWORDS, OR YOU HAVE ***ZERO*** SECURITY ON THIS
// BACKEND! To enforce your reading this notice (and hopefully paying
// attention to it, the backend will NOT run unless you comment out the LAST
// parameter in this list.
//
// You must ALSO read and follow the REQUIREMENTS file to set up
// the roles and database schema required. Do that BEFORE configuring
// the below parameters (yes, they must match!)
//
// ************************
// The Postgresql server (IP number or name)
define('STICKYNOTE_SERVER', 'localhost');
// Postgresql server port (5432 is Postgres default)
define('STICKYNOTE_PORT', '5432');
// The database on the server
define('STICKYNOTE_DATABASE', 'stickynote');
// The username to use for the role
define('STICKYNOTE_USER', 'stickynote');
// The password to use for the role, if any
define('STICKYNOTE_PASSWORD', 'stickynote');
// If defined then a delete REALLY DELETES; if not it marks the item deleted
// in the database but DOES NOT physically remove it.
//define('STICKYNOTE_REALLYDELETE', 'true');
// You MUST comment this out or the code will not run
//define('STICKYNOTE_MUSTNOTBESET', 'true');
create table note
(ordinal int primary key, login text, domain text,
inserted timestamp with time zone default now(),
modified timestamp with time zone default now(),
deleted boolean default false, subject text, content text,
categories text);
create table categories
(ordinal int references note(ordinal) on update cascade
on delete cascade, tag text);
create index note_login on note using btree(login, domain);
create index tag_ordinal on categories using btree(ordinal);
create sequence ordinal;
grant all on note, categories, ordinal to stickynote;
<?php
/***********************************************
* File : stickynote.php
* Project : PHP-Push
* Descr : This backend is based on 'Vcarddir' and implements a
* : StickyNote interface against a Postgres server
*
* Created : 8/28/2017
*
* Copyright 2017 Karl Denninger
*
* Karl Denninger released this code as AGPLv3 here (ZP)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Consult the REQUIREMENTS file for details on installation and setup
************************************************/
// config file
require_once("backend/stickynote/config.php");
class BackendStickyNote extends BackendDiff {
/**
* @var StickyNoteClient
*/
private $_stickynote;
private $_collection = array();
private $_user;
private $_domain;
private $_dbconn;
private $_result;
/**
* Constructor
*/
public function __construct() {
if (!function_exists("pg_connect")) {
throw new FatalException("BackendStickyNote(): Postgres extension php-pgsql not found", 0, null, LOGLEVEL_FATAL);
}
}
/**
* Login to the StickyNote backend
* NOTE: There is no ACTUAL authentication performed! You MUST have another
* ACTUAL authenticating backend defined, such as IMAP, which checks
* passwords. We simply accept what you give us, and ignore the password.
*
* @see IBackend::Logon()
*/
public function Logon($username, $domain, $password) {
if (defined('STICKYNOTE_MUSTNOTBESET'))
throw new FatalException("BackendStickyNote(): Configuration file has not been set up; review REQUIREMENTS and edit config.php.", 0, NULL, LOGLEVEL_FATAL);
$this->_user = $username;
$this->_domain = $domain;
$_connstring = sprintf("host='%s' port='%s' dbname='%s' user='%s' password='%s'", STICKYNOTE_SERVER, STICKYNOTE_PORT, STICKYNOTE_DATABASE, STICKYNOTE_USER, STICKYNOTE_PASSWORD);
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->pg_conn(): '%s'", $_connstring));
$this->_dbconn = pg_connect($_connstring);
if ($this->_dbconn == false)
throw new FatalException("BackendStickyNote(): Connection to Postgres backend failed", 0, NULL, LOGLEVEL_FATAL);
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->Logon(): User '%s' Domain '%s' accepted on StickyNote", $username, $domain));
return true;
}
/**
* The connections to Stickynote require that we shut down the Postgres
* connection if a valid one exists. So do that.
* @see IBackend::Logoff()
*/
public function Logoff() {
if ($this->_dbconn != null) {
pg_close($this->_dbconn);
$this->_dbconn = false;
}
$this->SaveStorages();
ZLog::Write(LOGLEVEL_DEBUG, "BackendStickyNote->Logoff(): disconnected from Postgres server");
return true;
}
/**
* StickyNote doesn't need to handle SendMail
* @see IBackend::SendMail()
*/
public function SendMail($sm) {
return false;
}
/**
* No attachments in StickyNote
* @see IBackend::GetAttachmentData()
*/
public function GetAttachmentData($attname) {
return false;
}
/**
* Deletes are always permanent deletes. Messages doesn't get moved.
* @see IBackend::GetWasteBasket()
*/
public function GetWasteBasket() {
return false;
}
/**
* Get a list of all the folders we are going to sync.
* There's only one....
* @see BackendDiff::GetFolderList()
*/
public function GetFolderList() {
$folders = array();
$folders[] = "Notes";
return $folders;
}
/**
* Returning a SyncFolder
* @see BackendDiff::GetFolder()
*/
public function GetFolder($id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->GetFolder('%s')", $id));
$folder = new SyncFolder();
$folder->parentid = "0";
$folder->displayname = "Notes";
$folder->serverid = $id;
$folder->type = SYNC_FOLDER_TYPE_NOTE;
return $folder;
}
/**
* Returns information on the folder.
* @see BackendDiff::StatFolder()
*/
public function StatFolder($id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->StatFolder('%s')", $id));
// Return the Display Name of the folder. No parent, as there's only one.
$folder = array();
$folder["mod"] = "Notes";
$folder["id"] = $id;
$folder["parent"] = "0";
return $folder;
}
/**
* ChangeFolder is not supported under StickyNote
* @see BackendDiff::ChangeFolder()
*/
public function ChangeFolder($folderid, $oldid, $displayname, $type) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->ChangeFolder('%s','%s','%s','%s')", $folderid, $oldid, $displayname, $type));
return false;
}
/**
* DeleteFolder is not supported under StickyNote
* @see BackendDiff::DeleteFolder()
*/
public function DeleteFolder($id, $parentid) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->DeleteFolder('%s','%s')", $id, $parentid));
return false;
}
/**
* Get a list of all the messages.
* @see BackendDiff::GetMessageList()
*/
public function GetMessageList($folderid, $cutoffdate) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->GetMessageList('%s','%s')", $folderid, $cutoffdate));
$messages = array();
$_param = array();
if ($cutoffdate) {
array_push($_param, $this->_user, $this->_domain, $cutoffdate);
$this->_result = pg_query_params($this->_dbconn, "select ordinal, extract(epoch from modified)::integer from note where modified <= timestamptz 'epoch' + $3 * interval '1 second' and login=$1 and domain=$2", $_param);
} else {
array_push($_param, $this->_user, $this->_domain);
$this->_result = pg_query_params($this->_dbconn, "select ordinal, extract(epoch from modified)::integer from note where login=$1 and domain=$2", $_param);
}
if (pg_result_status($this->_result) != PGSQL_TUPLES_OK) {
ZLog::Write(LOGLEVEL_WARN, sprintf("BackendStickyNote->GetMessageList(Failed to return a valid message list from database)"));
} else {
for ($_count = 0; $_count < pg_affected_rows($this->_result); $_count++) {
$message = array();
$message["id"] = pg_fetch_result($this->_result, $_count, 0);
$message["mod"] = pg_fetch_result($this->_result, $_count, 1);
$message["flags"] = 1; // Always mark as 'seen'
$messages[] = $message;
}
}
pg_free_result($this->_result);
return $messages;
}
/**
* Get a SyncObject by its ID (in this case, a SyncNote)
* @see BackendDiff::GetMessage()
*/
public function GetMessage($folderid, $id, $contentparameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->GetMessage('%s','%s')", $folderid, $id));
// Look up the message in the database
$_params = array();
array_push($_params, $id, $this->_user, $this->_domain);
$this->_result = pg_query_params($this->_dbconn, "select *, extract(epoch from modified)::integer as changed from note where ordinal = $1 and login = $2 and domain = $3", $_params);
if (pg_result_status($this->_result) != PGSQL_TUPLES_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->GetMessage(FAILED query for '%s','%s')", $folderid, $id));
return false;
}
if (pg_affected_rows($this->_result) != 1) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->GetMessage(FAILED lookup for '%s','%s')", $folderid, $id));
return false;
}
// Build the packet for a StickyNote
$_content = pg_fetch_result($this->_result, 0, "content");
$message = new SyncNote();
$message->asbody = new SyncBaseBody();
$message->asbody->type = 2;
$message->asbody->estimatedDataSize = strlen($_content);
$message->asbody->data = StringStreamWrapper::Open($_content);
unset($_content);
$message->subject = pg_fetch_result($this->_result, 0, "subject");
$message->lastmodified = pg_fetch_result($this->_result, 0, "changed");
$message->type = 'IPW.StickyNote';
pg_free_result($this->_result);
unset($_params);
// Get categories, if any, for this note and add them to the SyncObject
$_params = array();
array_push($_params, $id);
$this->_result = pg_query_params($this->_dbconn, "select tag from categories where ordinal=$1", $_params);
if (pg_affected_rows($this->_result) > 0) {
$_categories = array();
for ($_count = 0; $_count < pg_affected_rows($this->_result); $_count++) {
$_categories[] = pg_fetch_result($this->_result, $_count, 0);
}
$message->categories = $_categories;
}
pg_free_result($this->_result);
return $message;
}
/**
* Return id, flags and mod of a messageid
* @see BackendDiff::StatMessage()
*/
public function StatMessage($folderid, $id) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->StatMessage('%s','%s')", $folderid, $id));
$message = array();
$_params = array();
array_push($_params, $id, $this->_user, $this->_domain);
$this->_result = pg_query_params($this->_dbconn, "select extract(epoch from modified)::integer from note where ordinal=$1 and login=$2 and domain=$3", $_params);
if (pg_result_status($this->_result) != PGSQL_TUPLES_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->StatMessage(Stat call failed for '%s')", $id));
return $message;
}
if (!pg_num_rows($this->_result)) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->StatMessage(Stat for empty note '%s')", $id));
return $message;
}
$message['mod'] = pg_fetch_result($this->_result, 0, 0);
pg_free_result($this->_result);
$message['id'] = $id;
$message['flags'] = "1";
return $message;
}
/**
* Change/Add a message with contents received from ActiveSync
* @see BackendDiff::ChangeMessage()
*/
public function ChangeMessage($folderid, $id, $message, $contentParameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->ChangeMessage('%s','%s')", $folderid, $id));
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->ChangeMessage(Message '%s')", $message));
// If we have a null ID then it's a new note; allocate an ordinal for it,
// Then insert into the database and return the stat pointer for it.
// If we get an ID then it's an update; perform it and return stat pointer.
//
$_contents = stream_get_contents($message->asbody->data, 1024000);
if (!$id) {
$this->_result = pg_query($this->_dbconn, "select nextval('ordinal')");
if (pg_result_status($this->_result) != PGSQL_TUPLES_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Cannot get new sequence number for item')"));
return false;
}
$id = pg_fetch_result($this->_result, 0, 0);
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Begin");
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Transaction start failure!')"));
pg_free_result($this->_result);
return false;
}
pg_free_result($this->_result);
$_params = array();
array_push($_params, $id, $message->subject, $_contents, $this->_user, $this->_domain);
$this->_result = pg_query_params($this->_dbconn, "insert into note (ordinal, subject, content, login, domain) values ($1, $2, $3, $4, $5)", $_params);
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Cannot insert new item; fail!')"));
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Rollback");
pg_free_result($this->_result);
return false;
}
if (pg_affected_rows($this->_result) == 1) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->ChangeMessage('Insert of item %s (subj '%s') succeded')", $id, $message->subject));
} else {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Insert of item %s (subj '%s') failed')", $id, $message->subject));
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Rollback");
pg_free_result($this->_result);
return false;
}
unset ($_params);
pg_free_result($this->_result);
if ($message->categories) {
foreach ($message->categories as $_category) {
$_params = array();
array_push($_params, $id, $_category);
$this->_result = pg_query_params($this->_dbconn, "insert into categories (ordinal, tag) values ($1, $2)", $_params);
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Cannot insert category for item; fail!')"));
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Rollback");
pg_free_result($this->_result);
return(false);
}
pg_free_result($this->_result);
}
unset ($_category);
}
} else {
$_params = array();
array_push($_params, $message->subject, $_contents, $id, $this->_user, $this->_domain);
$this->_result = pg_query_params($this->_dbconn, "update note set subject=$1, content=$2, modified=now() where ordinal=$3 and login=$4 and domain=$5", $_params);
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Update of item %s failed!')", $id));
}
if (pg_affected_rows($this->_result) == 1) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->ChangeMessage('Update of item %s (subj '%s') succeded')", $id, $message->subject));
} else {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Update of item %s (subj '%s') failed (credential mismatch)')", $id, $message->subject));
}
pg_free_result($this->_result);
unset ($_params);
if ($message->categories) {
$_params = array();
array_push($_params, $id);
$this->_result = pg_query_params($this->_dbconn, "delete from categories where ordinal=$1", $_params);
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Cannot clear category for item; fail!')"));
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Rollback");
pg_free_result($this->_result);
return(false);
}
unset ($_params);
foreach ($message->categories as $_category) {
$_params = array();
array_push($_params, $id, $_category);
$this->_result = pg_query_params($this->_dbconn, "insert into categories (ordinal, tag) values ($1, $2)", $_params);
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Cannot insert category for item; fail!')"));
pg_free_result($this->_result);
$this->_result = pg_query($this->_dbconn, "Rollback");
pg_free_result($this->_result);
return(false);
}
pg_free_result($this->_result);
}
unset ($_category);
}
}
$this->_result = pg_query($this->_dbconn, "COMMIT");
if (pg_result_status($this->_result) != PGSQL_COMMAND_OK) {
ZLog::Write(LOGLEVEL_ERROR, sprintf("BackendStickyNote->ChangeMessage('Transaction commit FAIL!')"));
pg_free_result($this->_result);
return false;
}
pg_free_result($this->_result);
return $this->StatMessage($folderid, $id);
}
/**
* Change the read flag is not supported.
* @see BackendDiff::SetReadFlag()
*/
public function SetReadFlag($folderid, $id, $flags, $contentParameters) {
return false;
}
/**
* Delete a message from the StickyNote server.
* @see BackendDiff::DeleteMessage()
*/
public function DeleteMessage($folderid, $id, $contentParameters) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->DeleteMessage('%s','%s')", $folderid, $id));
$_params = array();
array_push($_params, $id, $this->_user, $this->_domain);
if (defined('STICKYNOTE_REALLYDELETE')) {
//
// Relation (foreign key) constraint deletes category entries when
// the parent is removed.
//
$this->_result = pg_query_params($this->_dbconn, "delete from note where ordinal=$1 and login=$2 and domain=$3");
if (pg_affected_rows($this->_result) != 1) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->DeleteMessage('%s','%s' failed)", $folderid, $id));
pg_free_result($this->_result);
return false;
}
} else {
$this->_result = pg_query_params($this->_dbconn, "update note set deleted = true where ordinal=$1 and login=$2 and domain=$3", $_params);
if (pg_affected_rows($this->_result) != 1) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendStickyNote->DeleteMessage('%s','%s' failed)", $folderid, $id));
pg_free_result($this->_result);
return false;
}
}
pg_free_result($this->_result);
return true;
}
/**
* Move a message is not supported by StickyNote.
* @see BackendDiff::MoveMessage()
*/
public function MoveMessage($folderid, $id, $newfolderid, $contentParameters) {
return false;
}
/**
* Indicates which AS version is supported by the backend.
*
* @access public
* @return string AS version constant
*/
public function GetSupportedASVersion() {
return ZPush::ASV_14;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment