import PIL
import io
import ldap
+import ldap.modlist
import logging
+import phonenumbers
import urllib.parse
import urllib.request
+from . import util
from .decorators import *
from .misc import Object
Object.__init__(self, backend)
self.dn = dn
- self.__attrs = attrs or {}
+ self.attributes = attrs or {}
def __str__(self):
return self.name
def ldap(self):
return self.accounts.ldap
- @property
- def attributes(self):
- return self.__attrs
+ def _exists(self, key):
+ try:
+ self.attributes[key]
+ except KeyError:
+ return False
- def _get_first_attribute(self, attr, default=None):
- if attr not in self.attributes:
- return default
+ return True
- res = self.attributes.get(attr, [])
- if res:
- return res[0]
+ def _get(self, key):
+ for value in self.attributes.get(key, []):
+ yield value
- def get(self, key):
- try:
- attribute = self.attributes[key]
- except KeyError:
- raise AttributeError(key)
+ def _get_bytes(self, key, default=None):
+ for value in self._get(key):
+ return value
+
+ return default
+
+ def _get_strings(self, key):
+ for value in self._get(key):
+ yield value.decode()
+
+ def _get_string(self, key, default=None):
+ for value in self._get_strings(key):
+ return value
+
+ return default
+
+ def _get_phone_numbers(self, key):
+ for value in self._get_strings(key):
+ yield phonenumbers.parse(value, None)
+
+ def _modify(self, modlist):
+ logging.debug("Modifying %s: %s" % (self.dn, modlist))
+
+ # Run modify operation
+ self.ldap.modify_s(self.dn, modlist)
+
+ def _set(self, key, values):
+ current = self._get(key)
+
+ # Don't do anything if nothing has changed
+ if list(current) == values:
+ return
+
+ # Remove all old values and add all new ones
+ modlist = []
- if len(attribute) == 1:
- return attribute[0]
+ if self._exists(key):
+ modlist.append((ldap.MOD_DELETE, key, None))
- return attribute
+ # Add new values
+ modlist.append((ldap.MOD_ADD, key, values))
+
+ # Run modify operation
+ self._modify(modlist)
+
+ # Update cache
+ self.attributes.update({ key : values })
+
+ def _set_bytes(self, key, values):
+ return self._set(key, values)
+
+ def _set_strings(self, key, values):
+ return self._set(key, [e.encode() for e in values])
+
+ def _set_string(self, key, value):
+ return self._set_strings(key, [value,])
def check_password(self, password):
"""
return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
or self.telephone_numbers or self.address
+ def can_be_managed_by(self, account):
+ """
+ Returns True if account is allowed to manage this account
+ """
+ # Admins can manage all accounts
+ if account.is_admin():
+ return True
+
+ # Users can manage themselves
+ return self == account
+
@property
def classes(self):
- return (x.decode() for x in self.attributes.get("objectClass", []))
+ return self._get_strings("objectClass")
@property
def uid(self):
- return self._get_first_attribute("uid").decode()
+ return self._get_string("uid")
@property
def name(self):
- return self._get_first_attribute("cn").decode()
+ return self._get_string("cn")
- @property
- def first_name(self):
- return self._get_first_attribute("givenName").decode()
+ # First Name
+
+ def get_first_name(self):
+ return self._get_string("givenName")
+
+ def set_first_name(self, first_name):
+ self._set_string("givenName", first_name)
+
+ # Update Common Name
+ self._set_string("cn", "%s %s" % (first_name, self.last_name))
+
+ first_name = property(get_first_name, set_first_name)
+
+ # Last Name
+
+ def get_last_name(self):
+ return self._get_string("sn")
+
+ def set_last_name(self, last_name):
+ self._set_string("sn", last_name)
+
+ # Update Common Name
+ self._set_string("cn", "%s %s" % (self.first_name, last_name))
+
+ last_name = property(get_last_name, set_last_name)
@lazy_property
def groups(self):
return groups
- @property
- def address(self):
- address = self._get_first_attribute("homePostalAddress", "".encode()).decode()
- address = address.replace(", ", "\n")
+ # Address
+
+ def get_address(self):
+ address = self._get_string("homePostalAddress")
+
+ if address:
+ return (line.strip() for line in address.split(","))
- return address
+ return []
+
+ def set_address(self, address):
+ data = ", ".join(address.splitlines())
+
+ self._set_bytes("homePostalAddress", data.encode())
+
+ address = property(get_address, set_address)
@property
def email(self):
# If everything else fails, we will go with the UID
return "%s@ipfire.org" % self.uid
+ # Mail Routing Address
+
+ def get_mail_routing_address(self):
+ return self._get_string("mailRoutingAddress", None)
+
+ def set_mail_routing_address(self, address):
+ self._set_string("mailRoutingAddress", address)
+
+ mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
+
@property
def sip_id(self):
if "sipUser" in self.classes:
- return self._get_first_attribute("sipAuthenticationUser").decode()
+ return self._get_string("sipAuthenticationUser")
if "sipRoutingObject" in self.classes:
- return self._get_first_attribute("sipLocalAddress").decode()
+ return self._get_string("sipLocalAddress")
@property
def sip_password(self):
- return self._get_first_attribute("sipPassword").decode()
+ return self._get_string("sipPassword")
+
+ @staticmethod
+ def _generate_sip_password():
+ return util.random_string(8)
@property
def sip_url(self):
return "%s@ipfire.org" % self.sip_id
def uses_sip_forwarding(self):
- if self.sip_routing_url:
+ if self.sip_routing_address:
return True
return False
- @property
- def sip_routing_url(self):
+ # SIP Routing
+
+ def get_sip_routing_address(self):
if "sipRoutingObject" in self.classes:
- return self._get_first_attribute("sipRoutingAddress").decode()
+ return self._get_string("sipRoutingAddress")
+
+ def set_sip_routing_address(self, address):
+ if not address:
+ address = None
+
+ # Don't do anything if nothing has changed
+ if self.get_sip_routing_address() == address:
+ return
+
+ if address:
+ modlist = [
+ # This is no longer a SIP user any more
+ (ldap.MOD_DELETE, "objectClass", b"sipUser"),
+ (ldap.MOD_DELETE, "sipAuthenticationUser", None),
+ (ldap.MOD_DELETE, "sipPassword", None),
+
+ (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
+ (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
+ (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
+ ]
+ else:
+ modlist = [
+ (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
+ (ldap.MOD_DELETE, "sipLocalAddress", None),
+ (ldap.MOD_DELETE, "sipRoutingAddress", None),
+
+ (ldap.MOD_ADD, "objectClass", b"sipUser"),
+ (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
+ (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
+ ]
+
+ # Run modification
+ self._modify(modlist)
+
+ # XXX Cache is invalid here
+
+ sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
@lazy_property
def sip_registrations(self):
def get_cdr(self, date=None, limit=None):
return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
- @property
- def telephone_numbers(self):
- return self._telephone_numbers + self.mobile_telephone_numbers \
- + self.home_telephone_numbers
+ # Phone Numbers
- @property
- def _telephone_numbers(self):
- return list((x.decode() for x in self.attributes.get("telephoneNumber") or []))
+ def get_phone_numbers(self):
+ ret = []
- @property
- def home_telephone_numbers(self):
- return list((x.decode() for x in self.attributes.get("homePhone") or []))
+ for field in ("telephoneNumber", "homePhone", "mobile"):
+ for number in self._get_phone_numbers(field):
+ ret.append(number)
- @property
- def mobile_telephone_numbers(self):
- return list((x.decode() for x in self.attributes.get("mobile") or []))
+ return ret
+
+ def set_phone_numbers(self, phone_numbers):
+ # Sort phone numbers by landline and mobile
+ _landline_numbers = []
+ _mobile_numbers = []
+
+ for number in phone_numbers:
+ try:
+ number = phonenumbers.parse(number, None)
+ except phonenumbers.phonenumberutil.NumberParseException:
+ continue
+
+ # Convert to string (in E.164 format)
+ s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
+
+ # Separate mobile numbers
+ if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
+ _mobile_numbers.append(s)
+ else:
+ _landline_numbers.append(s)
+
+ # Save
+ self._set_strings("telephoneNumber", _landline_numbers)
+ self._set_strings("mobile", _mobile_numbers)
+
+ phone_numbers = property(get_phone_numbers, set_phone_numbers)
@property
def _all_telephone_numbers(self):
- return [ self.sip_id, ] + self.telephone_numbers
+ return [ self.sip_id, ] + list(self.phone_numbers)
def avatar_url(self, size=None):
if self.backend.debug:
return url
def get_avatar(self, size=None):
- avatar = self._get_first_attribute("jpegPhoto")
+ avatar = self._get_bytes("jpegPhoto")
if not avatar:
return
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}{{ _("Edit %s") % account }}{% end block %}
+
+{% block main %}
+ <div class="row justify-content-center">
+ <div class="col-6">
+ <h4 class="mb-4">{{ _("Edit %s") % account }}</h4>
+
+ <form method="POST" action="">
+ {% raw xsrf_form_html() %}
+
+ <div class="form-row mb-3">
+ <div class="col">
+ <label>{{ _("First Name") }}</label>
+
+ <input type="text" class="form-control" name="first_name"
+ placeholder="{{ _("First Name") }}" value="{{ account.first_name }}" required>
+ </div>
+
+ <div class="col">
+ <label>{{ _("Last Name") }}</label>
+
+ <input type="text" class="form-control" name="last_name"
+ placeholder="{{ _("Last Name") }}" value="{{ account.last_name }}" required>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <label>{{ _("Address") }}</label>
+
+ <textarea type="text" class="form-control" name="address" rows="5" required
+ placeholder="{{ _("Address") }}">{{ "\n".join(account.address) }}</textarea>
+
+ <small class="form-text text-muted">
+ {{ _("Enter your full address including your country") }}
+ </small>
+ </div>
+
+ <fieldset>
+ <legend>{{ _("Email") }}</legend>
+
+ <div class="form-group">
+ <label>{{ _("Forward Emails") }}</label>
+
+ <input type="mail" class="form-control" name="mail_routing_address"
+ placeholder="{{ _("Email Address") }}" value="{{ account.mail_routing_address or "" }}">
+
+ <small class="form-text text-muted">
+ {{ _("All emails will be forwarded to this email address") }}
+ </small>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>{{ _("Telephone") }}</legend>
+
+ <div class="form-group">
+ <label>{{ _("Phone Numbers") }}</label>
+
+ <textarea type="text" class="form-control" name="phone_numbers" rows="5"
+ placeholder="{{ _("Phone Numbers") }}">{{ "\n".join((format_phone_number_to_e164(n) for n in account.phone_numbers)) }}</textarea>
+
+ <small class="form-text text-muted">
+ {{ _("Enter your landline and mobile phone numbers") }}
+ </small>
+ </div>
+
+ <div class="form-group">
+ <label>{{ _("Forward Calls") }}</label>
+
+ <input type="text" class="form-control" name="sip_routing_address"
+ placeholder="{{ _("SIP URI or Phone Number") }}" value="{{ account.sip_routing_address or "" }}">
+
+ <small class="form-text text-muted">
+ {{ _("All calls will be forwarded to this phone number or SIP URI") }}
+ </small>
+ </div>
+ </fieldset>
+
+ <input class="btn btn-primary btn-block" type="submit" value="{{ _("Save") }}">
+ </form>
+ </div>
+ </div>
+{% end block %}
+
+{% block right %}{% end block %}
import itertools
import os.path
import phonenumbers
+import phonenumbers.geocoder
import tornado.locale
import tornado.options
import tornado.web
# UI Modules
"ui_methods" : {
- "format_month_name" : self.format_month_name,
- "format_phone_number" : self.format_phone_number,
- "grouper" : grouper,
+ "format_month_name" : self.format_month_name,
+ "format_phone_number" : self.format_phone_number,
+ "format_phone_number_to_e164" : self.format_phone_number_to_e164,
+ "format_phone_number_location" : self.format_phone_number_location,
+ "grouper" : grouper,
},
"ui_modules" : {
# Blog
(r"/users/(\w+)", people.UserHandler),
(r"/users/(\w+)\.jpg", people.AvatarHandler),
(r"/users/(\w+)/calls(?:/(\d{4}-\d{2}-\d{2}))?", people.CallsHandler),
+ (r"/users/(\w+)/edit", people.UserEditHandler),
(r"/users/(\w+)/registrations", people.RegistrationsHandler),
] + authentication_handlers)
return month
- def format_phone_number(self, handler, s):
- try:
- number = phonenumbers.parse(s, None)
- except phonenumbers.phonenumberutil.NumberParseException:
- return s
+ def format_phone_number(self, handler, number):
+ if not isinstance(number, phonenumbers.PhoneNumber):
+ try:
+ number = phonenumbers.parse(s, None)
+ except phonenumbers.phonenumberutil.NumberParseException:
+ return number
return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.INTERNATIONAL)
+ def format_phone_number_to_e164(self, handler, number):
+ return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
+
+ def format_phone_number_location(self, handler, number):
+ s = [
+ phonenumbers.geocoder.description_for_number(number, handler.locale.code),
+ phonenumbers.region_code_for_number(number),
+ ]
+
+ return ", ".join((e for e in s if e))
+
def grouper(handler, iterator, n):
"""