openlp/openlp/core/common/__init__.py

510 lines
17 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
2022-02-01 10:10:57 +00:00
# Copyright (c) 2008-2022 OpenLP Developers #
2019-04-13 13:00:22 +00:00
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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 General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
2013-10-13 13:51:13 +00:00
The :mod:`common` module contains most of the components and libraries that make
OpenLP work.
"""
2014-10-06 19:10:03 +00:00
import hashlib
import importlib
2013-10-13 13:51:13 +00:00
import logging
2016-04-01 16:56:54 +00:00
import os
import re
2013-10-13 17:02:12 +00:00
import sys
2013-12-13 19:44:17 +00:00
import traceback
2014-10-06 19:10:03 +00:00
from ipaddress import IPv4Address, IPv6Address, AddressValueError
2016-04-04 20:14:04 +00:00
from shutil import which
2019-03-17 20:35:11 +00:00
from PyQt5 import QtGui
2015-11-07 00:49:40 +00:00
from PyQt5.QtCore import QCryptographicHash as QHash
2018-02-11 11:42:13 +00:00
from PyQt5.QtNetwork import QAbstractSocket, QHostAddress, QNetworkInterface
2017-12-28 08:27:44 +00:00
from chardet.universaldetector import UniversalDetector
2013-10-13 20:36:42 +00:00
try:
from distro import id as distro_id
except ImportError:
# The distro module is only valid for Linux, so if it doesn't exist, create a function that always returns False
def distro_id():
return False
2014-04-12 20:19:22 +00:00
log = logging.getLogger(__name__ + '.__init__')
2013-02-01 19:58:18 +00:00
2013-10-13 13:51:13 +00:00
2013-12-13 19:44:17 +00:00
FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
2018-04-16 19:30:48 +00:00
CONTROL_CHARS = re.compile(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]')
2019-07-03 13:23:23 +00:00
INVALID_FILE_CHARS = re.compile(r'[\\/:*?"<>|+\[\]%]')
2016-04-05 17:30:20 +00:00
IMAGES_FILTER = None
REPLACMENT_CHARS_MAP = str.maketrans({'\u2018': '\'', '\u2019': '\'', '\u201c': '"', '\u201d': '"', '\u2026': '...',
'\u2013': '-', '\u2014': '-', '\v': '\n\n', '\f': '\n\n'})
NEW_LINE_REGEX = re.compile(r' ?(\r\n?|\n) ?')
WHITESPACE_REGEX = re.compile(r'[ \t]+')
2020-02-27 21:48:45 +00:00
INTERFACE_FILTER = re.compile('^lo|loopback|docker|tun', re.IGNORECASE)
2013-12-13 19:44:17 +00:00
2018-02-11 11:42:13 +00:00
def get_network_interfaces():
"""
Creates a dictionary of local IPv4 interfaces on local machine.
If no active interfaces available, returns a dict of localhost IPv4 information
:returns: Dict of interfaces
"""
log.debug('Getting local IPv4 interface(es) information')
interfaces = {}
for interface in QNetworkInterface.allInterfaces():
interface_name = interface.name()
2019-07-03 06:30:01 +00:00
if INTERFACE_FILTER.search(interface_name):
log.debug('Filtering out interfaces we don\'t care about: {name}'.format(name=interface_name))
continue
log.debug('Checking for isValid and flags == IsUP | IsRunning')
if not interface.isValid() or not (interface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
continue
log.debug('Checking address(es) protocol')
for address in interface.addressEntries():
ip = address.ip()
log.debug('Checking for protocol == IPv4Protocol')
2018-02-24 16:10:02 +00:00
if ip.protocol() == QAbstractSocket.IPv4Protocol:
log.debug('Getting interface information')
interfaces[interface_name] = {
'ip': ip.toString(),
'broadcast': address.broadcast().toString(),
'netmask': address.netmask().toString(),
'prefix': address.prefixLength(),
'localnet': QHostAddress(address.netmask().toIPv4Address() &
ip.toIPv4Address()).toString()
}
log.debug('Adding {interface} to active list'.format(interface=interface.name()))
if len(interfaces) == 0:
2018-10-20 04:33:32 +00:00
log.warning('No active IPv4 network interfaces detected')
return interfaces
2018-02-11 11:42:13 +00:00
2013-12-13 19:44:17 +00:00
def trace_error_handler(logger):
"""
Log the calling path of an exception
2014-03-17 19:05:55 +00:00
:param logger: logger to use so traceback is logged to correct class
2013-12-13 19:44:17 +00:00
"""
2014-04-01 17:32:19 +00:00
log_string = "OpenLP Error trace"
2013-12-13 19:44:17 +00:00
for tb in traceback.extract_stack():
2017-10-07 07:05:07 +00:00
log_string += '\n File {file} at line {line} \n\t called {data}'.format(file=tb[0], line=tb[1], data=tb[3])
2014-04-01 17:32:19 +00:00
logger.error(log_string)
2013-12-13 19:44:17 +00:00
2013-12-15 16:50:09 +00:00
2019-07-03 13:23:23 +00:00
def extension_loader(glob_pattern, excluded_files=None):
"""
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
importers.
:param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
application directory. i.e. plugins/*/*plugin.py
2019-07-03 13:23:23 +00:00
:param list[str] | None excluded_files: A list of file names to exclude that the glob pattern may find.
:rtype: None
"""
2017-10-07 07:05:07 +00:00
from openlp.core.common.applocation import AppLocation
app_dir = AppLocation.get_directory(AppLocation.AppDir)
for extension_path in app_dir.glob(glob_pattern):
extension_path = extension_path.relative_to(app_dir)
2019-07-03 13:23:23 +00:00
if extension_path.name in (excluded_files or []):
continue
log.debug('Attempting to import %s', extension_path)
2017-05-15 10:09:59 +00:00
module_name = path_to_module(extension_path)
try:
2017-05-15 10:09:59 +00:00
importlib.import_module(module_name)
except (ImportError, OSError):
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
log.exception('Failed to import {module_name} on path {extension_path}'
.format(module_name=module_name, extension_path=extension_path))
2017-05-15 10:09:59 +00:00
2017-05-15 10:24:28 +00:00
2017-05-15 10:09:59 +00:00
def path_to_module(path):
"""
Convert a path to a module name (i.e openlp.core.common)
2017-05-15 10:24:28 +00:00
:param pathlib.Path path: The path to convert to a module name.
2017-05-15 10:09:59 +00:00
:return: The module name.
:rtype: str
"""
module_path = path.with_suffix('')
return 'openlp.' + '.'.join(module_path.parts)
2013-10-13 17:02:12 +00:00
def get_frozen_path(frozen_option, non_frozen_option):
"""
Return a path based on the system status.
:param frozen_option:
:param non_frozen_option:
2013-10-13 17:02:12 +00:00
"""
if hasattr(sys, 'frozen') and sys.frozen == 1:
return frozen_option
return non_frozen_option
2013-10-13 20:36:42 +00:00
class ThemeLevel(object):
"""
Provides an enumeration for the level a theme applies to
"""
Global = 1
Service = 2
Song = 3
class SlideLimits(object):
"""
Provides an enumeration for behaviour of OpenLP at the end limits of each service item when pressing the up/down
arrow keys
"""
End = 1
Wrap = 2
Next = 3
2013-12-13 19:44:17 +00:00
2019-07-20 12:27:28 +00:00
class Singleton(type):
"""
Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Create a new instance if one does not already exist.
"""
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
2013-12-13 19:44:17 +00:00
def de_hump(name):
"""
Change any Camel Case string to python string
"""
sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name)
return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower()
def is_win():
"""
Returns true if running on a system with a nt kernel e.g. Windows, Wine
:return: True if system is running a nt kernel false otherwise
"""
return os.name.startswith('nt')
def is_macosx():
"""
Returns true if running on a system with a darwin kernel e.g. Mac OS X
:return: True if system is running a darwin kernel false otherwise
"""
return sys.platform.startswith('darwin')
def is_linux(distro=None):
"""
Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc
:param distro: If not None, check if running that Linux distro
:return: True if system is running a linux kernel false otherwise
"""
result = sys.platform.startswith('linux')
if result and distro:
result = result and distro == distro_id()
return result
2014-10-06 19:10:03 +00:00
2019-10-07 17:12:38 +00:00
def is_64bit_instance():
"""
Returns true if the python/OpenLP instance running is 64 bit. If running a 32 bit instance on
a 64 bit system this will return false.
:return: True if the python/OpenLP instance running is 64 bit, otherwise False.
"""
return (sys.maxsize > 2**32)
2014-10-06 19:10:03 +00:00
def verify_ipv4(addr):
"""
Validate an IPv4 address
:param addr: Address to validate
:returns: bool
"""
try:
2017-10-07 07:05:07 +00:00
IPv4Address(addr)
2014-10-06 19:10:03 +00:00
return True
except AddressValueError:
return False
def verify_ipv6(addr):
"""
Validate an IPv6 address
:param addr: Address to validate
:returns: bool
"""
try:
2017-10-07 07:05:07 +00:00
IPv6Address(addr)
2014-10-06 19:10:03 +00:00
return True
except AddressValueError:
return False
def verify_ip_address(addr):
"""
Validate an IP address as either IPv4 or IPv6
:param addr: Address to validate
:returns: bool
"""
return True if verify_ipv4(addr) else verify_ipv6(addr)
def md5_hash(salt=None, data=None):
2014-10-06 19:10:03 +00:00
"""
Returns the hashed output of md5sum on salt,data
using Python3 hashlib
:param salt: Initial salt
:param data: OPTIONAL Data to hash
2014-10-06 19:10:03 +00:00
:returns: str
"""
log.debug('md5_hash(salt="{text}")'.format(text=salt))
if not salt and not data:
return None
2014-10-06 19:10:03 +00:00
hash_obj = hashlib.new('md5')
if salt:
hash_obj.update(salt)
if data:
2016-06-18 02:45:02 +00:00
hash_obj.update(data)
2014-10-06 19:10:03 +00:00
hash_value = hash_obj.hexdigest()
log.debug('md5_hash() returning "{text}"'.format(text=hash_value))
2014-10-06 19:10:03 +00:00
return hash_value
2020-05-07 05:18:37 +00:00
def sha256_file_hash(filename):
"""
Returns the hashed output of sha256 on the file content using Python3 hashlib
:param filename: Name of the file to hash
:returns: str
"""
log.debug('sha256_file_hash(filename="{filename}")'.format(filename=filename))
2020-05-07 05:18:37 +00:00
hash_obj = hashlib.sha256()
if not filename.exists():
return None
with filename.open('rb') as f:
2020-05-07 05:18:37 +00:00
for chunk in iter(lambda: f.read(65536), b''):
hash_obj.update(chunk)
return hash_obj.hexdigest()
def qmd5_hash(salt=None, data=None):
2014-10-06 19:10:03 +00:00
"""
2014-10-09 20:30:07 +00:00
Returns the hashed output of MD5Sum on salt, data
using PyQt5.QCryptographicHash. Function returns a
QByteArray instead of a text string.
If you need a string instead, call with
result = str(qmd5_hash(salt=..., data=...), encoding='ascii')
2014-10-06 19:10:03 +00:00
:param salt: Initial salt
:param data: OPTIONAL Data to hash
:returns: QByteArray
2014-10-06 19:10:03 +00:00
"""
log.debug('qmd5_hash(salt="{text}"'.format(text=salt))
if salt is None and data is None:
return None
2014-10-06 19:10:03 +00:00
hash_obj = QHash(QHash.Md5)
if salt:
hash_obj.addData(salt)
if data:
2016-06-18 02:45:02 +00:00
hash_obj.addData(data)
2014-10-06 19:10:03 +00:00
hash_value = hash_obj.result().toHex()
log.debug('qmd5_hash() returning "{hash}"'.format(hash=hash_value))
return hash_value
2014-10-06 19:10:03 +00:00
2014-10-31 20:12:06 +00:00
def clean_button_text(button_text):
"""
Clean the & and other characters out of button text
2014-10-31 20:12:06 +00:00
:param button_text: The text to clean
"""
return button_text.replace('&', '').replace('< ', '').replace(' >', '')
2014-10-06 19:10:03 +00:00
2014-10-31 20:12:06 +00:00
2016-04-01 17:28:40 +00:00
def add_actions(target, actions):
"""
Adds multiple actions to a menu or toolbar in one command.
:param target: The menu or toolbar to add actions to
:param actions: The actions to be added. An action consisting of the keyword ``None``
will result in a separator being inserted into the target.
"""
for action in actions:
if action is None:
target.addSeparator()
else:
target.addAction(action)
2016-04-04 20:14:04 +00:00
def get_uno_command(connection_type='pipe'):
"""
Returns the UNO command to launch an libreoffice.org instance.
"""
for command in ['libreoffice', 'soffice']:
if which(command):
break
else:
raise FileNotFoundError('Command not found')
OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
if connection_type == 'pipe':
CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
else:
CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
return '{cmd} {opt} {conn}'.format(cmd=command, opt=OPTIONS, conn=CONNECTION)
2016-04-04 20:14:04 +00:00
def get_uno_instance(resolver, connection_type='pipe'):
"""
Returns a running libreoffice.org instance.
:param resolver: The UNO resolver to use to find a running instance.
"""
log.debug('get UNO Desktop Openoffice - resolve')
if connection_type == 'pipe':
return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
else:
2016-04-04 20:27:33 +00:00
return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
2016-04-05 16:58:29 +00:00
def get_filesystem_encoding():
"""
Returns the name of the encoding used to convert Unicode filenames into system file names.
"""
encoding = sys.getfilesystemencoding()
if encoding is None:
encoding = sys.getdefaultencoding()
2016-04-05 17:10:51 +00:00
return encoding
def delete_file(file_path):
2016-04-05 17:10:51 +00:00
"""
Deletes a file from the system.
:param pathlib.Path file_path: The file, including path, to delete.
:return: True if the deletion was successful, or the file never existed. False otherwise.
:rtype: bool
2016-04-05 17:10:51 +00:00
"""
if not file_path:
2016-04-05 17:10:51 +00:00
return False
try:
if file_path.exists():
file_path.unlink()
2016-04-05 17:10:51 +00:00
return True
except OSError:
log.exception('Unable to delete file {file_path}'.format(file_path=file_path))
2016-04-05 17:30:20 +00:00
return False
def get_images_filter():
"""
Returns a filter string for a file dialog containing all the supported image formats.
"""
2017-10-07 07:05:07 +00:00
from openlp.core.common.i18n import translate
2016-04-05 17:30:20 +00:00
global IMAGES_FILTER
if not IMAGES_FILTER:
log.debug('Generating images filter.')
formats = list(map(bytes.decode, map(bytes, QtGui.QImageReader.supportedImageFormats())))
visible_formats = '(*.{text})'.format(text='; *.'.join(formats))
actual_formats = '(*.{text})'.format(text=' *.'.join(formats))
IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'),
visible=visible_formats,
actual=actual_formats)
2016-04-05 17:30:20 +00:00
return IMAGES_FILTER
def is_not_image_file(file_path):
2016-04-05 17:30:20 +00:00
"""
Validate that the file is not an image file.
:param pathlib.Path file_path: The file to be checked.
:return: If the file is not an image
:rtype: bool
2016-04-05 17:30:20 +00:00
"""
if not (file_path and file_path.exists()):
2016-04-05 17:30:20 +00:00
return True
else:
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
if file_path.suffix[1:].lower() in formats:
2016-04-05 17:30:20 +00:00
return False
return True
def clean_filename(filename):
"""
Removes invalid characters from the given ``filename``.
:param str filename: The "dirty" file name to clean.
:return: The cleaned string
:rtype: str
2016-04-05 17:30:20 +00:00
"""
2016-04-05 20:07:57 +00:00
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
2016-04-16 13:51:42 +00:00
def get_file_encoding(file_path):
"""
Utility function to incrementally detect the file encoding.
:param pathlib.Path file_path: Filename for the file to determine the encoding for.
:return: The name of the encoding detected
:rtype: str
"""
detector = UniversalDetector()
try:
with file_path.open('rb') as detect_file:
while not detector.done:
chunk = detect_file.read(1024)
if not chunk:
break
detector.feed(chunk)
except OSError:
log.exception('Error detecting file encoding')
finally:
return detector.close()['encoding']
2018-04-17 19:26:18 +00:00
def normalize_str(irregular_string):
"""
Normalize the supplied string. Remove unicode control chars and tidy up white space.
2018-04-17 19:26:18 +00:00
:param str irregular_string: The string to normalize.
:return: The normalized string
:rtype: str
"""
2018-04-17 19:26:18 +00:00
irregular_string = irregular_string.translate(REPLACMENT_CHARS_MAP)
irregular_string = CONTROL_CHARS.sub('', irregular_string)
irregular_string = NEW_LINE_REGEX.sub('\n', irregular_string)
return WHITESPACE_REGEX.sub(' ', irregular_string)